1// Copyright 2018 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7import {Isolate} from './model.js'; 8 9defineCustomElement('trace-file-reader', (templateText) => 10 class TraceFileReader extends HTMLElement { 11 constructor() { 12 super(); 13 const shadowRoot = this.attachShadow({mode: 'open'}); 14 shadowRoot.innerHTML = templateText; 15 this.addEventListener('click', e => this.handleClick(e)); 16 this.addEventListener('dragover', e => this.handleDragOver(e)); 17 this.addEventListener('drop', e => this.handleChange(e)); 18 this.$('#file').addEventListener('change', e => this.handleChange(e)); 19 this.$('#fileReader').addEventListener('keydown', e => this.handleKeyEvent(e)); 20 } 21 22 $(id) { 23 return this.shadowRoot.querySelector(id); 24 } 25 26 get section() { 27 return this.$('#fileReaderSection'); 28 } 29 30 updateLabel(text) { 31 this.$('#label').innerText = text; 32 } 33 34 handleKeyEvent(event) { 35 if (event.key == "Enter") this.handleClick(event); 36 } 37 38 handleClick(event) { 39 this.$('#file').click(); 40 } 41 42 handleChange(event) { 43 // Used for drop and file change. 44 event.preventDefault(); 45 var host = event.dataTransfer ? event.dataTransfer : event.target; 46 this.readFile(host.files[0]); 47 } 48 49 handleDragOver(event) { 50 event.preventDefault(); 51 } 52 53 connectedCallback() { 54 this.$('#fileReader').focus(); 55 } 56 57 readFile(file) { 58 if (!file) { 59 this.updateLabel('Failed to load file.'); 60 return; 61 } 62 this.$('#fileReader').blur(); 63 64 this.section.className = 'loading'; 65 const reader = new FileReader(); 66 67 if (['application/gzip', 'application/x-gzip'].includes(file.type)) { 68 reader.onload = (e) => { 69 try { 70 const textResult = pako.inflate(e.target.result, {to: 'string'}); 71 this.processRawText(file, textResult); 72 this.section.className = 'success'; 73 this.$('#fileReader').classList.add('done'); 74 } catch (err) { 75 console.error(err); 76 this.section.className = 'failure'; 77 } 78 }; 79 // Delay the loading a bit to allow for CSS animations to happen. 80 setTimeout(() => reader.readAsArrayBuffer(file), 0); 81 } else if (file.type == 'text/html') { 82 // try extracting the data from a results.html file 83 reader.onload = (e) => { 84 try { 85 let html = document.createElement('html'); 86 html.innerHTML = e.target.result; 87 for (let dataScript of html.querySelectorAll('#viewer-data')) { 88 const base64 = dataScript.innerText.slice(1,-1); 89 const binary = globalThis.atob(base64); 90 const textResult = pako.inflate(binary, {to: 'string'}); 91 this.processRawText(file, textResult); 92 } 93 this.section.className = 'success'; 94 this.$('#fileReader').classList.add('done'); 95 } catch (err) { 96 console.error(err); 97 this.section.className = 'failure'; 98 } 99 }; 100 // Delay the loading a bit to allow for CSS animations to happen. 101 setTimeout(() => reader.readAsText(file), 0); 102 } else { 103 reader.onload = (e) => { 104 try { 105 this.processRawText(file, e.target.result); 106 this.section.className = 'success'; 107 this.$('#fileReader').classList.add('done'); 108 } catch (err) { 109 console.error(err); 110 this.section.className = 'failure'; 111 } 112 }; 113 // Delay the loading a bit to allow for CSS animations to happen. 114 setTimeout(() => reader.readAsText(file), 0); 115 } 116 } 117 118 processRawText(file, result) { 119 let return_data; 120 if (result.includes('V8.GC_Objects_Stats')) { 121 return_data = this.createModelFromChromeTraceFile(result); 122 } else { 123 let contents = result.split('\n'); 124 return_data = this.createModelFromV8TraceFile(contents); 125 } 126 this.extendAndSanitizeModel(return_data); 127 this.updateLabel('Finished loading \'' + file.name + '\'.'); 128 this.dispatchEvent(new CustomEvent( 129 'change', {bubbles: true, composed: true, detail: return_data})); 130 } 131 132 createOrUpdateEntryIfNeeded(data, entry) { 133 console.assert(entry.isolate, 'entry should have an isolate'); 134 if (!(entry.isolate in data)) { 135 data[entry.isolate] = new Isolate(entry.isolate); 136 } 137 const data_object = data[entry.isolate]; 138 if (('id' in entry) && !(entry.id in data_object.gcs)) { 139 data_object.gcs[entry.id] = {non_empty_instance_types: new Set()}; 140 } 141 if ('time' in entry) { 142 if (data_object.end === null || data_object.end < entry.time) { 143 data_object.end = entry.time; 144 } 145 if (data_object.start === null || data_object.start > entry.time) { 146 data_object.start = entry.time; 147 } 148 } 149 } 150 151 createDatasetIfNeeded(data, entry, data_set) { 152 if (!(data_set in data[entry.isolate].gcs[entry.id])) { 153 data[entry.isolate].gcs[entry.id][data_set] = { 154 instance_type_data: {}, 155 non_empty_instance_types: new Set(), 156 overall: 0 157 }; 158 data[entry.isolate].data_sets.add(data_set); 159 } 160 } 161 162 addFieldTypeData(data, isolate, gc_id, data_set, tagged_fields, 163 inobject_smi_fields, embedder_fields, unboxed_double_fields, 164 boxed_double_fields, string_data, other_raw_fields) { 165 data[isolate].gcs[gc_id][data_set].field_data = { 166 tagged_fields, 167 inobject_smi_fields, 168 embedder_fields, 169 unboxed_double_fields, 170 boxed_double_fields, 171 string_data, 172 other_raw_fields 173 }; 174 } 175 176 addInstanceTypeData(data, isolate, gc_id, data_set, instance_type, entry) { 177 data[isolate].gcs[gc_id][data_set].instance_type_data[instance_type] = { 178 overall: entry.overall, 179 count: entry.count, 180 histogram: entry.histogram, 181 over_allocated: entry.over_allocated, 182 over_allocated_histogram: entry.over_allocated_histogram 183 }; 184 data[isolate].gcs[gc_id][data_set].overall += entry.overall; 185 if (entry.overall !== 0) { 186 data[isolate].gcs[gc_id][data_set].non_empty_instance_types.add( 187 instance_type); 188 data[isolate].gcs[gc_id].non_empty_instance_types.add(instance_type); 189 data[isolate].non_empty_instance_types.add(instance_type); 190 } 191 } 192 193 extendAndSanitizeModel(data) { 194 const checkNonNegativeProperty = (obj, property) => { 195 console.assert(obj[property] >= 0, 'negative property', obj, property); 196 }; 197 198 Object.values(data).forEach(isolate => isolate.finalize()); 199 } 200 201 createModelFromChromeTraceFile(contents) { 202 const data = Object.create(null); // Final data container. 203 const parseOneGCEvent = (actual_data) => { 204 Object.keys(actual_data).forEach(data_set => { 205 const string_entry = actual_data[data_set]; 206 try { 207 const entry = JSON.parse(string_entry); 208 this.createOrUpdateEntryIfNeeded(data, entry); 209 this.createDatasetIfNeeded(data, entry, data_set); 210 const isolate = entry.isolate; 211 const time = entry.time; 212 const gc_id = entry.id; 213 data[isolate].gcs[gc_id].time = time; 214 215 const field_data = entry.field_data; 216 this.addFieldTypeData(data, isolate, gc_id, data_set, 217 field_data.tagged_fields, 218 field_data.inobject_smi_fields, 219 field_data.embedder_fields, 220 field_data.unboxed_double_fields, 221 field_data.boxed_double_fields, 222 field_data.string_data, 223 field_data.other_raw_fields); 224 225 data[isolate].gcs[gc_id][data_set].bucket_sizes = 226 entry.bucket_sizes; 227 for (let [instance_type, value] of Object.entries( 228 entry.type_data)) { 229 // Trace file format uses markers that do not have actual 230 // properties. 231 if (!('overall' in value)) continue; 232 this.addInstanceTypeData( 233 data, isolate, gc_id, data_set, instance_type, value); 234 } 235 } catch (e) { 236 console.error('Unable to parse data set entry', e); 237 } 238 }); 239 }; 240 console.log(`Processing log as chrome trace file.`); 241 try { 242 let gc_events_filter = (event) => { 243 if (event.name == 'V8.GC_Objects_Stats') { 244 parseOneGCEvent(event.args); 245 } 246 return oboe.drop; 247 }; 248 249 let oboe_stream = oboe(); 250 // Trace files support two formats. 251 oboe_stream 252 // 1) {traceEvents: [ data ]} 253 .node('traceEvents.*', gc_events_filter) 254 // 2) [ data ] 255 .node('!.*', gc_events_filter) 256 .fail(() => { throw new Error("Trace data parse failed!"); }); 257 oboe_stream.emit('data', contents); 258 } catch (e) { 259 console.error('Unable to parse chrome trace file.', e); 260 } 261 return data; 262 } 263 264 createModelFromV8TraceFile(contents) { 265 console.log('Processing log as V8 trace file.'); 266 contents = contents.map(function(line) { 267 try { 268 // Strip away a potentially present adb logcat prefix. 269 line = line.replace(/^I\/v8\s*\(\d+\):\s+/g, ''); 270 return JSON.parse(line); 271 } catch (e) { 272 console.log('Unable to parse line: \'' + line + '\' (' + e + ')'); 273 } 274 return null; 275 }); 276 277 const data = Object.create(null); // Final data container. 278 for (var entry of contents) { 279 if (entry === null || entry.type === undefined) { 280 continue; 281 } 282 if (entry.type === 'zone') { 283 this.createOrUpdateEntryIfNeeded(data, entry); 284 const stacktrace = ('stacktrace' in entry) ? entry.stacktrace : []; 285 data[entry.isolate].samples.zone[entry.time] = { 286 allocated: entry.allocated, 287 pooled: entry.pooled, 288 stacktrace: stacktrace 289 }; 290 } else if ( 291 entry.type === 'zonecreation' || entry.type === 'zonedestruction') { 292 this.createOrUpdateEntryIfNeeded(data, entry); 293 data[entry.isolate].zonetags.push( 294 Object.assign({opening: entry.type === 'zonecreation'}, entry)); 295 } else if (entry.type === 'gc_descriptor') { 296 this.createOrUpdateEntryIfNeeded(data, entry); 297 data[entry.isolate].gcs[entry.id].time = entry.time; 298 if ('zone' in entry) 299 data[entry.isolate].gcs[entry.id].malloced = entry.zone; 300 } else if (entry.type === 'field_data') { 301 this.createOrUpdateEntryIfNeeded(data, entry); 302 this.createDatasetIfNeeded(data, entry, entry.key); 303 this.addFieldTypeData(data, entry.isolate, entry.id, entry.key, 304 entry.tagged_fields, entry.embedder_fields, entry.inobject_smi_fields, 305 entry.unboxed_double_fields, entry.boxed_double_fields, 306 entry.string_data, entry.other_raw_fields); 307 } else if (entry.type === 'instance_type_data') { 308 if (entry.id in data[entry.isolate].gcs) { 309 this.createOrUpdateEntryIfNeeded(data, entry); 310 this.createDatasetIfNeeded(data, entry, entry.key); 311 this.addInstanceTypeData( 312 data, entry.isolate, entry.id, entry.key, 313 entry.instance_type_name, entry); 314 } 315 } else if (entry.type === 'bucket_sizes') { 316 if (entry.id in data[entry.isolate].gcs) { 317 this.createOrUpdateEntryIfNeeded(data, entry); 318 this.createDatasetIfNeeded(data, entry, entry.key); 319 data[entry.isolate].gcs[entry.id][entry.key].bucket_sizes = 320 entry.sizes; 321 } 322 } else { 323 console.log('Unknown entry type: ' + entry.type); 324 } 325 } 326 return data; 327 } 328}); 329