Commit dbffd66e authored by Camillo Bruni's avatar Camillo Bruni Committed by Commit Bot

[tools] Add ToolTip support for system-analyzer

- Add ToolTip helper that tracks scrolling target elements
- Auto hide if the target scrolls out of view
- ToolTip position depends on target position
- Add basic tooltips for maps in the transition view, entries in
  timeline tracks and the source panel

Drive-by-fix:
- Move events.mjs to view/ folder
- Add basic toString methods on various log entries
- Add requestAnimationFrame update support for V8CustomElement

Bug: v8:10644
Change-Id: I1059733cd094a986b715547b3d5747eefbc54bc5
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2551103
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: 's avatarMarja Hölttä <marja@chromium.org>
Cr-Commit-Position: refs/heads/master@{#71434}
parent 7b17b5e3
......@@ -20,7 +20,7 @@ found in the LICENSE file. -->
let module = await import('./index.mjs');
globalThis.app = new module.App("#log-file-reader", "#map-panel", "#map-stats-panel",
"#timeline-panel", "#ic-panel", "#map-track", "#ic-track", "#deopt-track",
"#source-panel");
"#source-panel", "#tool-tip");
})();
</script>
......@@ -92,6 +92,8 @@ found in the LICENSE file. -->
</head>
<body>
<tool-tip id="tool-tip"></tool-tip>
<section id="file-reader">
<log-file-reader id="log-file-reader"></log-file-reader>
</section>
......
......@@ -5,10 +5,10 @@
import {SourcePosition} from '../profile.mjs';
import {State} from './app-model.mjs';
import {FocusEvent, SelectionEvent, SelectTimeEvent} from './events.mjs';
import {IcLogEntry} from './log/ic.mjs';
import {MapLogEntry} from './log/map.mjs';
import {Processor} from './processor.mjs';
import {FocusEvent, SelectionEvent, SelectTimeEvent, ToolTipEvent,} from './view/events.mjs';
import {$, CSSColor} from './view/helper.mjs';
class App {
......@@ -18,7 +18,7 @@ class App {
_startupPromise;
constructor(
fileReaderId, mapPanelId, mapStatsPanelId, timelinePanelId, icPanelId,
mapTrackId, icTrackId, deoptTrackId, sourcePanelId) {
mapTrackId, icTrackId, deoptTrackId, sourcePanelId, toolTipId) {
this._view = {
__proto__: null,
logFileReader: $(fileReaderId),
......@@ -29,7 +29,8 @@ class App {
mapTrack: $(mapTrackId),
icTrack: $(icTrackId),
deoptTrack: $(deoptTrackId),
sourcePanel: $(sourcePanelId)
sourcePanel: $(sourcePanelId),
toolTip: $(toolTipId),
};
this.toggleSwitch = $('.theme-switch input[type="checkbox"]');
this.toggleSwitch.addEventListener('change', (e) => this.switchTheme(e));
......@@ -47,6 +48,7 @@ class App {
import('./view/stats-panel.mjs'),
import('./view/map-panel.mjs'),
import('./view/source-panel.mjs'),
import('./view/tool-tip.mjs'),
]);
document.addEventListener(
'keydown', e => this._navigation?.handleKeyDown(e));
......@@ -56,6 +58,8 @@ class App {
FocusEvent.name, e => this.handleShowEntryDetail(e));
document.addEventListener(
SelectTimeEvent.name, e => this.handleTimeRangeSelect(e));
document.addEventListener(ToolTipEvent.name, e => this.handleToolTip(e));
window.addEventListener('scroll', e => console.log(e));
}
handleShowEntries(e) {
......@@ -126,6 +130,11 @@ class App {
this._view.sourcePanel.selectedSourcePositions = [sourcePositions];
}
handleToolTip(event) {
this._view.toolTip.positionOrTargetNode = event.positionOrTargetNode;
this._view.toolTip.content = event.content;
}
handleFileUploadStart(e) {
this.restartApp();
$('#container').className = 'initial';
......
......@@ -7,4 +7,7 @@ export class DeoptLogEntry extends LogEntry {
constructor(type, time) {
super(type, time);
}
toString() {
return `Deopt(${this.type})`;
}
}
......@@ -30,6 +30,10 @@ export class IcLogEntry extends LogEntry {
this.modifier = modifier;
}
toString() {
return `IC(${this.type}):\n${this.state}`;
}
parseMapProperties(parts, offset) {
let next = parts[++offset];
if (!next.startsWith('dict')) return offset;
......
......@@ -44,6 +44,7 @@ class MapLogEntry extends LogEntry {
filePosition = '';
script = '';
id = -1;
description = '';
constructor(id, time) {
if (!time) throw new Error('Invalid time');
super(id, time);
......@@ -51,6 +52,10 @@ class MapLogEntry extends LogEntry {
this.id = id;
}
toString() {
return `Map(${this.id}):\n${this.description}`;
}
finalizeRootMap(id) {
let stack = [this];
while (stack.length > 0) {
......
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
class SelectionEvent extends CustomEvent {
export class SelectionEvent extends CustomEvent {
// TODO: turn into static class fields once Safari supports it.
static get name() {
return 'showentries';
......@@ -16,7 +16,7 @@ class SelectionEvent extends CustomEvent {
}
}
class FocusEvent extends CustomEvent {
export class FocusEvent extends CustomEvent {
static get name() {
return 'showentrydetail';
}
......@@ -26,7 +26,7 @@ class FocusEvent extends CustomEvent {
}
}
class SelectTimeEvent extends CustomEvent {
export class SelectTimeEvent extends CustomEvent {
static get name() {
return 'timerangeselect';
}
......@@ -37,7 +37,7 @@ class SelectTimeEvent extends CustomEvent {
}
}
class SynchronizeSelectionEvent extends CustomEvent {
export class SynchronizeSelectionEvent extends CustomEvent {
static get name() {
return 'syncselection';
}
......@@ -48,4 +48,25 @@ class SynchronizeSelectionEvent extends CustomEvent {
}
}
export {SelectionEvent, FocusEvent, SelectTimeEvent, SynchronizeSelectionEvent};
export class ToolTipEvent extends CustomEvent {
static get name() {
return 'showtooltip';
}
constructor(content, positionOrTargetNode) {
super(ToolTipEvent.name, {bubbles: true, composed: true});
this._content = content;
if (!positionOrTargetNode && !node) {
throw Error('Either provide a valid position or targetNode');
}
this._positionOrTargetNode = positionOrTargetNode;
}
get content() {
return this._content;
}
get positionOrTargetNode() {
return this._positionOrTargetNode;
}
}
\ No newline at end of file
......@@ -168,11 +168,17 @@ class V8CustomElement extends HTMLElement {
return this.shadowRoot.querySelectorAll(query);
}
update() {
// Use timeout tasks to asynchronously update the UI without blocking.
clearTimeout(this._updateTimeoutId);
const kDelayMs = 5;
this._updateTimeoutId = setTimeout(this._updateCallback, kDelayMs);
update(useAnimation = false) {
if (useAnimation) {
window.cancelAnimationFrame(this._updateTimeoutId);
this._updateTimeoutId =
window.requestAnimationFrame(this._updateCallback);
} else {
// Use timeout tasks to asynchronously update the UI without blocking.
clearTimeout(this._updateTimeoutId);
const kDelayMs = 5;
this._updateTimeoutId = setTimeout(this._updateCallback, kDelayMs);
}
}
_update() {
......
......@@ -2,11 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {FocusEvent, SelectionEvent, SelectTimeEvent} from '../events.mjs';
import {Group} from '../ic-model.mjs';
import {IcLogEntry} from '../log/ic.mjs';
import {MapLogEntry} from '../log/map.mjs';
import {FocusEvent, SelectionEvent, SelectTimeEvent} from './events.mjs';
import {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
......
......@@ -5,9 +5,9 @@ import './stats-panel.mjs';
import './map-panel/map-details.mjs';
import './map-panel/map-transitions.mjs';
import {FocusEvent} from '../events.mjs';
import {MapLogEntry} from '../log/map.mjs';
import {FocusEvent} from './events.mjs';
import {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement('view/map-panel',
......
// 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 {FocusEvent} from '../../events.mjs';
import {FocusEvent} from '../events.mjs';
import {DOM, V8CustomElement} from '../helper.mjs';
DOM.defineCustomElement(
......
// 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 {FocusEvent, SelectionEvent} from '../../events.mjs';
import {FocusEvent, SelectionEvent, ToolTipEvent} from '../events.mjs';
import {CSSColor} from '../helper.mjs';
import {DOM, V8CustomElement} from '../helper.mjs';
......@@ -14,6 +14,7 @@ DOM.defineCustomElement(
currentMap = undefined;
_toggleSubtreeHandler = this._handleToggleSubtree.bind(this);
_selectMapHandler = this._handleSelectMap.bind(this);
_mouseoverMapHandler = this._handleMouseoverMap.bind(this);
constructor() {
super(templateText);
......@@ -169,6 +170,7 @@ DOM.defineCustomElement(
if (map.edge) node.style.backgroundColor = this._typeToColor(map.edge);
node.map = map;
node.onclick = this._selectMapHandler
node.onmouseover = this._mouseoverMapHandler
if (map.children.length > 1) {
node.innerText = map.children.length;
const showSubtree = DOM.div('showSubtransitions');
......@@ -186,7 +188,13 @@ DOM.defineCustomElement(
this._selectMap(event.currentTarget.map)
}
_handleMouseoverMap(event) {
this.dispatchEvent(new ToolTipEvent(
event.currentTarget.map.toString(), event.currentTarget));
}
_handleToggleSubtree(event) {
event.preventDefault();
const node = event.currentTarget.parentElement;
let map = node.map;
event.target.classList.toggle('opened');
......@@ -209,5 +217,6 @@ DOM.defineCustomElement(
transitionsNode.removeChild(subtransitionNodes[i]);
}
}
return false;
}
});
// 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 {FocusEvent, SelectionEvent} from '../events.mjs';
import {IcLogEntry} from '../log/ic.mjs';
import {MapLogEntry} from '../log/map.mjs';
import {FocusEvent, SelectionEvent, ToolTipEvent} from './events.mjs';
import {delay, DOM, formatBytes, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement('view/source-panel',
......@@ -111,6 +111,14 @@ DOM.defineCustomElement('view/source-panel',
handleSourcePositionClick(e) {
this.selectLogEntries(e.target.sourcePosition.entries)
}
handleSourcePositionMouseOver(e) {
const entries = e.target.sourcePosition.entries;
let list =
entries
.map(entry => `${entry.__proto__.constructor.name}: ${entry.type}`)
.join('<br/>');
this.dispatchEvent(new ToolTipEvent(list, e.target));
}
selectLogEntries(logEntries) {
let icLogEntries = [];
......@@ -181,6 +189,7 @@ function* lineIterator(source) {
class LineBuilder {
_script;
_clickHandler;
_mouseoverHandler;
_sourcePositions;
_selection;
_sourcePositionToMarkers = new Map();
......@@ -189,6 +198,7 @@ class LineBuilder {
this._script = script;
this._selection = new Set(highlightPositions);
this._clickHandler = panel.handleSourcePositionClick.bind(panel);
this._mouseoverHandler = panel.handleSourcePositionMouseOver.bind(panel);
// TODO: sort on script finalization.
script.sourcePositions.sort((a, b) => {
if (a.line === b.line) return a.column - b.column;
......@@ -234,6 +244,7 @@ class LineBuilder {
marker.textContent = text;
marker.sourcePosition = sourcePosition;
marker.onclick = this._clickHandler;
marker.onmouseover = this._mouseoverHandler;
return marker;
}
}
\ No newline at end of file
// 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 {SelectionEvent} from '../events.mjs';
import {SelectionEvent} from './events.mjs';
import {DOM, LazyTable, V8CustomElement} from './helper.mjs';
......
......@@ -4,7 +4,7 @@
import './timeline/timeline-track.mjs';
import {SynchronizeSelectionEvent} from '../events.mjs';
import {SynchronizeSelectionEvent} from './events.mjs';
import {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
......
......@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {FocusEvent, SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent} from '../../events.mjs';
import {kChunkHeight, kChunkWidth} from '../../log/map.mjs';
import {MapLogEntry} from '../../log/map.mjs';
import {FocusEvent, SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs';
import {CSSColor, DOM, V8CustomElement} from '../helper.mjs';
const kColors = [
......@@ -372,8 +372,9 @@ DOM.defineCustomElement('view/timeline/timeline-track',
// topmost map (at chunk.height) == map #0.
let relativeIndex =
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
let map = chunk.at(relativeIndex);
this.dispatchEvent(new FocusEvent(map));
let logEntry = chunk.at(relativeIndex);
this.dispatchEvent(new FocusEvent(logEntry));
this.dispatchEvent(new ToolTipEvent(logEntry.toString(), event.target));
}
handleChunkClick(event) {
......
<!-- 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. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
:host {
position: absolute;
z-index: 100;
}
#content {
background-color: var(--surface-color);
border: 3px var(--primary-color) solid;
border-radius: 10px;
min-width: 100px;
min-height: 100px;
padding: 10px;
box-shadow: 0px 0px 10px rgba(0,0,0,0.5);
}
#body {
display: none;
position: absolute;
--tip-offset: 10px;
--tip-width: 10px;
--tip-height: 15px;
}
#body.top {
bottom: var(--tip-height);
}
#body.bottom {
top: var(--tip-height);
}
#body.left {
right: calc(var(--tip-offset) * -1 - var(--tip-width));
}
#body.right {
left: calc(var(--tip-offset) * -1 - var(--tip-width));
}
.tip {
width: 0;
height: 0;
border-style: solid;
position: absolute;
border-width: var(--tip-height) var(--tip-width) 0 var(--tip-width);
border-color: var(--primary-color) transparent transparent transparent;
pointer-events: none;
}
.top > .tip {
bottom: calc(var(--tip-height) * -1);
}
.bottom > .tip {
top: calc(var(--tip-height) * -1);
transform: scaleY(-1);
}
.left > .tip {
right: var(--tip-offset);
}
.right > .tip {
left: var(--tip-offset);
}
</style>
<div id="body">
<div id="content">
</div>
<div class="tip"></div>
</div>
// 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 {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
'view/tool-tip', (templateText) => class Tooltip extends V8CustomElement {
_targetNode;
_content;
_isHidden = true;
constructor() {
super(templateText);
this._intersectionObserver = new IntersectionObserver((entries) => {
if (entries[0].intersectionRatio <= 0) {
this.hide();
} else {
this.show();
this.update(true);
}
});
}
_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.update(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) {
this._intersectionObserver.observe(targetNode);
this.update(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();
this.show();
if (typeof content === 'string') {
this.contentNode.innerHTML = content;
} else {
const newContent = DOM.div();
newContent.appendChild(content);
this.contentNode.replaceWih(newContent);
newContent.id = 'content';
}
}
hide() {
this._isHidden = true;
this.bodyNode.style.display = 'none';
}
show() {
this.bodyNode.style.display = 'block';
this._isHidden = false;
}
get bodyNode() {
return this.$('#body');
}
get contentNode() {
return this.$('#content');
}
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment