v8/tools/heap-stats/trace-file-reader.js
Igor Sheludko f2d0550a84 [tools] Fix parsing of Chrome tracing files by v8-heap-stats
Use Oboe.js streaming JSON parser for reading tracing file which
provides the following advantages:
1) streaming parsing allows keeping alive only relevant entries which
   should consume less memory when parsing of huge files (although
   currently the whole file is kept in memory anyway),
2) avoids the need to sanitize tracing file

Bug: v8:10155
Change-Id: Id5268264a610eff804672d09b3e9f3ac353b67de
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2120542
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Reviewed-by: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#66888}
2020-03-27 10:53:37 +00:00

308 lines
10 KiB
JavaScript

// Copyright 2018 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
import {Isolate} from './model.js';
defineCustomElement('trace-file-reader', (templateText) =>
class TraceFileReader extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this.addEventListener('click', e => this.handleClick(e));
this.addEventListener('dragover', e => this.handleDragOver(e));
this.addEventListener('drop', e => this.handleChange(e));
this.$('#file').addEventListener('change', e => this.handleChange(e));
this.$('#fileReader').addEventListener('keydown', e => this.handleKeyEvent(e));
}
$(id) {
return this.shadowRoot.querySelector(id);
}
get section() {
return this.$('#fileReaderSection');
}
updateLabel(text) {
this.$('#label').innerText = text;
}
handleKeyEvent(event) {
if (event.key == "Enter") this.handleClick(event);
}
handleClick(event) {
this.$('#file').click();
}
handleChange(event) {
// Used for drop and file change.
event.preventDefault();
var host = event.dataTransfer ? event.dataTransfer : event.target;
this.readFile(host.files[0]);
}
handleDragOver(event) {
event.preventDefault();
}
connectedCallback() {
this.$('#fileReader').focus();
}
readFile(file) {
if (!file) {
this.updateLabel('Failed to load file.');
return;
}
this.$('#fileReader').blur();
this.section.className = 'loading';
const reader = new FileReader();
if (['application/gzip', 'application/x-gzip'].includes(file.type)) {
reader.onload = (e) => {
try {
const textResult = pako.inflate(e.target.result, {to: 'string'});
this.processRawText(file, textResult);
this.section.className = 'success';
this.$('#fileReader').classList.add('done');
} catch (err) {
console.error(err);
this.section.className = 'failure';
}
};
// Delay the loading a bit to allow for CSS animations to happen.
setTimeout(() => reader.readAsArrayBuffer(file), 0);
} else {
reader.onload = (e) => {
try {
this.processRawText(file, e.target.result);
this.section.className = 'success';
this.$('#fileReader').classList.add('done');
} catch (err) {
console.error(err);
this.section.className = 'failure';
}
};
// Delay the loading a bit to allow for CSS animations to happen.
setTimeout(() => reader.readAsText(file), 0);
}
}
processRawText(file, result) {
let return_data;
if (result.includes('V8.GC_Objects_Stats')) {
return_data = this.createModelFromChromeTraceFile(result);
} else {
let contents = result.split('\n');
return_data = this.createModelFromV8TraceFile(contents);
}
this.extendAndSanitizeModel(return_data);
this.updateLabel('Finished loading \'' + file.name + '\'.');
this.dispatchEvent(new CustomEvent(
'change', {bubbles: true, composed: true, detail: return_data}));
}
createOrUpdateEntryIfNeeded(data, entry) {
console.assert(entry.isolate, 'entry should have an isolate');
if (!(entry.isolate in data)) {
data[entry.isolate] = new Isolate(entry.isolate);
}
const data_object = data[entry.isolate];
if (('id' in entry) && !(entry.id in data_object.gcs)) {
data_object.gcs[entry.id] = {non_empty_instance_types: new Set()};
}
if ('time' in entry) {
if (data_object.end === null || data_object.end < entry.time) {
data_object.end = entry.time;
}
if (data_object.start === null || data_object.start > entry.time) {
data_object.start = entry.time;
}
}
}
createDatasetIfNeeded(data, entry, data_set) {
if (!(data_set in data[entry.isolate].gcs[entry.id])) {
data[entry.isolate].gcs[entry.id][data_set] = {
instance_type_data: {},
non_empty_instance_types: new Set(),
overall: 0
};
data[entry.isolate].data_sets.add(data_set);
}
}
addFieldTypeData(data, isolate, gc_id, data_set, tagged_fields,
inobject_smi_fields, embedder_fields, unboxed_double_fields,
boxed_double_fields, string_data, other_raw_fields) {
data[isolate].gcs[gc_id][data_set].field_data = {
tagged_fields,
inobject_smi_fields,
embedder_fields,
unboxed_double_fields,
boxed_double_fields,
string_data,
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,
count: entry.count,
histogram: entry.histogram,
over_allocated: entry.over_allocated,
over_allocated_histogram: entry.over_allocated_histogram
};
data[isolate].gcs[gc_id][data_set].overall += entry.overall;
if (entry.overall !== 0) {
data[isolate].gcs[gc_id][data_set].non_empty_instance_types.add(
instance_type);
data[isolate].gcs[gc_id].non_empty_instance_types.add(instance_type);
data[isolate].non_empty_instance_types.add(instance_type);
}
}
extendAndSanitizeModel(data) {
const checkNonNegativeProperty = (obj, property) => {
console.assert(obj[property] >= 0, 'negative property', obj, property);
};
Object.values(data).forEach(isolate => isolate.finalize());
}
createModelFromChromeTraceFile(contents) {
const data = Object.create(null); // Final data container.
const parseOneGCEvent = (actual_data) => {
Object.keys(actual_data).forEach(data_set => {
const string_entry = actual_data[data_set];
try {
const entry = JSON.parse(string_entry);
this.createOrUpdateEntryIfNeeded(data, entry);
this.createDatasetIfNeeded(data, entry, data_set);
const isolate = entry.isolate;
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.inobject_smi_fields,
field_data.embedder_fields,
field_data.unboxed_double_fields,
field_data.boxed_double_fields,
field_data.string_data,
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(
entry.type_data)) {
// Trace file format uses markers that do not have actual
// properties.
if (!('overall' in value)) continue;
this.addInstanceTypeData(
data, isolate, gc_id, data_set, instance_type, value);
}
} catch (e) {
console.error('Unable to parse data set entry', e);
}
});
};
console.log(`Processing log as chrome trace file.`);
try {
let gc_events_filter = (event) => {
if (event.name == 'V8.GC_Objects_Stats') {
parseOneGCEvent(event.args);
}
return oboe.drop;
};
let oboe_stream = oboe();
// Trace files support two formats.
oboe_stream
// 1) {traceEvents: [ data ]}
.node('traceEvents.*', gc_events_filter)
// 2) [ data ]
.node('!.*', gc_events_filter)
.fail(() => { throw new Error("Trace data parse failed!"); });
oboe_stream.emit('data', contents);
} catch (e) {
console.error('Unable to parse chrome trace file.', e);
}
return data;
}
createModelFromV8TraceFile(contents) {
console.log('Processing log as V8 trace file.');
contents = contents.map(function(line) {
try {
// Strip away a potentially present adb logcat prefix.
line = line.replace(/^I\/v8\s*\(\d+\):\s+/g, '');
return JSON.parse(line);
} catch (e) {
console.log('Unable to parse line: \'' + line + '\' (' + e + ')');
}
return null;
});
const data = Object.create(null); // Final data container.
for (var entry of contents) {
if (entry === null || entry.type === undefined) {
continue;
}
if (entry.type === 'zone') {
this.createOrUpdateEntryIfNeeded(data, entry);
const stacktrace = ('stacktrace' in entry) ? entry.stacktrace : [];
data[entry.isolate].samples.zone[entry.time] = {
allocated: entry.allocated,
pooled: entry.pooled,
stacktrace: stacktrace
};
} else if (
entry.type === 'zonecreation' || entry.type === 'zonedestruction') {
this.createOrUpdateEntryIfNeeded(data, entry);
data[entry.isolate].zonetags.push(
Object.assign({opening: entry.type === 'zonecreation'}, entry));
} else if (entry.type === 'gc_descriptor') {
this.createOrUpdateEntryIfNeeded(data, entry);
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.inobject_smi_fields,
entry.unboxed_double_fields, entry.boxed_double_fields,
entry.string_data, entry.other_raw_fields);
} else if (entry.type === 'instance_type_data') {
if (entry.id in data[entry.isolate].gcs) {
this.createOrUpdateEntryIfNeeded(data, entry);
this.createDatasetIfNeeded(data, entry, entry.key);
this.addInstanceTypeData(
data, entry.isolate, entry.id, entry.key,
entry.instance_type_name, entry);
}
} else if (entry.type === 'bucket_sizes') {
if (entry.id in data[entry.isolate].gcs) {
this.createOrUpdateEntryIfNeeded(data, entry);
this.createDatasetIfNeeded(data, entry, entry.key);
data[entry.isolate].gcs[entry.id][entry.key].bucket_sizes =
entry.sizes;
}
} else {
console.log('Unknown entry type: ' + entry.type);
}
}
return data;
}
});