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