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