blob: 7b7cb6c5e46bd2771914053e6998b5204d5edf63 [file] [log] [blame]
// 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;
existing_zone_stats.freed += zone.freed;
} else {
zones.set(zone.name, { allocated: zone.allocated,
used: zone.used,
freed: zone.freed });
}
});
}
const time = entry_stats.time;
const sample = {
time: time,
allocated: entry_stats.allocated,
used: entry_stats.used,
freed: entry_stats.freed,
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;
}
});