From 8b1cfdf6820de204d4ea09e49e2c86297854f34a Mon Sep 17 00:00:00 2001 From: Camillo Bruni Date: Tue, 8 Nov 2022 12:23:52 +0100 Subject: [PATCH] [tools] Improve SystemAnalyzer tooltip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Debounce creating tooltips to declutter the UI - CTRL-mouse move causes immediate tooltips - Use icons and help text on tooltip buttons - Recreate tooltip target nodes in timeline views to avoid moving the existing tooltip if the update is debounced Change-Id: I65a885827ebfeafc09c1c08e2cfe9c2dd448edca Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4012720 Commit-Queue: Camillo Bruni Reviewed-by: Marja Hรถlttรค Cr-Commit-Position: refs/heads/main@{#84120} --- tools/system-analyzer/index.html | 5 +- tools/system-analyzer/index.mjs | 11 +- tools/system-analyzer/view/events.mjs | 7 +- tools/system-analyzer/view/helper.mjs | 22 ++ tools/system-analyzer/view/list-panel.mjs | 2 +- .../view/map-panel/map-transitions.mjs | 4 +- tools/system-analyzer/view/profiler-panel.mjs | 2 +- .../view/property-link-table.mjs | 10 +- tools/system-analyzer/view/script-panel.mjs | 6 +- .../view/timeline/timeline-overview.mjs | 3 +- .../view/timeline/timeline-track-base.mjs | 22 +- .../timeline/timeline-track-stacked-base.mjs | 4 +- .../timeline/timeline-track-template.html | 3 +- tools/system-analyzer/view/tool-tip.mjs | 267 +++++++++--------- 14 files changed, 213 insertions(+), 155 deletions(-) diff --git a/tools/system-analyzer/index.html b/tools/system-analyzer/index.html index 9a8fd60b28..8518014666 100644 --- a/tools/system-analyzer/index.html +++ b/tools/system-analyzer/index.html @@ -181,11 +181,14 @@ found in the LICENSE file. -->

Keyboard Shortcuts for Navigation

+
CTRL + Mouse Move
+
Show tooltips immediately
+
A
Scroll left
D
-
Sroll right
+
Scroll right
SHIFT + Arrow Up
Follow Map transition forward (first child)
diff --git a/tools/system-analyzer/index.mjs b/tools/system-analyzer/index.mjs index 882c3250bd..5cbb5b3732 100644 --- a/tools/system-analyzer/index.mjs +++ b/tools/system-analyzer/index.mjs @@ -328,12 +328,11 @@ class App { throw new Error( `Unknown tooltip content type: ${content.constructor?.name}`); } - this.setToolTip(content, event.positionOrTargetNode); - } - - setToolTip(content, positionOrTargetNode) { - this._view.toolTip.positionOrTargetNode = positionOrTargetNode; - this._view.toolTip.content = content; + this._view.toolTip.data = { + content: content, + positionOrTargetNode: event.positionOrTargetNode, + immediate: event.immediate, + }; } restartApp() { diff --git a/tools/system-analyzer/view/events.mjs b/tools/system-analyzer/view/events.mjs index 024ed27f3c..02406b2ff6 100644 --- a/tools/system-analyzer/view/events.mjs +++ b/tools/system-analyzer/view/events.mjs @@ -76,13 +76,14 @@ export class ToolTipEvent extends AppEvent { return 'showtooltip'; } - constructor(content, positionOrTargetNode) { + constructor(content, positionOrTargetNode, immediate) { super(ToolTipEvent.name); if (!positionOrTargetNode) { throw Error('Either provide a valid position or targetNode'); } this._content = content; this._positionOrTargetNode = positionOrTargetNode; + this._immediate = immediate; } get content() { @@ -92,4 +93,8 @@ export class ToolTipEvent extends AppEvent { get positionOrTargetNode() { return this._positionOrTargetNode; } + + get immediate() { + return this._immediate; + } } diff --git a/tools/system-analyzer/view/helper.mjs b/tools/system-analyzer/view/helper.mjs index c28a9bfb4d..4389a92317 100644 --- a/tools/system-analyzer/view/helper.mjs +++ b/tools/system-analyzer/view/helper.mjs @@ -322,5 +322,27 @@ export function gradientStopsFromGroups( return stops; } +export class Debouncer { + constructor(callback, timeout = 250) { + this._callback = callback; + this._timeout = timeout; + this._timeoutId = 0; + } + + callNow(...args) { + this.clear(); + return this._callback(...args); + } + + call(...args) { + this.clear() this._timeoutId = + window.setTimeout(this._callback, this._timeout, ...args) + } + + clear() { + clearTimeout(this._timeoutId); + } +} + export * from '../helper.mjs'; export * from '../../js/web-api-helper.mjs' diff --git a/tools/system-analyzer/view/list-panel.mjs b/tools/system-analyzer/view/list-panel.mjs index efe8cb9d2b..c6d5ae5e39 100644 --- a/tools/system-analyzer/view/list-panel.mjs +++ b/tools/system-analyzer/view/list-panel.mjs @@ -127,7 +127,7 @@ DOM.defineCustomElement('view/list-panel', _logEntryMouseOverHandler(e) { const group = e.currentTarget.group; - this.dispatchEvent(new ToolTipEvent(group.key, e.currentTarget)); + this.dispatchEvent(new ToolTipEvent(group.key, e.currentTarget, e.ctrlKey)); } _handleDetailsClick(event) { diff --git a/tools/system-analyzer/view/map-panel/map-transitions.mjs b/tools/system-analyzer/view/map-panel/map-transitions.mjs index 1a15f548c5..68e3ec3c93 100644 --- a/tools/system-analyzer/view/map-panel/map-transitions.mjs +++ b/tools/system-analyzer/view/map-panel/map-transitions.mjs @@ -147,8 +147,8 @@ DOM.defineCustomElement('./view/map-panel/map-transitions', } _handleMouseoverMap(event) { - this.dispatchEvent( - new ToolTipEvent(event.currentTarget.map, event.currentTarget)); + this.dispatchEvent(new ToolTipEvent( + event.currentTarget.map, event.currentTarget, event.ctrlKey)); } _handleToggleSubtree(event) { diff --git a/tools/system-analyzer/view/profiler-panel.mjs b/tools/system-analyzer/view/profiler-panel.mjs index b262a7029e..868a488c85 100644 --- a/tools/system-analyzer/view/profiler-panel.mjs +++ b/tools/system-analyzer/view/profiler-panel.mjs @@ -295,7 +295,7 @@ DOM.defineCustomElement('view/profiler-panel', const profileNode = e.target.data; if (!profileNode) return; const logEntry = profileNode.codeEntry.logEntry; - this.dispatchEvent(new ToolTipEvent(logEntry, e.target)); + this.dispatchEvent(new ToolTipEvent(logEntry, e.target, e.ctrlKey)); } _handleFlameChartClick(e) { diff --git a/tools/system-analyzer/view/property-link-table.mjs b/tools/system-analyzer/view/property-link-table.mjs index 95c496d261..c5cc707c37 100644 --- a/tools/system-analyzer/view/property-link-table.mjs +++ b/tools/system-analyzer/view/property-link-table.mjs @@ -111,16 +111,20 @@ DOM.defineCustomElement('view/property-link-table', if (this._object === undefined) return; if (!this._instanceLinkButtons) return; const footer = DOM.div('footer'); - let showButton = footer.appendChild(DOM.button('Show', this._showHandler)); + let showButton = + footer.appendChild(DOM.button('๐Ÿ” Details', this._showHandler)); showButton.data = this._object; + showButton.title = `Show details for ${this._object}` if (this._object.sourcePosition) { let showSourcePositionButton = footer.appendChild( - DOM.button('Source Position', this._showSourcePositionHandler)); + DOM.button('๐Ÿ“ Source Position', this._showSourcePositionHandler)); showSourcePositionButton.data = this._object; + showSourcePositionButton.title = 'Open the source position'; } let showRelatedButton = footer.appendChild( - DOM.button('Show Related', this._showRelatedHandler)); + DOM.button('๐Ÿ•ธ Related', this._showRelatedHandler)); showRelatedButton.data = this._object; + showRelatedButton.title = 'Show all related events in all panels'; this._fragment.appendChild(footer); } diff --git a/tools/system-analyzer/view/script-panel.mjs b/tools/system-analyzer/view/script-panel.mjs index a9de42205a..11f6e6d7b2 100644 --- a/tools/system-analyzer/view/script-panel.mjs +++ b/tools/system-analyzer/view/script-panel.mjs @@ -198,7 +198,7 @@ DOM.defineCustomElement('view/script-panel', break; } toolTipContent.appendChild(sourceMapContent); - this.dispatchEvent(new ToolTipEvent(toolTipContent, e.target)); + this.dispatchEvent(new ToolTipEvent(toolTipContent, e.target, e.ctrlKey)); } handleShowToolTipEntries(event) { @@ -233,8 +233,8 @@ class ToolTipTableBuilder { tr.appendChild(DOM.td(name)); tr.appendChild(DOM.td(subtypeName)); tr.appendChild(DOM.td(entries.length)); - const button = - DOM.button('Show', this._scriptPanel.showToolTipEntriesHandler); + const button = DOM.button('๐Ÿ”Ž', this._scriptPanel.showToolTipEntriesHandler); + button.title = `Show all ${entries.length} ${name || subtypeName} entries.` button.data = entries; tr.appendChild(DOM.td(button)); this.tableNode.appendChild(tr); diff --git a/tools/system-analyzer/view/timeline/timeline-overview.mjs b/tools/system-analyzer/view/timeline/timeline-overview.mjs index 6086192fa9..8f40bccb3e 100644 --- a/tools/system-analyzer/view/timeline/timeline-overview.mjs +++ b/tools/system-analyzer/view/timeline/timeline-overview.mjs @@ -110,7 +110,8 @@ DOM.defineCustomElement('view/timeline/timeline-overview', if (!toolTipContent) { toolTipContent = `Time ${formatDurationMicros(timeMicros)}`; } - this.dispatchEvent(new ToolTipEvent(toolTipContent, this._indicatorNode)); + this.dispatchEvent( + new ToolTipEvent(toolTipContent, this._indicatorNode, e.ctrlKey)); } _findLogEntryAtTime(time, maxTimeDistance) { diff --git a/tools/system-analyzer/view/timeline/timeline-track-base.mjs b/tools/system-analyzer/view/timeline/timeline-track-base.mjs index 17bbcf21e7..2774167dd0 100644 --- a/tools/system-analyzer/view/timeline/timeline-track-base.mjs +++ b/tools/system-analyzer/view/timeline/timeline-track-base.mjs @@ -30,7 +30,7 @@ export class TimelineTrackBase extends V8CustomElement { this.timelineChunks = this.$('#timelineChunks'); this.timelineSamples = this.$('#timelineSamples'); this.timelineNode = this.$('#timeline'); - this.toolTipTargetNode = this.$('#toolTipTarget'); + this._toolTipTargetNode = undefined; this.hitPanelNode = this.$('#hitPanel'); this.timelineAnnotationsNode = this.$('#timelineAnnotations'); this.timelineMarkersNode = this.$('#timelineMarkers'); @@ -356,8 +356,8 @@ export class TimelineTrackBase extends V8CustomElement { _updateToolTip(event) { if (!this._focusedEntry) return false; - this.dispatchEvent( - new ToolTipEvent(this._focusedEntry, this.toolTipTargetNode)); + this.dispatchEvent(new ToolTipEvent( + this._focusedEntry, this._toolTipTargetNode, event.ctrlKey)); event.stopImmediatePropagation(); } @@ -419,13 +419,27 @@ export class TimelineTrackBase extends V8CustomElement { (kTimelineHeight - event.layerY) / chunk.height * (chunk.size() - 1)); if (relativeIndex > chunk.size()) return false; const logEntry = chunk.at(relativeIndex); - const style = this.toolTipTargetNode.style; + const node = this.getToolTipTargetNode(logEntry); + if (!node) return logEntry; + const style = node.style; style.left = `${chunk.index * kChunkWidth}px`; style.top = `${kTimelineHeight - chunk.height}px`; style.height = `${chunk.height}px`; style.width = `${kChunkVisualWidth}px`; return logEntry; } + + getToolTipTargetNode(logEntry) { + let node = this._toolTipTargetNode; + if (node) { + if (node.logEntry === logEntry) return undefined; + node.parentNode.removeChild(node); + } + node = this._toolTipTargetNode = DOM.div('toolTipTarget'); + node.logEntry = logEntry; + this.$('#cropper').appendChild(node); + return node; + } }; class SelectionHandler { diff --git a/tools/system-analyzer/view/timeline/timeline-track-stacked-base.mjs b/tools/system-analyzer/view/timeline/timeline-track-stacked-base.mjs index 05fac28865..0d35147982 100644 --- a/tools/system-analyzer/view/timeline/timeline-track-stacked-base.mjs +++ b/tools/system-analyzer/view/timeline/timeline-track-stacked-base.mjs @@ -61,7 +61,9 @@ export class TimelineTrackStackedBase extends TimelineTrackBase { const item = this._getDrawableItemForEvent(event); const logEntry = this._drawableItemToLogEntry(item); if (item === undefined) return undefined; - const style = this.toolTipTargetNode.style; + const node = this.getToolTipTargetNode(logEntry); + if (!node) return logEntry; + const style = node.style; style.left = `${event.layerX}px`; style.top = `${(item.depth + 1) * kItemHeight}px`; style.height = `${kItemHeight}px` diff --git a/tools/system-analyzer/view/timeline/timeline-track-template.html b/tools/system-analyzer/view/timeline/timeline-track-template.html index d56f9d3ee1..3053e116ea 100644 --- a/tools/system-analyzer/view/timeline/timeline-track-template.html +++ b/tools/system-analyzer/view/timeline/timeline-track-template.html @@ -40,7 +40,7 @@ found in the LICENSE file. --> pointer-events: none; } - #toolTipTarget { + .toolTipTarget { position: absolute; } @@ -237,7 +237,6 @@ found in the LICENSE file. --> -
diff --git a/tools/system-analyzer/view/tool-tip.mjs b/tools/system-analyzer/view/tool-tip.mjs index 08714047bc..41a7c1e164 100644 --- a/tools/system-analyzer/view/tool-tip.mjs +++ b/tools/system-analyzer/view/tool-tip.mjs @@ -2,137 +2,146 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import {DOM, V8CustomElement} from './helper.mjs'; +import {Debouncer, DOM, V8CustomElement} from './helper.mjs'; -DOM.defineCustomElement( - 'view/tool-tip', (templateText) => class Tooltip extends V8CustomElement { - _targetNode; - _content; - _isHidden = true; +DOM.defineCustomElement('view/tool-tip', + (templateText) => + class Tooltip extends V8CustomElement { + _targetNode; + _content; + _isHidden = true; + _debouncedSetData = new Debouncer((...args) => this._setData(...args), 500) - constructor() { - super(templateText); - this._intersectionObserver = new IntersectionObserver((entries) => { - if (entries[0].intersectionRatio <= 0) { - this.hide(); - } else { - this.show(); - this.requestUpdate(true); - } - }); - document.addEventListener('click', (event) => { - // Only hide the tooltip if we click anywhere outside of it. - let target = event.target; - while (target) { - if (target == this) return; - target = target.parentNode; - } - this.hide() - }); - } - - _update() { - if (!this._targetNode || this._isHidden) return; - const rect = this._targetNode.getBoundingClientRect(); - rect.x += rect.width / 2; - let atRight = this._useRight(rect.x); - let atBottom = this._useBottom(rect.y); - if (atBottom) rect.y += rect.height; - this._setPosition(rect, atRight, atBottom); - this.requestUpdate(true); - } - - set positionOrTargetNode(positionOrTargetNode) { - if (positionOrTargetNode.nodeType === undefined) { - this.position = positionOrTargetNode; - } else { - this.targetNode = positionOrTargetNode; - } - } - - set targetNode(targetNode) { - this._intersectionObserver.disconnect(); - this._targetNode = targetNode; - if (targetNode === undefined) return; - if (!(targetNode instanceof SVGElement)) { - this._intersectionObserver.observe(targetNode); - } - this.requestUpdate(true); - } - - set position(position) { - this._targetNode = undefined; - this._setPosition( - position, this._useRight(position.x), this._useBottom(position.y)); - } - - _setPosition(viewportPosition, atRight, atBottom) { - const horizontalMode = atRight ? 'right' : 'left'; - const verticalMode = atBottom ? 'bottom' : 'top'; - this.bodyNode.className = horizontalMode + ' ' + verticalMode; - const pageX = viewportPosition.x + window.scrollX; - this.style.left = `${pageX}px`; - const pageY = viewportPosition.y + window.scrollY; - this.style.top = `${pageY}px`; - } - - _useBottom(viewportY) { - return viewportY <= 400; - } - - _useRight(viewportX) { - return viewportX < document.documentElement.clientWidth / 2; - } - - set content(content) { - if (!content) return this.hide(); + constructor() { + super(templateText); + this._intersectionObserver = new IntersectionObserver((entries) => { + if (entries[0].intersectionRatio <= 0) { + this.hide(); + } else { this.show(); - if (this._content === content) return; - this._content = content; - - if (typeof content === 'string') { - this.contentNode.innerHTML = content; - this.contentNode.className = 'textContent'; - } else if (content?.nodeType && content?.nodeName) { - this._setContentNode(content); - } else { - if (this.contentNode.firstChild?.localName == 'property-link-table') { - this.contentNode.firstChild.propertyDict = content; - } else { - const node = DOM.element('property-link-table'); - node.instanceLinkButtons = true; - node.propertyDict = content; - this._setContentNode(node); - } - } - } - - _setContentNode(content) { - const newContent = DOM.div(); - newContent.appendChild(content); - this.contentNode.replaceWith(newContent); - newContent.id = 'content'; - } - - hide() { - this._content = undefined; - if (this._isHidden) return; - this._isHidden = true; - this.bodyNode.style.display = 'none'; - this.targetNode = undefined; - } - - show() { - if (!this._isHidden) return; - this.bodyNode.style.display = 'block'; - this._isHidden = false; - } - - get bodyNode() { - return this.$('#body'); - } - - get contentNode() { - return this.$('#content'); + this.requestUpdate(true); } }); + document.addEventListener('click', (event) => { + // Only hide the tooltip if we click anywhere outside of it. + let target = event.target; + while (target) { + if (target == this) return; + target = target.parentNode; + } + this.hide() + }); + } + + _update() { + if (!this._targetNode || this._isHidden) return; + if (!this._targetNode.parentNode) return; + const rect = this._targetNode.getBoundingClientRect(); + rect.x += rect.width / 2; + let atRight = this._useRight(rect.x); + let atBottom = this._useBottom(rect.y); + if (atBottom) rect.y += rect.height; + this._setPosition(rect, atRight, atBottom); + this.requestUpdate(true); + } + + set data({content, positionOrTargetNode, immediate}) { + if (immediate) { + this._debouncedSetData.callNow(content, positionOrTargetNode) + } else { + this._debouncedSetData.call(content, positionOrTargetNode) + } + } + + _setData(content, positionOrTargetNode) { + if (positionOrTargetNode.nodeType === undefined) { + this._targetNode = undefined; + const position = positionOrTargetNode; + this._setPosition( + position, this._useRight(position.x), this._useBottom(position.y)); + } else { + this._setTargetNode(positionOrTargetNode); + } + this._setContent(content); + } + + _setTargetNode(targetNode) { + this._intersectionObserver.disconnect(); + this._targetNode = targetNode; + if (targetNode === undefined) return; + if (!(targetNode instanceof SVGElement)) { + this._intersectionObserver.observe(targetNode); + } + this.requestUpdate(true); + } + + _setPosition(viewportPosition, atRight, atBottom) { + const horizontalMode = atRight ? 'right' : 'left'; + const verticalMode = atBottom ? 'bottom' : 'top'; + this.bodyNode.className = horizontalMode + ' ' + verticalMode; + const pageX = viewportPosition.x + window.scrollX; + this.style.left = `${pageX}px`; + const pageY = viewportPosition.y + window.scrollY; + this.style.top = `${pageY}px`; + } + + _useBottom(viewportY) { + return viewportY <= 400; + } + + _useRight(viewportX) { + return viewportX < document.documentElement.clientWidth / 2; + } + + _setContent(content) { + if (!content) return this.hide(); + this.show(); + if (this._content === content) return; + this._content = content; + + if (typeof content === 'string') { + this.contentNode.innerHTML = content; + this.contentNode.className = 'textContent'; + } else if (content?.nodeType && content?.nodeName) { + this._setContentNode(content); + } else { + if (this.contentNode.firstChild?.localName == 'property-link-table') { + this.contentNode.firstChild.propertyDict = content; + } else { + const node = DOM.element('property-link-table'); + node.instanceLinkButtons = true; + node.propertyDict = content; + this._setContentNode(node); + } + } + } + + _setContentNode(content) { + const newContent = DOM.div(); + newContent.appendChild(content); + this.contentNode.replaceWith(newContent); + newContent.id = 'content'; + } + + hide() { + this._content = undefined; + if (this._isHidden) return; + this._isHidden = true; + this.bodyNode.style.display = 'none'; + this.targetNode = undefined; + } + + show() { + if (!this._isHidden) return; + this.bodyNode.style.display = 'block'; + this._isHidden = false; + } + + get bodyNode() { + return this.$('#body'); + } + + get contentNode() { + return this.$('#content'); + } +});