2020-06-03 09:26:09 +00:00
|
|
|
// Copyright 2020 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 {
|
|
|
|
// Decode data as strings of 64Kb chunks. Bigger chunks may cause
|
|
|
|
// parsing failures in Oboe.js.
|
|
|
|
const chunkedInflate = new pako.Inflate(
|
|
|
|
{to: 'string', chunkSize: 65536}
|
|
|
|
);
|
|
|
|
let processingState = undefined;
|
|
|
|
chunkedInflate.onData = (chunk) => {
|
|
|
|
if (processingState === undefined) {
|
|
|
|
processingState = this.startProcessing(file, chunk);
|
|
|
|
} else {
|
|
|
|
processingState.processChunk(chunk);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
chunkedInflate.onEnd = () => {
|
|
|
|
if (processingState !== undefined) {
|
|
|
|
const result_data = processingState.endProcessing();
|
|
|
|
this.processLoadedData(file, result_data);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
console.log("======");
|
|
|
|
const textResult = chunkedInflate.push(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.readAsArrayBuffer(file), 0);
|
|
|
|
} else {
|
|
|
|
reader.onload = (e) => {
|
|
|
|
try {
|
|
|
|
// Process the whole file in at once.
|
|
|
|
const processingState = this.startProcessing(file, e.target.result);
|
|
|
|
const dataModel = processingState.endProcessing();
|
|
|
|
this.processLoadedData(file, dataModel);
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
processLoadedData(file, dataModel) {
|
|
|
|
console.log("Trace file parsed successfully.");
|
|
|
|
this.extendAndSanitizeModel(dataModel);
|
|
|
|
this.updateLabel('Finished loading \'' + file.name + '\'.');
|
|
|
|
this.dispatchEvent(new CustomEvent(
|
|
|
|
'change', {bubbles: true, composed: true, detail: dataModel}));
|
|
|
|
}
|
|
|
|
|
|
|
|
createOrUpdateEntryIfNeeded(data, entry) {
|
|
|
|
console.assert(entry.isolate, 'entry should have an isolate');
|
|
|
|
if (!(entry.isolate in data)) {
|
|
|
|
data[entry.isolate] = new Isolate(entry.isolate);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extendAndSanitizeModel(data) {
|
|
|
|
const checkNonNegativeProperty = (obj, property) => {
|
|
|
|
console.assert(obj[property] >= 0, 'negative property', obj, property);
|
|
|
|
};
|
|
|
|
|
|
|
|
Object.values(data).forEach(isolate => isolate.finalize());
|
|
|
|
}
|
|
|
|
|
|
|
|
processOneZoneStatsEntry(data, entry_stats) {
|
|
|
|
this.createOrUpdateEntryIfNeeded(data, entry_stats);
|
|
|
|
const isolate_data = data[entry_stats.isolate];
|
|
|
|
let zones = undefined;
|
|
|
|
const entry_zones = entry_stats.zones;
|
|
|
|
if (entry_zones !== undefined) {
|
|
|
|
zones = new Map();
|
|
|
|
entry_zones.forEach(zone => {
|
|
|
|
// There might be multiple occurrences of the same zone in the set,
|
|
|
|
// combine numbers in this case.
|
|
|
|
const existing_zone_stats = zones.get(zone.name);
|
|
|
|
if (existing_zone_stats !== undefined) {
|
|
|
|
existing_zone_stats.allocated += zone.allocated;
|
|
|
|
existing_zone_stats.used += zone.used;
|
2020-07-28 13:38:56 +00:00
|
|
|
existing_zone_stats.freed += zone.freed;
|
2020-06-03 09:26:09 +00:00
|
|
|
} else {
|
2020-07-28 13:38:56 +00:00
|
|
|
zones.set(zone.name, { allocated: zone.allocated,
|
|
|
|
used: zone.used,
|
|
|
|
freed: zone.freed });
|
2020-06-03 09:26:09 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
const time = entry_stats.time;
|
|
|
|
const sample = {
|
|
|
|
time: time,
|
|
|
|
allocated: entry_stats.allocated,
|
|
|
|
used: entry_stats.used,
|
2020-07-28 13:38:56 +00:00
|
|
|
freed: entry_stats.freed,
|
2020-06-03 09:26:09 +00:00
|
|
|
zones: zones
|
|
|
|
};
|
|
|
|
isolate_data.samples.set(time, sample);
|
|
|
|
}
|
|
|
|
|
|
|
|
startProcessing(file, chunk) {
|
|
|
|
const isV8TraceFile = chunk.includes('v8-zone-trace');
|
|
|
|
const processingState =
|
|
|
|
isV8TraceFile ? this.startProcessingAsV8TraceFile(file)
|
|
|
|
: this.startProcessingAsChromeTraceFile(file);
|
|
|
|
|
|
|
|
processingState.processChunk(chunk);
|
|
|
|
return processingState;
|
|
|
|
}
|
|
|
|
|
|
|
|
startProcessingAsChromeTraceFile(file) {
|
|
|
|
console.log(`Processing log as chrome trace file.`);
|
|
|
|
const data = Object.create(null); // Final data container.
|
|
|
|
const parseOneZoneEvent = (actual_data) => {
|
|
|
|
if ('stats' in actual_data) {
|
|
|
|
try {
|
|
|
|
const entry_stats = JSON.parse(actual_data.stats);
|
|
|
|
this.processOneZoneStatsEntry(data, entry_stats);
|
|
|
|
} catch (e) {
|
|
|
|
console.error('Unable to parse data set entry', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const zone_events_filter = (event) => {
|
|
|
|
if (event.name == 'V8.Zone_Stats') {
|
|
|
|
parseOneZoneEvent(event.args);
|
|
|
|
}
|
|
|
|
return oboe.drop;
|
|
|
|
};
|
|
|
|
|
|
|
|
const oboe_stream = oboe();
|
|
|
|
// Trace files support two formats.
|
|
|
|
oboe_stream
|
|
|
|
// 1) {traceEvents: [ data ]}
|
|
|
|
.node('traceEvents.*', zone_events_filter)
|
|
|
|
// 2) [ data ]
|
|
|
|
.node('!.*', zone_events_filter)
|
|
|
|
.fail((errorReport) => {
|
|
|
|
throw new Error("Trace data parse failed: " + errorReport.thrown);
|
|
|
|
});
|
|
|
|
|
|
|
|
let failed = false;
|
|
|
|
|
|
|
|
const processingState = {
|
|
|
|
file: file,
|
|
|
|
|
|
|
|
processChunk(chunk) {
|
|
|
|
if (failed) return false;
|
|
|
|
try {
|
|
|
|
oboe_stream.emit('data', chunk);
|
|
|
|
return true;
|
|
|
|
} catch (e) {
|
|
|
|
console.error('Unable to parse chrome trace file.', e);
|
|
|
|
failed = true;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
endProcessing() {
|
|
|
|
if (failed) return null;
|
|
|
|
oboe_stream.emit('end');
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return processingState;
|
|
|
|
}
|
|
|
|
|
|
|
|
startProcessingAsV8TraceFile(file) {
|
|
|
|
console.log('Processing log as V8 trace file.');
|
|
|
|
const data = Object.create(null); // Final data container.
|
|
|
|
|
|
|
|
const processOneLine = (line) => {
|
|
|
|
try {
|
|
|
|
// Strip away a potentially present adb logcat prefix.
|
|
|
|
line = line.replace(/^I\/v8\s*\(\d+\):\s+/g, '');
|
|
|
|
|
|
|
|
const entry = JSON.parse(line);
|
|
|
|
if (entry === null || entry.type === undefined) return;
|
|
|
|
if ((entry.type === 'v8-zone-trace') && ('stats' in entry)) {
|
|
|
|
const entry_stats = entry.stats;
|
|
|
|
this.processOneZoneStatsEntry(data, entry_stats);
|
|
|
|
} else {
|
|
|
|
console.log('Unknown entry type: ' + entry.type);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.log('Unable to parse line: \'' + line + '\' (' + e + ')');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let prev_chunk_leftover = "";
|
|
|
|
|
|
|
|
const processingState = {
|
|
|
|
file: file,
|
|
|
|
|
|
|
|
processChunk(chunk) {
|
|
|
|
const contents = chunk.split('\n');
|
|
|
|
const last_line = contents.pop();
|
|
|
|
const linesCount = contents.length;
|
|
|
|
if (linesCount == 0) {
|
|
|
|
// There was only one line in the chunk, it may still be unfinished.
|
|
|
|
prev_chunk_leftover += last_line;
|
|
|
|
} else {
|
|
|
|
contents[0] = prev_chunk_leftover + contents[0];
|
|
|
|
prev_chunk_leftover = last_line;
|
|
|
|
for (let line of contents) {
|
|
|
|
processOneLine(line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
endProcessing() {
|
|
|
|
if (prev_chunk_leftover.length > 0) {
|
|
|
|
processOneLine(prev_chunk_leftover);
|
|
|
|
prev_chunk_leftover = "";
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return processingState;
|
|
|
|
}
|
|
|
|
});
|