[tools] Add system-analyzer list view

Bug: v8:10644
Change-Id: I83801396fe683173349d14a7590828ec86587eac
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2575122
Reviewed-by: Sathya Gunasekaran  <gsathya@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#71655}
This commit is contained in:
Camillo Bruni 2020-12-07 09:44:54 +01:00 committed by Commit Bot
parent ab4d9717f2
commit 88f7740636
28 changed files with 644 additions and 711 deletions

View File

@ -37,9 +37,18 @@ export class SourcePosition {
this.column = column;
this.entries = [];
}
addEntry(entry) {
this.entries.push(entry);
}
toString() {
return `${this.script.name}:${this.line}:${this.column}`;
}
toStringLong() {
return this.toString();
}
}
export class Script {
@ -90,7 +99,13 @@ export class Script {
columnToSourcePosition.set(column, sourcePosition);
}
toString() {
return this.name;
}
toStringLong() {
return this.source;
}
}

View File

@ -25,3 +25,50 @@ export function formatSeconds(millis) {
export function delay(time) {
return new Promise(resolver => setTimeout(resolver, time));
}
export class Group {
constructor(key, id, parentTotal, entries) {
this.key = key;
this.id = id;
this.count = 1;
this.entries = entries;
this.parentTotal = parentTotal;
}
get percent() {
return this.count / this.parentTotal * 100;
}
add() {
this.count++;
}
addEntry(entry) {
this.count++;
this.entries.push(entry);
}
}
export function groupBy(array, keyFunction, collect = false) {
if (array.length === 0) return [];
if (keyFunction === undefined) keyFunction = each => each;
const keyToGroup = new Map();
const groups = [];
let id = 0;
// This is performance critical, resorting to for-loop
for (let each of array) {
const key = keyFunction(each);
let group = keyToGroup.get(key);
if (group !== undefined) {
collect ? group.addEntry(each) : group.add();
continue;
}
let entries = undefined;
if (collect) entries = [each];
group = new Group(key, id++, array.length, entries);
groups.push(group);
keyToGroup.set(key, group);
}
// Sort by count
return groups.sort((a, b) => b.count - a.count);
}

View File

@ -1,56 +0,0 @@
// 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 {IcLogEntry} from './log/ic.mjs';
// For compatibility with console scripts:
print = console.log;
export class Group {
constructor(property, key, entry) {
this.property = property;
this.key = key;
this.count = 1;
this.entries = [entry];
this.percentage = undefined;
this.groups = undefined;
}
add(entry) {
this.count++;
this.entries.push(entry)
}
createSubGroups() {
// TODO: use Map
this.groups = {};
for (const propertyName of IcLogEntry.propertyNames) {
if (this.property == propertyName) continue;
this.groups[propertyName] = Group.groupBy(this.entries, propertyName);
}
}
static groupBy(entries, property) {
let accumulator = Object.create(null);
let length = entries.length;
for (let entry of entries) {
let key = entry[property];
if (accumulator[key] == undefined) {
accumulator[key] = new Group(property, key, entry);
} else {
let group = accumulator[key];
if (group.entries == undefined) console.log([group, entry]);
group.add(entry)
}
}
let result = [];
for (let key in accumulator) {
let group = accumulator[key];
group.percentage = Math.round(group.count / length * 100 * 100) / 100;
result.push(group);
}
result.sort((a, b) => {return b.count - a.count});
return result;
}
}

View File

@ -110,9 +110,10 @@ dd {
.panel {
background-color: var(--surface-color);
color: var(--on-surface-color);
padding: 10px 10px 10px 10px;
padding: 10px;
border-radius: 10px;
border: 3px solid rgba(var(--border-color), 0.2);
overflow: hidden;
}
.panelBody {
@ -121,8 +122,18 @@ dd {
margin: 0 -10px -10px 0;
}
.panel > h2 {
margin-top: 5px;
.panel > h2, .panelTitle {
margin: -10px -10px 0 -10px;
padding: 5px 10px 5px 10px;
background-color: rgba(var(--border-color),0.2);
border-radius: 7px 7px 0 0;
font-weight: 400;
}
.panel > select{
width: calc(100% + 20px);
margin: 0 -10px 10px -10px;
}
button {
@ -200,4 +211,22 @@ button:hover {
background-color: var(--primary-color);
color: var(--on-primary-color);
cursor: pointer;
}
#legend {
position: absolute;
right: 10px;
top: 10px;
background-color: var(--surface-color);
border-radius: 5px;
border: 3px solid rgba(var(--border-color), 0.2);
padding: 0 10px 0 10px;
}
#legend dt {
font-family: monospace;
}
#legend h3 {
margin-top: 10px;
}

View File

@ -74,7 +74,7 @@ found in the LICENSE file. -->
width: 100%;
}
.panels{
.panels {
display: grid;
align-content: center;
grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
@ -98,16 +98,40 @@ found in the LICENSE file. -->
<section id="container" class="initial">
<timeline-panel id="timeline-panel">
<timeline-track id="map-track"></timeline-track>
<timeline-track id="ic-track"></timeline-track>
<timeline-track id="deopt-track"></timeline-track>
<timeline-track id="code-track"></timeline-track>
<timeline-track id="api-track"></timeline-track>
<timeline-track id="map-track" title="Map"></timeline-track>
<timeline-track id="ic-track" title="IC"></timeline-track>
<timeline-track id="deopt-track" title="Deopt"></timeline-track>
<timeline-track id="code-track" title="Code"></timeline-track>
<timeline-track id="api-track" title="API"></timeline-track>
</timeline-panel>
<div class="panels">
<map-panel id="map-panel"></map-panel>
<stats-panel id="map-stats-panel"></stats-panel>
<ic-panel id="ic-panel" onchange="app.handleSelectIc(event)"></ic-panel>
<list-panel id="ic-list" title="ICs">
<div id="legend">
<h3>Legend</h3>
<dl>
<dt>0</dt>
<dd>uninitialized</dd>
<dt>X</dt>
<dd>no feedback</dd>
<dt>1</dt>
<dd>monomorphic</dd>
<dt>^</dt>
<dd>recompute handler</dd>
<dt>P</dt>
<dd>polymorphic</dd>
<dt>N</dt>
<dd>megamorphic</dd>
<dt>G</dt>
<dd>generic</dd>
</dl>
</div>
</list-panel>
<list-panel id="map-list" title="Maps"></list-panel>
<list-panel id="deopt-list" title="Deopts"></list-panel>
<list-panel id="code-list" title="Code"></list-panel>
<list-panel id="api-list" title="API"></list-panel>
<source-panel id="source-panel"></source-panel>
<code-panel id="code-panel"></code-panel>
</div>

View File

@ -12,7 +12,7 @@ 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';
import {$, CSSColor, groupBy} from './view/helper.mjs';
class App {
_state;
@ -23,17 +23,24 @@ class App {
this._view = {
__proto__: null,
logFileReader: $('#log-file-reader'),
mapPanel: $('#map-panel'),
mapStatsPanel: $('#map-stats-panel'),
timelinePanel: $('#timeline-panel'),
mapTrack: $('#map-track'),
icTrack: $('#ic-track'),
icPanel: $('#ic-panel'),
deoptTrack: $('#deopt-track'),
codePanel: $('#code-panel'),
codeTrack: $('#code-track'),
apiTrack: $('#api-track'),
icList: $('#ic-list'),
mapList: $('#map-list'),
codeList: $('#code-list'),
deoptList: $('#deopt-list'),
apiList: $('#api-list'),
mapPanel: $('#map-panel'),
codePanel: $('#code-panel'),
sourcePanel: $('#source-panel'),
toolTip: $('#tool-tip'),
};
this.toggleSwitch = $('.theme-switch input[type="checkbox"]');
@ -47,9 +54,8 @@ class App {
async runAsyncInitialize() {
await Promise.all([
import('./view/ic-panel.mjs'),
import('./view/list-panel.mjs'),
import('./view/timeline-panel.mjs'),
import('./view/stats-panel.mjs'),
import('./view/map-panel.mjs'),
import('./view/source-panel.mjs'),
import('./view/code-panel.mjs'),
@ -72,16 +78,8 @@ class App {
}
showEntries(entries) {
const groups = new Map();
for (let entry of entries) {
const group = groups.get(entry.constructor);
if (group !== undefined) {
group.push(entry);
} else {
groups.set(entry.constructor, [entry]);
}
}
groups.forEach(entries => this.showEntriesOfSingleType(entries));
groupBy(entries, each => each.constructor, true)
.forEach(group => this.showEntriesOfSingleType(group.entries));
}
showEntriesOfSingleType(entries) {
@ -126,33 +124,32 @@ class App {
showMapEntries(entries) {
this._state.selectedMapLogEntries = entries;
this._view.mapPanel.selectedMapLogEntries = entries;
this._view.mapStatsPanel.selectedLogEntries = entries;
this._view.mapPanel.selectedLogEntries = entries;
this._view.mapList.selectedLogEntries = entries;
}
showIcEntries(entries) {
this._state.selectedIcLogEntries = entries;
this._view.icPanel.selectedLogEntries = entries;
this._view.icList.selectedLogEntries = entries;
}
showDeoptEntries(entries) {
// TODO: create list panel.
this._state.selectedDeoptLogEntries = entries;
this._view.deoptList.selectedLogEntries = entries;
}
showCodeEntries(entries) {
// TODO: create list panel
this._state.selectedCodeLogEntries = entries;
this._view.codePanel.selectedEntries = entries;
this._view.codeList.selectedLogEntries = entries;
}
showApiEntries(entries) {
// TODO: create list panel
this._state.selectedApiLogEntries = entries;
this._view.apiList.selectedLogEntries = entries;
}
showSourcePositions(entries) {
// TODO: Handle multiple source position selection events
this._view.sourcePanel.selectedSourcePositions = entries
}
@ -163,11 +160,11 @@ class App {
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.showCodeEntries(this._state.codeTimeline.selection ?? []);
this.showApiEntries(this._state.apiTimeline.selection ?? []);
this.showMapEntries(this._state.mapTimeline.selectionOrSelf);
this.showIcEntries(this._state.icTimeline.selectionOrSelf);
this.showDeoptEntries(this._state.deoptTimeline.selectionOrSelf);
this.showCodeEntries(this._state.codeTimeline.selectionOrSelf);
this.showApiEntries(this._state.apiTimeline.selectionOrSelf);
this._view.timelinePanel.timeSelection = {start, end};
}
@ -175,25 +172,28 @@ class App {
this._state.map = entry;
this._view.mapTrack.selectedEntry = entry;
this._view.mapPanel.map = entry;
this._view.mapList.selectedLogEntry = entry;
}
selectIcLogEntry(entry) {
this._state.ic = entry;
this._view.icPanel.selectedEntry = [entry];
this._view.icList.selectedLogEntry = entry;
}
selectCodeLogEntry(entry) {
this._state.code = entry;
this._view.codePanel.entry = entry;
this._view.codeList.selectedLogEntry = entry;
}
selectDeoptLogEntry(entry) {
// TODO
this._view.deoptList.selectedLogEntry = entry;
}
selectApiLogEntry(entry) {
this._state.apiLogEntry = entry;
this._view.apiTrack.selectedEntry = entry;
this._view.apiList.selectedLogEntry = entry;
}
selectSourcePosition(sourcePositions) {
@ -227,12 +227,12 @@ class App {
const apiTimeline = processor.apiTimeline;
this._state.setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline);
// Transitions must be set before timeline for stats panel.
this._view.mapPanel.timeline = mapTimeline;
this._view.mapStatsPanel.transitions =
this._state.mapTimeline.transitions;
this._view.mapStatsPanel.timeline = mapTimeline;
this._view.icPanel.timeline = icTimeline;
this._view.icList.timeline = icTimeline;
this._view.mapList.timeline = mapTimeline;
this._view.deoptList.timeline = deoptTimeline;
this._view.codeList.timeline = codeTimeline;
this._view.apiList.timeline = apiTimeline;
this._view.sourcePanel.data = processor.scripts;
this._view.codePanel.timeline = codeTimeline;
this.refreshTimelineTrackView();

View File

@ -8,7 +8,16 @@ export class ApiLogEntry extends LogEntry {
super(type, time);
this._name = name;
}
toString() {
return `Api(${this.type})`;
}
toStringLong() {
return `Api(${this.type}): ${this._name}`;
}
static get propertyNames() {
return ['type', 'name'];
}
}

View File

@ -8,16 +8,32 @@ export class DeoptLogEntry extends LogEntry {
type, time, deoptReason, deoptLocation, scriptOffset, instructionStart,
codeSize, inliningId) {
super(type, time);
this._deoptReason = deoptReason;
this._deoptLocation = deoptLocation;
this._reason = deoptReason;
this._location = deoptLocation;
this._scriptOffset = scriptOffset;
this._instructionStart = instructionStart;
this._codeSize = codeSize;
this._inliningId = inliningId;
}
get reason() {
return this._reason;
}
get location() {
return this._location;
}
toString() {
return `Deopt(${this.type})${this._deoptReason}: ${this._deoptLocation}`;
return `Deopt(${this.type})`;
}
toStringLong() {
return `Deopt(${this.type})${this._reason}: ${this._location}`;
}
static get propertyNames() {
return ['type', 'reason', 'location', 'script', 'sourcePosition'];
}
}
@ -28,11 +44,23 @@ export class CodeLogEntry extends LogEntry {
this._entry = entry;
}
get kind() {
return this._kind;
}
toString() {
return `Code(${this.type})`;
}
toStringLong() {
return `Code(${this.type}): ${this._entry.toString()}`;
}
get disassemble() {
return this._entry?.source?.disassemble;
}
static get propertyNames() {
return ['type', 'kind', 'script', 'sourcePosition'];
}
}

View File

@ -6,7 +6,7 @@ import {LogEntry} from './log.mjs';
export class IcLogEntry extends LogEntry {
constructor(
type, fn_file, time, line, column, key, oldState, newState, map, reason,
script, modifier, additional) {
modifier, additional) {
super(type, time);
this.category = 'other';
if (this.type.indexOf('Store') !== -1) {
@ -18,7 +18,6 @@ export class IcLogEntry extends LogEntry {
this.functionName = parts[0];
this.file = parts[1];
let position = line + ':' + column;
this.filePosition = this.file + ':' + position;
this.oldState = oldState;
this.newState = newState;
this.state = this.oldState + ' → ' + this.newState;
@ -26,11 +25,14 @@ export class IcLogEntry extends LogEntry {
this.map = map;
this.reason = reason;
this.additional = additional;
this.script = script;
this.modifier = modifier;
}
toString() {
return `IC(${this.type})`;
}
toStringLong() {
return `IC(${this.type}):\n${this.state}`;
}
@ -62,8 +64,8 @@ export class IcLogEntry extends LogEntry {
static get propertyNames() {
return [
'type', 'category', 'functionName', 'filePosition', 'state', 'key', 'map',
'reason', 'file'
'type', 'category', 'functionName', 'script', 'sourcePosition', 'state',
'key', 'map', 'reason', 'file'
];
}
}

View File

@ -17,8 +17,20 @@ export class LogEntry {
return this._type;
}
get script() {
return this.sourcePosition?.script;
}
toString() {
return `${this.constructor.name}(${this._type})`;
}
toStringLong() {
return this.toString();
}
// Returns an Array of all possible #type values.
static get allTypes() {
throw new Error('Not implemented.');
}
}
}

View File

@ -41,7 +41,6 @@ class MapLogEntry extends LogEntry {
super(undefined, time);
this.id = id;
MapLogEntry.set(id, this);
this.id = -1;
this.edge = undefined;
this.children = [];
this.depth = 0;
@ -50,11 +49,14 @@ class MapLogEntry extends LogEntry {
this.leftId = 0;
this.rightId = 0;
this.filePosition = '';
this.script = '';
this.description = '';
}
toString() {
return `Map(${this.id})`;
}
toStringLong() {
return `Map(${this.id}):\n${this.description}`;
}
@ -138,7 +140,15 @@ class MapLogEntry extends LogEntry {
}
get type() {
return this.edge === undefined ? 'new' : this.edge.type;
return this.edge?.type ?? 'new';
}
get reason() {
return this.edge?.reason;
}
get property() {
return this.edge?.name;
}
isBootstrapped() {
@ -176,6 +186,10 @@ class MapLogEntry extends LogEntry {
this.cache.set(id, [map]);
}
}
static get propertyNames() {
return ['id', 'type', 'reason', 'property', 'script', 'sourcePosition'];
}
}
MapLogEntry.cache = new Map();

View File

@ -274,16 +274,17 @@ export class Processor extends LogReader {
}
processPropertyIC(
type, pc, time, line, column, old_state, new_state, map, key, modifier,
type, pc, time, line, column, old_state, new_state, mapId, key, modifier,
slow_reason) {
this._lastTimestamp = time;
let profileEntry = this._profile.findEntry(pc);
let fnName = this.formatProfileEntry(profileEntry);
let script = this.getProfileEntryScript(profileEntry);
const profileEntry = this._profile.findEntry(pc);
const fnName = this.formatProfileEntry(profileEntry);
const script = this.getProfileEntryScript(profileEntry);
const map = this.getOrCreateMapEntry(mapId, time);
// TODO: Use SourcePosition here directly
let entry = new IcLogEntry(
type, fnName, time, line, column, key, old_state, new_state, map,
slow_reason, script, modifier);
slow_reason, modifier);
if (script) {
entry.sourcePosition = script.addSourcePosition(line, column, entry);
}
@ -320,8 +321,8 @@ export class Processor extends LogReader {
// Skip normalized maps that were cached so we don't introduce multiple
// edges with the same source and target map.
if (type === 'NormalizeCached') return;
const from_ = this.getMapEntry(from, time_);
const to_ = this.getMapEntry(to, time_);
const from_ = this.getOrCreateMapEntry(from, time_);
const to_ = this.getOrCreateMapEntry(to, time_);
if (type === 'Normalize') {
// Fix a bug where we log "Normalize" transitions for maps created from
// the NormalizedMapCache.
@ -336,8 +337,7 @@ export class Processor extends LogReader {
to_.filePosition = this.formatProfileEntry(profileEntry, line, column);
let script = this.getProfileEntryScript(profileEntry);
if (script) {
to_.script = script;
to_.sourcePosition = to_.script.addSourcePosition(line, column, to_)
to_.sourcePosition = script.addSourcePosition(line, column, to_)
}
if (to_.parent() !== undefined && to_.parent() === from_) {
// Fix bug where we double log transitions.
@ -350,7 +350,7 @@ export class Processor extends LogReader {
deprecateMap(type, time, id) {
this._lastTimestamp = time;
this.getMapEntry(id, time).deprecate();
this.getOrCreateMapEntry(id, time).deprecate();
}
processMapCreate(time, id) {
@ -362,7 +362,7 @@ export class Processor extends LogReader {
processMapDetails(time, id, string) {
// TODO(cbruni): fix initial map logging.
const map = this.getMapEntry(id, time);
const map = this.getOrCreateMapEntry(id, time);
map.description = string;
}
@ -373,7 +373,7 @@ export class Processor extends LogReader {
return map;
}
getMapEntry(id, time) {
getOrCreateMapEntry(id, time) {
if (id === '0x000000000000') return undefined;
const map = MapLogEntry.get(id, time);
if (map !== undefined) return map;

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {groupBy} from './helper.mjs'
class Timeline {
// Class:
_model;
@ -30,6 +32,10 @@ class Timeline {
return this._selection;
}
get selectionOrSelf() {
return this._selection ?? this;
}
set selection(value) {
this._selection = value;
}
@ -177,9 +183,9 @@ class Timeline {
}
getBreakdown(keyFunction) {
if (keyFunction) return breakdown(this._values, keyFunction);
if (keyFunction) return groupBy(this._values, keyFunction);
if (this._breakdown === undefined) {
this._breakdown = breakdown(this._values, each => each.type);
this._breakdown = groupBy(this._values, each => each.type);
}
return this._breakdown;
}
@ -261,7 +267,7 @@ class Chunk {
}
getBreakdown(keyFunction) {
return breakdown(this.items, keyFunction);
return groupBy(this.items, keyFunction);
}
filter() {
@ -269,26 +275,4 @@ class Chunk {
}
}
function breakdown(array, keyFunction) {
if (array.length === 0) return [];
if (keyFunction === undefined) keyFunction = each => each;
const typeToindex = new Map();
const breakdown = [];
let id = 0;
// This is performance critical, resorting to for-loop
for (let i = 0; i < array.length; i++) {
const each = array[i];
const type = keyFunction(each);
const index = typeToindex.get(type);
if (index === void 0) {
typeToindex.set(type, breakdown.length);
breakdown.push({type: type, count: 0, id: id++});
} else {
breakdown[index].count++;
}
}
// Sort by count
return breakdown.sort((a, b) => b.count - a.count);
}
export {Timeline, Chunk};

View File

@ -200,28 +200,60 @@ class V8CustomElement extends HTMLElement {
}
}
class LazyTable {
constructor(table, rowData, rowElementCreator) {
this._table = table;
this._rowData = rowData;
this._rowElementCreator = rowElementCreator;
const tbody = table.querySelector('tbody');
table.replaceChild(document.createElement('tbody'), tbody);
table.querySelector('tfoot td').onclick = (e) => this._addMoreRows();
this._addMoreRows();
class Chunked {
constructor(iterable, limit) {
this._iterator = iterable[Symbol.iterator]();
this._limit = limit;
}
_nextRowDataSlice() {
return this._rowData.splice(0, 100);
* next() {
for (let i = 0; i < this._limit; i++) {
const {value, done} = this._iterator.next();
if (done) {
this._iterator = undefined;
return;
};
yield value;
}
}
get hasMore() {
return this._iterator !== undefined;
}
}
class LazyTable {
constructor(table, rowData, rowElementCreator, limit = 100) {
this._table = table;
this._chunkedRowData = new Chunked(rowData, limit);
this._rowElementCreator = rowElementCreator;
if (table.tBodies.length == 0) {
table.appendChild(DOM.tbody());
} else {
table.replaceChild(DOM.tbody(), table.tBodies[0]);
}
if (!table.tFoot) {
const td = table.appendChild(DOM.element('tfoot'))
.appendChild(DOM.tr('clickable'))
.appendChild(DOM.td(`Show more...`));
td.setAttribute('colspan', 100);
}
this._clickHandler = this._addMoreRows.bind(this);
table.tFoot.addEventListener('click', this._clickHandler);
this._addMoreRows();
}
_addMoreRows() {
const fragment = new DocumentFragment();
for (let row of this._nextRowDataSlice()) {
for (let row of this._chunkedRowData.next()) {
const tr = this._rowElementCreator(row);
fragment.appendChild(tr);
}
this._table.querySelector('tbody').appendChild(fragment);
this._table.tBodies[0].appendChild(fragment);
if (!this._chunkedRowData.hasMore) {
this._table.tFoot.removeEventListener('click', this._clickHandler);
this._table.tFoot.style.display = 'none';
}
}
}

View File

@ -1,98 +0,0 @@
<!-- 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>
.count {
text-align: right;
width: 5em;
}
.percentage {
text-align: right;
width: 5em;
}
.key {
padding-left: 1em;
}
.drilldown-group-title {
font-weight: bold;
padding: 0.5em 0 0.2em 0;
}
.toggle {
width: 1em;
text-align: center;
cursor: -webkit-zoom-in;
color: rgba(var(--border-color), 1);
}
.toggle::before {
content: "▶";
}
.open .toggle::before {
content: "▼";
}
.panel {
position: relative;
min-height: 200px;
}
#legend {
position: absolute;
right: 10px;
top: 10px;
background-color: var(--surface-color);
border-radius: 5px;
border: 3px solid rgba(var(--border-color), 0.2);
padding: 0 10px 0 10px;
}
#legend dt {
font-family: monospace;
}
#legend h3 {
margin-top: 10px;
}
.scroller {
max-height: 800px;
overflow-y: scroll;
}
</style>
<div class="panel">
<h2>IC Panel <span id="count"></span></h2>
<div id="legend">
<h3>Legend</h3>
<dl>
<dt>0</dt>
<dd>uninitialized</dd>
<dt>X</dt>
<dd>no feedback</dd>
<dt>1</dt>
<dd>monomorphic</dd>
<dt>^</dt>
<dd>recompute handler</dd>
<dt>P</dt>
<dd>polymorphic</dd>
<dt>N</dt>
<dd>megamorphic</dd>
<dt>G</dt>
<dd>generic</dd>
</dl>
</div>
<p>
Group by IC-property:
<select id="group-key"></select>
</p>
<div class="panelBody">
<table id="table" width="100%">
<tbody id="table-body">
</tbody>
</table>
</div>
</div>

View File

@ -1,199 +0,0 @@
// 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 {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(
'view/ic-panel', (templateText) => class ICPanel extends V8CustomElement {
_selectedLogEntries;
_selectedLogEntry;
_timeline;
_detailsClickHandler = this.handleDetailsClick.bind(this);
_mapClickHandler = this.handleMapClick.bind(this);
_fileClickHandler = this.handleFilePositionClick.bind(this);
constructor() {
super(templateText);
this.initGroupKeySelect();
this.groupKey.addEventListener('change', e => this.update());
}
set timeline(value) {
console.assert(value !== undefined, 'timeline undefined!');
this._timeline = value;
this.selectedLogEntries = this._timeline.all;
this.update();
}
get groupKey() {
return this.$('#group-key');
}
get table() {
return this.$('#table');
}
get tableBody() {
return this.$('#table-body');
}
get count() {
return this.$('#count');
}
get spanSelectAll() {
return this.querySelectorAll('span');
}
set selectedLogEntries(value) {
this._selectedLogEntries = value;
this.update();
}
set selectedLogEntry(entry) {
// TODO: show details
}
_update() {
this._updateCount();
this._updateTable();
}
_updateCount() {
this.count.innerHTML = `length=${this._selectedLogEntries.length}`;
}
_updateTable(event) {
let select = this.groupKey;
let key = select.options[select.selectedIndex].text;
DOM.removeAllChildren(this.tableBody);
let groups = Group.groupBy(this._selectedLogEntries, key, true);
this._render(groups, this.tableBody);
}
escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
handleMapClick(e) {
const group = e.target.parentNode.group;
const id = group.key;
const selectedMapLogEntries =
this.searchIcLogEntryToMapLogEntry(id, group.entries);
this.dispatchEvent(new SelectionEvent(selectedMapLogEntries));
}
searchIcLogEntryToMapLogEntry(id, icLogEntries) {
// searches for mapLogEntries using the id, time
const selectedMapLogEntriesSet = new Set();
for (const icLogEntry of icLogEntries) {
const selectedMap = MapLogEntry.get(id, icLogEntry.time);
selectedMapLogEntriesSet.add(selectedMap);
}
return Array.from(selectedMapLogEntriesSet);
}
// TODO(zcankara) Handle in the processor for events with source
// positions.
handleFilePositionClick(e) {
const tr = e.target.parentNode;
const sourcePosition = tr.group.entries[0].sourcePosition;
this.dispatchEvent(new FocusEvent(sourcePosition));
}
_render(groups, parent) {
const fragment = document.createDocumentFragment();
const max = Math.min(1000, groups.length)
for (let i = 0; i < max; i++) {
const group = groups[i];
const tr = DOM.tr();
tr.group = group;
const details = tr.appendChild(DOM.td('', 'toggle'));
details.onclick = this._detailsClickHandler;
tr.appendChild(DOM.td(group.percentage + '%', 'percentage'));
tr.appendChild(DOM.td(group.count, 'count'));
const valueTd = tr.appendChild(DOM.td(group.key, 'key'));
if (group.property === 'map') {
valueTd.onclick = this._mapClickHandler;
valueTd.classList.add('clickable');
} else if (group.property == 'filePosition') {
valueTd.classList.add('clickable');
valueTd.onclick = this._fileClickHandler;
}
fragment.appendChild(tr);
}
const omitted = groups.length - max;
if (omitted > 0) {
const tr = DOM.tr();
const tdNode = tr.appendChild(DOM.td(`Omitted ${omitted} entries.`));
tdNode.colSpan = 4;
fragment.appendChild(tr);
}
parent.appendChild(fragment);
}
handleDetailsClick(event) {
const tr = event.target.parentNode;
const group = tr.group;
// Create subgroup in-place if the don't exist yet.
if (group.groups === undefined) {
group.createSubGroups();
this.renderDrilldown(group, tr);
}
let detailsTr = tr.nextSibling;
if (tr.classList.contains('open')) {
tr.classList.remove('open');
detailsTr.style.display = 'none';
} else {
tr.classList.add('open');
detailsTr.style.display = 'table-row';
}
}
renderDrilldown(group, previousSibling) {
let tr = DOM.tr('entry-details');
tr.style.display = 'none';
// indent by one td.
tr.appendChild(DOM.td());
let td = DOM.td();
td.colSpan = 3;
for (let key in group.groups) {
this.renderDrilldownGroup(td, group.groups[key], key);
}
tr.appendChild(td);
// Append the new TR after previousSibling.
previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling)
}
renderDrilldownGroup(td, children, key) {
const max = 20;
const div = DOM.div('drilldown-group-title');
div.textContent =
`Grouped by ${key} [top ${max} out of ${children.length}]`;
td.appendChild(div);
const table = DOM.table();
this._render(children.slice(0, max), table, false)
td.appendChild(table);
}
initGroupKeySelect() {
const select = this.groupKey;
select.options.length = 0;
for (const propertyName of IcLogEntry.propertyNames) {
const option = document.createElement('option');
option.text = propertyName;
select.add(option);
}
}
});

View File

@ -0,0 +1,62 @@
<!-- 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>
.count {
text-align: right;
width: 5em;
}
.percentage {
text-align: right;
width: 4em;
}
.key {
padding-left: 1em;
}
.drilldown-group-title {
font-weight: bold;
padding: 0.5em 0 0.2em 0;
}
.toggle {
width: 1em;
text-align: left;
cursor: -webkit-zoom-in;
color: rgba(var(--border-color), 1);
}
.toggle::before {
content: "▶";
}
.open .toggle::before {
content: "▼";
}
.panel {
position: relative;
}
.panelBody {
height: 400px;
}
.scroller {
max-height: 800px;
overflow-y: scroll;
}
</style>
<div class="panel">
<h2 id="title"></h2>
<slot></slot>
<select id="group-key"></select>
<div id="content" class="panelBody">
<table id="table" width="100%">
</table>
</div>
</div>

View File

@ -0,0 +1,181 @@
// 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 {IcLogEntry} from '../log/ic.mjs';
import {MapLogEntry} from '../log/map.mjs';
import {FocusEvent, SelectionEvent, SelectTimeEvent} from './events.mjs';
import {groupBy, LazyTable} from './helper.mjs';
import {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement('view/list-panel',
(templateText) =>
class ListPanel extends V8CustomElement {
_selectedLogEntries;
_selectedLogEntry;
_timeline;
_detailsClickHandler = this._handleDetailsClick.bind(this);
_mapClickHandler = this._handleMapClick.bind(this);
_fileClickHandler = this._handleFilePositionClick.bind(this);
constructor() {
super(templateText);
this.groupKey.addEventListener('change', e => this.update());
}
static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == 'title') {
this.$('#title').innerHTML = newValue;
}
}
set timeline(value) {
console.assert(value !== undefined, 'timeline undefined!');
this._timeline = value;
this.selectedLogEntries = this._timeline.all;
this.initGroupKeySelect();
}
set selectedLogEntries(entries) {
this._selectedLogEntries = entries;
this.update();
}
set selectedLogEntry(entry) {
// TODO: show details
}
get entryClass() {
return this._timeline.at(0)?.constructor;
}
get groupKey() {
return this.$('#group-key');
}
get table() {
return this.$('#table');
}
get spanSelectAll() {
return this.querySelectorAll('span');
}
get _propertyNames() {
return this.entryClass?.propertyNames ?? [];
}
_update() {
DOM.removeAllChildren(this.table);
const propertyName = this.groupKey.selectedOptions[0].text;
const groups =
groupBy(this._selectedLogEntries, each => each[propertyName], true);
this._render(groups, this.table);
}
createSubgroups(group) {
const map = new Map();
for (let propertyName of this._propertyNames) {
map.set(
propertyName,
groupBy(group.entries, each => each[propertyName], true));
}
return map;
}
_handleMapClick(e) {
const group = e.currentTarget.group;
this.dispatchEvent(new FocusEvent(group.entries[0].map));
}
_handleFilePositionClick(e) {
const group = e.currentTarget.group;
const sourcePosition = group.entries[0].sourcePosition;
this.dispatchEvent(new FocusEvent(sourcePosition));
}
_render(groups, table) {
let last;
new LazyTable(table, groups, group => {
if (last && last.count < group.count) {
console.log(last, group);
}
last = group;
const tr = DOM.tr();
tr.group = group;
const details = tr.appendChild(DOM.td('', 'toggle'));
details.onclick = this._detailsClickHandler;
tr.appendChild(DOM.td(`${group.percent.toFixed(2)}%`, 'percentage'));
tr.appendChild(DOM.td(group.count, 'count'));
const valueTd = tr.appendChild(DOM.td(`${group.key}`, 'key'));
if (group.key instanceof MapLogEntry) {
tr.onclick = this._mapClickHandler;
valueTd.classList.add('clickable');
} else if (group.key instanceof SourcePosition) {
valueTd.classList.add('clickable');
tr.onclick = this._fileClickHandler;
}
return tr;
}, 10);
}
_handleDetailsClick(event) {
event.stopPropagation();
const tr = event.target.parentNode;
const group = tr.group;
// Create subgroup in-place if the don't exist yet.
if (tr.groups === undefined) {
const groups = tr.groups = this.createSubgroups(group);
this.renderDrilldown(groups, tr);
}
const detailsTr = tr.nextSibling;
if (tr.classList.contains('open')) {
tr.classList.remove('open');
detailsTr.style.display = 'none';
} else {
tr.classList.add('open');
detailsTr.style.display = 'table-row';
}
}
renderDrilldown(groups, previousSibling) {
const tr = DOM.tr('entry-details');
tr.style.display = 'none';
// indent by one td.
tr.appendChild(DOM.td());
const td = DOM.td();
td.colSpan = 3;
groups.forEach((group, key) => {
this.renderDrilldownGroup(td, group, key);
});
tr.appendChild(td);
// Append the new TR after previousSibling.
previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling);
}
renderDrilldownGroup(td, group, key) {
const div = DOM.div('drilldown-group-title');
div.textContent = `Grouped by ${key}`;
td.appendChild(div);
const table = DOM.table();
this._render(group, table, false)
td.appendChild(table);
}
initGroupKeySelect() {
const select = this.groupKey;
select.options.length = 0;
for (const propertyName of this._propertyNames) {
const option = DOM.element('option');
option.text = propertyName;
select.add(option);
}
}
});

View File

@ -1,7 +1,6 @@
// 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 './stats-panel.mjs';
import './map-panel/map-details.mjs';
import './map-panel/map-transitions.mjs';
@ -66,11 +65,12 @@ DOM.defineCustomElement('view/map-panel',
}
}
set selectedMapLogEntries(list) {
this.mapTransitionsPanel.selectedMapLogEntries = list;
if (list.length === 1) this.mapDetailsPanel.map = list[0];
set selectedLogEntries(list) {
this.mapTransitionsPanel.selectedLogEntries = list;
if (list.length === 1) this.mapDetailsPanel.map = list.first();
}
get selectedMapLogEntries() {
return this.mapTransitionsPanel.selectedMapLogEntries;
get selectedLogEntries() {
return this.mapTransitionsPanel.selectedLogEntries;
}
});

View File

@ -11,7 +11,7 @@ DOM.defineCustomElement(
_timeline;
_map;
_edgeToColor = new Map();
_selectedMapLogEntries;
_selectedLogEntries;
_displayedMapsInTree;
_toggleSubtreeHandler = this._handleToggleSubtree.bind(this);
_selectMapHandler = this._handleSelectMap.bind(this);
@ -49,13 +49,13 @@ DOM.defineCustomElement(
});
}
set selectedMapLogEntries(list) {
this._selectedMapLogEntries = list;
set selectedLogEntries(list) {
this._selectedLogEntries = list;
this.update();
}
get selectedMapLogEntries() {
return this._selectedMapLogEntries;
get selectedLogEntries() {
return this._selectedLogEntries;
}
_handleTransitionViewChange(e) {
@ -80,7 +80,7 @@ DOM.defineCustomElement(
DOM.removeAllChildren(this.transitionView);
this._displayedMapsInTree = new Set();
// Limit view to 200 maps for performance reasons.
this.selectedMapLogEntries.slice(0, 200).forEach(
this.selectedLogEntries.slice(0, 200).forEach(
(map) => this._addMapAndParentTransitions(map));
this._displayedMapsInTree = undefined;
this.transitionView.style.display = '';
@ -177,7 +177,7 @@ DOM.defineCustomElement(
_handleMouseoverMap(event) {
this.dispatchEvent(new ToolTipEvent(
event.currentTarget.map.toString(), event.currentTarget));
event.currentTarget.map.toStringLong(), event.currentTarget));
}
_handleToggleSubtree(event) {

View File

@ -59,11 +59,6 @@ found in the LICENSE file. -->
box-shadow: 0px 0px 0px 0px var(--secondary-color);
}
}
#script-dropdown {
width: 100%;
margin-bottom: 10px;
}
</style>
<div class="panel">
<h2>Source Panel</h2>

View File

@ -1,6 +1,7 @@
// 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 {groupBy} from '../helper.mjs';
import {IcLogEntry} from '../log/ic.mjs';
import {MapLogEntry} from '../log/map.mjs';
@ -116,11 +117,18 @@ DOM.defineCustomElement('view/source-panel',
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));
let text = groupBy(entries, each => each.constructor, true)
.map(group => {
let text = `${group.key.name}: ${group.count}\n`
text += groupBy(group.entries, each => each.type, true)
.map(group => {
return ` - ${group.key}: ${group.count}`;
})
.join('\n');
return text;
})
.join('\n');
this.dispatchEvent(new ToolTipEvent(text, e.target));
}
selectLogEntries(logEntries) {

View File

@ -1,73 +0,0 @@
<!-- 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>
#stats {
display: flex;
height: 250px;
background-color: var(--surface-color);
padding: 10px 10px 10px 10px;
margin: auto;
}
table {
flex: 1;
max-height: 250px;
display: inline-block;
overflow-y: scroll;
border-collapse: collapse;
}
table td {
padding: 2px;
}
table thead td {
border-bottom: 1px var(--on-surface-color) dotted;
}
table tbody td {
cursor: pointer;
}
#nameTable tr {
max-width: 200px;
}
#nameTable tr td:nth-child(1) {
text-align: right;
}
#typeTable {
text-align: right;
max-width: 380px;
}
#typeTable tr td:nth-child(2) {
text-align: left;
}
</style>
<div class="panel">
<h2>Map Stats</h2>
<section id="stats">
<table id="typeTable" class="statsTable">
<thead>
<tr><td></td><td>Type</td><td>Count</td><td>Percent</td></tr>
</thead>
<tbody></tbody>
</table>
<table id="nameTable">
<thead>
<tr><td>Count</td><td>Propery Name</td></tr>
</thead>
<tbody></tbody>
<tfoot>
<tr><td colspan="2" class="clickable">Show more...</td></tr>
</tfoo>
</table>
</section>
</div>

View File

@ -1,130 +0,0 @@
// 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 {DOM, LazyTable, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
'view/stats-panel',
(templateText) => class StatsPanel extends V8CustomElement {
_timeline;
_transitions;
_selectedLogEntries;
constructor() {
super(templateText);
}
get stats() {
return this.$('#stats');
}
set timeline(timeline) {
this._timeline = timeline;
this.selectedLogEntries = timeline.all
}
set selectedLogEntries(entries) {
this._selectedLogEntries = entries;
this.update();
}
set transitions(value) {
this._transitions = value;
}
_filterUniqueTransitions(filter) {
// Returns a list of Maps whose parent is not in the list.
return this._selectedLogEntries.filter((map) => {
if (filter(map) === false) return false;
let parent = map.parent();
if (parent === undefined) return true;
return filter(parent) === false;
});
}
_update() {
this._updateGeneralStats();
this._updateNamedTransitionsStats();
}
_updateGeneralStats() {
console.assert(this._timeline !== undefined, 'Timeline not set yet!');
let pairs = [
['Transitions', 'primary', (e) => e.edge && e.edge.isTransition()],
['Fast to Slow', 'violet', (e) => e.edge && e.edge.isFastToSlow()],
['Slow to Fast', 'orange', (e) => e.edge && e.edge.isSlowToFast()],
['Initial Map', 'yellow', (e) => e.edge && e.edge.isInitial()],
[
'Replace Descriptors',
'red',
(e) => e.edge && e.edge.isReplaceDescriptors(),
],
[
'Copy as Prototype',
'red',
(e) => e.edge && e.edge.isCopyAsPrototype(),
],
[
'Optimize as Prototype',
null,
(e) => e.edge && e.edge.isOptimizeAsPrototype(),
],
['Deprecated', null, (e) => e.isDeprecated()],
['Bootstrapped', 'green', (e) => e.isBootstrapped()],
['Total', null, (e) => true],
];
let tbody = document.createElement('tbody');
let total = this._selectedLogEntries.length;
pairs.forEach(([name, color, filter]) => {
let row = DOM.tr();
if (color !== null) {
row.appendChild(DOM.td(DOM.div(['colorbox', color])));
} else {
row.appendChild(DOM.td(''));
}
row.classList.add('clickable');
row.onclick = (e) => {
// lazily compute the stats
let node = e.target.parentNode;
if (node.maps == undefined) {
node.maps = this._filterUniqueTransitions(filter);
}
this.dispatchEvent(new SelectionEvent(node.maps));
};
row.appendChild(DOM.td(name));
let count = this._count(filter);
row.appendChild(DOM.td(count));
let percent = Math.round((count / total) * 1000) / 10;
row.appendChild(DOM.td(percent.toFixed(1) + '%'));
tbody.appendChild(row);
});
this.$('#typeTable').replaceChild(tbody, this.$('#typeTable tbody'));
}
_count(filter) {
let count = 0;
for (const map of this._selectedLogEntries) {
if (filter(map)) count++;
}
return count;
}
_updateNamedTransitionsStats() {
let rowData = Array.from(this._transitions.entries());
rowData.sort((a, b) => b[1].length - a[1].length);
new LazyTable(this.$('#nameTable'), rowData, ([name, maps]) => {
let row = DOM.tr();
row.maps = maps;
row.classList.add('clickable');
row.addEventListener(
'click',
(e) => this.dispatchEvent(new SelectionEvent(
e.target.parentNode.maps.map((map) => map.to))));
row.appendChild(DOM.td(maps.length));
row.appendChild(DOM.td(name));
return row;
});
}
});

View File

@ -7,11 +7,22 @@ found in the LICENSE file. -->
<style>
.panel {
padding-bottom: 0px;
position: relative;
}
.titleBackground {
position: absolute;
left: 0px;
top: 34px;
border-radius: 0 0 0 7px;
height: calc(100% - 34px);
width: 30px;
background-color: rgba(var(--border-color),0.2);
}
</style>
</head>
<div class="panel">
<h2>Timeline Panel</h2>
<div class="titleBackground"></div>
<div>
<slot></slot>
</div>

View File

@ -63,6 +63,19 @@ found in the LICENSE file. -->
font-size: 10px;
}
#title {
position: relative;
float: left;
width: 20px;
writing-mode: vertical-rl;
text-orientation: mixed;
margin: 0 0 0 -10px;
padding: 5px 5px 0px 5px;
overflow: hidden;
border-radius: 7px;
font-weight: 400;
}
.legend {
position: relative;
float: right;
@ -102,6 +115,10 @@ found in the LICENSE file. -->
background-color: var(--timeline-background-color);
}
#selection {
display: none;
}
#rightHandle,
#leftHandle {
background-color: rgba(200, 200, 200, 0.5);
@ -124,6 +141,8 @@ found in the LICENSE file. -->
position: absolute;
}
</style>
<h3 id="title"></h3>
<div class="legend">
<table id="legendTable">
<thead>

View File

@ -34,6 +34,16 @@ DOM.defineCustomElement('view/timeline/timeline-track',
this.isLocked = false;
}
static get observedAttributes() {
return ['title'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == 'title') {
this.$('#title').innerHTML = newValue;
}
}
_handleFilterTimeline(type) {
this._updateChunks();
}
@ -99,8 +109,8 @@ DOM.defineCustomElement('view/timeline/timeline-track',
}
_updateChunks() {
this._chunks = this._timeline.chunks(
this.nofChunks, each => this._legend.filter(each));
this._chunks =
this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
this.update();
}
@ -142,9 +152,9 @@ DOM.defineCustomElement('view/timeline/timeline-track',
let increment = 0;
let lastHeight = 0.0;
const stops = [];
for (let breakdown of chunk.getBreakdown(map => map.type)) {
const color = this._legend.colorForType(breakdown.type);
increment += breakdown.count;
for (let group of chunk.getBreakdown(map => map.type)) {
const color = this._legend.colorForType(group.key);
increment += group.count;
let height = (increment / total * kHeight) | 0;
stops.push(`${color} ${lastHeight}px ${height}px`)
lastHeight = height;
@ -232,7 +242,7 @@ DOM.defineCustomElement('view/timeline/timeline-track',
event.layerY / event.target.offsetHeight * (chunk.size() - 1));
let logEntry = chunk.at(relativeIndex);
this.dispatchEvent(new FocusEvent(logEntry));
this.dispatchEvent(new ToolTipEvent(logEntry.toString(), event.target));
this.dispatchEvent(new ToolTipEvent(logEntry.toStringLong(), event.target));
}
_handleChunkClick(event) {
@ -520,6 +530,7 @@ class Legend {
_timeline;
_typesFilters = new Map();
_typeClickHandler = this._handleTypeClick.bind(this);
_filterPredicate = this.filter.bind(this);
onFilter = () => {};
constructor(table) {
@ -528,14 +539,21 @@ class Legend {
set timeline(timeline) {
this._timeline = timeline;
this._typesFilters =
new Map(timeline.getBreakdown().map(each => [each.type, true]));
this._colors = new Map(
timeline.getBreakdown().map(each => [each.type, CSSColor.at(each.id)]));
const groups = timeline.getBreakdown();
this._typesFilters = new Map(groups.map(each => [each.key, true]));
this._colors =
new Map(groups.map(each => [each.key, CSSColor.at(each.id)]));
}
get selection() {
return this._timeline.selection ?? this._timeline;
return this._timeline.selectionOrSelf;
}
get filterPredicate() {
for (let visible of this._typesFilters.values()) {
if (!visible) return this._filterPredicate;
}
return undefined;
}
colorForType(type) {
@ -549,12 +567,11 @@ class Legend {
update() {
const tbody = DOM.tbody();
const missingTypes = new Set(this._typesFilters.keys());
this.selection.getBreakdown().forEach(each => {
tbody.appendChild(this._breakdownRow(each));
missingTypes.delete(each.type);
this.selection.getBreakdown().forEach(group => {
tbody.appendChild(this._addTypeRow(group));
missingTypes.delete(group.key);
});
missingTypes.forEach(
each => tbody.appendChild(this._row('', each, 0, '0%')));
missingTypes.forEach(key => tbody.appendChild(this._row('', key, 0, '0%')));
if (this._timeline.selection) {
tbody.appendChild(
this._row('', 'Selection', this.selection.length, '100%'));
@ -572,21 +589,20 @@ class Legend {
return row
}
_breakdownRow(breakdown) {
const color = this.colorForType(breakdown.type);
_addTypeRow(group) {
const color = this.colorForType(group.key);
const colorDiv = DOM.div('colorbox');
if (this._typesFilters.get(breakdown.type)) {
if (this._typesFilters.get(group.key)) {
colorDiv.style.backgroundColor = color;
} else {
colorDiv.style.borderColor = color;
colorDiv.style.backgroundColor = CSSColor.backgroundImage;
}
let percent =
`${(breakdown.count / this.selection.length * 100).toFixed(1)}%`;
const row = this._row(colorDiv, breakdown.type, breakdown.count, percent);
let percent = `${(group.count / this.selection.length * 100).toFixed(1)}%`;
const row = this._row(colorDiv, group.key, group.count, percent);
row.className = 'clickable';
row.onclick = this._typeClickHandler;
row.data = breakdown.type;
row.data = group.key;
return row;
}

View File

@ -70,6 +70,7 @@ DOM.defineCustomElement(
_useBottom(viewportY) {
return viewportY <= 400;
}
_useRight(viewportX) {
return viewportX < document.documentElement.clientWidth / 2;
}