• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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