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