Commit 41e7e173 authored by Igor Sheludko's avatar Igor Sheludko Committed by Commit Bot

[heap-stats] Also collect object field stats.

In particular:
* number of pointer fields
* number embedder fields
* number boxed fields
* number of unboxed double field
* number of raw data fields

Bug: v8:7703
Change-Id: I22a310d941317a0f34f67536e55fbfab5f5354cd
Reviewed-on: https://chromium-review.googlesource.com/1056532Reviewed-by: 's avatarCamillo Bruni <cbruni@chromium.org>
Reviewed-by: 's avatarMichael Lippautz <mlippautz@chromium.org>
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#53188}
parent d171ed41
......@@ -24,6 +24,101 @@ namespace internal {
static base::LazyMutex object_stats_mutex = LAZY_MUTEX_INITIALIZER;
class FieldStatsCollector : public ObjectVisitor {
public:
FieldStatsCollector(size_t* tagged_fields_count,
size_t* embedder_fields_count,
size_t* unboxed_double_fields_count,
size_t* raw_fields_count)
: tagged_fields_count_(tagged_fields_count),
embedder_fields_count_(embedder_fields_count),
unboxed_double_fields_count_(unboxed_double_fields_count),
raw_fields_count_(raw_fields_count) {}
void RecordStats(HeapObject* host) {
size_t old_pointer_fields_count = *tagged_fields_count_;
host->Iterate(this);
size_t tagged_fields_count_in_object =
*tagged_fields_count_ - old_pointer_fields_count;
int object_size_in_words = host->Size() / kPointerSize;
DCHECK_LE(tagged_fields_count_in_object, object_size_in_words);
size_t raw_fields_count_in_object =
object_size_in_words - tagged_fields_count_in_object;
if (host->IsJSObject()) {
JSObjectFieldStats field_stats = GetInobjectFieldStats(host->map());
// Embedder fields are already included into pointer words.
DCHECK_LE(field_stats.embedded_fields_count_,
tagged_fields_count_in_object);
tagged_fields_count_in_object -= field_stats.embedded_fields_count_;
*tagged_fields_count_ -= field_stats.embedded_fields_count_;
*embedder_fields_count_ += field_stats.embedded_fields_count_;
// The rest are data words.
DCHECK_LE(field_stats.unboxed_double_fields_count_,
raw_fields_count_in_object);
raw_fields_count_in_object -= field_stats.unboxed_double_fields_count_;
*unboxed_double_fields_count_ += field_stats.unboxed_double_fields_count_;
}
*raw_fields_count_ += raw_fields_count_in_object;
}
void VisitPointers(HeapObject* host, Object** start, Object** end) override {
*tagged_fields_count_ += (end - start);
}
void VisitPointers(HeapObject* host, MaybeObject** start,
MaybeObject** end) override {
*tagged_fields_count_ += (end - start);
}
private:
struct JSObjectFieldStats {
JSObjectFieldStats()
: embedded_fields_count_(0), unboxed_double_fields_count_(0) {}
unsigned embedded_fields_count_ : kDescriptorIndexBitCount;
unsigned unboxed_double_fields_count_ : kDescriptorIndexBitCount;
};
std::unordered_map<Map*, JSObjectFieldStats> field_stats_cache_;
JSObjectFieldStats GetInobjectFieldStats(Map* map);
size_t* const tagged_fields_count_;
size_t* const embedder_fields_count_;
size_t* const unboxed_double_fields_count_;
size_t* const raw_fields_count_;
};
FieldStatsCollector::JSObjectFieldStats
FieldStatsCollector::GetInobjectFieldStats(Map* map) {
auto iter = field_stats_cache_.find(map);
if (iter != field_stats_cache_.end()) {
return iter->second;
}
// Iterate descriptor array and calculate stats.
JSObjectFieldStats stats;
stats.embedded_fields_count_ = JSObject::GetEmbedderFieldCount(map);
if (!map->is_dictionary_map()) {
int nof = map->NumberOfOwnDescriptors();
DescriptorArray* descriptors = map->instance_descriptors();
for (int descriptor = 0; descriptor < nof; descriptor++) {
PropertyDetails details = descriptors->GetDetails(descriptor);
if (details.location() == kField) {
FieldIndex index = FieldIndex::ForDescriptor(map, descriptor);
// Stop on first out-of-object field.
if (!index.is_inobject()) break;
if (details.representation().IsDouble() &&
map->IsUnboxedDoubleField(index)) {
++stats.unboxed_double_fields_count_;
}
}
}
}
field_stats_cache_.insert(std::make_pair(map, stats));
return stats;
}
void ObjectStats::ClearObjectStats(bool clear_last_time_stats) {
memset(object_counts_, 0, sizeof(object_counts_));
memset(object_sizes_, 0, sizeof(object_sizes_));
......@@ -34,6 +129,10 @@ void ObjectStats::ClearObjectStats(bool clear_last_time_stats) {
memset(object_counts_last_time_, 0, sizeof(object_counts_last_time_));
memset(object_sizes_last_time_, 0, sizeof(object_sizes_last_time_));
}
tagged_fields_count_ = 0;
embedder_fields_count_ = 0;
unboxed_double_fields_count_ = 0;
raw_fields_count_ = 0;
}
// Tell the compiler to never inline this: occasionally, the optimizer will
......@@ -89,6 +188,16 @@ void ObjectStats::PrintJSON(const char* key) {
PrintF("{ ");
PrintKeyAndId(key, gc_count);
PrintF("\"type\": \"gc_descriptor\", \"time\": %f }\n", time);
// field_data
PrintF("{ ");
PrintKeyAndId(key, gc_count);
PrintF("\"type\": \"field_data\"");
PrintF(", \"tagged_fields\": %zu", tagged_fields_count_ * kPointerSize);
PrintF(", \"embedder_fields\": %zu", embedder_fields_count_ * kPointerSize);
PrintF(", \"unboxed_double_fields\": %zu",
unboxed_double_fields_count_ * kDoubleSize);
PrintF(", \"other_raw_fields\": %zu", raw_fields_count_ * kPointerSize);
PrintF(" }\n");
// bucket_sizes
PrintF("{ ");
PrintKeyAndId(key, gc_count);
......@@ -134,6 +243,16 @@ void ObjectStats::Dump(std::stringstream& stream) {
stream << "\"isolate\":\"" << reinterpret_cast<void*>(isolate()) << "\",";
stream << "\"id\":" << gc_count << ",";
stream << "\"time\":" << time << ",";
// field_data
stream << "\"field_data\":{";
stream << "\"tagged_fields\":" << (tagged_fields_count_ * kPointerSize);
stream << ",\"embedder_fields\":" << (embedder_fields_count_ * kPointerSize);
stream << ",\"unboxed_double_fields\": "
<< (unboxed_double_fields_count_ * kDoubleSize);
stream << ",\"other_raw_fields\":" << (raw_fields_count_ * kPointerSize);
stream << "}, ";
stream << "\"bucket_sizes\":[";
for (int i = 0; i < kNumberOfBuckets; i++) {
stream << (1 << (kFirstBucketShift + i));
......@@ -208,7 +327,10 @@ class ObjectStatsCollectorImpl {
ObjectStatsCollectorImpl(Heap* heap, ObjectStats* stats);
void CollectGlobalStatistics();
void CollectStatistics(HeapObject* obj, Phase phase);
enum class CollectFieldStats { kNo, kYes };
void CollectStatistics(HeapObject* obj, Phase phase,
CollectFieldStats collect_field_stats);
private:
enum CowMode {
......@@ -267,6 +389,7 @@ class ObjectStatsCollectorImpl {
ObjectStats* stats_;
MarkCompactCollector::NonAtomicMarkingState* marking_state_;
std::unordered_set<HeapObject*> virtual_objects_;
FieldStatsCollector field_stats_collector_;
};
ObjectStatsCollectorImpl::ObjectStatsCollectorImpl(Heap* heap,
......@@ -274,7 +397,10 @@ ObjectStatsCollectorImpl::ObjectStatsCollectorImpl(Heap* heap,
: heap_(heap),
stats_(stats),
marking_state_(
heap->mark_compact_collector()->non_atomic_marking_state()) {}
heap->mark_compact_collector()->non_atomic_marking_state()),
field_stats_collector_(
&stats->tagged_fields_count_, &stats->embedder_fields_count_,
&stats->unboxed_double_fields_count_, &stats->raw_fields_count_) {}
bool ObjectStatsCollectorImpl::ShouldRecordObject(HeapObject* obj,
CowMode check_cow_array) {
......@@ -502,7 +628,8 @@ void ObjectStatsCollectorImpl::RecordVirtualFixedArrayDetails(
}
}
void ObjectStatsCollectorImpl::CollectStatistics(HeapObject* obj, Phase phase) {
void ObjectStatsCollectorImpl::CollectStatistics(
HeapObject* obj, Phase phase, CollectFieldStats collect_field_stats) {
Map* map = obj->map();
switch (phase) {
case kPhase1:
......@@ -540,6 +667,9 @@ void ObjectStatsCollectorImpl::CollectStatistics(HeapObject* obj, Phase phase) {
break;
case kPhase2:
RecordObjectStats(obj, map->instance_type(), obj->Size());
if (collect_field_stats == CollectFieldStats::kYes) {
field_stats_collector_.RecordStats(obj);
}
break;
}
}
......@@ -796,10 +926,12 @@ class ObjectStatsVisitor {
bool Visit(HeapObject* obj, int size) {
if (marking_state_->IsBlack(obj)) {
live_collector_->CollectStatistics(obj, phase_);
live_collector_->CollectStatistics(
obj, phase_, ObjectStatsCollectorImpl::CollectFieldStats::kYes);
} else {
DCHECK(!marking_state_->IsGrey(obj));
dead_collector_->CollectStatistics(obj, phase_);
dead_collector_->CollectStatistics(
obj, phase_, ObjectStatsCollectorImpl::CollectFieldStats::kNo);
}
return true;
}
......
......@@ -144,6 +144,13 @@ class ObjectStats {
// Detailed histograms by InstanceType.
size_t size_histogram_[OBJECT_STATS_COUNT][kNumberOfBuckets];
size_t over_allocated_histogram_[OBJECT_STATS_COUNT][kNumberOfBuckets];
size_t tagged_fields_count_;
size_t embedder_fields_count_;
size_t unboxed_double_fields_count_;
size_t raw_fields_count_;
friend class ObjectStatsCollectorImpl;
};
class ObjectStatsCollector {
......
......@@ -100,18 +100,20 @@ found in the LICENSE file. -->
</select>
</li>
<li>
<label for="dataset-select">
Data set
<label for="data-view-select">
Data view
</label>
<select id="dataset-select">
<select id="data-view-select">
<option>No data</option>
</select>
</li>
<li>
<input type="checkbox" id="merge-categories" checked=checked />
<label for="merge-categories">
Merge categories
<label for="dataset-select">
Data set
</label>
<select id="dataset-select">
<option>No data</option>
</select>
</li>
<li>
<label for="gc-select">
......
......@@ -8,6 +8,10 @@ const details_selection_template =
document.currentScript.ownerDocument.querySelector(
'#details-selection-template');
const VIEW_BY_INSTANCE_TYPE = 'by-instance-type';
const VIEW_BY_INSTANCE_CATEGORY = 'by-instance-category';
const VIEW_BY_FIELD_TYPE = 'by-field-type';
class DetailsSelection extends HTMLElement {
constructor() {
super();
......@@ -15,14 +19,14 @@ class DetailsSelection extends HTMLElement {
shadowRoot.appendChild(details_selection_template.content.cloneNode(true));
this.isolateSelect.addEventListener(
'change', e => this.handleIsolateChange(e));
this.dataViewSelect.addEventListener(
'change', e => this.notifySelectionChanged(e));
this.datasetSelect.addEventListener(
'change', e => this.notifySelectionChanged(e));
this.gcSelect.addEventListener(
'change', e => this.notifySelectionChanged(e));
'change', e => this.notifySelectionChanged(e));
this.$('#csv-export-btn')
.addEventListener('click', e => this.exportCurrentSelection(e));
this.$('#merge-categories')
.addEventListener('change', e => this.notifySelectionChanged(e));
this.$('#category-filter-btn')
.addEventListener('click', e => this.filterCurrentSelection(e));
this.$('#category-auto-filter-btn')
......@@ -62,6 +66,10 @@ class DetailsSelection extends HTMLElement {
return this.shadowRoot.querySelectorAll(query);
}
get dataViewSelect() {
return this.$('#data-view-select');
}
get datasetSelect() {
return this.$('#dataset-select');
}
......@@ -128,6 +136,7 @@ class DetailsSelection extends HTMLElement {
resetUI(resetIsolateSelect) {
if (resetIsolateSelect) removeAllChildren(this.isolateSelect);
removeAllChildren(this.dataViewSelect);
removeAllChildren(this.datasetSelect);
removeAllChildren(this.gcSelect);
this.clearCategories();
......@@ -148,6 +157,13 @@ class DetailsSelection extends HTMLElement {
return;
}
this.resetUI(false);
this.populateSelect(
'#data-view-select', [
[VIEW_BY_INSTANCE_TYPE, 'Selected instance types'],
[VIEW_BY_INSTANCE_CATEGORY, 'Selected type categories'],
[VIEW_BY_FIELD_TYPE, 'Field type statistics']
],
(key, label) => label, VIEW_BY_INSTANCE_TYPE);
this.populateSelect(
'#dataset-select', this.selectedIsolate.data_sets.entries(), null,
'live');
......@@ -168,14 +184,19 @@ class DetailsSelection extends HTMLElement {
notifySelectionChanged(e) {
if (!this.selection.isolate) return;
this.selection.data_view = this.dataViewSelect.value;
this.selection.categories = {};
for (let category of CATEGORIES.keys()) {
const selected = this.selectedInCategory(category);
if (selected.length > 0) this.selection.categories[category] = selected;
if (this.selection.data_view === VIEW_BY_FIELD_TYPE) {
this.$('#categories').style.display = 'none';
} else {
for (let category of CATEGORIES.keys()) {
const selected = this.selectedInCategory(category);
if (selected.length > 0) this.selection.categories[category] = selected;
}
this.$('#categories').style.display = 'block';
}
this.selection.category_names = CATEGORY_NAMES;
this.selection.data_set = this.datasetSelect.value;
this.selection.merge_categories = this.$('#merge-categories').checked;
this.selection.gc = this.gcSelect.value;
this.setButtonState(false);
this.updatePercentagesInCategory();
......
......@@ -57,6 +57,26 @@ class GlobalTimeline extends HTMLElement {
}
}
getFieldData() {
const labels = ['Time', 'Ptr compression benefit', 'Embedder fields',
'Tagged fields', 'Other raw fields', 'Unboxed doubles'];
const chart_data = [labels];
const isolate_data = this.data[this.selection.isolate];
Object.keys(isolate_data.gcs).forEach(gc_key => {
const gc_data = isolate_data.gcs[gc_key];
const data_set = gc_data[this.selection.data_set].field_data;
const data = [];
data.push(gc_data.time * kMillis2Seconds);
data.push(data_set.tagged_fields / KB / 2); // Pointer compression benefit
data.push(data_set.embedder_fields / KB);
data.push(data_set.tagged_fields / KB);
data.push(data_set.other_raw_fields / KB);
data.push(data_set.unboxed_double_fields / KB);
chart_data.push(data);
});
return chart_data;
}
getCategoryData() {
const categories = Object.keys(this.selection.categories)
.map(k => this.selection.category_names.get(k));
......@@ -102,14 +122,19 @@ class GlobalTimeline extends HTMLElement {
return chart_data;
}
drawChart() {
console.assert(this.data, 'invalid data');
console.assert(this.selection, 'invalid selection');
getChartData() {
switch (this.selection.data_view) {
case VIEW_BY_FIELD_TYPE:
return this.getFieldData();
case VIEW_BY_INSTANCE_CATEGORY:
return this.getCategoryData();
case VIEW_BY_INSTANCE_TYPE:
default:
return this.getInstanceTypeData();
}
}
const chart_data = (this.selection.merge_categories) ?
this.getCategoryData() :
this.getInstanceTypeData();
const data = google.visualization.arrayToDataTable(chart_data);
getChartOptions() {
const options = {
isStacked: true,
hAxis: {
......@@ -126,6 +151,27 @@ class GlobalTimeline extends HTMLElement {
pointSize: 5,
explorer: {},
};
switch (this.selection.data_view) {
case VIEW_BY_FIELD_TYPE:
// Overlay pointer compression benefit on top of the graph
return Object.assign(options, {
series: {0: {type: 'line', lineDashStyle: [13, 13]}},
});
case VIEW_BY_INSTANCE_CATEGORY:
case VIEW_BY_INSTANCE_TYPE:
default:
return options;
}
}
drawChart() {
console.assert(this.data, 'invalid data');
console.assert(this.selection, 'invalid selection');
const chart_data = this.getChartData();
const data = google.visualization.arrayToDataTable(chart_data);
const options = this.getChartOptions();
const chart = new google.visualization.AreaChart(this.$('#chart'));
this.show();
chart.draw(data, google.charts.Line.convertOptions(options));
......
......@@ -38,7 +38,10 @@ class HistogramViewer extends HTMLElement {
}
isValid() {
return this.data && this.selection;
return this.data && this.selection &&
(this.selection.data_view === VIEW_BY_INSTANCE_CATEGORY ||
this.selection.data_view === VIEW_BY_INSTANCE_TYPE);
;
}
hide() {
......@@ -49,11 +52,21 @@ class HistogramViewer extends HTMLElement {
this.$('#container').style.display = 'block';
}
getOverallValue() {
switch (this.selection.data_view) {
case VIEW_BY_FIELD_TYPE:
return NaN;
case VIEW_BY_INSTANCE_CATEGORY:
return this.getPropertyForCategory('overall');
case VIEW_BY_INSTANCE_TYPE:
default:
return this.getPropertyForInstanceTypes('overall');
}
}
stateChanged() {
if (this.isValid()) {
const overall_bytes = (this.selection.merge_categories) ?
this.getPropertyForCategory('overall') :
this.getPropertyForInstanceTypes('overall');
const overall_bytes = this.getOverallValue();
this.$('#overall').innerHTML = `Overall: ${overall_bytes / KB} KB`;
this.drawChart();
} else {
......@@ -143,10 +156,20 @@ class HistogramViewer extends HTMLElement {
return [labels, ...data];
}
getChartData() {
switch (this.selection.data_view) {
case VIEW_BY_FIELD_TYPE:
return this.getFieldData();
case VIEW_BY_INSTANCE_CATEGORY:
return this.getCategoryData();
case VIEW_BY_INSTANCE_TYPE:
default:
return this.getInstanceTypeData();
}
}
drawChart() {
const chart_data = (this.selection.merge_categories) ?
this.getCategoryData() :
this.getInstanceTypeData();
const chart_data = this.getChartData();
const data = google.visualization.arrayToDataTable(chart_data);
const options = {
legend: {position: 'top', maxLines: '1'},
......
......@@ -126,6 +126,16 @@ class TraceFileReader extends HTMLElement {
}
}
addFieldTypeData(data, isolate, gc_id, data_set, tagged_fields,
embedder_fields, unboxed_double_fields, other_raw_fields) {
data[isolate].gcs[gc_id][data_set].field_data = {
tagged_fields,
embedder_fields,
unboxed_double_fields,
other_raw_fields
};
}
addInstanceTypeData(data, isolate, gc_id, data_set, instance_type, entry) {
data[isolate].gcs[gc_id][data_set].instance_type_data[instance_type] = {
overall: entry.overall,
......@@ -194,6 +204,13 @@ class TraceFileReader extends HTMLElement {
const time = entry.time;
const gc_id = entry.id;
data[isolate].gcs[gc_id].time = time;
const field_data = entry.field_data;
this.addFieldTypeData(data, isolate, gc_id, data_set,
field_data.tagged_fields, field_data.embedder_fields,
field_data.unboxed_double_fields,
field_data.other_raw_fields);
data[isolate].gcs[gc_id][data_set].bucket_sizes =
entry.bucket_sizes;
for (let [instance_type, value] of Object.entries(
......@@ -251,6 +268,12 @@ class TraceFileReader extends HTMLElement {
data[entry.isolate].gcs[entry.id].time = entry.time;
if ('zone' in entry)
data[entry.isolate].gcs[entry.id].malloced = entry.zone;
} else if (entry.type === 'field_data') {
this.createOrUpdateEntryIfNeeded(data, entry);
this.createDatasetIfNeeded(data, entry, entry.key);
this.addFieldTypeData(data, entry.isolate, entry.id, entry.key,
entry.tagged_fields, entry.embedder_fields,
entry.unboxed_double_fields, entry.other_raw_fields);
} else if (entry.type === 'instance_type_data') {
if (entry.id in data[entry.isolate].gcs) {
this.createOrUpdateEntryIfNeeded(data, entry);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment