[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:
parent
ab4d9717f2
commit
88f7740636
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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};
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
62
tools/system-analyzer/view/list-panel-template.html
Normal file
62
tools/system-analyzer/view/list-panel-template.html
Normal 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>
|
181
tools/system-analyzer/view/list-panel.mjs
Normal file
181
tools/system-analyzer/view/list-panel.mjs
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
@ -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;
|
||||
});
|
||||
}
|
||||
});
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -70,6 +70,7 @@ DOM.defineCustomElement(
|
||||
_useBottom(viewportY) {
|
||||
return viewportY <= 400;
|
||||
}
|
||||
|
||||
_useRight(viewportX) {
|
||||
return viewportX < document.documentElement.clientWidth / 2;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user