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
5import {Script, SourcePosition} from '../profile.mjs';
6
7import {State} from './app-model.mjs';
8import {CodeLogEntry} from './log/code.mjs';
9import {DeoptLogEntry} from './log/code.mjs';
10import {SharedLibLogEntry} from './log/code.mjs';
11import {IcLogEntry} from './log/ic.mjs';
12import {LogEntry} from './log/log.mjs';
13import {MapLogEntry} from './log/map.mjs';
14import {TickLogEntry} from './log/tick.mjs';
15import {TimerLogEntry} from './log/timer.mjs';
16import {Processor} from './processor.mjs';
17import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
18import {$, groupBy} from './view/helper.mjs';
19
20class App {
21  _state;
22  _view;
23  _navigation;
24  _startupPromise;
25  constructor() {
26    this._view = {
27      __proto__: null,
28      logFileReader: $('#log-file-reader'),
29
30      timelinePanel: $('#timeline-panel'),
31      tickTrack: $('#tick-track'),
32      mapTrack: $('#map-track'),
33      icTrack: $('#ic-track'),
34      deoptTrack: $('#deopt-track'),
35      codeTrack: $('#code-track'),
36      timerTrack: $('#timer-track'),
37
38      icList: $('#ic-list'),
39      mapList: $('#map-list'),
40      codeList: $('#code-list'),
41      deoptList: $('#deopt-list'),
42
43      mapPanel: $('#map-panel'),
44      codePanel: $('#code-panel'),
45      scriptPanel: $('#script-panel'),
46
47      toolTip: $('#tool-tip'),
48    };
49    this._view.logFileReader.addEventListener(
50        'fileuploadstart', this.handleFileUploadStart.bind(this));
51    this._view.logFileReader.addEventListener(
52        'fileuploadchunk', this.handleFileUploadChunk.bind(this));
53    this._view.logFileReader.addEventListener(
54        'fileuploadend', this.handleFileUploadEnd.bind(this));
55    this._startupPromise = this._loadCustomElements();
56    this._view.codeTrack.svg = true;
57  }
58
59  static get allEventTypes() {
60    return new Set([
61      SourcePosition,
62      MapLogEntry,
63      IcLogEntry,
64      CodeLogEntry,
65      DeoptLogEntry,
66      SharedLibLogEntry,
67      TickLogEntry,
68      TimerLogEntry,
69    ]);
70  }
71
72  async _loadCustomElements() {
73    await Promise.all([
74      import('./view/list-panel.mjs'),
75      import('./view/timeline-panel.mjs'),
76      import('./view/map-panel.mjs'),
77      import('./view/script-panel.mjs'),
78      import('./view/code-panel.mjs'),
79      import('./view/property-link-table.mjs'),
80      import('./view/tool-tip.mjs'),
81    ]);
82    this._addEventListeners();
83  }
84
85  _addEventListeners() {
86    document.addEventListener(
87        'keydown', e => this._navigation?.handleKeyDown(e));
88    document.addEventListener(
89        SelectRelatedEvent.name, this.handleSelectRelatedEntries.bind(this));
90    document.addEventListener(
91        SelectionEvent.name, this.handleSelectEntries.bind(this))
92    document.addEventListener(
93        FocusEvent.name, this.handleFocusLogEntry.bind(this));
94    document.addEventListener(
95        SelectTimeEvent.name, this.handleTimeRangeSelect.bind(this));
96    document.addEventListener(ToolTipEvent.name, this.handleToolTip.bind(this));
97  }
98
99  handleSelectRelatedEntries(e) {
100    e.stopImmediatePropagation();
101    this.selectRelatedEntries(e.entry);
102  }
103
104  selectRelatedEntries(entry) {
105    let entries = [entry];
106    switch (entry.constructor) {
107      case SourcePosition:
108        entries = entries.concat(entry.entries);
109        break;
110      case MapLogEntry:
111        entries = this._state.icTimeline.filter(each => each.map === entry);
112        break;
113      case IcLogEntry:
114        if (entry.map) entries.push(entry.map);
115        break;
116      case DeoptLogEntry:
117        // TODO select map + code entries
118        if (entry.fileSourcePosition) entries.push(entry.fileSourcePosition);
119        break;
120      case Script:
121        entries = entry.entries.concat(entry.sourcePositions);
122        break;
123      case TimerLogEntry:
124      case CodeLogEntry:
125      case TickLogEntry:
126      case SharedLibLogEntry:
127        break;
128      default:
129        throw new Error(`Unknown selection type: ${entry.constructor?.name}`);
130    }
131    if (entry.sourcePosition) {
132      entries.push(entry.sourcePosition);
133      // TODO: find the matching Code log entries.
134    }
135    this.selectEntries(entries);
136  }
137
138  static isClickable(object) {
139    if (typeof object !== 'object') return false;
140    if (object instanceof LogEntry) return true;
141    if (object instanceof SourcePosition) return true;
142    if (object instanceof Script) return true;
143    return false;
144  }
145
146  handleSelectEntries(e) {
147    e.stopImmediatePropagation();
148    this.selectEntries(e.entries);
149  }
150
151  selectEntries(entries) {
152    const missingTypes = App.allEventTypes;
153    groupBy(entries, each => each.constructor, true).forEach(group => {
154      this.selectEntriesOfSingleType(group.entries);
155      missingTypes.delete(group.key);
156    });
157    missingTypes.forEach(
158        type => this.selectEntriesOfSingleType([], type, false));
159  }
160
161  selectEntriesOfSingleType(entries, type, focusView = true) {
162    const entryType = entries[0]?.constructor ?? type;
163    switch (entryType) {
164      case Script:
165        entries = entries.flatMap(script => script.sourcePositions);
166        return this.showSourcePositions(entries, focusView);
167      case SourcePosition:
168        return this.showSourcePositions(entries, focusView);
169      case MapLogEntry:
170        return this.showMapEntries(entries, focusView);
171      case IcLogEntry:
172        return this.showIcEntries(entries, focusView);
173      case CodeLogEntry:
174        return this.showCodeEntries(entries, focusView);
175      case DeoptLogEntry:
176        return this.showDeoptEntries(entries, focusView);
177      case SharedLibLogEntry:
178        return this.showSharedLibEntries(entries, focusView);
179      case TimerLogEntry:
180      case TickLogEntry:
181        break;
182      default:
183        throw new Error(`Unknown selection type: ${entryType?.name}`);
184    }
185  }
186
187  showMapEntries(entries, focusView = true) {
188    this._view.mapPanel.selectedLogEntries = entries;
189    this._view.mapList.selectedLogEntries = entries;
190    if (focusView) this._view.mapPanel.show();
191  }
192
193  showIcEntries(entries, focusView = true) {
194    this._view.icList.selectedLogEntries = entries;
195    if (focusView) this._view.icList.show();
196  }
197
198  showDeoptEntries(entries, focusView = true) {
199    this._view.deoptList.selectedLogEntries = entries;
200    if (focusView) this._view.deoptList.show();
201  }
202
203  showSharedLibEntries(entries, focusView = true) {}
204
205  showCodeEntries(entries, focusView = true) {
206    this._view.codePanel.selectedEntries = entries;
207    this._view.codeList.selectedLogEntries = entries;
208    if (focusView) this._view.codePanel.show();
209  }
210
211  showTickEntries(entries, focusView = true) {}
212  showTimerEntries(entries, focusView = true) {}
213
214  showSourcePositions(entries, focusView = true) {
215    this._view.scriptPanel.selectedSourcePositions = entries
216    if (focusView) this._view.scriptPanel.show();
217  }
218
219  handleTimeRangeSelect(e) {
220    e.stopImmediatePropagation();
221    this.selectTimeRange(e.start, e.end, e.focus, e.zoom);
222  }
223
224  selectTimeRange(start, end, focus = false, zoom = false) {
225    this._state.selectTimeRange(start, end);
226    this.showMapEntries(this._state.mapTimeline.selectionOrSelf, false);
227    this.showIcEntries(this._state.icTimeline.selectionOrSelf, false);
228    this.showDeoptEntries(this._state.deoptTimeline.selectionOrSelf, false);
229    this.showCodeEntries(this._state.codeTimeline.selectionOrSelf, false);
230    this.showTickEntries(this._state.tickTimeline.selectionOrSelf, false);
231    this.showTimerEntries(this._state.timerTimeline.selectionOrSelf, false);
232    this._view.timelinePanel.timeSelection = {start, end, focus, zoom};
233  }
234
235  handleFocusLogEntry(e) {
236    e.stopImmediatePropagation();
237    this.focusLogEntry(e.entry);
238  }
239
240  focusLogEntry(entry) {
241    switch (entry.constructor) {
242      case Script:
243        return this.focusSourcePosition(entry.sourcePositions[0]);
244      case SourcePosition:
245        return this.focusSourcePosition(entry);
246      case MapLogEntry:
247        return this.focusMapLogEntry(entry);
248      case IcLogEntry:
249        return this.focusIcLogEntry(entry);
250      case CodeLogEntry:
251        return this.focusCodeLogEntry(entry);
252      case DeoptLogEntry:
253        return this.focusDeoptLogEntry(entry);
254      case SharedLibLogEntry:
255        return this.focusDeoptLogEntry(entry);
256      case TickLogEntry:
257        return this.focusTickLogEntry(entry);
258      case TimerLogEntry:
259        return this.focusTimerLogEntry(entry);
260      default:
261        throw new Error(`Unknown selection type: ${entry.constructor?.name}`);
262    }
263  }
264
265  focusMapLogEntry(entry, focusSourcePosition = true) {
266    this._state.map = entry;
267    this._view.mapTrack.focusedEntry = entry;
268    this._view.mapPanel.map = entry;
269    if (focusSourcePosition) {
270      this.focusCodeLogEntry(entry.code, false);
271      this.focusSourcePosition(entry.sourcePosition);
272    }
273    this._view.mapPanel.show();
274  }
275
276  focusIcLogEntry(entry) {
277    this._state.ic = entry;
278    this.focusMapLogEntry(entry.map, false);
279    this.focusCodeLogEntry(entry.code, false);
280    this.focusSourcePosition(entry.sourcePosition);
281  }
282
283  focusCodeLogEntry(entry, focusSourcePosition = true) {
284    this._state.code = entry;
285    this._view.codePanel.entry = entry;
286    if (focusSourcePosition) this.focusSourcePosition(entry.sourcePosition);
287    this._view.codePanel.show();
288  }
289
290  focusDeoptLogEntry(entry) {
291    this._state.deoptLogEntry = entry;
292    this.focusCodeLogEntry(entry.code, false);
293    this.focusSourcePosition(entry.sourcePosition);
294  }
295
296  focusSharedLibLogEntry(entry) {
297    // no-op.
298  }
299
300  focusTickLogEntry(entry) {
301    this._state.tickLogEntry = entry;
302    this._view.tickTrack.focusedEntry = entry;
303  }
304
305  focusTimerLogEntry(entry) {
306    this._state.timerLogEntry = entry;
307    this._view.timerTrack.focusedEntry = entry;
308  }
309
310  focusSourcePosition(sourcePosition) {
311    if (!sourcePosition) return;
312    this._view.scriptPanel.focusedSourcePositions = [sourcePosition];
313    this._view.scriptPanel.show();
314  }
315
316  handleToolTip(event) {
317    let content = event.content;
318    if (typeof content !== 'string' &&
319        !(content?.nodeType && content?.nodeName)) {
320      content = content?.toolTipDict;
321    }
322    if (!content) {
323      throw new Error(
324          `Unknown tooltip content type: ${content.constructor?.name}`);
325    }
326    this.setToolTip(content, event.positionOrTargetNode);
327  }
328
329  setToolTip(content, positionOrTargetNode) {
330    this._view.toolTip.positionOrTargetNode = positionOrTargetNode;
331    this._view.toolTip.content = content;
332  }
333
334  restartApp() {
335    this._state = new State();
336    this._navigation = new Navigation(this._state, this._view);
337  }
338
339  handleFileUploadStart(e) {
340    this.restartApp();
341    $('#container').className = 'initial';
342    this._processor = new Processor();
343    this._processor.setProgressCallback(
344        e.detail.totalSize, e.detail.progressCallback);
345  }
346
347  async handleFileUploadChunk(e) {
348    this._processor.processChunk(e.detail);
349  }
350
351  async handleFileUploadEnd(e) {
352    try {
353      const processor = this._processor;
354      await processor.finalize();
355      await this._startupPromise;
356
357      this._state.profile = processor.profile;
358      const mapTimeline = processor.mapTimeline;
359      const icTimeline = processor.icTimeline;
360      const deoptTimeline = processor.deoptTimeline;
361      const codeTimeline = processor.codeTimeline;
362      const tickTimeline = processor.tickTimeline;
363      const timerTimeline = processor.timerTimeline;
364      this._state.setTimelines(
365          mapTimeline, icTimeline, deoptTimeline, codeTimeline, tickTimeline,
366          timerTimeline);
367      this._view.mapPanel.timeline = mapTimeline;
368      this._view.icList.timeline = icTimeline;
369      this._view.mapList.timeline = mapTimeline;
370      this._view.deoptList.timeline = deoptTimeline;
371      this._view.codeList.timeline = codeTimeline;
372      this._view.scriptPanel.scripts = processor.scripts;
373      this._view.codePanel.timeline = codeTimeline;
374      this._view.codePanel.timeline = codeTimeline;
375      this.refreshTimelineTrackView();
376    } catch (e) {
377      this._view.logFileReader.error = 'Log file contains errors!'
378      throw (e);
379    } finally {
380      $('#container').className = 'loaded';
381      this.fileLoaded = true;
382    }
383  }
384
385  refreshTimelineTrackView() {
386    this._view.mapTrack.data = this._state.mapTimeline;
387    this._view.icTrack.data = this._state.icTimeline;
388    this._view.deoptTrack.data = this._state.deoptTimeline;
389    this._view.codeTrack.data = this._state.codeTimeline;
390    this._view.tickTrack.data = this._state.tickTimeline;
391    this._view.timerTrack.data = this._state.timerTimeline;
392  }
393}
394
395class Navigation {
396  _view;
397  constructor(state, view) {
398    this.state = state;
399    this._view = view;
400  }
401
402  get map() {
403    return this.state.map
404  }
405
406  set map(value) {
407    this.state.map = value
408  }
409
410  get chunks() {
411    return this.state.mapTimeline.chunks;
412  }
413
414  increaseTimelineResolution() {
415    this._view.timelinePanel.nofChunks *= 1.5;
416    this.state.nofChunks *= 1.5;
417  }
418
419  decreaseTimelineResolution() {
420    this._view.timelinePanel.nofChunks /= 1.5;
421    this.state.nofChunks /= 1.5;
422  }
423
424  updateUrl() {
425    let entries = this.state.entries;
426    let params = new URLSearchParams(entries);
427    window.history.pushState(entries, '', '?' + params.toString());
428  }
429
430  scrollLeft() {}
431
432  scrollRight() {}
433
434  handleKeyDown(event) {
435    switch (event.key) {
436      case 'd':
437        this.scrollLeft();
438        return false;
439      case 'a':
440        this.scrollRight();
441        return false;
442      case '+':
443        this.increaseTimelineResolution();
444        return false;
445      case '-':
446        this.decreaseTimelineResolution();
447        return false;
448    }
449  }
450}
451
452export {App};
453