// 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};