diff --git a/tools/profile.mjs b/tools/profile.mjs index d426e4456c..4127b34b07 100644 --- a/tools/profile.mjs +++ b/tools/profile.mjs @@ -546,8 +546,8 @@ export class Profile { addDisassemble(start, kind, disassemble) { const entry = this.codeMap_.findDynamicEntryByStartAddress(start); - if (!entry) return; - this.getOrCreateSourceInfo(entry).setDisassemble(disassemble); + if (entry) this.getOrCreateSourceInfo(entry).setDisassemble(disassemble); + return entry; } getOrCreateSourceInfo(entry) { diff --git a/tools/system-analyzer/app-model.mjs b/tools/system-analyzer/app-model.mjs index 57e008ccf1..4e339cb0d5 100644 --- a/tools/system-analyzer/app-model.mjs +++ b/tools/system-analyzer/app-model.mjs @@ -19,6 +19,7 @@ class State { _codeTimeline; _apiTimeline; _tickTimeline; + _timerTimeline; _minStartTime = Number.POSITIVE_INFINITY; _maxEndTime = Number.NEGATIVE_INFINITY; @@ -42,13 +43,14 @@ class State { setTimelines( mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline, - tickTimeline) { + tickTimeline, timerTimeline) { this._mapTimeline = mapTimeline; this._icTimeline = icTimeline; this._deoptTimeline = deoptTimeline; this._codeTimeline = codeTimeline; this._apiTimeline = apiTimeline; this._tickTimeline = tickTimeline; + this._timerTimeline = timerTimeline; for (let timeline of arguments) { if (timeline === undefined) return; this._minStartTime = Math.min(this._minStartTime, timeline.startTime); @@ -84,10 +86,15 @@ class State { return this._tickTimeline; } + get timerTimeline() { + return this._timerTimeline; + } + get timelines() { return [ this._mapTimeline, this._icTimeline, this._deoptTimeline, - this._codeTimeline, this._apiTimeline, this._tickTimeline + this._codeTimeline, this._apiTimeline, this._tickTimeline, + this._timerTimeline ]; } diff --git a/tools/system-analyzer/helper.mjs b/tools/system-analyzer/helper.mjs index 359bbcdd98..24f550b161 100644 --- a/tools/system-analyzer/helper.mjs +++ b/tools/system-analyzer/helper.mjs @@ -18,8 +18,30 @@ export function formatBytes(bytes) { return bytes.toFixed(2) + units[index]; } -export function formatMicroSeconds(millis) { - return (millis * kMicro2Milli).toFixed(1) + 'ms'; +export function formatMicroSeconds(micro) { + return (micro * kMicro2Milli).toFixed(1) + 'ms'; +} + +export function formatDurationMicros(micros, secondsDigits = 3) { + return formatDurationMillis(micros * kMicro2Milli, secondsDigits); +} + +export function formatDurationMillis(millis, secondsDigits = 3) { + if (millis < 1000) { + if (millis < 1) { + return (millis / kMicro2Milli).toFixed(1) + 'ns'; + } + return millis.toFixed(2) + 'ms'; + } + let seconds = millis / 1000; + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + seconds = seconds % 60; + let buffer = '' + if (hours > 0) buffer += hours + 'h '; + if (hours > 0 || minutes > 0) buffer += minutes + 'm '; + buffer += seconds.toFixed(secondsDigits) + 's' + return buffer; } export function delay(time) { diff --git a/tools/system-analyzer/index.css b/tools/system-analyzer/index.css index 08c09d8102..4525f0d9b4 100644 --- a/tools/system-analyzer/index.css +++ b/tools/system-analyzer/index.css @@ -48,6 +48,10 @@ body { background-color: var(--background-color); } +h3 { + margin-block-end: 0.3em; +} + section { margin-bottom: 10px; } diff --git a/tools/system-analyzer/index.html b/tools/system-analyzer/index.html index 8b060a9740..e85a59d6e6 100644 --- a/tools/system-analyzer/index.html +++ b/tools/system-analyzer/index.html @@ -56,6 +56,7 @@ found in the LICENSE file. -->
+ diff --git a/tools/system-analyzer/index.mjs b/tools/system-analyzer/index.mjs index 030c72b690..d5125eadd1 100644 --- a/tools/system-analyzer/index.mjs +++ b/tools/system-analyzer/index.mjs @@ -13,6 +13,7 @@ import {IcLogEntry} from './log/ic.mjs'; import {LogEntry} from './log/log.mjs'; import {MapLogEntry} from './log/map.mjs'; import {TickLogEntry} from './log/tick.mjs'; +import {TimerLogEntry} from './log/timer.mjs'; import {Processor} from './processor.mjs'; import {Timeline} from './timeline.mjs' import {FocusEvent, SelectionEvent, SelectRelatedEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs'; @@ -35,6 +36,7 @@ class App { deoptTrack: $('#deopt-track'), codeTrack: $('#code-track'), apiTrack: $('#api-track'), + timerTrack: $('#timer-track'), icList: $('#ic-list'), mapList: $('#map-list'), @@ -60,8 +62,15 @@ class App { static get allEventTypes() { return new Set([ - SourcePosition, MapLogEntry, IcLogEntry, ApiLogEntry, CodeLogEntry, - DeoptLogEntry, SharedLibLogEntry, TickLogEntry + SourcePosition, + MapLogEntry, + IcLogEntry, + ApiLogEntry, + CodeLogEntry, + DeoptLogEntry, + SharedLibLogEntry, + TickLogEntry, + TimerLogEntry, ]); } @@ -112,6 +121,7 @@ class App { case Script: entries = entry.entries.concat(entry.sourcePositions); break; + case TimerLogEntry: case ApiLogEntry: case CodeLogEntry: case TickLogEntry: @@ -169,6 +179,7 @@ class App { return this.showDeoptEntries(entries); case SharedLibLogEntry: return this.showSharedLibEntries(entries); + case TimerLogEntry: case TickLogEntry: break; default: @@ -206,6 +217,7 @@ class App { } showTickEntries(entries, focusView = true) {} + showTimerEntries(entries, focusView = true) {} showSourcePositions(entries, focusView = true) { this._view.scriptPanel.selectedSourcePositions = entries @@ -225,6 +237,7 @@ class App { this.showCodeEntries(this._state.codeTimeline.selectionOrSelf, false); this.showApiEntries(this._state.apiTimeline.selectionOrSelf, false); this.showTickEntries(this._state.tickTimeline.selectionOrSelf, false); + this.showTimerEntries(this._state.timerTimeline.selectionOrSelf, false); this._view.timelinePanel.timeSelection = {start, end}; } @@ -253,6 +266,8 @@ class App { return this.focusDeoptLogEntry(entry); case TickLogEntry: return this.focusTickLogEntry(entry); + case TimerLogEntry: + return this.focusTimerLogEntry(entry); default: throw new Error(`Unknown selection type: ${entry.constructor?.name}`); } @@ -304,6 +319,11 @@ class App { this._view.tickTrack.focusedEntry = entry; } + focusTimerLogEntry(entry) { + this._state.timerLogEntry = entry; + this._view.timerTrack.focusedEntry = entry; + } + focusSourcePosition(sourcePosition) { if (!sourcePosition) return; this._view.scriptPanel.focusedSourcePositions = [sourcePosition]; @@ -357,9 +377,10 @@ class App { const codeTimeline = processor.codeTimeline; const apiTimeline = processor.apiTimeline; const tickTimeline = processor.tickTimeline; + const timerTimeline = processor.timerTimeline; this._state.setTimelines( mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline, - tickTimeline); + tickTimeline, timerTimeline); this._view.mapPanel.timeline = mapTimeline; this._view.icList.timeline = icTimeline; this._view.mapList.timeline = mapTimeline; @@ -368,6 +389,7 @@ class App { this._view.apiList.timeline = apiTimeline; this._view.scriptPanel.scripts = processor.scripts; this._view.codePanel.timeline = codeTimeline; + this._view.codePanel.timeline = codeTimeline; this.refreshTimelineTrackView(); } catch (e) { this._view.logFileReader.error = 'Log file contains errors!' @@ -385,6 +407,7 @@ class App { this._view.codeTrack.data = this._state.codeTimeline; this._view.apiTrack.data = this._state.apiTimeline; this._view.tickTrack.data = this._state.tickTimeline; + this._view.timerTrack.data = this._state.timerTimeline; } } diff --git a/tools/system-analyzer/log/code.mjs b/tools/system-analyzer/log/code.mjs index 3190189daa..2f43dadaec 100644 --- a/tools/system-analyzer/log/code.mjs +++ b/tools/system-analyzer/log/code.mjs @@ -61,6 +61,10 @@ export class CodeLogEntry extends LogEntry { return this._kind; } + get isBuiltinKind() { + return this._kindName === 'Builtin'; + } + get kindName() { return this._kindName; } @@ -70,7 +74,7 @@ export class CodeLogEntry extends LogEntry { } get functionName() { - return this._entry.functionName; + return this._entry.functionName ?? this._entry.getRawName(); } get size() { diff --git a/tools/system-analyzer/log/timer.mjs b/tools/system-analyzer/log/timer.mjs new file mode 100644 index 0000000000..d2ca02a46c --- /dev/null +++ b/tools/system-analyzer/log/timer.mjs @@ -0,0 +1,56 @@ +// Copyright 2021 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 {formatDurationMicros} from '../helper.mjs'; + +import {LogEntry} from './log.mjs'; + +export class TimerLogEntry extends LogEntry { + constructor(type, startTime, endTime = -1) { + super(type, startTime); + this._endTime = endTime; + this.depth = 0; + } + + end(time) { + if (this.isInitialized) throw new Error('Invalid timer change'); + this._endTime = time; + } + + get isInitialized() { + return this._endTime !== -1; + } + + get startTime() { + return this._time; + } + + get endTime() { + return this._endTime; + } + + get duration() { + return this._endTime - this._time; + } + + covers(time) { + return this._time <= time && time <= this._endTime; + } + + get toolTipDict() { + const dict = super.toolTipDict; + dict.startTime = formatDurationMicros(dict.startTime); + dict.endTime = formatDurationMicros(dict.endTime); + dict.duration = formatDurationMicros(dict.duration); + return dict; + } + + static get propertyNames() { + return [ + 'type', + 'startTime', + 'endTime', + 'duration', + ]; + } +} \ No newline at end of file diff --git a/tools/system-analyzer/processor.mjs b/tools/system-analyzer/processor.mjs index 5a94bf9f77..00757cb86d 100644 --- a/tools/system-analyzer/processor.mjs +++ b/tools/system-analyzer/processor.mjs @@ -10,6 +10,7 @@ import {CodeLogEntry, DeoptLogEntry, SharedLibLogEntry} from './log/code.mjs'; import {IcLogEntry} from './log/ic.mjs'; import {Edge, MapLogEntry} from './log/map.mjs'; import {TickLogEntry} from './log/tick.mjs'; +import {TimerLogEntry} from './log/timer.mjs'; import {Timeline} from './timeline.mjs'; // =========================================================================== @@ -22,6 +23,7 @@ export class Processor extends LogReader { _icTimeline = new Timeline(); _mapTimeline = new Timeline(); _tickTimeline = new Timeline(); + _timerTimeline = new Timeline(); _formatPCRegexp = /(.*):[0-9]+:[0-9]+$/; _lastTimestamp = 0; _lastCodeLogEntry; @@ -93,8 +95,14 @@ export class Processor extends LogReader { 'active-runtime-timer': undefined, 'heap-sample-begin': undefined, 'heap-sample-end': undefined, - 'timer-event-start': undefined, - 'timer-event-end': undefined, + 'timer-event-start': { + parsers: [parseString, parseInt], + processor: this.processTimerEventStart + }, + 'timer-event-end': { + parsers: [parseString, parseInt], + processor: this.processTimerEventEnd + }, 'map-create': {parsers: [parseInt, parseString], processor: this.processMapCreate}, 'map': { @@ -479,6 +487,24 @@ export class Processor extends LogReader { new ApiLogEntry(type, this._lastTimestamp, name, arg1)); } + processTimerEventStart(type, time) { + const entry = new TimerLogEntry(type, time); + this._timerTimeline.push(entry); + } + + processTimerEventEnd(type, time) { + // Timer-events are infrequent, and not deeply nested, doing a linear walk + // is usually good enough. + for (let i = this._timerTimeline.length - 1; i >= 0; i--) { + const timer = this._timerTimeline.at(i); + if (timer.type == type && !timer.isInitialized) { + timer.end(time); + return; + } + } + console.error('Couldn\'t find matching timer event start', {type, time}); + } + get icTimeline() { return this._icTimeline; } @@ -503,6 +529,10 @@ export class Processor extends LogReader { return this._tickTimeline; } + get timerTimeline() { + return this._timerTimeline; + } + get scripts() { return this._profile.scripts_.filter(script => script !== undefined); } diff --git a/tools/system-analyzer/view/code-panel-template.html b/tools/system-analyzer/view/code-panel-template.html index ce2530a9bb..9f784da9f3 100644 --- a/tools/system-analyzer/view/code-panel-template.html +++ b/tools/system-analyzer/view/code-panel-template.html @@ -4,11 +4,24 @@ found in the LICENSE file. --> + +
diff --git a/tools/system-analyzer/view/code-panel.mjs b/tools/system-analyzer/view/code-panel.mjs index 7595029dd0..793b31cde7 100644 --- a/tools/system-analyzer/view/code-panel.mjs +++ b/tools/system-analyzer/view/code-panel.mjs @@ -4,6 +4,12 @@ import {SelectRelatedEvent} from './events.mjs'; import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs'; +const kRegisters = ['rsp', 'rbp', 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi']; +// Add Interpreter and x64 registers +for (let i = 0; i < 14; i++) { + kRegisters.push(`r${i}`); +} + DOM.defineCustomElement('view/code-panel', (templateText) => class CodePanel extends CollapsableElement { @@ -13,6 +19,11 @@ DOM.defineCustomElement('view/code-panel', constructor() { super(templateText); + this._codeSelectNode = this.$('#codeSelect'); + this._disassemblyNode = this.$('#disassembly'); + this._sourceNode = this.$('#sourceCode'); + this._registerSelector = new RegisterSelector(this._disassemblyNode); + this._codeSelectNode.onchange = this._handleSelectCode.bind(this); this.$('#selectedRelatedButton').onclick = this._handleSelectRelated.bind(this) @@ -41,7 +52,7 @@ DOM.defineCustomElement('view/code-panel', script: entry.script, type: entry.type, kind: entry.kindName, - variants: entry.variants, + variants: entry.variants.length > 1 ? entry.variants : undefined, }; } else { this.$('#properties').propertyDict = {}; @@ -49,24 +60,34 @@ DOM.defineCustomElement('view/code-panel', this.requestUpdate(); } - get _disassemblyNode() { - return this.$('#disassembly'); - } - - get _sourceNode() { - return this.$('#sourceCode'); - } - - get _codeSelectNode() { - return this.$('#codeSelect'); - } - _update() { this._updateSelect(); - this._disassemblyNode.innerText = this._entry?.code ?? ''; + this._formatDisassembly(); this._sourceNode.innerText = this._entry?.source ?? ''; } + _formatDisassembly() { + if (!this._entry?.code) { + this._disassemblyNode.innerText = ''; + return; + } + const rawCode = this._entry?.code; + try { + this._disassemblyNode.innerText = rawCode; + let formattedCode = this._disassemblyNode.innerHTML; + for (let register of kRegisters) { + const button = `${register}` + formattedCode = formattedCode.replaceAll(register, button); + } + // Let's replace the base-address since it doesn't add any value. + // TODO + this._disassemblyNode.innerHTML = formattedCode; + } catch (e) { + console.error(e); + this._disassemblyNode.innerText = rawCode; + } + } + _updateSelect() { const select = this._codeSelectNode; if (select.data === this._selectedEntries) return; @@ -76,13 +97,19 @@ DOM.defineCustomElement('view/code-panel', this._selectedEntries.slice().sort((a, b) => a.time - b.time); for (const code of this._selectedEntries) { const option = DOM.element('option'); - option.text = - `${code.functionName}(...) t=${formatMicroSeconds(code.time)} size=${ - formatBytes(code.size)} script=${code.script?.toString()}`; + option.text = this._entrySummary(code); option.data = code; select.add(option); } } + _entrySummary(code) { + if (code.isBuiltinKind) { + return `${code.functionName}(...) t=${ + formatMicroSeconds(code.time)} size=${formatBytes(code.size)}`; + } + return `${code.functionName}(...) t=${formatMicroSeconds(code.time)} size=${ + formatBytes(code.size)} script=${code.script?.toString()}`; + } _handleSelectCode() { this.entry = this._codeSelectNode.selectedOptions[0].data; @@ -92,4 +119,37 @@ DOM.defineCustomElement('view/code-panel', if (!this._entry) return; this.dispatchEvent(new SelectRelatedEvent(this._entry)); } -}); \ No newline at end of file +}); + +class RegisterSelector { + _currentRegister; + constructor(node) { + this._node = node; + this._node.onmousemove = this._handleDisassemblyMouseMove.bind(this); + } + + _handleDisassemblyMouseMove(event) { + const target = event.target; + if (!target.classList.contains('register')) { + this._clear(); + return; + }; + this._select(target.innerText); + } + + _clear() { + if (this._currentRegister == undefined) return; + for (let node of this._node.querySelectorAll('.register')) { + node.classList.remove('selected'); + } + } + + _select(register) { + if (register == this._currentRegister) return; + this._clear(); + this._currentRegister = register; + for (let node of this._node.querySelectorAll(`.register.${register}`)) { + node.classList.add('selected'); + } + } +} \ No newline at end of file diff --git a/tools/system-analyzer/view/property-link-table-template.html b/tools/system-analyzer/view/property-link-table-template.html index 9b5dced367..85f2cdc178 100644 --- a/tools/system-analyzer/view/property-link-table-template.html +++ b/tools/system-analyzer/view/property-link-table-template.html @@ -11,11 +11,27 @@ found in the LICENSE file. --> } .properties > tbody > tr > td:nth-child(2n+1):after { content: ':'; + } +.properties > tbody > tr > td:nth-child(2n+1) { + padding-right: 3px; } + +.properties > tbody > tr > td:nth-child(2n+2) { + width: 100%; +} + +.properties > tfoot { + text-align: right; +} + .properties { min-width: 350px; border-collapse: collapse; } + +h3 { + margin-block-start: 0em; +}
diff --git a/tools/system-analyzer/view/property-link-table.mjs b/tools/system-analyzer/view/property-link-table.mjs index 6aa9923d95..d2e2dce60b 100644 --- a/tools/system-analyzer/view/property-link-table.mjs +++ b/tools/system-analyzer/view/property-link-table.mjs @@ -14,6 +14,7 @@ DOM.defineCustomElement( _instanceLinkButtons = false; _logEntryClickHandler = this._handleLogEntryClick.bind(this); _logEntryRelatedHandler = this._handleLogEntryRelated.bind(this); + _arrayValueSelectHandler = this._handleArrayValueSelect.bind(this); constructor() { super(template); @@ -78,6 +79,7 @@ DOM.defineCustomElement( return DOM.text(`${array.length} items`); } const select = DOM.element('select'); + select.onchange = this._arrayValueSelectHandler; for (let value of array) { const option = DOM.element('option'); option.innerText = value.toString(); @@ -106,11 +108,15 @@ DOM.defineCustomElement( showRelatedButton.data = this._instance; } - _handleLogEntryClick(e) { - this.dispatchEvent(new FocusEvent(e.currentTarget.data)); + _handleArrayValueSelect(event) { + const logEntry = event.currentTarget.selectedOptions[0].data; + this.dispatchEvent(new FocusEvent(logEntry)); + } + _handleLogEntryClick(event) { + this.dispatchEvent(new FocusEvent(event.currentTarget.data)); } - _handleLogEntryRelated(e) { - this.dispatchEvent(new SelectRelatedEvent(e.currentTarget.data)); + _handleLogEntryRelated(event) { + this.dispatchEvent(new SelectRelatedEvent(event.currentTarget.data)); } }); diff --git a/tools/system-analyzer/view/timeline-panel.mjs b/tools/system-analyzer/view/timeline-panel.mjs index 38badb4742..35d8f02893 100644 --- a/tools/system-analyzer/view/timeline-panel.mjs +++ b/tools/system-analyzer/view/timeline-panel.mjs @@ -5,6 +5,7 @@ import './timeline/timeline-track.mjs'; import './timeline/timeline-track-map.mjs'; import './timeline/timeline-track-tick.mjs'; +import './timeline/timeline-track-timer.mjs'; import {SynchronizeSelectionEvent} from './events.mjs'; import {DOM, V8CustomElement} from './helper.mjs'; diff --git a/tools/system-analyzer/view/timeline/timeline-track-base.mjs b/tools/system-analyzer/view/timeline/timeline-track-base.mjs index aa65f798ef..8760b0223e 100644 --- a/tools/system-analyzer/view/timeline/timeline-track-base.mjs +++ b/tools/system-analyzer/view/timeline/timeline-track-base.mjs @@ -326,7 +326,7 @@ export class TimelineTrackBase extends V8CustomElement { _handleMouseMove(event) { if (event.button !== 0) return; if (this._selectionHandler.isSelecting) return false; - if (this.isLocked) { + if (this.isLocked && this._focusedEntry) { this._updateToolTip(event); return false; } diff --git a/tools/system-analyzer/view/timeline/timeline-track-stacked-base.mjs b/tools/system-analyzer/view/timeline/timeline-track-stacked-base.mjs new file mode 100644 index 0000000000..24b389b959 --- /dev/null +++ b/tools/system-analyzer/view/timeline/timeline-track-stacked-base.mjs @@ -0,0 +1,144 @@ +// Copyright 2021 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 {delay} from '../../helper.mjs'; +import {Timeline} from '../../timeline.mjs'; +import {SelectTimeEvent} from '../events.mjs'; +import {CSSColor, DOM, SVG} from '../helper.mjs'; + +import {TimelineTrackBase} from './timeline-track-base.mjs' + +const kItemHeight = 8; + +export class TimelineTrackStackedBase extends TimelineTrackBase { + _originalContentWidth = 0; + _drawableItems = new Timeline(); + + _updateChunks() { + // We don't need to update the chunks here. + this._updateDimensions(); + this.requestUpdate(); + } + + set data(timeline) { + super.data = timeline; + this._contentWidth = 0; + this._prepareDrawableItems(); + } + + _handleDoubleClick(event) { + if (event.button !== 0) return; + this._selectionHandler.clearSelection(); + const item = this._getDrawableItemForEvent(event); + if (item === undefined) return; + event.stopImmediatePropagation(); + this.dispatchEvent(new SelectTimeEvent(item.startTime, item.endTime)); + return false; + } + + _getStackDepthForEvent(event) { + return Math.floor(event.layerY / kItemHeight) - 1; + } + + _getDrawableItemForEvent(event) { + const depth = this._getStackDepthForEvent(event); + const time = this.positionToTime(event.pageX); + const index = this._drawableItems.find(time); + for (let i = index - 1; i > 0; i--) { + const item = this._drawableItems.at(i); + if (item.depth != depth) continue; + if (item.endTime < time) continue; + return item; + } + return undefined; + } + + _drawableItemToLogEntry(item) { + return item; + } + + _getEntryForEvent(event) { + const item = this._getDrawableItemForEvent(event); + const logEntry = this._drawableItemToLogEntry(item); + if (item === undefined) return undefined; + const style = this.toolTipTargetNode.style; + style.left = `${event.layerX}px`; + style.top = `${(item.depth + 1) * kItemHeight}px`; + style.height = `${kItemHeight}px` + return logEntry; + } + + _prepareDrawableItems() { + // Subclass responsibility. + } + + _adjustStackDepth(maxDepth) { + // Account for empty top line + maxDepth++; + this._adjustHeight(maxDepth * kItemHeight); + } + + _scaleContent(currentWidth) { + if (this._originalContentWidth == 0) return; + // Instead of repainting just scale the content. + const ratio = currentWidth / this._originalContentWidth; + this._scalableContentNode.style.transform = `scale(${ratio}, 1)`; + this.style.setProperty('--txt-scale', `scale(${1 / ratio}, 1)`); + } + + async _drawContent() { + if (this._originalContentWidth > 0) return; + this._originalContentWidth = parseInt(this.timelineMarkersNode.style.width); + this._scalableContentNode.innerHTML = ''; + let buffer = ''; + const add = async () => { + const svg = SVG.svg(); + svg.innerHTML = buffer; + this._scalableContentNode.appendChild(svg); + buffer = ''; + await delay(50); + }; + const items = this._drawableItems.values; + for (let i = 0; i < items.length; i++) { + if ((i % 3000) == 0) await add(); + buffer += this._drawItem(items[i], i); + } + add(); + } + + _drawItem(item, i, outline = false) { + const x = this.timeToPosition(item.time); + const y = (item.depth + 1) * kItemHeight; + let width = item.duration * this._timeToPixel; + if (outline) { + return ``; + } + let color = this._legend.colorForType(item.type); + if (i % 2 == 1) { + color = CSSColor.darken(color, 20); + } + return ``; + } + + _drawItemText(item) { + const type = item.type; + const kHeight = 9; + const x = this.timeToPosition(item.time); + const y = item.depth * (kHeight + 1); + let width = item.duration * this._timeToPixel; + width -= width * 0.1; + + let buffer = ''; + if (width < 15 || type == 'Other') return buffer; + const rawName = item.entry.getName(); + if (rawName.length == 0) return buffer; + const kChartWidth = 5; + const maxChars = Math.floor(width / kChartWidth) + const text = rawName.substr(0, maxChars); + buffer += `${text}` + return buffer; + } +} \ No newline at end of file diff --git a/tools/system-analyzer/view/timeline/timeline-track-tick.mjs b/tools/system-analyzer/view/timeline/timeline-track-tick.mjs index 7766be0942..502504beb4 100644 --- a/tools/system-analyzer/view/timeline/timeline-track-tick.mjs +++ b/tools/system-analyzer/view/timeline/timeline-track-tick.mjs @@ -5,28 +5,23 @@ import {delay} from '../../helper.mjs'; import {TickLogEntry} from '../../log/tick.mjs'; import {Timeline} from '../../timeline.mjs'; -import {SelectTimeEvent} from '../events.mjs'; -import {CSSColor, DOM, SVG} from '../helper.mjs'; +import {DOM, SVG} from '../helper.mjs'; -import {TimelineTrackBase} from './timeline-track-base.mjs' - -const kFlameHeight = 8; +import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs' class Flame { - constructor(time, entry, depth, id) { - this.time = time; - this.entry = entry; + constructor(time, logEntry, depth) { + this._time = time; + this._logEntry = logEntry; this.depth = depth; - this.id = id; - this.duration = -1; + this._duration = -1; this.parent = undefined; this.children = []; } - static add(time, entry, stack, flames) { + static add(time, logEntry, stack, flames) { const depth = stack.length; - const id = flames.length; - const newFlame = new Flame(time, entry, depth, id) + const newFlame = new Flame(time, logEntry, depth) if (depth > 0) { const parent = stack[depth - 1]; newFlame.parent = parent; @@ -37,204 +32,100 @@ class Flame { } stop(time) { - this.duration = time - this.time + if (this._duration !== -1) throw new Error('Already stopped'); + this._duration = time - this._time } - get start() { - return this.time; + get time() { + return this._time; } - get end() { - return this.time + this.duration; + get logEntry() { + return this._logEntry; + } + + get startTime() { + return this._time; + } + + get endTime() { + return this._time + this._duration; + } + + get duration() { + return this._duration; } get type() { - return TickLogEntry.extractCodeEntryType(this.entry); + return TickLogEntry.extractCodeEntryType(this._logEntry?.entry); } } -DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-tick', - (templateText) => - class TimelineTrackTick extends TimelineTrackBase { - _flames = new Timeline(); - _originalContentWidth = 0; +DOM.defineCustomElement( + 'view/timeline/timeline-track', 'timeline-track-tick', + (templateText) => class TimelineTrackTick extends TimelineTrackStackedBase { + constructor() { + super(templateText); + this._annotations = new Annotations(this); + } - constructor() { - super(templateText); - this._annotations = new Annotations(this); - } + _prepareDrawableItems() { + const tmpFlames = []; + // flameStack = [bottom, ..., top]; + const flameStack = []; + const ticks = this._timeline.values; + let maxDepth = 0; - _updateChunks() { - // We don't need to update the chunks here. - this._updateDimensions(); - this.requestUpdate(); - } - - set data(timeline) { - super.data = timeline; - this._contentWidth = 0; - this._updateFlames(); - } - - _handleDoubleClick(event) { - if (event.button !== 0) return; - this._selectionHandler.clearSelection(); - const flame = this._getFlameForEvent(event); - if (flame === undefined) return; - event.stopImmediatePropagation(); - this.dispatchEvent(new SelectTimeEvent(flame.start, flame.end)); - return false; - } - - _getFlameDepthForEvent(event) { - return Math.floor(event.layerY / kFlameHeight) - 1; - } - - _getFlameForEvent(event) { - const depth = this._getFlameDepthForEvent(event); - const time = this.positionToTime(event.pageX); - const index = this._flames.find(time); - for (let i = index - 1; i > 0; i--) { - const flame = this._flames.at(i); - if (flame.depth != depth) continue; - if (flame.end < time) continue; - return flame; - } - return undefined; - } - - _getEntryForEvent(event) { - const depth = this._getFlameDepthForEvent(event); - const time = this.positionToTime(event.pageX); - const index = this._timeline.find(time); - const tick = this._timeline.at(index); - let stack = tick.stack; - if (index > 0 && tick.time > time) { - stack = this._timeline.at(index - 1).stack; - } - // tick.stack = [top, ...., bottom]; - const logEntry = stack[stack.length - depth - 1]?.logEntry ?? false; - // Filter out raw pc entries. - if (typeof logEntry == 'number' || logEntry === false) return false; - this.toolTipTargetNode.style.left = `${event.layerX}px`; - this.toolTipTargetNode.style.top = `${(depth + 2) * kFlameHeight}px`; - return logEntry; - } - - _updateFlames() { - const tmpFlames = []; - // flameStack = [bottom, ..., top]; - const flameStack = []; - const ticks = this._timeline.values; - let maxDepth = 0; - - for (let tickIndex = 0; tickIndex < ticks.length; tickIndex++) { - const tick = ticks[tickIndex]; - maxDepth = Math.max(maxDepth, tick.stack.length); - // tick.stack = [top, .... , bottom]; - for (let stackIndex = tick.stack.length - 1; stackIndex >= 0; - stackIndex--) { - const entry = tick.stack[stackIndex]; - const flameStackIndex = tick.stack.length - stackIndex - 1; - if (flameStackIndex < flameStack.length) { - if (flameStack[flameStackIndex].entry === entry) continue; - for (let k = flameStackIndex; k < flameStack.length; k++) { - flameStack[k].stop(tick.time); + for (let tickIndex = 0; tickIndex < ticks.length; tickIndex++) { + const tick = ticks[tickIndex]; + const tickStack = tick.stack; + maxDepth = Math.max(maxDepth, tickStack.length); + // tick.stack = [top, .... , bottom]; + for (let stackIndex = tickStack.length - 1; stackIndex >= 0; + stackIndex--) { + const codeEntry = tickStack[stackIndex]; + // codeEntry is either a CodeEntry or a raw pc. + const logEntry = codeEntry?.logEntry; + const flameStackIndex = tickStack.length - stackIndex - 1; + if (flameStackIndex < flameStack.length) { + if (flameStack[flameStackIndex].logEntry === logEntry) continue; + for (let k = flameStackIndex; k < flameStack.length; k++) { + flameStack[k].stop(tick.time); + } + flameStack.length = flameStackIndex; + } + Flame.add(tick.time, logEntry, flameStack, tmpFlames); + } + if (tickStack.length < flameStack.length) { + for (let k = tickStack.length; k < flameStack.length; k++) { + flameStack[k].stop(tick.time); + } + flameStack.length = tickStack.length; } - flameStack.length = flameStackIndex; } - Flame.add(tick.time, entry, flameStack, tmpFlames); - } - if (tick.stack.length < flameStack.length) { - for (let k = tick.stack.length; k < flameStack.length; k++) { - flameStack[k].stop(tick.time); + const lastTime = ticks[ticks.length - 1].time; + for (let k = 0; k < flameStack.length; k++) { + flameStack[k].stop(lastTime); } - flameStack.length = tick.stack.length; + this._drawableItems = new Timeline(Flame, tmpFlames); + this._annotations.flames = this._drawableItems; + this._adjustStackDepth(maxDepth); } - } - const lastTime = ticks[ticks.length - 1].time; - for (let k = 0; k < flameStack.length; k++) { - flameStack[k].stop(lastTime); - } - this._flames = new Timeline(Flame, tmpFlames); - this._annotations.flames = this._flames; - // Account for empty top line - maxDepth++; - this._adjustHeight(maxDepth * kFlameHeight); - } - _scaleContent(currentWidth) { - if (this._originalContentWidth == 0) return; - // Instead of repainting just scale the flames - const ratio = currentWidth / this._originalContentWidth; - this._scalableContentNode.style.transform = `scale(${ratio}, 1)`; - this.style.setProperty('--txt-scale', `scale(${1 / ratio}, 1)`); - } - - async _drawContent() { - if (this._originalContentWidth > 0) return; - this._originalContentWidth = parseInt(this.timelineMarkersNode.style.width); - this._scalableContentNode.innerHTML = ''; - let buffer = ''; - const add = () => { - const svg = SVG.svg(); - svg.innerHTML = buffer; - this._scalableContentNode.appendChild(svg); - buffer = ''; - }; - const rawFlames = this._flames.values; - for (let i = 0; i < rawFlames.length; i++) { - if ((i % 3000) == 0) { - add(); - await delay(50); + _drawAnnotations(logEntry, time) { + if (time === undefined) { + time = this.relativePositionToTime(this._timelineScrollLeft); + } + this._annotations.update(logEntry, time); } - buffer += this.drawFlame(rawFlames[i], i); - } - add(); - } - drawFlame(flame, i, outline = false) { - const x = this.timeToPosition(flame.time); - const y = (flame.depth + 1) * kFlameHeight; - let width = flame.duration * this._timeToPixel; - if (outline) { - return ``; - } - let color = this._legend.colorForType(flame.type); - if (i % 2 == 1) { - color = CSSColor.darken(color, 20); - } - return ``; - } - - drawFlameText(flame) { - let type = flame.type; - const kHeight = 9; - const x = this.timeToPosition(flame.time); - const y = flame.depth * (kHeight + 1); - let width = flame.duration * this._timeToPixel; - width -= width * 0.1; - - let buffer = ''; - if (width < 15 || type == 'Other') return buffer; - const rawName = flame.entry.getName(); - if (rawName.length == 0) return buffer; - const kChartWidth = 5; - const maxChars = Math.floor(width / kChartWidth) - const text = rawName.substr(0, maxChars); - buffer += `${text}` - return buffer; - } - - _drawAnnotations(logEntry, time) { - if (time === undefined) { - time = this.relativePositionToTime(this._timelineScrollLeft); - } - this._annotations.update(logEntry, time); - } -}) + _drawableItemToLogEntry(flame) { + const logEntry = flame?.logEntry; + if (logEntry === undefined || typeof logEntry == 'number') + return undefined; + return logEntry; + } + }) class Annotations { _flames; diff --git a/tools/system-analyzer/view/timeline/timeline-track-timer.mjs b/tools/system-analyzer/view/timeline/timeline-track-timer.mjs new file mode 100644 index 0000000000..62ee07aff7 --- /dev/null +++ b/tools/system-analyzer/view/timeline/timeline-track-timer.mjs @@ -0,0 +1,41 @@ +// Copyright 2021 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 {CSSColor, DOM, SVG, V8CustomElement} from '../helper.mjs'; + +import {TimelineTrackBase} from './timeline-track-base.mjs' +import {TimelineTrackStackedBase} from './timeline-track-stacked-base.mjs' + +DOM.defineCustomElement( + 'view/timeline/timeline-track', 'timeline-track-timer', + (templateText) => + class TimelineTrackTimer extends TimelineTrackStackedBase { + constructor() { + super(templateText); + } + + _prepareDrawableItems() { + const stack = []; + let maxDepth = 0; + for (let i = 0; i < this._timeline.length; i++) { + const timer = this._timeline.at(i); + let insertDepth = -1; + for (let depth = 0; depth < stack.length; depth++) { + const pendingTimer = stack[depth]; + if (pendingTimer === undefined) { + if (insertDepth === -1) insertDepth = depth; + } else if (pendingTimer.endTime <= timer.startTime) { + stack[depth] == undefined; + if (insertDepth === -1) insertDepth = depth; + } + } + if (insertDepth === -1) insertDepth = stack.length; + stack[insertDepth] = timer; + timer.depth = insertDepth; + maxDepth = Math.max(maxDepth, insertDepth); + } + this._drawableItems = this._timeline; + this._adjustStackDepth(maxDepth++); + } + }); \ No newline at end of file