| // 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; |
| } |
| }); |