1// Copyright 2020 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 // Decode data as strings of 64Kb chunks. Bigger chunks may cause 71 // parsing failures in Oboe.js. 72 const chunkedInflate = new pako.Inflate( 73 {to: 'string', chunkSize: 65536} 74 ); 75 let processingState = undefined; 76 chunkedInflate.onData = (chunk) => { 77 if (processingState === undefined) { 78 processingState = this.startProcessing(file, chunk); 79 } else { 80 processingState.processChunk(chunk); 81 } 82 }; 83 chunkedInflate.onEnd = () => { 84 if (processingState !== undefined) { 85 const result_data = processingState.endProcessing(); 86 this.processLoadedData(file, result_data); 87 } 88 }; 89 console.log("======"); 90 const textResult = chunkedInflate.push(e.target.result); 91 92 this.section.className = 'success'; 93 this.$('#fileReader').classList.add('done'); 94 } catch (err) { 95 console.error(err); 96 this.section.className = 'failure'; 97 } 98 }; 99 // Delay the loading a bit to allow for CSS animations to happen. 100 setTimeout(() => reader.readAsArrayBuffer(file), 0); 101 } else { 102 reader.onload = (e) => { 103 try { 104 // Process the whole file in at once. 105 const processingState = this.startProcessing(file, e.target.result); 106 const dataModel = processingState.endProcessing(); 107 this.processLoadedData(file, dataModel); 108 109 this.section.className = 'success'; 110 this.$('#fileReader').classList.add('done'); 111 } catch (err) { 112 console.error(err); 113 this.section.className = 'failure'; 114 } 115 }; 116 // Delay the loading a bit to allow for CSS animations to happen. 117 setTimeout(() => reader.readAsText(file), 0); 118 } 119 } 120 121 processLoadedData(file, dataModel) { 122 console.log("Trace file parsed successfully."); 123 this.extendAndSanitizeModel(dataModel); 124 this.updateLabel('Finished loading \'' + file.name + '\'.'); 125 this.dispatchEvent(new CustomEvent( 126 'change', {bubbles: true, composed: true, detail: dataModel})); 127 } 128 129 createOrUpdateEntryIfNeeded(data, entry) { 130 console.assert(entry.isolate, 'entry should have an isolate'); 131 if (!(entry.isolate in data)) { 132 data[entry.isolate] = new Isolate(entry.isolate); 133 } 134 } 135 136 extendAndSanitizeModel(data) { 137 const checkNonNegativeProperty = (obj, property) => { 138 console.assert(obj[property] >= 0, 'negative property', obj, property); 139 }; 140 141 Object.values(data).forEach(isolate => isolate.finalize()); 142 } 143 144 processOneZoneStatsEntry(data, entry_stats) { 145 this.createOrUpdateEntryIfNeeded(data, entry_stats); 146 const isolate_data = data[entry_stats.isolate]; 147 let zones = undefined; 148 const entry_zones = entry_stats.zones; 149 if (entry_zones !== undefined) { 150 zones = new Map(); 151 entry_zones.forEach(zone => { 152 // There might be multiple occurrences of the same zone in the set, 153 // combine numbers in this case. 154 const existing_zone_stats = zones.get(zone.name); 155 if (existing_zone_stats !== undefined) { 156 existing_zone_stats.allocated += zone.allocated; 157 existing_zone_stats.used += zone.used; 158 existing_zone_stats.freed += zone.freed; 159 } else { 160 zones.set(zone.name, { allocated: zone.allocated, 161 used: zone.used, 162 freed: zone.freed }); 163 } 164 }); 165 } 166 const time = entry_stats.time; 167 const sample = { 168 time: time, 169 allocated: entry_stats.allocated, 170 used: entry_stats.used, 171 freed: entry_stats.freed, 172 zones: zones 173 }; 174 isolate_data.samples.set(time, sample); 175 } 176 177 startProcessing(file, chunk) { 178 const isV8TraceFile = chunk.includes('v8-zone-trace'); 179 const processingState = 180 isV8TraceFile ? this.startProcessingAsV8TraceFile(file) 181 : this.startProcessingAsChromeTraceFile(file); 182 183 processingState.processChunk(chunk); 184 return processingState; 185 } 186 187 startProcessingAsChromeTraceFile(file) { 188 console.log(`Processing log as chrome trace file.`); 189 const data = Object.create(null); // Final data container. 190 const parseOneZoneEvent = (actual_data) => { 191 if ('stats' in actual_data) { 192 try { 193 const entry_stats = JSON.parse(actual_data.stats); 194 this.processOneZoneStatsEntry(data, entry_stats); 195 } catch (e) { 196 console.error('Unable to parse data set entry', e); 197 } 198 } 199 }; 200 const zone_events_filter = (event) => { 201 if (event.name == 'V8.Zone_Stats') { 202 parseOneZoneEvent(event.args); 203 } 204 return oboe.drop; 205 }; 206 207 const oboe_stream = oboe(); 208 // Trace files support two formats. 209 oboe_stream 210 // 1) {traceEvents: [ data ]} 211 .node('traceEvents.*', zone_events_filter) 212 // 2) [ data ] 213 .node('!.*', zone_events_filter) 214 .fail((errorReport) => { 215 throw new Error("Trace data parse failed: " + errorReport.thrown); 216 }); 217 218 let failed = false; 219 220 const processingState = { 221 file: file, 222 223 processChunk(chunk) { 224 if (failed) return false; 225 try { 226 oboe_stream.emit('data', chunk); 227 return true; 228 } catch (e) { 229 console.error('Unable to parse chrome trace file.', e); 230 failed = true; 231 return false; 232 } 233 }, 234 235 endProcessing() { 236 if (failed) return null; 237 oboe_stream.emit('end'); 238 return data; 239 }, 240 }; 241 return processingState; 242 } 243 244 startProcessingAsV8TraceFile(file) { 245 console.log('Processing log as V8 trace file.'); 246 const data = Object.create(null); // Final data container. 247 248 const processOneLine = (line) => { 249 try { 250 // Strip away a potentially present adb logcat prefix. 251 line = line.replace(/^I\/v8\s*\(\d+\):\s+/g, ''); 252 253 const entry = JSON.parse(line); 254 if (entry === null || entry.type === undefined) return; 255 if ((entry.type === 'v8-zone-trace') && ('stats' in entry)) { 256 const entry_stats = entry.stats; 257 this.processOneZoneStatsEntry(data, entry_stats); 258 } else { 259 console.log('Unknown entry type: ' + entry.type); 260 } 261 } catch (e) { 262 console.log('Unable to parse line: \'' + line + '\' (' + e + ')'); 263 } 264 }; 265 266 let prev_chunk_leftover = ""; 267 268 const processingState = { 269 file: file, 270 271 processChunk(chunk) { 272 const contents = chunk.split('\n'); 273 const last_line = contents.pop(); 274 const linesCount = contents.length; 275 if (linesCount == 0) { 276 // There was only one line in the chunk, it may still be unfinished. 277 prev_chunk_leftover += last_line; 278 } else { 279 contents[0] = prev_chunk_leftover + contents[0]; 280 prev_chunk_leftover = last_line; 281 for (let line of contents) { 282 processOneLine(line); 283 } 284 } 285 return true; 286 }, 287 288 endProcessing() { 289 if (prev_chunk_leftover.length > 0) { 290 processOneLine(prev_chunk_leftover); 291 prev_chunk_leftover = ""; 292 } 293 return data; 294 }, 295 }; 296 return processingState; 297 } 298}); 299