| // Copyright 2020 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import {SourcePosition} from '../profile.mjs'; |
| |
| import {State} from './app-model.mjs'; |
| import {FocusEvent, SelectionEvent, SelectTimeEvent} from './events.mjs'; |
| import {$} from './helper.mjs'; |
| import {IcLogEntry} from './log/ic.mjs'; |
| import {MapLogEntry} from './log/map.mjs'; |
| import {Processor} from './processor.mjs'; |
| |
| class App { |
| _state; |
| _view; |
| _navigation; |
| _startupPromise; |
| constructor( |
| fileReaderId, mapPanelId, mapStatsPanelId, timelinePanelId, icPanelId, |
| mapTrackId, icTrackId, deoptTrackId, sourcePanelId) { |
| this._view = { |
| __proto__: null, |
| logFileReader: $(fileReaderId), |
| icPanel: $(icPanelId), |
| mapPanel: $(mapPanelId), |
| mapStatsPanel: $(mapStatsPanelId), |
| timelinePanel: $(timelinePanelId), |
| mapTrack: $(mapTrackId), |
| icTrack: $(icTrackId), |
| deoptTrack: $(deoptTrackId), |
| sourcePanel: $(sourcePanelId) |
| }; |
| this.toggleSwitch = $('.theme-switch input[type="checkbox"]'); |
| this.toggleSwitch.addEventListener('change', (e) => this.switchTheme(e)); |
| this._view.logFileReader.addEventListener( |
| 'fileuploadstart', (e) => this.handleFileUploadStart(e)); |
| this._view.logFileReader.addEventListener( |
| 'fileuploadend', (e) => this.handleFileUploadEnd(e)); |
| this._startupPromise = this.runAsyncInitialize(); |
| } |
| |
| async runAsyncInitialize() { |
| await Promise.all([ |
| import('./ic-panel.mjs'), |
| import('./timeline-panel.mjs'), |
| import('./stats-panel.mjs'), |
| import('./map-panel.mjs'), |
| import('./source-panel.mjs'), |
| ]); |
| document.addEventListener( |
| 'keydown', e => this._navigation?.handleKeyDown(e)); |
| document.addEventListener( |
| SelectionEvent.name, e => this.handleShowEntries(e)); |
| document.addEventListener( |
| FocusEvent.name, e => this.handleShowEntryDetail(e)); |
| document.addEventListener( |
| SelectTimeEvent.name, e => this.handleTimeRangeSelect(e)); |
| } |
| |
| handleShowEntries(e) { |
| if (e.entries[0] instanceof MapLogEntry) { |
| this.showMapEntries(e.entries); |
| } else if (e.entries[0] instanceof IcLogEntry) { |
| this.showIcEntries(e.entries); |
| } else if (e.entries[0] instanceof SourcePosition) { |
| this.showSourcePositionEntries(e.entries); |
| } else { |
| throw new Error('Unknown selection type!'); |
| } |
| e.stopPropagation(); |
| } |
| showMapEntries(entries) { |
| this._state.selectedMapLogEntries = entries; |
| this._view.mapPanel.selectedMapLogEntries = entries; |
| this._view.mapStatsPanel.selectedLogEntries = entries; |
| } |
| showIcEntries(entries) { |
| this._state.selectedIcLogEntries = entries; |
| this._view.icPanel.selectedLogEntries = entries; |
| } |
| showDeoptEntries(entries) { |
| this._state.selectedDeoptLogEntries = entries; |
| } |
| showSourcePositionEntries(entries) { |
| // TODO: Handle multiple source position selection events |
| this._view.sourcePanel.selectedSourcePositions = entries |
| } |
| |
| handleTimeRangeSelect(e) { |
| this.selectTimeRange(e.start, e.end); |
| e.stopPropagation(); |
| } |
| |
| selectTimeRange(start, end) { |
| this._state.selectTimeRange(start, end); |
| this.showMapEntries(this._state.mapTimeline.selection); |
| this.showIcEntries(this._state.icTimeline.selection); |
| this.showDeoptEntries(this._state.deoptTimeline.selection); |
| this._view.timelinePanel.timeSelection = {start, end}; |
| } |
| |
| handleShowEntryDetail(e) { |
| if (e.entry instanceof MapLogEntry) { |
| this.selectMapLogEntry(e.entry); |
| } else if (e.entry instanceof IcLogEntry) { |
| this.selectICLogEntry(e.entry); |
| } else if (e.entry instanceof SourcePosition) { |
| this.selectSourcePosition(e.entry); |
| } else { |
| throw new Error('Unknown selection type!'); |
| } |
| e.stopPropagation(); |
| } |
| selectMapLogEntry(entry) { |
| this._state.map = entry; |
| this._view.mapTrack.selectedEntry = entry; |
| this._view.mapPanel.map = entry; |
| } |
| selectICLogEntry(entry) { |
| this._state.ic = entry; |
| this._view.icPanel.selectedLogEntries = [entry]; |
| } |
| selectSourcePosition(sourcePositions) { |
| if (!sourcePositions.script) return; |
| this._view.sourcePanel.selectedSourcePositions = [sourcePositions]; |
| } |
| |
| handleFileUploadStart(e) { |
| this.restartApp(); |
| $('#container').className = 'initial'; |
| } |
| |
| restartApp() { |
| this._state = new State(); |
| this._navigation = new Navigation(this._state, this._view); |
| } |
| |
| async handleFileUploadEnd(e) { |
| await this._startupPromise; |
| try { |
| const processor = new Processor(e.detail); |
| const mapTimeline = processor.mapTimeline; |
| const icTimeline = processor.icTimeline; |
| const deoptTimeline = processor.deoptTimeline; |
| this._state.mapTimeline = mapTimeline; |
| this._state.icTimeline = icTimeline; |
| this._state.deoptTimeline = deoptTimeline; |
| // Transitions must be set before timeline for stats panel. |
| this._view.mapPanel.timeline = mapTimeline; |
| this._view.mapTrack.data = mapTimeline; |
| this._view.mapStatsPanel.transitions = |
| this._state.mapTimeline.transitions; |
| this._view.mapStatsPanel.timeline = mapTimeline; |
| this._view.icPanel.timeline = icTimeline; |
| this._view.icTrack.data = icTimeline; |
| this._view.deoptTrack.data = deoptTimeline; |
| this._view.sourcePanel.data = processor.scripts |
| } catch (e) { |
| this._view.logFileReader.error = 'Log file contains errors!' |
| throw (e); |
| } finally { |
| $('#container').className = 'loaded'; |
| this.fileLoaded = true; |
| } |
| } |
| |
| refreshTimelineTrackView() { |
| this._view.mapTrack.data = this._state.mapTimeline; |
| this._view.icTrack.data = this._state.icTimeline; |
| this._view.deoptTrack.data = this._state.deoptTimeline; |
| } |
| |
| switchTheme(event) { |
| document.documentElement.dataset.theme = |
| event.target.checked ? 'light' : 'dark'; |
| if (this.fileLoaded) { |
| this.refreshTimelineTrackView(); |
| } |
| } |
| } |
| |
| class Navigation { |
| _view; |
| constructor(state, view) { |
| this.state = state; |
| this._view = view; |
| } |
| get map() { |
| return this.state.map |
| } |
| set map(value) { |
| this.state.map = value |
| } |
| get chunks() { |
| return this.state.mapTimeline.chunks; |
| } |
| increaseTimelineResolution() { |
| this._view.timelinePanel.nofChunks *= 1.5; |
| this.state.nofChunks *= 1.5; |
| } |
| decreaseTimelineResolution() { |
| this._view.timelinePanel.nofChunks /= 1.5; |
| this.state.nofChunks /= 1.5; |
| } |
| selectNextEdge() { |
| if (!this.map) return; |
| if (this.map.children.length != 1) return; |
| this.map = this.map.children[0].to; |
| this._view.mapTrack.selectedEntry = this.map; |
| this.updateUrl(); |
| this._view.mapPanel.map = this.map; |
| } |
| selectPrevEdge() { |
| if (!this.map) return; |
| if (!this.map.parent()) return; |
| this.map = this.map.parent(); |
| this._view.mapTrack.selectedEntry = this.map; |
| this.updateUrl(); |
| this._view.mapPanel.map = this.map; |
| } |
| selectDefaultMap() { |
| this.map = this.chunks[0].at(0); |
| this._view.mapTrack.selectedEntry = this.map; |
| this.updateUrl(); |
| this._view.mapPanel.map = this.map; |
| } |
| moveInChunks(next) { |
| if (!this.map) return this.selectDefaultMap(); |
| let chunkIndex = this.map.chunkIndex(this.chunks); |
| let chunk = this.chunks[chunkIndex]; |
| let index = chunk.indexOf(this.map); |
| if (next) { |
| chunk = chunk.next(this.chunks); |
| } else { |
| chunk = chunk.prev(this.chunks); |
| } |
| if (!chunk) return; |
| index = Math.min(index, chunk.size() - 1); |
| this.map = chunk.at(index); |
| this._view.mapTrack.selectedEntry = this.map; |
| this.updateUrl(); |
| this._view.mapPanel.map = this.map; |
| } |
| moveInChunk(delta) { |
| if (!this.map) return this.selectDefaultMap(); |
| let chunkIndex = this.map.chunkIndex(this.chunks) |
| let chunk = this.chunks[chunkIndex]; |
| let index = chunk.indexOf(this.map) + delta; |
| let map; |
| if (index < 0) { |
| map = chunk.prev(this.chunks).last(); |
| } else if (index >= chunk.size()) { |
| map = chunk.next(this.chunks).first() |
| } else { |
| map = chunk.at(index); |
| } |
| this.map = map; |
| this._view.mapTrack.selectedEntry = this.map; |
| this.updateUrl(); |
| this._view.mapPanel.map = this.map; |
| } |
| updateUrl() { |
| let entries = this.state.entries; |
| let params = new URLSearchParams(entries); |
| window.history.pushState(entries, '', '?' + params.toString()); |
| } |
| handleKeyDown(event) { |
| switch (event.key) { |
| case 'ArrowUp': |
| event.preventDefault(); |
| if (event.shiftKey) { |
| this.selectPrevEdge(); |
| } else { |
| this.moveInChunk(-1); |
| } |
| return false; |
| case 'ArrowDown': |
| event.preventDefault(); |
| if (event.shiftKey) { |
| this.selectNextEdge(); |
| } else { |
| this.moveInChunk(1); |
| } |
| return false; |
| case 'ArrowLeft': |
| this.moveInChunks(false); |
| break; |
| case 'ArrowRight': |
| this.moveInChunks(true); |
| break; |
| case '+': |
| this.increaseTimelineResolution(); |
| break; |
| case '-': |
| this.decreaseTimelineResolution(); |
| break; |
| } |
| } |
| } |
| |
| export {App}; |