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