[tools] Add api events timeline-track to system-analyzer

- Clean up entry selection code
- Add source positions for code and deopt events
- Fix log entry selection from script
- Improve log parsing speed

Bug: v8:10644
Change-Id: Ie466679132b8ce24506ecf75223118b32275f931
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2569756
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Reviewed-by: Sathya Gunasekaran  <gsathya@chromium.org>
Cr-Commit-Position: refs/heads/master@{#71624}
This commit is contained in:
Camillo Bruni 2020-12-03 18:48:45 +01:00 committed by Commit Bot
parent ddbc2ba4a1
commit 1d7aa2f8d0
16 changed files with 294 additions and 138 deletions

View File

@ -26,6 +26,7 @@ group("v8_mjsunit") {
"../../tools/profile_view.mjs",
"../../tools/splaytree.mjs",
"../../tools/system-analyzer/helper.mjs",
"../../tools/system-analyzer/log/api.mjs",
"../../tools/system-analyzer/log/code.mjs",
"../../tools/system-analyzer/log/ic.mjs",
"../../tools/system-analyzer/log/log.mjs",

View File

@ -62,7 +62,11 @@ export class CsvParser {
}
// Convert the selected escape sequence to a single character.
let escapeChars = string.substring(pos, nextPos);
result += String.fromCharCode(parseInt(escapeChars, 16));
if (escapeChars === '2C') {
result += ',';
} else {
result += String.fromCharCode(parseInt(escapeChars, 16));
}
}
// Continue looking for the next escape sequence.

View File

@ -47,6 +47,7 @@ export class Script {
source;
// Map<line, Map<column, SourcePosition>>
lineToColumn = new Map();
_entries = [];
constructor(id) {
this.id = id;
@ -62,6 +63,10 @@ export class Script {
return this.source.length;
}
get entries() {
return this._entries;
}
addSourcePosition(line, column, entry) {
let sourcePosition = this.lineToColumn.get(line)?.get(column);
if (sourcePosition === undefined) {
@ -69,6 +74,7 @@ export class Script {
this._addSourcePosition(line, column, sourcePosition);
}
sourcePosition.addEntry(entry);
this._entries.push(entry);
return sourcePosition;
}
@ -83,10 +89,12 @@ export class Script {
this.sourcePositions.push(sourcePosition);
columnToSourcePosition.set(column, sourcePosition);
}
}
class SourceInfo{
class SourceInfo {
script;
start;
end;
@ -107,6 +115,10 @@ class SourceInfo{
setDisassemble(code) {
this.disassemble = code;
}
getSourceCode() {
return this.script.source?.substring(this.start, this.end);
}
}
/**
@ -170,7 +182,7 @@ export class Profile {
return this.CodeState.IGNITION;
case '-':
return this.CodeState.NATIVE_CONTEXT_INDEPENDENT;
case '=':
case '+':
return this.CodeState.TURBOPROP;
case '*':
return this.CodeState.TURBOFAN;
@ -639,9 +651,14 @@ class DynamicFuncCodeEntry extends CodeEntry {
constructor(size, type, func, state) {
super(size, '', type);
this.func = func;
func.addDynamicCode(this);
this.state = state;
}
getSourceCode() {
return this.source?.getSourceCode();
}
static STATE_PREFIX = ["", "~", "-", "+", "*"];
getState() {
return DynamicFuncCodeEntry.STATE_PREFIX[this.state];
@ -675,10 +692,26 @@ class DynamicFuncCodeEntry extends CodeEntry {
* @constructor
*/
class FunctionEntry extends CodeEntry {
// Contains the list of generated code for this function.
_codeEntries = new Set();
constructor(name) {
super(0, name);
}
addDynamicCode(code) {
if (code.func != this) {
throw new Error("Adding dynamic code to wrong function");
}
this._codeEntries.add(code);
}
getSourceCode() {
// All code entries should map to the same source positions.
return this._codeEntries.values().next().value.getSourceCode();
}
/**
* Returns node name.
*/

View File

@ -17,6 +17,7 @@ class State {
_mapTimeline;
_deoptTimeline;
_codeTimeline;
_apiTimeline;
_minStartTime = Number.POSITIVE_INFINITY;
_maxEndTime = Number.NEGATIVE_INFINITY;
@ -38,11 +39,13 @@ class State {
}
}
setTimelines(mapTimeline, icTimeline, deoptTimeline, codeTimeline) {
setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline) {
this._mapTimeline = mapTimeline;
this._icTimeline = icTimeline;
this._deoptTimeline = deoptTimeline;
this._codeTimeline = codeTimeline;
this._apiTimeline = apiTimeline;
for (let timeline of arguments) {
if (timeline === undefined) return;
this._minStartTime = Math.min(this._minStartTime, timeline.startTime);
@ -70,9 +73,14 @@ class State {
return this._codeTimeline;
}
get apiTimeline() {
return this._apiTimeline;
}
get timelines() {
return [
this.mapTimeline, this.icTimeline, this.deoptTimeline, this.codeTimeline
this._mapTimeline, this._icTimeline, this._deoptTimeline,
this._codeTimeline, this._apiTimeline
];
}

View File

@ -18,9 +18,7 @@ found in the LICENSE file. -->
// Delay loading of the main App
(async function() {
let module = await import('./index.mjs');
globalThis.app = new module.App("#log-file-reader", "#map-panel", "#map-stats-panel",
"#timeline-panel", "#ic-panel", "#map-track", "#ic-track", "#deopt-track",
"#code-track", "#source-panel", "#code-panel", "#tool-tip");
globalThis.app = new module.App();
})();
</script>
@ -104,6 +102,7 @@ found in the LICENSE file. -->
<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-panel>
<div class="panels">
<map-panel id="map-panel"></map-panel>
@ -159,16 +158,17 @@ found in the LICENSE file. -->
<code>--trace-maps</code>
</a>
</dt>
<dd>Log<a href="https://v8.dev/blog/fast-properties" target="_blank">
Maps</a></dd>
<dd>
Log<a href="https://v8.dev/blog/fast-properties">Maps</a>
</dd>
<dt>
<a href="https://source.chromium.org/search?q=FLAG_trace_ic">
<code>--trace-ic</code>
</a>
</dt>
<dd>Log
<a href="https://mathiasbynens.be/notes/shapes-ics" target="_blank">
ICs</a></dd>
<dd>
Log <a href="https://mathiasbynens.be/notes/shapes-ics">ICs</a>
</dd>
<dt>
<a href="https://source.chromium.org/search?q=FLAG_log_source_code">
<code>--log-source-code</code>
@ -181,6 +181,12 @@ found in the LICENSE file. -->
</a>
</dt>
<dd>Log detailed generated generated code</dd>
<dt>
<a href="https://source.chromium.org/search?q=FLAG_log_api">
<code>--log-api</code>
</a>
</dt>
<dd>Log various API uses.</dd>
</dl>
<h3>Keyboard Shortcuts for Navigation</h3>

View File

@ -5,6 +5,8 @@
import {SourcePosition} from '../profile.mjs';
import {State} from './app-model.mjs';
import {ApiLogEntry} from './log/api.mjs';
import {DeoptLogEntry} from './log/code.mjs';
import {CodeLogEntry} from './log/code.mjs';
import {IcLogEntry} from './log/ic.mjs';
import {MapLogEntry} from './log/map.mjs';
@ -17,24 +19,22 @@ class App {
_view;
_navigation;
_startupPromise;
constructor(
fileReaderId, mapPanelId, mapStatsPanelId, timelinePanelId, icPanelId,
mapTrackId, icTrackId, deoptTrackId, codeTrackId, sourcePanelId,
codePanelId, toolTipId) {
constructor() {
this._view = {
__proto__: null,
logFileReader: $(fileReaderId),
icPanel: $(icPanelId),
mapPanel: $(mapPanelId),
mapStatsPanel: $(mapStatsPanelId),
timelinePanel: $(timelinePanelId),
mapTrack: $(mapTrackId),
icTrack: $(icTrackId),
deoptTrack: $(deoptTrackId),
codeTrack: $(codeTrackId),
sourcePanel: $(sourcePanelId),
codePanel: $(codePanelId),
toolTip: $(toolTipId),
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'),
sourcePanel: $('#source-panel'),
toolTip: $('#tool-tip'),
};
this.toggleSwitch = $('.theme-switch input[type="checkbox"]');
this.toggleSwitch.addEventListener('change', (e) => this.switchTheme(e));
@ -67,19 +67,61 @@ class App {
}
handleShowEntries(e) {
const entry = e.entries[0];
if (entry instanceof MapLogEntry) {
this.showMapEntries(e.entries);
} else if (entry instanceof IcLogEntry) {
this.showIcEntries(e.entries);
} else if (entry instanceof SourcePosition) {
this.showSourcePositionEntries(e.entries);
} else if (e.entries[0] instanceof CodeLogEntry) {
this.showCodeEntries(e.entries);
} else {
throw new Error('Unknown selection type!');
}
e.stopPropagation();
this.showEntries(e.entries);
}
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));
}
showEntriesOfSingleType(entries) {
switch (entries[0].constructor) {
case SourcePosition:
return this.showSourcePositions(entries);
case MapLogEntry:
return this.showMapEntries(entries);
case IcLogEntry:
return this.showIcEntries(entries);
case ApiLogEntry:
return this.showApiEntries(entries);
case CodeLogEntry:
return this.showCodeEntries(entries);
case DeoptLogEntry:
return this.showDeoptEntries(entries);
default:
throw new Error('Unknown selection type!');
}
}
handleShowEntryDetail(e) {
e.stopPropagation();
const entry = e.entry;
switch (entry.constructor) {
case SourcePosition:
return this.selectSourcePosition(entry);
case MapLogEntry:
return this.selectMapLogEntry(entry);
case IcLogEntry:
return this.selectIcLogEntry(entry);
case ApiLogEntry:
return this.selectApiLogEntry(entry);
case CodeLogEntry:
return this.selectCodeLogEntry(entry);
case DeoptLogEntry:
return this.selectDeoptLogEntry(entry);
default:
throw new Error('Unknown selection type!');
}
}
showMapEntries(entries) {
@ -94,17 +136,22 @@ class App {
}
showDeoptEntries(entries) {
// TODO: creat list panel.
// TODO: create list panel.
this._state.selectedDeoptLogEntries = entries;
}
showCodeEntries(entries) {
// TODO: creat list panel
// TODO: create list panel
this._state.selectedCodeLogEntries = entries;
this._view.codePanel.selectedEntries = entries;
}
showSourcePositionEntries(entries) {
showApiEntries(entries) {
// TODO: create list panel
this._state.selectedApiLogEntries = entries;
}
showSourcePositions(entries) {
// TODO: Handle multiple source position selection events
this._view.sourcePanel.selectedSourcePositions = entries
}
@ -120,32 +167,17 @@ class App {
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._view.timelinePanel.timeSelection = {start, end};
}
handleShowEntryDetail(e) {
const entry = e.entry;
if (entry instanceof MapLogEntry) {
this.selectMapLogEntry(e.entry);
} else if (entry instanceof IcLogEntry) {
this.selectICLogEntry(e.entry);
} else if (entry instanceof SourcePosition) {
this.selectSourcePosition(e.entry);
} else if (e.entry instanceof CodeLogEntry) {
this.selectCodeLogEntry(e.entry);
} else {
throw new Error('Unknown selection type!');
}
e.stopPropagation();
}
selectMapLogEntry(entry) {
this._state.map = entry;
this._view.mapTrack.selectedEntry = entry;
this._view.mapPanel.map = entry;
}
selectICLogEntry(entry) {
selectIcLogEntry(entry) {
this._state.ic = entry;
this._view.icPanel.selectedEntry = [entry];
}
@ -155,6 +187,15 @@ class App {
this._view.codePanel.entry = entry;
}
selectDeoptLogEntry(entry) {
// TODO
}
selectApiLogEntry(entry) {
this._state.apiLogEntry = entry;
this._view.apiTrack.selectedEntry = entry;
}
selectSourcePosition(sourcePositions) {
if (!sourcePositions.script) return;
this._view.sourcePanel.selectedSourcePositions = [sourcePositions];
@ -183,8 +224,9 @@ class App {
const icTimeline = processor.icTimeline;
const deoptTimeline = processor.deoptTimeline;
const codeTimeline = processor.codeTimeline;
const apiTimeline = processor.apiTimeline;
this._state.setTimelines(
mapTimeline, icTimeline, deoptTimeline, codeTimeline);
mapTimeline, icTimeline, deoptTimeline, codeTimeline, apiTimeline);
// Transitions must be set before timeline for stats panel.
this._view.mapPanel.timeline = mapTimeline;
this._view.mapStatsPanel.transitions =
@ -208,6 +250,7 @@ class App {
this._view.icTrack.data = this._state.icTimeline;
this._view.deoptTrack.data = this._state.deoptTimeline;
this._view.codeTrack.data = this._state.codeTimeline;
this._view.apiTrack.data = this._state.apiTimeline;
}
switchTheme(event) {

View File

@ -0,0 +1,14 @@
// 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 {LogEntry} from './log.mjs';
export class ApiLogEntry extends LogEntry {
constructor(type, time, name) {
super(type, time);
this._name = name;
}
toString() {
return `Api(${this.type}): ${this._name}`;
}
}

View File

@ -14,7 +14,7 @@ export class IcLogEntry extends LogEntry {
} else if (this.type.indexOf('Load') !== -1) {
this.category = 'Load';
}
let parts = fn_file.split(' ');
const parts = fn_file.split(' ');
this.functionName = parts[0];
this.file = parts[1];
let position = line + ':' + column;

View File

@ -3,19 +3,20 @@
// found in the LICENSE file.
export class LogEntry {
_time;
_type;
constructor(type, time) {
// TODO(zcankara) remove type and add empty getters to override
this._time = time;
this._type = type;
this.sourcePosition = undefined;
}
get time() {
return this._time;
}
get type() {
return this._type;
}
// Returns an Array of all possible #type values.
static get allTypes() {
throw new Error('Not implemented.');

View File

@ -34,17 +34,6 @@ define(Array.prototype, 'last', function() {
// Map Log Events
class MapLogEntry extends LogEntry {
id = -1;
edge = undefined;
children = [];
depth = 0;
_isDeprecated = false;
deprecatedTargets = null;
leftId = 0;
rightId = 0;
filePosition = '';
script = '';
description = '';
constructor(id, time) {
if (!time) throw new Error('Invalid time');
// Use MapLogEntry.type getter instead of property, since we only know the
@ -52,6 +41,17 @@ 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;
this._isDeprecated = false;
this.deprecatedTargets = null;
this.leftId = 0;
this.rightId = 0;
this.filePosition = '';
this.script = '';
this.description = '';
}
toString() {

View File

@ -5,6 +5,7 @@
import {LogReader, parseString, parseVarArgs} from '../logreader.mjs';
import {Profile} from '../profile.mjs';
import {ApiLogEntry} from './log/api.mjs';
import {CodeLogEntry, DeoptLogEntry} from './log/code.mjs';
import {IcLogEntry} from './log/ic.mjs';
import {Edge, MapLogEntry} from './log/map.mjs';
@ -18,7 +19,10 @@ export class Processor extends LogReader {
_icTimeline = new Timeline();
_deoptTimeline = new Timeline();
_codeTimeline = new Timeline();
_apiTimeline = new Timeline();
_formatPCRegexp = /(.*):[0-9]+:[0-9]+$/;
_lastTimestamp = 0;
_lastCodeLogEntry;
MAJOR_VERSION = 7;
MINOR_VERSION = 6;
constructor(logString) {
@ -115,6 +119,10 @@ export class Processor extends LogReader {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'StoreInArrayLiteralIC')
},
'api': {
parsers: [parseString, parseVarArgs],
processor: this.processApiEvent
},
};
if (logString) this.processString(logString);
}
@ -182,7 +190,21 @@ export class Processor extends LogReader {
});
}
processV8Version(majorVersion, minorVersion) {
if ((majorVersion == this.MAJOR_VERSION &&
minorVersion <= this.MINOR_VERSION) ||
(majorVersion < this.MAJOR_VERSION)) {
window.alert(
`Unsupported version ${majorVersion}.${minorVersion}. \n` +
`Please use the matching tool for given the V8 version.`);
}
}
processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
this._lastTimestamp = timestamp;
if (timestamp == 5724567) {
console.log(start);
}
let entry;
let stateName = '';
if (maybe_func.length) {
@ -194,26 +216,20 @@ export class Processor extends LogReader {
} else {
entry = this._profile.addCode(type, name, timestamp, start, size);
}
this._codeTimeline.push(
new CodeLogEntry(type + stateName, timestamp, kind, entry));
this._lastCodeLogEntry =
new CodeLogEntry(type + stateName, timestamp, kind, entry);
this._codeTimeline.push(this._lastCodeLogEntry);
}
processCodeDeopt(
timestamp, codeSize, instructionStart, inliningId, scriptOffset,
deoptKind, deoptLocation, deoptReason) {
this._deoptTimeline.push(new DeoptLogEntry(
this._lastTimestamp = timestamp;
const logEntry = new DeoptLogEntry(
deoptKind, timestamp, deoptReason, deoptLocation, scriptOffset,
instructionStart, codeSize, inliningId));
}
processV8Version(majorVersion, minorVersion) {
if ((majorVersion == this.MAJOR_VERSION &&
minorVersion <= this.MINOR_VERSION) ||
(majorVersion < this.MAJOR_VERSION)) {
window.alert(
`Unsupported version ${majorVersion}.${minorVersion}. \n` +
`Please use the matching tool for given the V8 version.`);
}
instructionStart, codeSize, inliningId);
this._deoptTimeline.push(logEntry);
this.addSourcePosition(this._profile.findEntry(instructionStart), logEntry);
}
processScriptSource(scriptId, url, source) {
@ -233,11 +249,24 @@ export class Processor extends LogReader {
}
processCodeSourceInfo(
start, script, startPos, endPos, sourcePositions, inliningPositions,
start, scriptId, startPos, endPos, sourcePositions, inliningPositions,
inlinedFunctions) {
this._profile.addSourcePositions(
start, script, startPos, endPos, sourcePositions, inliningPositions,
start, scriptId, startPos, endPos, sourcePositions, inliningPositions,
inlinedFunctions);
let profileEntry = this._profile.findEntry(start);
if (profileEntry !== this._lastCodeLogEntry._entry) return;
this.addSourcePosition(profileEntry, this._lastCodeLogEntry);
this._lastCodeLogEntry = undefined;
}
addSourcePosition(profileEntry, logEntry) {
let script = this.getProfileEntryScript(profileEntry);
const parts = profileEntry.getRawName().split(':');
if (parts.length < 3) return;
const line = parseInt(parts[parts.length - 2]);
const column = parseInt(parts[parts.length - 1]);
logEntry.sourcePosition = script.addSourcePosition(line, column, logEntry);
}
processCodeDisassemble(start, kind, disassemble) {
@ -247,6 +276,7 @@ export class Processor extends LogReader {
processPropertyIC(
type, pc, time, line, column, old_state, new_state, map, key, modifier,
slow_reason) {
this._lastTimestamp = time;
let profileEntry = this._profile.findEntry(pc);
let fnName = this.formatProfileEntry(profileEntry);
let script = this.getProfileEntryScript(profileEntry);
@ -284,6 +314,7 @@ export class Processor extends LogReader {
}
processMap(type, time, from, to, pc, line, column, reason, name) {
this._lastTimestamp = time;
const time_ = parseInt(time);
if (type === 'Deprecate') return this.deprecateMap(type, time_, from);
// Skip normalized maps that were cached so we don't introduce multiple
@ -318,12 +349,14 @@ export class Processor extends LogReader {
}
deprecateMap(type, time, id) {
this._lastTimestamp = time;
this.getMapEntry(id, time).deprecate();
}
processMapCreate(time, id) {
// map-create events might override existing maps if the addresses get
// recycled. Hence we do not check for existing maps.
this._lastTimestamp = time;
this.createMapEntry(id, time);
}
@ -334,6 +367,7 @@ export class Processor extends LogReader {
}
createMapEntry(id, time) {
this._lastTimestamp = time;
const map = new MapLogEntry(id, time);
this._mapTimeline.push(map);
return map;
@ -357,6 +391,16 @@ export class Processor extends LogReader {
return script;
}
processApiEvent(name, varArgs) {
if (varArgs.length == 0) {
varArgs = [name];
const index = name.indexOf(':');
if (index > 0) name = name.substr(0, index);
}
this._apiTimeline.push(
new ApiLogEntry(name, this._lastTimestamp, varArgs[0]));
}
get icTimeline() {
return this._icTimeline;
}
@ -373,6 +417,10 @@ export class Processor extends LogReader {
return this._codeTimeline;
}
get apiTimeline() {
return this._apiTimeline;
}
get scripts() {
return this._profile.scripts_.filter(script => script !== undefined);
}

View File

@ -25,6 +25,8 @@ DOM.defineCustomElement(
set selectedEntries(entries) {
this._selectedEntries = entries;
// TODO: add code selection dropdown
this._entry = entries.first();
this.update();
}

View File

@ -105,7 +105,7 @@ DOM.defineCustomElement('view/source-panel',
const option =
this.scriptDropdown.options[this.scriptDropdown.selectedIndex];
this.script = option.script;
this.selectLogEntries(this._script.entries());
this.selectLogEntries(this._script.entries);
}
handleSourcePositionClick(e) {
@ -124,21 +124,7 @@ DOM.defineCustomElement('view/source-panel',
}
selectLogEntries(logEntries) {
let icLogEntries = [];
let mapLogEntries = [];
for (const entry of logEntries) {
if (entry instanceof MapLogEntry) {
mapLogEntries.push(entry);
} else if (entry instanceof IcLogEntry) {
icLogEntries.push(entry);
}
}
if (icLogEntries.length > 0) {
this.dispatchEvent(new SelectionEvent(icLogEntries));
}
if (mapLogEntries.length > 0) {
this.dispatchEvent(new SelectionEvent(mapLogEntries));
}
this.dispatchEvent(new SelectionEvent(logEntries));
}
});

View File

@ -4,6 +4,11 @@ found in the LICENSE file. -->
<head>
<link href="./index.css" rel="stylesheet">
<style>
.panel {
padding-bottom: 0px;
}
</style>
</head>
<div class="panel">
<h2>Timeline Panel</h2>

View File

@ -63,36 +63,40 @@ found in the LICENSE file. -->
font-size: 10px;
}
#legend {
.legend {
position: relative;
float: right;
width: 100%;
max-width: 280px;
padding-left: 20px;
padding-top: 10px;
height: calc(200px + 12px);
overflow-y: scroll;
margin-right: -10px;
padding-right: 2px;
}
#legendTable {
width: 280px;
border-collapse: collapse;
}
th,
td {
width: 200px;
text-align: left;
padding-bottom: 3px;
padding: 1px 3px 2px 3px;
}
/* right align numbers */
#legend td:nth-of-type(4n+3),
#legend td:nth-of-type(4n+4) {
text-align: right;
}
/* Center colors */
#legend td:nth-of-type(4n+1) {
text-align: center;;
#legendTable td:nth-of-type(4n+1) {
text-align: center;
padding-top: 3px;
}
.legendTypeColumn {
/* Left align text*/
#legendTable td:nth-of-type(4n+2) {
text-align: left;
width: 100%;
}
/* right align numbers */
#legendTable td:nth-of-type(4n+3),
#legendTable td:nth-of-type(4n+4) {
text-align: right;
}
.timeline {
background-color: var(--timeline-background-color);
@ -120,17 +124,18 @@ found in the LICENSE file. -->
position: absolute;
}
</style>
<table id="legend" class="typeStatsTable">
<thead>
<tr>
<td></td>
<td>Type</td>
<td>Count</td>
<td>Percent</td>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="legend">
<table id="legendTable">
<thead>
<tr>
<td>Type</td>
<td>Count</td>
<td>Percent</td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="timeline">
<div id="selection">
<div id="leftHandle"></div>

View File

@ -25,7 +25,7 @@ DOM.defineCustomElement('view/timeline/timeline-track',
constructor() {
super(templateText);
this._selectionHandler = new SelectionHandler(this);
this._legend = new Legend(this.$('#legend'));
this._legend = new Legend(this.$('#legendTable'));
this._legend.onFilter = (type) => this._handleFilterTimeline();
this.timelineNode.addEventListener(
'scroll', e => this._handleTimelineScroll(e));