| // 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'; |
| |
| const trace_file_reader_template = |
| document.currentScript.ownerDocument.querySelector( |
| '#trace-file-reader-template'); |
| |
| class TraceFileReader extends HTMLElement { |
| constructor() { |
| super(); |
| const shadowRoot = this.attachShadow({mode: 'open'}); |
| shadowRoot.appendChild(trace_file_reader_template.content.cloneNode(true)); |
| 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 contents = result.split('\n'); |
| const return_data = (result.includes('V8.GC_Objects_Stats')) ? |
| this.createModelFromChromeTraceFile(contents) : |
| 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, |
| 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, |
| 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) { |
| // Trace files support two formats. |
| // {traceEvents: [ data ]} |
| const kObjectTraceFile = { |
| name: 'object', |
| endToken: ']}', |
| getDataArray: o => o.traceEvents |
| }; |
| // [ data ] |
| const kArrayTraceFile = { |
| name: 'array', |
| endToken: ']', |
| getDataArray: o => o |
| }; |
| const handler = |
| (contents[0][0] === '{') ? kObjectTraceFile : kArrayTraceFile; |
| console.log(`Processing log as chrome trace file (${handler.name}).`); |
| |
| // Pop last line in log as it might be broken. |
| contents.pop(); |
| // Remove trailing comma. |
| contents[contents.length - 1] = contents[contents.length - 1].slice(0, -1); |
| // Terminate JSON. |
| const sanitized_contents = [...contents, handler.endToken].join(''); |
| |
| const data = Object.create(null); // Final data container. |
| try { |
| const raw_data = JSON.parse(sanitized_contents); |
| const raw_array_data = handler.getDataArray(raw_data); |
| raw_array_data.filter(e => e.name === 'V8.GC_Objects_Stats') |
| .forEach(trace_data => { |
| const actual_data = trace_data.args; |
| const data_sets = new Set(Object.keys(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.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( |
| 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.log('Unable to parse data set entry', e); |
| } |
| }); |
| }); |
| } 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.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); |
| 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; |
| } |
| } |
| |
| customElements.define('trace-file-reader', TraceFileReader); |