[tools] Add Indicium

Indicium is a new tool that integrates all our Map and IC processing
tools into one tool.

This CL does not attempt to cleanly integrate the Map Processor
and IC explorer, but provides an in initial starting point for further
integration work.

Bug: v8:10644
Change-Id: I753c116fd409c8c07613bf15f22e14aa1e8c8a0e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2259935
Commit-Queue: Sathya Gunasekaran  <gsathya@chromium.org>
Reviewed-by: Sathya Gunasekaran  <gsathya@chromium.org>
Reviewed-by: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#68605}
This commit is contained in:
zeynepCankara 2020-06-29 22:21:56 +03:00 committed by Commit Bot
parent 83ac374209
commit e7357f1902
15 changed files with 3173 additions and 0 deletions

View File

@ -0,0 +1,31 @@
// 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.
const KB = 1024;
const MB = KB * KB;
const GB = MB * KB;
const kMillis2Seconds = 1 / 1000;
function formatBytes(bytes) {
const units = ['B', 'KiB', 'MiB', 'GiB'];
const divisor = 1024;
let index = 0;
while (index < units.length && bytes >= divisor) {
index++;
bytes /= divisor;
}
return bytes.toFixed(2) + units[index];
}
function formatSeconds(millis) {
return (millis * kMillis2Seconds).toFixed(2) + 's';
}
function defineCustomElement(name, generator) {
let htmlTemplatePath = name + '-template.html';
fetch(htmlTemplatePath)
.then(stream => stream.text())
.then(
templateText => customElements.define(name, generator(templateText)));
}

View File

@ -0,0 +1,71 @@
// 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.
'use strict';
let entries = [];
export let properties = [
'type',
'category',
'functionName',
'filePosition',
'state',
'key',
'map',
'reason',
'file',
];
// 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() {
this.groups = {};
for (let i = 0; i < properties.length; i++) {
let subProperty = properties[i];
if (this.property == subProperty) continue;
this.groups[subProperty] = Group.groupBy(this.entries, subProperty);
}
}
static groupBy(entries, property) {
let accumulator = Object.create(null);
let length = entries.length;
for (let i = 0; i < length; i++) {
let entry = entries[i];
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

@ -0,0 +1,97 @@
<!-- 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. -->
<style>
.ic-panel {
background-color: #355EC2;
padding: 20px 20px 20px 20px ;
margin: auto;
}
#ic-panel {
background-color: #232323;
padding: 10px 10px 10px 10px ;
margin: auto;
overflow-x: scroll;
}
.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;
}
.entry-details {
}
.entry-details TD {
}
.details {
width: 0.1em;
}
.details span {
padding: 0 0.4em 0 0.4em;
background-color: black;
color: white;
border-radius: 25px;
text-align: center;
cursor: -webkit-zoom-in;
}
#legend {
padding-right: 20px;
}
</style>
<div class="ic-panel">
<section id="ic-panel">
<h1>IC Explorer</h1>
<div id="legend">
<div style="float:right; border-style: solid; border-width: 1px; padding:20px">
0 uninitialized<br>
X no feedback<br>
1 monomorphic<br>
^ recompute handler<br>
P polymorphic<br>
N megamorphic<br>
G generic
</div>
</div>
<h2>Data</h2>
<p>Trace Count: <span id="count">0</span></p>
<h2>Result</h2>
<p>
Group-Key:
<select id="group-key"></select>
</p>
Filter number of items
<br></br>
<input type="search" id="filter-input" placeholder="Number of items"></input>
<button id="filterICBtn">Filter</button>
<p>
<table id="table" width="100%">
<tbody id="table-body">
</tbody>
</table>
</p>
</section>
</div>

View File

@ -0,0 +1,217 @@
// 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, properties} from './ic-model.js';
defineCustomElement('ic-panel', (templateText) =>
class ICPanel extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this.groupKeySelect.addEventListener(
'change', e => this.updateTable(e));
this.filterICBtnSelect.addEventListener(
'click', e => this.handleICFilter(e));
this._noOfItems = 100;
}
$(id) {
return this.shadowRoot.querySelector(id);
}
querySelectorAll(query) {
return this.shadowRoot.querySelectorAll(query);
}
get groupKeySelect() {
return this.$('#group-key');
}
get filterICBtnSelect() {
return this.$('#filterICBtn');
}
get tableSelect() {
return this.$('#table');
}
get tableBodySelect() {
return this.$('#table-body');
}
get countSelect() {
return this.$('#count');
}
get spanSelectAll(){
return this.querySelectorAll("span");
}
updateTable(event) {
let select = this.groupKeySelect;
let key = select.options[select.selectedIndex].text;
let tableBody = this.tableBodySelect;
this.removeAllChildren(tableBody);
let groups = Group.groupBy(entries, key, true);
this.display(groups, tableBody);
}
escapeHtml(unsafe) {
if (!unsafe) return "";
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
processValue(unsafe) {
if (!unsafe) return "";
if (!unsafe.startsWith("http")) return this.escapeHtml(unsafe);
let a = document.createElement("a");
a.href = unsafe;
a.textContent = unsafe;
return a;
}
removeAllChildren(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
td(tr, content, className) {
let node = document.createElement("td");
if (typeof content == "object") {
node.appendChild(content);
} else {
node.innerHTML = content;
}
node.className = className;
tr.appendChild(node);
return node
}
set noOfItems(value){
this._noOfItems = value;
}
get noOfItems(){
return this._noOfItems;
}
display(entries, parent) {
let fragment = document.createDocumentFragment();
let max = Math.min(this.noOfItems, entries.length)
for (let i = 0; i < max; i++) {
let entry = entries[i];
let tr = document.createElement("tr");
tr.entry = entry;
let details = this.td(tr,'<span>&#8505;</a>', 'details');
details.onclick = _ => this.toggleDetails(details);
this.td(tr, entry.percentage + "%", 'percentage');
this.td(tr, entry.count, 'count');
this.td(tr, this.processValue(entry.key), 'key');
fragment.appendChild(tr);
}
let omitted = entries.length - max;
if (omitted > 0) {
let tr = document.createElement("tr");
let tdNode = td(tr, 'Omitted ' + omitted + " entries.");
tdNode.colSpan = 4;
fragment.appendChild(tr);
}
parent.appendChild(fragment);
}
displayDrilldown(entry, previousSibling) {
let tr = document.createElement('tr');
tr.className = "entry-details";
tr.style.display = "none";
// indent by one td.
tr.appendChild(document.createElement("td"));
let td = document.createElement("td");
td.colSpan = 3;
for (let key in entry.groups) {
td.appendChild(this.displayDrilldownGroup(entry, key));
}
tr.appendChild(td);
// Append the new TR after previousSibling.
previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling)
}
displayDrilldownGroup(entry, key) {
let max = 20;
let group = entry.groups[key];
let div = document.createElement("div")
div.className = 'drilldown-group-title'
div.textContent = key + ' [top ' + max + ' out of ' + group.length + ']';
let table = document.createElement("table");
this.display(group.slice(0, max), table, false)
div.appendChild(table);
return div;
}
toggleDetails(node) {
let tr = node.parentNode;
let entry = tr.entry;
// Create subgroup in-place if the don't exist yet.
if (entry.groups === undefined) {
entry.createSubGroups();
this.displayDrilldown(entry, tr);
}
let details = tr.nextSibling;
let display = details.style.display;
if (display != "none") {
display = "none";
} else {
display = "table-row"
};
details.style.display = display;
}
initGroupKeySelect() {
let select = this.groupKeySelect;
select.options.length = 0;
for (let i in properties) {
let option = document.createElement("option");
option.text = properties[i];
select.add(option);
}
}
//TODO(zc): Function processing the timestamps of ICEvents
// Processes the IC Events which have V8Map's in the map-processor
processICEventTime(){
let ICTimeToEvent = new Map();
// save the occurance time of V8Maps
let eventTimes = []
console.log("Num of stats: " + entries.length);
// fetch V8 Maps from the IC Events
entries.forEach(element => {
let v8Map = V8Map.get("0x" + element.map);
if(!v8Map){
ICTimeToEvent.set(-1, element);
} else {
ICTimeToEvent.set(v8Map.time, element);
eventTimes.push(v8Map.time);
}
});
eventTimes.sort();
// save the IC events which have Map states
let eventsList = [];
for(let i = 0; i < eventTimes.length; i++){
eventsList.push(ICTimeToEvent.get(eventTimes[i]));
}
return eventList;
}
handleICFilter(e){
let noOfItemsInput = parseInt(this.$('#filter-input').value);
this.noOfItems = noOfItemsInput;
this.updateTable(e);
}
});

View File

@ -0,0 +1,247 @@
// 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.
/**
* Parser for dynamic code optimization state.
*/
function parseState(s) {
switch (s) {
case '':
return Profile.CodeState.COMPILED;
case '~':
return Profile.CodeState.OPTIMIZABLE;
case '*':
return Profile.CodeState.OPTIMIZED;
}
throw new Error('unknown code state: ' + s);
}
class IcProcessor extends LogReader {
constructor() {
super();
let propertyICParser = [
parseInt, parseInt, parseInt, parseString, parseString, parseInt,
parseString, parseString, parseString
];
LogReader.call(this, {
'code-creation': {
parsers: [
parseString, parseInt, parseInt, parseInt, parseInt, parseString,
parseVarArgs
],
processor: this.processCodeCreation
},
'code-move':
{parsers: [parseInt, parseInt], processor: this.processCodeMove},
'code-delete': {parsers: [parseInt], processor: this.processCodeDelete},
'sfi-move':
{parsers: [parseInt, parseInt], processor: this.processFunctionMove},
'LoadGlobalIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'LoadGlobalIC')
},
'StoreGlobalIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'StoreGlobalIC')
},
'LoadIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'LoadIC')
},
'StoreIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'StoreIC')
},
'KeyedLoadIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'KeyedLoadIC')
},
'KeyedStoreIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'KeyedStoreIC')
},
'StoreInArrayLiteralIC': {
parsers: propertyICParser,
processor: this.processPropertyIC.bind(this, 'StoreInArrayLiteralIC')
},
});
this.profile_ = new Profile();
this.LoadGlobalIC = 0;
this.StoreGlobalIC = 0;
this.LoadIC = 0;
this.StoreIC = 0;
this.KeyedLoadIC = 0;
this.KeyedStoreIC = 0;
this.StoreInArrayLiteralIC = 0;
}
/**
* @override
*/
printError(str) {
print(str);
}
processString(string) {
let end = string.length;
let current = 0;
let next = 0;
let line;
let i = 0;
let entry;
while (current < end) {
next = string.indexOf('\n', current);
if (next === -1) break;
i++;
line = string.substring(current, next);
current = next + 1;
this.processLogLine(line);
}
}
processLogFile(fileName) {
this.collectEntries = true;
this.lastLogFileName_ = fileName;
let line;
while (line = readline()) {
this.processLogLine(line);
}
print();
print('=====================');
print('LoadGlobal: ' + this.LoadGlobalIC);
print('StoreGlobal: ' + this.StoreGlobalIC);
print('Load: ' + this.LoadIC);
print('Store: ' + this.StoreIC);
print('KeyedLoad: ' + this.KeyedLoadIC);
print('KeyedStore: ' + this.KeyedStoreIC);
print('StoreInArrayLiteral: ' + this.StoreInArrayLiteralIC);
}
addEntry(entry) {
this.entries.push(entry);
}
processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
if (maybe_func.length) {
let funcAddr = parseInt(maybe_func[0]);
let state = parseState(maybe_func[1]);
this.profile_.addFuncCode(
type, name, timestamp, start, size, funcAddr, state);
} else {
this.profile_.addCode(type, name, timestamp, start, size);
}
}
processCodeMove(from, to) {
this.profile_.moveCode(from, to);
}
processCodeDelete(start) {
this.profile_.deleteCode(start);
}
processFunctionMove(from, to) {
this.profile_.moveFunc(from, to);
}
formatName(entry) {
if (!entry) return '<unknown>';
let name = entry.func.getName();
let re = /(.*):[0-9]+:[0-9]+$/;
let array = re.exec(name);
if (!array) return name;
return entry.getState() + array[1];
}
// TODO(zc): Process the IC event togather with time
processPropertyIC(
type, pc, line, column, old_state, new_state, map, name, modifier,
slow_reason) {
this[type]++;
let entry = this.profile_.findEntry(pc);
print(
type + ' (' + old_state + '->' + new_state + modifier + ') at ' +
this.formatName(entry) + ':' + line + ':' + column + ' ' + name +
' (map 0x' + map.toString(16) + ')' +
(slow_reason ? ' ' + slow_reason : ''));
}
}
// ================
let entries = [];
let properties = [
'type',
'category',
'functionName',
'filePosition',
'state',
'key',
'map',
'reason',
'file',
];
class CustomIcProcessor extends IcProcessor {
constructor() {
super();
this.entries = [];
}
functionName(pc) {
let entry = this.profile_.findEntry(pc);
return this.formatName(entry);
}
processPropertyIC(
type, pc, line, column, old_state, new_state, map, key, modifier,
slow_reason) {
let fnName = this.functionName(pc);
this.entries.push(new Entry(
type, fnName, line, column, key, old_state, new_state, map,
slow_reason));
}
};
class Entry {
constructor(
type, fn_file, line, column, key, oldState, newState, map, reason,
additional) {
this.type = type;
this.category = 'other';
if (this.type.indexOf('Store') !== -1) {
this.category = 'Store';
} else if (this.type.indexOf('Load') !== -1) {
this.category = 'Load';
}
let parts = fn_file.split(' ');
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;
this.key = key;
this.map = map.toString(16);
this.reason = reason;
this.additional = additional;
}
parseMapProperties(parts, offset) {
let next = parts[++offset];
if (!next.startsWith('dict')) return offset;
this.propertiesMode = next.substr(5) == '0' ? 'fast' : 'slow';
this.numberOfOwnProperties = parts[++offset].substr(4);
next = parts[++offset];
this.instanceType = next.substr(5, next.length - 6);
return offset;
}
parsePositionAndFile(parts, start) {
// find the position of 'at' in the parts array.
let offset = start;
for (let i = start + 1; i < parts.length; i++) {
offset++;
if (parts[i] == 'at') break;
}
if (parts[offset] !== 'at') return -1;
this.position = parts.slice(start, offset).join(' ');
offset += 1;
this.isNative = parts[offset] == 'native'
offset += this.isNative ? 1 : 0;
this.file = parts[offset];
return offset;
}
}

View File

@ -0,0 +1,222 @@
<!DOCTYPE html>
<!-- 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. -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Indicium</title>
<link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'>
<!-- <link rel="icon" type="image/png" href="/images/favicon.png"/> -->
<!-- <script type="module" src="index.js"></script> -->
<script src="helper.js"></script>
<script type="module" src="log-file-reader.mjs"></script>
<script type="module" src="map-panel.mjs"></script>
<script type="module" src="timeline-panel.mjs"></script>
<script type="module" src="ic-panel.mjs"></script>
<script src="../splaytree.js"></script>
<script src="../codemap.js"></script>
<script src="../csvparser.js"></script>
<script src="../consarray.js"></script>
<script src="../profile.js"></script>
<script src="../profile_view.js"></script>
<script src="../logreader.js"></script>
<script src="../arguments.js"></script>
<script src="../SourceMap.js"></script>
<script src="./map-processor.js"></script>
<script src="./ic-processor.js"></script>
<script src="./map-model.js"></script>
<style>
body {
font-family: 'Roboto', sans-serif;
color: white;
margin-left: 5%;
margin-right: 5%;
background-color: #041531;
}
.colorbox {
width: 10px;
height: 10px;
border: 1px black solid;
}
#instructions {
padding: 10px 10px 60px 10px ;
margin: auto;
}
#stats {
display: flex;
height: 250px;
background-color: #232323;
padding: 10px 10px 10px 10px ;
margin: auto;
}
.stats-panel {
background-color: #355EC2;
padding: 20px 20px 20px 20px ;
margin: auto;
}
#stats table {
flex: 1;
padding-right: 50px;
max-height: 250px;
display: inline-block;
overflow-y: scroll;
}
#stats table td {
cursor: pointer;
}
#stats .transitionTable {
overflow-y: scroll;
}
#stats .transitionTable tr {
max-width: 200px;
}
#stats .transitionType {
text-align: right;
max-width: 380px;
}
#stats .transitionType tr td:nth-child(2) {
text-align: left;
}
#stats table thead td {
border-bottom: 1px black dotted;
}
/*
.indicium-logo {
width: 380px;
height: 165px;
background-image: url(./images/indicium-logo.png);
background-size: cover;
margin-top: 35px;
}
*/
</style>
<script>
'use strict';
// Event handlers
document.onkeydown = handleKeyDown;
function handleKeyDown(event) {
stateGlobal.navigation = document.state.navigation;
let nav = document.state.navigation;
switch(event.key) {
case "ArrowUp":
event.preventDefault();
if (event.shiftKey) {
nav.selectPrevEdge();
} else {
nav.moveInChunk(-1);
}
return false;
case "ArrowDown":
event.preventDefault();
if (event.shiftKey) {
nav.selectNextEdge();
} else {
nav.moveInChunk(1);
}
return false;
case "ArrowLeft":
nav.moveInChunks(false);
break;
case "ArrowRight":
nav.moveInChunks(true);
break;
case "+":
nav.increaseTimelineResolution();
break;
case "-":
nav.decreaseTimelineResolution();
break;
}
}
// Update application state
function updateDocumentState(){
document.state = stateGlobal.state;
try {
document.state.timeline = stateGlobal.timeline;
} catch (error) {
console.log(error);
console.log("cannot assign timeline to state!");
}
}
// Map event log processing
function handleLoadTextMapProcessor(text) {
let mapProcessor = new MapProcessor();
return mapProcessor.processString(text);
}
// IC event file reading and log processing
function loadFileIC(file) {
let reader = new FileReader();
reader.onload = function(evt) {
let icProcessor = new CustomIcProcessor();
icProcessor.processString(this.result);
entries = icProcessor.entries;
$("ic-panel").countSelect.innerHTML = entries.length;
$("ic-panel").updateTable(entries);
}
reader.readAsText(file);
$("ic-panel").initGroupKeySelect();
}
function $(id) { return document.querySelector(id); }
// holds the state of the application
let stateGlobal = Object.create(null);
// call when a new file uploaded
function globalDataUpload(e) {
stateGlobal.timeline = e.detail;
if(!e.detail) return;
// instantiate the app logic
stateGlobal.fileData = e.detail;
stateGlobal.state = new State();
stateGlobal.timeline = handleLoadTextMapProcessor(stateGlobal.fileData.chunk);
updateDocumentState();
// process the IC explorer
loadFileIC(stateGlobal.fileData.file);
}
function globalSearchBarEvent(e) {
if(!e.detail.isValidMap) return;
document.state.map = e.detail.map;
}
</script>
</head>
<body>
<!-- <div class="indicium-logo"></div> -->
<div id="content">
<section id="file-reader">
<br></br>
<log-file-reader id="log-file-reader" onchange="globalDataUpload(event)"></log-file-reader>
<br></br>
</section>
<div class="stats-panel">
<section id="stats"><h2>Stats</h2></section>
</div>
<timeline-panel id="timeline-panel" onchange="globalDataChanged(event)"></timeline-panel>
<map-panel id="map-panel" onclick="globalSearchBarEvent(event)"></map-panel>
<ic-panel id="ic-panel" onchange="globalDataChanged(event)"></ic-panel>
</div>
<div id="instructions">
<h2>Instructions</h2>
<p>Unified web interface for analyzing the trace information of the Maps/ICs</p>
<ul>
<li> Visualize Map trees that have gathered</li>
<li><code> /path/to/d8 --trace-maps $FILE</code></li>
<li> Visualize IC events that have gathered</li>
<li><code> /path/to/d8 --trace_ic $FILE (your_script.js) </code></li>
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,143 @@
// 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.
'use strict';
// TODO(zc) make imports work
// import "./helper.js";
import '../splaytree.js';
import '../codemap.js';
import '../csvparser.js';
import '../consarray.js';
import '../profile.js';
import '../profile_view.js';
import '../logreader.js';
import '../arguments.js';
import '../SourceMap.js';
import './map-processor.js';
import './ic-processor.js';
import './map-model.js';
define(Array.prototype, 'histogram', function(mapFn) {
let histogram = [];
for (let i = 0; i < this.length; i++) {
let value = this[i];
let index = Math.round(mapFn(value))
let bucket = histogram[index];
if (bucket !== undefined) {
bucket.push(value);
} else {
histogram[index] = [value];
}
}
for (let i = 0; i < histogram.length; i++) {
histogram[i] = histogram[i] || [];
}
return histogram;
});
Object.defineProperty(Edge.prototype, 'getColor', {
value: function() {
return transitionTypeToColor(this.type);
}
});
// ===================================
// Controller logic of the application
// Event handlers
document.onkeydown = handleKeyDown;
function handleKeyDown(event) {
stateGlobal.navigation = document.state.navigation;
let nav = document.state.navigation;
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
if (event.shiftKey) {
nav.selectPrevEdge();
} else {
nav.moveInChunk(-1);
}
return false;
case 'ArrowDown':
event.preventDefault();
if (event.shiftKey) {
nav.selectNextEdge();
} else {
nav.moveInChunk(1);
}
return false;
case 'ArrowLeft':
nav.moveInChunks(false);
break;
case 'ArrowRight':
nav.moveInChunks(true);
break;
case '+':
nav.increaseTimelineResolution();
break;
case '-':
nav.decreaseTimelineResolution();
break;
}
}
// Update application state
function updateDocumentState() {
document.state = stateGlobal.state;
try {
document.state.timeline = stateGlobal.timeline;
} catch (error) {
console.log(error);
console.log('cannot assign timeline to state!');
}
}
// Map event log processing
function handleLoadTextMapProcessor(text) {
let mapProcessor = new MapProcessor();
return mapProcessor.processString(text);
}
// IC event file reading and log processing
/*
function loadFileIC(file) {
let reader = new FileReader();
reader.onload = function(evt) {
let icProcessor = new CustomIcProcessor();
icProcessor.processString(this.result);
entries = icProcessor.entries;
$('ic-panel').countSelect.innerHTML = entries.length;
$('ic-panel').updateTable(entries);
} reader.readAsText(file);
$('ic-panel').initGroupKeySelect();
}
*/
function $(id) {
return document.querySelector(id);
}
// holds the state of the application
let stateGlobal = Object.create(null);
// call when a new file uploaded
function globalDataUpload(e) {
stateGlobal.timeline = e.detail;
if (!e.detail) return;
// instantiate the app logic
stateGlobal.fileData = e.detail;
stateGlobal.state = new State();
stateGlobal.timeline = handleLoadTextMapProcessor(stateGlobal.fileData.chunk);
updateDocumentState();
// process the IC explorer
loadFileIC(stateGlobal.fileData.file);
}
function globalSearchBarEvent(e) {
if (!e.detail.isValidMap) return;
document.state.map = e.detail.map;
}

View File

@ -0,0 +1,82 @@
<!-- 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. -->
<style>
#fileReader {
width: 100%;
height: 100px;
line-height: 100px;
text-align: center;
border: solid 1px #000000;
border-radius: 5px;
cursor: pointer;
transition: all 0.5s ease-in-out;
}
#fileReader.done {
height: 20px;
line-height: 20px;
}
#fileReader:hover {
background-color: #e0edfe ;
color: black;
}
.loading #fileReader {
cursor: wait;
}
#fileReader > input {
display: none;
}
#loader {
display: none;
}
.loading #loader {
display: block;
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.5);
}
#spinner {
position: absolute;
width: 100px;
height: 100px;
top: 40%;
left: 50%;
margin-left: -50px;
border: 30px solid #000;
border-top: 30px solid #36E;
border-radius: 50%;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<section id="fileReaderSection">
<div id="fileReader" tabindex=1 >
<span id="label">
Drag and drop a v8.log file into this area, or click to choose from disk.
</span>
<input id="file" type="file" name="file">
</div>
<div id="loader">
<div id="spinner"></div>
</div>
</section>

View File

@ -0,0 +1,79 @@
// 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.
defineCustomElement('log-file-reader', (templateText) =>
class LogFileReader extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this.addEventListener('click', e => this.handleClick(e));
this.addEventListener('dragover', e => this.handleDragOver(e));
this.addEventListener('drop', e => this.handleChange(e));
this.$('#file').addEventListener('change', e => this.handleChange(e));
this.$('#fileReader').addEventListener('keydown', e => this.handleKeyEvent(e));
}
$(id) {
return this.shadowRoot.querySelector(id);
}
get section() {
return this.$('#fileReaderSection');
}
updateLabel(text) {
this.$('#label').innerText = text;
}
handleKeyEvent(event) {
if (event.key == "Enter") this.handleClick(event);
}
handleClick(event) {
this.$('#file').click();
}
handleChange(event) {
// Used for drop and file change.
event.preventDefault();
var host = event.dataTransfer ? event.dataTransfer : event.target;
this.readFile(host.files[0]);
}
handleDragOver(event) {
event.preventDefault();
}
connectedCallback() {
this.$('#fileReader').focus();
}
readFile(file) {
if (!file) {
this.updateLabel('Failed to load file.');
return;
}
this.$('#fileReader').blur();
this.section.className = 'loading';
const reader = new FileReader();
reader.onload = (e) => {
try {
let dataModel = Object.create(null);
dataModel.file = file;
dataModel.chunk = e.target.result;
this.updateLabel('Finished loading \'' + file.name + '\'.');
this.dispatchEvent(new CustomEvent(
'change', {bubbles: true, composed: true, detail: dataModel}));
this.section.className = 'success';
this.$('#fileReader').classList.add('done');
} catch (err) {
console.error(err);
this.section.className = 'failure';
}
};
// Delay the loading a bit to allow for CSS animations to happen.
setTimeout(() => reader.readAsText(file), 0);
}
});

View File

@ -0,0 +1,788 @@
// 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.
'use strict';
// const kChunkHeight = 250;
// const kChunkWidth = 10;
class State {
constructor() {
this._nofChunks = 400;
this._map = undefined;
this._timeline = undefined;
this._chunks = undefined;
this._view = new View(this);
this._navigation = new Navigation(this, this.view);
}
get timeline() {
return this._timeline
}
set timeline(value) {
this._timeline = value;
this.updateChunks();
this.view.updateTimeline();
this.view.updateStats();
}
get chunks() {
return this._chunks
}
get nofChunks() {
return this._nofChunks
}
set nofChunks(count) {
this._nofChunks = count;
this.updateChunks();
this.view.updateTimeline();
}
get view() {
return this._view
}
get navigation() {
return this._navigation
}
get map() {
return this._map
}
set map(value) {
this._map = value;
this._navigation.updateUrl();
this.view.updateMapDetails();
this.view.redraw();
}
updateChunks() {
this._chunks = this._timeline.chunks(this._nofChunks);
}
get entries() {
if (!this.map) return {};
return {
map: this.map.id, time: this.map.time
}
}
}
// =========================================================================
// DOM Helper
function $(id) {
return document.getElementById(id)
}
function removeAllChildren(node) {
while (node.lastChild) {
node.removeChild(node.lastChild);
}
}
function selectOption(select, match) {
let options = select.options;
for (let i = 0; i < options.length; i++) {
if (match(i, options[i])) {
select.selectedIndex = i;
return;
}
}
}
function div(classes) {
let node = document.createElement('div');
if (classes !== void 0) {
if (typeof classes === 'string') {
node.classList.add(classes);
} else {
classes.forEach(cls => node.classList.add(cls));
}
}
return node;
}
function table(className) {
let node = document.createElement('table')
if (className) node.classList.add(className)
return node;
}
function td(textOrNode) {
let node = document.createElement('td');
if (typeof textOrNode === 'object') {
node.appendChild(textOrNode);
} else {
node.innerText = textOrNode;
}
return node;
}
function tr() {
return document.createElement('tr');
}
class Navigation {
constructor(state, view) {
this.state = state;
this.view = view;
}
get map() {
return this.state.map
}
set map(value) {
this.state.map = value
}
get chunks() {
return this.state.chunks
}
increaseTimelineResolution() {
this.state.nofChunks *= 1.5;
}
decreaseTimelineResolution() {
this.state.nofChunks /= 1.5;
}
selectNextEdge() {
if (!this.map) return;
if (this.map.children.length != 1) return;
this.map = this.map.children[0].to;
}
selectPrevEdge() {
if (!this.map) return;
if (!this.map.parent()) return;
this.map = this.map.parent();
}
selectDefaultMap() {
this.map = this.chunks[0].at(0);
}
moveInChunks(next) {
if (!this.map) return this.selectDefaultMap();
let chunkIndex = this.map.chunkIndex(this.chunks);
let chunk = this.chunks[chunkIndex];
let index = chunk.indexOf(this.map);
if (next) {
chunk = chunk.next(this.chunks);
} else {
chunk = chunk.prev(this.chunks);
}
if (!chunk) return;
index = Math.min(index, chunk.size() - 1);
this.map = chunk.at(index);
}
moveInChunk(delta) {
if (!this.map) return this.selectDefaultMap();
let chunkIndex = this.map.chunkIndex(this.chunks)
let chunk = this.chunks[chunkIndex];
let index = chunk.indexOf(this.map) + delta;
let map;
if (index < 0) {
map = chunk.prev(this.chunks).last();
} else if (index >= chunk.size()) {
map = chunk.next(this.chunks).first()
} else {
map = chunk.at(index);
}
this.map = map;
}
updateUrl() {
let entries = this.state.entries;
let params = new URLSearchParams(entries);
window.history.pushState(entries, '', '?' + params.toString());
}
}
class View {
constructor(state) {
this.state = state;
setInterval(this.updateOverviewWindow, 50);
this.backgroundCanvas = document.createElement('canvas');
this.transitionView =
new TransitionView(state, $('map-panel').transitionViewSelect);
this.statsView = new StatsView(state, $('#stats'));
this.isLocked = false;
}
get chunks() {
return this.state.chunks
}
get timeline() {
return this.state.timeline
}
get map() {
return this.state.map
}
updateStats() {
this.statsView.update();
}
updateMapDetails() {
let details = '';
if (this.map) {
details += 'ID: ' + this.map.id;
details += '\nSource location: ' + this.map.filePosition;
details += '\n' + this.map.description;
}
$('map-panel').mapDetailsSelect.innerText = details;
this.transitionView.showMap(this.map);
}
updateTimeline() {
let chunksNode = $('timeline-panel').timelineChunksSelect;
removeAllChildren(chunksNode);
let chunks = this.chunks;
let max = chunks.max(each => each.size());
let start = this.timeline.startTime;
let end = this.timeline.endTime;
let duration = end - start;
const timeToPixel = chunks.length * kChunkWidth / duration;
let addTimestamp = (time, name) => {
let timeNode = div('timestamp');
timeNode.innerText = name;
timeNode.style.left = ((time - start) * timeToPixel) + 'px';
chunksNode.appendChild(timeNode);
};
let backgroundTodo = [];
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
let height = (chunk.size() / max * kChunkHeight);
chunk.height = height;
if (chunk.isEmpty()) continue;
let node = div();
node.className = 'chunk';
node.style.left = (i * kChunkWidth) + 'px';
node.style.height = height + 'px';
node.chunk = chunk;
node.addEventListener('mousemove', e => this.handleChunkMouseMove(e));
node.addEventListener('click', e => this.handleChunkClick(e));
node.addEventListener('dblclick', e => this.handleChunkDoubleClick(e));
backgroundTodo.push([chunk, node])
chunksNode.appendChild(node);
chunk.markers.forEach(marker => addTimestamp(marker.time, marker.name));
}
this.asyncSetTimelineChunkBackground(backgroundTodo)
// Put a time marker roughly every 20 chunks.
let expected = duration / chunks.length * 20;
let interval = (10 ** Math.floor(Math.log10(expected)));
let correction = Math.log10(expected / interval);
correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5;
interval *= correction;
let time = start;
while (time < end) {
addTimestamp(time, ((time - start) / 1000) + ' ms');
time += interval;
}
this.drawOverview();
this.redraw();
}
handleChunkMouseMove(event) {
if (this.isLocked) return false;
let chunk = event.target.chunk;
if (!chunk) return;
// topmost map (at chunk.height) == map #0.
let relativeIndex =
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
let map = chunk.at(relativeIndex);
this.state.map = map;
}
handleChunkClick(event) {
this.isLocked = !this.isLocked;
}
handleChunkDoubleClick(event) {
this.isLocked = true;
let chunk = event.target.chunk;
if (!chunk) return;
this.transitionView.showMaps(chunk.getUniqueTransitions());
}
asyncSetTimelineChunkBackground(backgroundTodo) {
const kIncrement = 100;
let start = 0;
let delay = 1;
while (start < backgroundTodo.length) {
let end = Math.min(start + kIncrement, backgroundTodo.length);
setTimeout((from, to) => {
for (let i = from; i < to; i++) {
let [chunk, node] = backgroundTodo[i];
this.setTimelineChunkBackground(chunk, node);
}
}, delay++, start, end);
start = end;
}
}
setTimelineChunkBackground(chunk, node) {
// Render the types of transitions as bar charts
const kHeight = chunk.height;
const kWidth = 1;
this.backgroundCanvas.width = kWidth;
this.backgroundCanvas.height = kHeight;
let ctx = this.backgroundCanvas.getContext('2d');
ctx.clearRect(0, 0, kWidth, kHeight);
let y = 0;
let total = chunk.size();
let type, count;
if (true) {
chunk.getTransitionBreakdown().forEach(([type, count]) => {
ctx.fillStyle = transitionTypeToColor(type);
let height = count / total * kHeight;
ctx.fillRect(0, y, kWidth, y + height);
y += height;
});
} else {
chunk.items.forEach(map => {
ctx.fillStyle = transitionTypeToColor(map.getType());
let y = chunk.yOffset(map);
ctx.fillRect(0, y, kWidth, y + 1);
});
}
let imageData = this.backgroundCanvas.toDataURL('image/webp', 0.2);
node.style.backgroundImage = 'url(' + imageData + ')';
}
updateOverviewWindow() {
let indicator = $('timeline-panel').timelineOverviewIndicatorSelect;
let totalIndicatorWidth =
$('timeline-panel').timelineOverviewSelect.offsetWidth;
let div = $('timeline-panel').timelineSelect;
let timelineTotalWidth =
$('timeline-panel').timelineCanvasSelect.offsetWidth;
let factor = $('timeline-panel').timelineOverviewSelect.offsetWidth /
timelineTotalWidth;
let width = div.offsetWidth * factor;
let left = div.scrollLeft * factor;
indicator.style.width = width + 'px';
indicator.style.left = left + 'px';
}
drawOverview() {
const height = 50;
const kFactor = 2;
let canvas = this.backgroundCanvas;
canvas.height = height;
canvas.width = window.innerWidth;
let ctx = canvas.getContext('2d');
let chunks = this.state.timeline.chunkSizes(canvas.width * kFactor);
let max = chunks.max();
ctx.clearRect(0, 0, canvas.width, height);
ctx.strokeStyle = 'black';
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.moveTo(0, height);
for (let i = 0; i < chunks.length; i++) {
ctx.lineTo(i / kFactor, height - chunks[i] / max * height);
}
ctx.lineTo(chunks.length, height);
ctx.stroke();
ctx.closePath();
ctx.fill();
let imageData = canvas.toDataURL('image/webp', 0.2);
$('timeline-panel').timelineOverviewSelect.style.backgroundImage =
'url(' + imageData + ')';
}
redraw() {
let canvas = $('timeline-panel').timelineCanvasSelect;
canvas.width = (this.chunks.length + 1) * kChunkWidth;
canvas.height = kChunkHeight;
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, kChunkHeight);
if (!this.state.map) return;
this.drawEdges(ctx);
}
setMapStyle(map, ctx) {
ctx.fillStyle = map.edge && map.edge.from ? 'black' : 'green';
}
setEdgeStyle(edge, ctx) {
let color = edge.getColor();
ctx.strokeStyle = color;
ctx.fillStyle = color;
}
markMap(ctx, map) {
let [x, y] = map.position(this.state.chunks);
ctx.beginPath();
this.setMapStyle(map, ctx);
ctx.arc(x, y, 3, 0, 2 * Math.PI);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = 'white';
ctx.arc(x, y, 2, 0, 2 * Math.PI);
ctx.fill();
}
markSelectedMap(ctx, map) {
let [x, y] = map.position(this.state.chunks);
ctx.beginPath();
this.setMapStyle(map, ctx);
ctx.arc(x, y, 6, 0, 2 * Math.PI);
ctx.stroke();
}
drawEdges(ctx) {
// Draw the trace of maps in reverse order to make sure the outgoing
// transitions of previous maps aren't drawn over.
const kMaxOutgoingEdges = 100;
let nofEdges = 0;
let stack = [];
let current = this.state.map;
while (current && nofEdges < kMaxOutgoingEdges) {
nofEdges += current.children.length;
stack.push(current);
current = current.parent();
}
ctx.save();
this.drawOutgoingEdges(ctx, this.state.map, 3);
ctx.restore();
let labelOffset = 15;
let xPrev = 0;
while (current = stack.pop()) {
if (current.edge) {
this.setEdgeStyle(current.edge, ctx);
let [xTo, yTo] = this.drawEdge(ctx, current.edge, true, labelOffset);
if (xTo == xPrev) {
labelOffset += 8;
} else {
labelOffset = 15
}
xPrev = xTo;
}
this.markMap(ctx, current);
current = current.parent();
ctx.save();
// this.drawOutgoingEdges(ctx, current, 1);
ctx.restore();
}
// Mark selected map
this.markSelectedMap(ctx, this.state.map);
}
drawEdge(ctx, edge, showLabel = true, labelOffset = 20) {
if (!edge.from || !edge.to) return [-1, -1];
let [xFrom, yFrom] = edge.from.position(this.chunks);
let [xTo, yTo] = edge.to.position(this.chunks);
let sameChunk = xTo == xFrom;
if (sameChunk) labelOffset += 8;
ctx.beginPath();
ctx.moveTo(xFrom, yFrom);
let offsetX = 20;
let offsetY = 20;
let midX = xFrom + (xTo - xFrom) / 2;
let midY = (yFrom + yTo) / 2 - 100;
if (!sameChunk) {
ctx.quadraticCurveTo(midX, midY, xTo, yTo);
} else {
ctx.lineTo(xTo, yTo);
}
if (!showLabel) {
ctx.stroke();
} else {
let centerX, centerY;
if (!sameChunk) {
centerX = (xFrom / 2 + midX + xTo / 2) / 2;
centerY = (yFrom / 2 + midY + yTo / 2) / 2;
} else {
centerX = xTo;
centerY = yTo;
}
ctx.moveTo(centerX, centerY);
ctx.lineTo(centerX + offsetX, centerY - labelOffset);
ctx.stroke();
ctx.textAlign = 'left';
ctx.fillText(
edge.toString(), centerX + offsetX + 2, centerY - labelOffset)
}
return [xTo, yTo];
}
drawOutgoingEdges(ctx, map, max = 10, depth = 0) {
if (!map) return;
if (depth >= max) return;
ctx.globalAlpha = 0.5 - depth * (0.3 / max);
ctx.strokeStyle = '#666';
const limit = Math.min(map.children.length, 100)
for (let i = 0; i < limit; i++) {
let edge = map.children[i];
this.drawEdge(ctx, edge, true);
this.drawOutgoingEdges(ctx, edge.to, max, depth + 1);
}
}
}
class TransitionView {
constructor(state, node) {
this.state = state;
this.container = node;
this.currentNode = node;
this.currentMap = undefined;
}
selectMap(map) {
this.currentMap = map;
this.state.map = map;
}
showMap(map) {
if (this.currentMap === map) return;
this.currentMap = map;
this._showMaps([map]);
}
showMaps(list, name) {
this.state.view.isLocked = true;
this._showMaps(list);
}
_showMaps(list, name) {
// Hide the container to avoid any layouts.
this.container.style.display = 'none';
removeAllChildren(this.container);
list.forEach(map => this.addMapAndParentTransitions(map));
this.container.style.display = ''
}
addMapAndParentTransitions(map) {
if (map === void 0) return;
this.currentNode = this.container;
let parents = map.getParents();
if (parents.length > 0) {
this.addTransitionTo(parents.pop());
parents.reverse().forEach(each => this.addTransitionTo(each));
}
let mapNode = this.addSubtransitions(map);
// Mark and show the selected map.
mapNode.classList.add('selected');
if (this.selectedMap == map) {
setTimeout(
() => mapNode.scrollIntoView(
{behavior: 'smooth', block: 'nearest', inline: 'nearest'}),
1);
}
}
addMapNode(map) {
let node = div('map');
if (map.edge) node.classList.add(map.edge.getColor());
node.map = map;
node.addEventListener('click', () => this.selectMap(map));
if (map.children.length > 1) {
node.innerText = map.children.length;
let showSubtree = div('showSubtransitions');
showSubtree.addEventListener('click', (e) => this.toggleSubtree(e, node));
node.appendChild(showSubtree);
} else if (map.children.length == 0) {
node.innerHTML = '&#x25CF;'
}
this.currentNode.appendChild(node);
return node;
}
addSubtransitions(map) {
let mapNode = this.addTransitionTo(map);
// Draw outgoing linear transition line.
let current = map;
while (current.children.length == 1) {
current = current.children[0].to;
this.addTransitionTo(current);
}
return mapNode;
}
addTransitionEdge(map) {
let classes = ['transitionEdge', map.edge.getColor()];
let edge = div(classes);
let labelNode = div('transitionLabel');
labelNode.innerText = map.edge.toString();
edge.appendChild(labelNode);
return edge;
}
addTransitionTo(map) {
// transition[ transitions[ transition[...], transition[...], ...]];
let transition = div('transition');
if (map.isDeprecated()) transition.classList.add('deprecated');
if (map.edge) {
transition.appendChild(this.addTransitionEdge(map));
}
let mapNode = this.addMapNode(map);
transition.appendChild(mapNode);
let subtree = div('transitions');
transition.appendChild(subtree);
this.currentNode.appendChild(transition);
this.currentNode = subtree;
return mapNode;
}
toggleSubtree(event, node) {
let map = node.map;
event.target.classList.toggle('opened');
let transitionsNode = node.parentElement.querySelector('.transitions');
let subtransitionNodes = transitionsNode.children;
if (subtransitionNodes.length <= 1) {
// Add subtransitions excepth the one that's already shown.
let visibleTransitionMap = subtransitionNodes.length == 1 ?
transitionsNode.querySelector('.map').map :
void 0;
map.children.forEach(edge => {
if (edge.to != visibleTransitionMap) {
this.currentNode = transitionsNode;
this.addSubtransitions(edge.to);
}
});
} else {
// remove all but the first (currently selected) subtransition
for (let i = subtransitionNodes.length - 1; i > 0; i--) {
transitionsNode.removeChild(subtransitionNodes[i]);
}
}
}
}
class StatsView {
constructor(state, node) {
this.state = state;
this.node = node;
}
get timeline() {
return this.state.timeline
}
get transitionView() {
return this.state.view.transitionView;
}
update() {
removeAllChildren(this.node);
this.updateGeneralStats();
this.updateNamedTransitionsStats();
}
updateGeneralStats() {
let pairs = [
['Total', null, e => true],
['Transitions', 'black', 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()],
];
let text = '';
let tableNode = table('transitionType');
tableNode.innerHTML =
'<thead><tr><td>Color</td><td>Type</td><td>Count</td><td>Percent</td></tr></thead>';
let name, filter;
let total = this.timeline.size();
pairs.forEach(([name, color, filter]) => {
let row = tr();
if (color !== null) {
row.appendChild(td(div(['colorbox', color])));
} else {
row.appendChild(td(''));
}
row.onclick = (e) => {
// lazily compute the stats
let node = e.target.parentNode;
if (node.maps == undefined) {
node.maps = this.timeline.filterUniqueTransitions(filter);
}
this.transitionView.showMaps(node.maps);
};
row.appendChild(td(name));
let count = this.timeline.count(filter);
row.appendChild(td(count));
let percent = Math.round(count / total * 1000) / 10;
row.appendChild(td(percent.toFixed(1) + '%'));
tableNode.appendChild(row);
});
this.node.appendChild(tableNode);
};
updateNamedTransitionsStats() {
let tableNode = table('transitionTable');
let nameMapPairs = Array.from(this.timeline.transitions.entries());
tableNode.innerHTML =
'<thead><tr><td>Propery Name</td><td>#</td></tr></thead>';
nameMapPairs.sort((a, b) => b[1].length - a[1].length).forEach(([
name, maps
]) => {
let row = tr();
row.maps = maps;
row.addEventListener(
'click',
e => this.transitionView.showMaps(
e.target.parentNode.maps.map(map => map.to)));
row.appendChild(td(name));
row.appendChild(td(maps.length));
tableNode.appendChild(row);
});
this.node.appendChild(tableNode);
}
}
// =========================================================================
function transitionTypeToColor(type) {
switch (type) {
case 'new':
return 'green';
case 'Normalize':
return 'violet';
case 'SlowToFast':
return 'orange';
case 'InitialMap':
return 'yellow';
case 'Transition':
return 'black';
case 'ReplaceDescriptors':
return 'red';
}
return 'black';
}
// ======================= histogram ==========
Object.defineProperty(Edge.prototype, 'getColor', { value:function() {
return transitionTypeToColor(this.type);
}});
define(Array.prototype, "histogram", function(mapFn) {
let histogram = [];
for (let i = 0; i < this.length; i++) {
let value = this[i];
let index = Math.round(mapFn(value))
let bucket = histogram[index];
if (bucket !== undefined) {
bucket.push(value);
} else {
histogram[index] = [value];
}
}
for (let i = 0; i < histogram.length; i++) {
histogram[i] = histogram[i] || [];
}
return histogram;
});

View File

@ -0,0 +1,198 @@
<!-- 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. -->
<style>
.map-panel {
background-color: #355EC2;
padding: 20px 20px 20px 20px ;
margin: auto;
}
#map-panel {
background-color: #232323;
padding: 10px 10px 10px 10px ;
margin: auto;
}
#mapDetails {
font-family: monospace;
white-space: pre;
overflow-x: scroll;
}
#transitionView {
overflow-x: scroll;
white-space: nowrap;
min-height: 50px;
max-height: 200px;
padding: 50px 0 0 0;
margin-top: -25px;
width: 100%;
}
.map {
width: 20px;
height: 20px;
display: inline-block;
border-radius: 50%;
background-color: black;
border: 4px solid white;
font-size: 10px;
text-align: center;
line-height: 18px;
color: white;
vertical-align: top;
margin-top: -13px;
/* raise z-index */
position: relative;
z-index: 2;
cursor: pointer;
}
.map.selected {
border-color: black;
}
.transitions {
display: inline-block;
margin-left: -15px;
}
.transition {
min-height: 55px;
margin: 0 0 -2px 2px;
}
/* gray out deprecated transitions */
.deprecated > .transitionEdge,
.deprecated > .map {
opacity: 0.5;
}
.deprecated > .transition {
border-color: rgba(0, 0, 0, 0.5);
}
/* Show a border for all but the first transition */
.transition:nth-of-type(2),
.transition:nth-last-of-type(n+2) {
border-left: 2px solid;
margin-left: 0px;
}
/* special case for 2 transitions */
.transition:nth-last-of-type(1) {
border-left: none;
}
/* topmost transitions are not related */
#transitionView > .transition {
border-left: none;
}
/* topmost transition edge needs initial offset to be aligned */
#transitionView > .transition > .transitionEdge {
margin-left: 13px;
}
.transitionEdge {
height: 2px;
width: 80px;
display: inline-block;
margin: 0 0 2px 0;
background-color: black;
vertical-align: top;
padding-left: 15px;
}
.transitionLabel {
color: black;
transform: rotate(-15deg);
transform-origin: top left;
margin-top: -10px;
font-size: 10px;
white-space: normal;
word-break: break-all;
background-color: rgba(255,255,255,0.5);
}
.black{
background-color: black;
}
.red {
background-color: red;
}
.green {
background-color: green;
}
.yellow {
background-color: yellow;
color: black;
}
.blue {
background-color: blue;
}
.orange {
background-color: orange;
}
.violet {
background-color: violet;
color: black;
}
.showSubtransitions {
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 10px solid black;
cursor: zoom-in;
margin: 4px 0 0 4px;
}
.showSubtransitions.opened {
border-top: none;
border-bottom: 10px solid black;
cursor: zoom-out;
}
#tooltip {
position: absolute;
width: 10px;
height: 10px;
background-color: red;
pointer-events: none;
z-index: 100;
display: none;
}
#searchBarInput {
width: 200px;
}
</style>
<div class="map-panel">
<section id="map-panel">
<h2>Transitions</h2>
<section id="transitionView"></section>
<br/>
<h2>Search Map by Address</h2>
<section id="searchBar"></section>
<input type="search" id="searchBarInput" placeholder="Search maps by address.."></input>
<button id="searchBarBtn">Search</button>
<h2>Selected Map</h2>
<section id="mapDetails"></section>
<div id="tooltip">
<div id="tooltipContents"></div>
</div>
</section>
</div>

View File

@ -0,0 +1,70 @@
// 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.
defineCustomElement('map-panel', (templateText) =>
class MapPanel extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this.transitionViewSelect.addEventListener(
'mousemove', e => this.handleTransitionViewChange(e));
this.$('#searchBarBtn').addEventListener(
'click', e => this.handleSearchBar(e));
}
$(id) {
return this.shadowRoot.querySelector(id);
}
querySelectorAll(query) {
return this.shadowRoot.querySelectorAll(query);
}
get transitionViewSelect() {
return this.$('#transitionView');
}
get searchBarSelect() {
return this.$('#searchBar');
}
get mapDetailsSelect() {
return this.$('#mapDetails');
}
get tooltipSelect() {
return this.$('#tooltip');
}
get tooltipContentsSelect() {
return this.$('#tooltipContents');
}
handleTransitionViewChange(e){
this.tooltipSelect.style.left = e.pageX + "px";
this.tooltipSelect.style.top = e.pageY + "px";
let map = e.target.map;
if (map) {
this.tooltipContentsSelect.innerText = map.description;
}
}
handleSearchBar(e){
let dataModel = Object.create(null);
let searchBar = this.$('#searchBarInput');
let searchBarInput = searchBar.value;
//access the map from model cache
let selectedMap = V8Map.get(searchBarInput);
if(selectedMap){
dataModel.isValidMap = true;
dataModel.map = selectedMap;
searchBar.className = "green";
} else {
dataModel.isValidMap = false;
searchBar.className = "red";
}
this.dispatchEvent(new CustomEvent(
'click', {bubbles: true, composed: true, detail: dataModel}));
}
});

View File

@ -0,0 +1,762 @@
// 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.
// ===========================================================================
const kChunkHeight = 250;
const kChunkWidth = 10;
function define(prototype, name, fn) {
Object.defineProperty(prototype, name, {value: fn, enumerable: false});
}
define(Array.prototype, 'max', function(fn) {
if (this.length === 0) return undefined;
if (fn === undefined) fn = (each) => each;
let max = fn(this[0]);
for (let i = 1; i < this.length; i++) {
max = Math.max(max, fn(this[i]));
}
return max;
})
define(Array.prototype, 'first', function() {
return this[0]
});
define(Array.prototype, 'last', function() {
return this[this.length - 1]
});
// ===========================================================================
class MapProcessor extends LogReader {
constructor() {
super();
this.dispatchTable_ = {
__proto__: null,
'code-creation': {
parsers: [
parseString, parseInt, parseInt, parseInt, parseInt, parseString,
parseVarArgs
],
processor: this.processCodeCreation
},
'code-move': {
parsers: [parseInt, parseInt],
'sfi-move':
{parsers: [parseInt, parseInt], processor: this.processCodeMove},
'code-delete': {parsers: [parseInt], processor: this.processCodeDelete},
processor: this.processFunctionMove
},
'map-create':
{parsers: [parseInt, parseString], processor: this.processMapCreate},
'map': {
parsers: [
parseString, parseInt, parseString, parseString, parseInt, parseInt,
parseString, parseString, parseString
],
processor: this.processMap
},
'map-details': {
parsers: [parseInt, parseString, parseString],
processor: this.processMapDetails
}
};
this.profile_ = new Profile();
this.timeline_ = new Timeline();
this.formatPCRegexp_ = /(.*):[0-9]+:[0-9]+$/;
}
printError(str) {
console.error(str);
throw str
}
processString(string) {
let end = string.length;
let current = 0;
let next = 0;
let line;
let i = 0;
let entry;
try {
while (current < end) {
next = string.indexOf('\n', current);
if (next === -1) break;
i++;
line = string.substring(current, next);
current = next + 1;
this.processLogLine(line);
}
} catch (e) {
console.error('Error occurred during parsing, trying to continue: ' + e);
}
return this.finalize();
}
processLogFile(fileName) {
this.collectEntries = true;
this.lastLogFileName_ = fileName;
let i = 1;
let line;
try {
while (line = readline()) {
this.processLogLine(line);
i++;
}
} catch (e) {
console.error(
'Error occurred during parsing line ' + i +
', trying to continue: ' + e);
}
return this.finalize();
}
finalize() {
// TODO(cbruni): print stats;
this.timeline_.finalize();
return this.timeline_;
}
addEntry(entry) {
this.entries.push(entry);
}
/**
* Parser for dynamic code optimization state.
*/
parseState(s) {
switch (s) {
case '':
return Profile.CodeState.COMPILED;
case '~':
return Profile.CodeState.OPTIMIZABLE;
case '*':
return Profile.CodeState.OPTIMIZED;
}
throw new Error('unknown code state: ' + s);
}
processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
if (maybe_func.length) {
let funcAddr = parseInt(maybe_func[0]);
let state = this.parseState(maybe_func[1]);
this.profile_.addFuncCode(
type, name, timestamp, start, size, funcAddr, state);
} else {
this.profile_.addCode(type, name, timestamp, start, size);
}
}
processCodeMove(from, to) {
this.profile_.moveCode(from, to);
}
processCodeDelete(start) {
this.profile_.deleteCode(start);
}
processFunctionMove(from, to) {
this.profile_.moveFunc(from, to);
}
formatPC(pc, line, column) {
let entry = this.profile_.findEntry(pc);
if (!entry) return '<unknown>'
if (entry.type === 'Builtin') {
return entry.name;
}
let name = entry.func.getName();
let array = this.formatPCRegexp_.exec(name);
if (array === null) {
entry = name;
} else {
entry = entry.getState() + array[1];
}
return entry + ':' + line + ':' + column;
}
processMap(type, time, from, to, pc, line, column, reason, name) {
let time_ = parseInt(time);
if (type === 'Deprecate') return this.deprecateMap(type, time_, from);
let from_ = this.getExistingMap(from, time_);
let to_ = this.getExistingMap(to, time_);
let edge = new Edge(type, name, reason, time, from_, to_);
to_.filePosition = this.formatPC(pc, line, column);
edge.finishSetup();
}
deprecateMap(type, time, id) {
this.getExistingMap(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.
let map = this.createMap(id, time);
}
processMapDetails(time, id, string) {
// TODO(cbruni): fix initial map logging.
let map = this.getExistingMap(id, time);
map.description = string;
}
createMap(id, time) {
let map = new V8Map(id, time);
this.timeline_.push(map);
return map;
}
getExistingMap(id, time) {
if (id === '0x000000000000') return undefined;
let map = V8Map.get(id, time);
if (map === undefined) {
console.error('No map details provided: id=' + id);
// Manually patch in a map to continue running.
return this.createMap(id, time);
};
return map;
}
}
// ===========================================================================
class V8Map {
constructor(id, time = -1) {
if (!id) throw 'Invalid ID';
this.id = id;
this.time = time;
if (!(time > 0)) throw 'Invalid time';
this.description = '';
this.edge = void 0;
this.children = [];
this.depth = 0;
this._isDeprecated = false;
this.deprecationTargets = null;
V8Map.set(id, this);
this.leftId = 0;
this.rightId = 0;
this.filePosition = '';
}
finalizeRootMap(id) {
let stack = [this];
while (stack.length > 0) {
let current = stack.pop();
if (current.leftId !== 0) {
console.error('Skipping potential parent loop between maps:', current)
continue;
}
current.finalize(id)
id += 1;
current.children.forEach(edge => stack.push(edge.to))
// TODO implement rightId
}
return id;
}
finalize(id) {
// Initialize preorder tree traversal Ids for fast subtree inclusion checks
if (id <= 0) throw 'invalid id';
let currentId = id;
this.leftId = currentId
}
parent() {
if (this.edge === void 0) return void 0;
return this.edge.from;
}
isDeprecated() {
return this._isDeprecated;
}
deprecate() {
this._isDeprecated = true;
}
isRoot() {
return this.edge === void 0 || this.edge.from === void 0;
}
contains(map) {
return this.leftId < map.leftId && map.rightId < this.rightId;
}
addEdge(edge) {
this.children.push(edge);
}
chunkIndex(chunks) {
// Did anybody say O(n)?
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
if (chunk.isEmpty()) continue;
if (chunk.last().time < this.time) continue;
return i;
}
return -1;
}
position(chunks) {
let index = this.chunkIndex(chunks);
let xFrom = (index + 0.5) * kChunkWidth;
let yFrom = kChunkHeight - chunks[index].yOffset(this);
return [xFrom, yFrom];
}
transitions() {
let transitions = Object.create(null);
let current = this;
while (current) {
let edge = current.edge;
if (edge && edge.isTransition()) {
transitions[edge.name] = edge;
}
current = current.parent()
}
return transitions;
}
getType() {
return this.edge === void 0 ? 'new' : this.edge.type;
}
isBootstrapped() {
return this.edge === void 0;
}
getParents() {
let parents = [];
let current = this.parent();
while (current) {
parents.push(current);
current = current.parent();
}
return parents;
}
static get(id, time = undefined) {
let maps = this.cache.get(id);
if (maps) {
for (let i = 0; i < maps.length; i++) {
// TODO: Implement time based map search
if (maps[i].time === time) {
return maps[i];
}
}
// default return the latest
return maps[maps.length - 1];
}
}
static set(id, map) {
if (this.cache.has(id)) {
this.cache.get(id).push(map);
} else {
this.cache.set(id, [map]);
}
}
}
V8Map.cache = new Map();
// ===========================================================================
class Edge {
constructor(type, name, reason, time, from, to) {
this.type = type;
this.name = name;
this.reason = reason;
this.time = time;
this.from = from;
this.to = to;
}
finishSetup() {
let from = this.from;
if (from) from.addEdge(this);
let to = this.to;
if (to === undefined) return;
to.edge = this;
if (from === undefined) return;
if (to === from) throw 'From and to must be distinct.';
if (to.time < from.time) {
console.error('invalid time order');
}
let newDepth = from.depth + 1;
if (to.depth > 0 && to.depth != newDepth) {
console.error('Depth has already been initialized');
}
to.depth = newDepth;
}
chunkIndex(chunks) {
// Did anybody say O(n)?
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i];
if (chunk.isEmpty()) continue;
if (chunk.last().time < this.time) continue;
return i;
}
return -1;
}
parentEdge() {
if (!this.from) return undefined;
return this.from.edge;
}
chainLength() {
let length = 0;
let prev = this;
while (prev) {
prev = this.parent;
length++;
}
return length;
}
isTransition() {
return this.type === 'Transition'
}
isFastToSlow() {
return this.type === 'Normalize'
}
isSlowToFast() {
return this.type === 'SlowToFast'
}
isInitial() {
return this.type === 'InitialMap'
}
isBootstrapped() {
return this.type === 'new'
}
isReplaceDescriptors() {
return this.type === 'ReplaceDescriptors'
}
isCopyAsPrototype() {
return this.reason === 'CopyAsPrototype'
}
isOptimizeAsPrototype() {
return this.reason === 'OptimizeAsPrototype'
}
symbol() {
if (this.isTransition()) return '+';
if (this.isFastToSlow()) return '⊡';
if (this.isSlowToFast()) return '⊛';
if (this.isReplaceDescriptors()) {
if (this.name) return '+';
return '∥';
}
return '';
}
toString() {
let s = this.symbol();
if (this.isTransition()) return s + this.name;
if (this.isFastToSlow()) return s + this.reason;
if (this.isCopyAsPrototype()) return s + 'Copy as Prototype';
if (this.isOptimizeAsPrototype()) {
return s + 'Optimize as Prototype';
}
if (this.isReplaceDescriptors() && this.name) {
return this.type + ' ' + this.symbol() + this.name;
}
return this.type + ' ' + (this.reason ? this.reason : '') + ' ' +
(this.name ? this.name : '')
}
}
// ===========================================================================
class Marker {
constructor(time, name) {
this.time = parseInt(time);
this.name = name;
}
}
// ===========================================================================
class Timeline {
constructor() {
this.values = [];
this.transitions = new Map();
this.markers = [];
this.startTime = 0;
this.endTime = 0;
}
push(map) {
let time = map.time;
if (!this.isEmpty() && this.last().time > time) {
// Invalid insertion order, might happen without --single-process,
// finding insertion point.
let insertionPoint = this.find(time);
this.values.splice(insertionPoint, map);
} else {
this.values.push(map);
}
if (time > 0) {
this.endTime = Math.max(this.endTime, time);
if (this.startTime === 0) {
this.startTime = time;
} else {
this.startTime = Math.min(this.startTime, time);
}
}
}
addMarker(time, message) {
this.markers.push(new Marker(time, message));
}
finalize() {
let id = 0;
this.forEach(map => {
if (map.isRoot()) id = map.finalizeRootMap(id + 1);
if (map.edge && map.edge.name) {
let edge = map.edge;
let list = this.transitions.get(edge.name);
if (list === undefined) {
this.transitions.set(edge.name, [edge]);
} else {
list.push(edge);
}
}
});
this.markers.sort((a, b) => b.time - a.time);
}
at(index) {
return this.values[index]
}
isEmpty() {
return this.size() === 0
}
size() {
return this.values.length
}
first() {
return this.values.first()
}
last() {
return this.values.last()
}
duration() {
return this.last().time - this.first().time
}
forEachChunkSize(count, fn) {
const increment = this.duration() / count;
let currentTime = this.first().time + increment;
let index = 0;
for (let i = 0; i < count; i++) {
let nextIndex = this.find(currentTime, index);
let nextTime = currentTime + increment;
fn(index, nextIndex, currentTime, nextTime);
index = nextIndex
currentTime = nextTime;
}
}
chunkSizes(count) {
let chunks = [];
this.forEachChunkSize(count, (start, end) => chunks.push(end - start));
return chunks;
}
chunks(count) {
let chunks = [];
let emptyMarkers = [];
this.forEachChunkSize(count, (start, end, startTime, endTime) => {
let items = this.values.slice(start, end);
let markers = this.markersAt(startTime, endTime);
chunks.push(new Chunk(chunks.length, startTime, endTime, items, markers));
});
return chunks;
}
range(start, end) {
const first = this.find(start);
if (first < 0) return [];
const last = this.find(end, first);
return this.values.slice(first, last);
}
find(time, offset = 0) {
return this.basicFind(this.values, each => each.time - time, offset);
}
markersAt(startTime, endTime) {
let start = this.basicFind(this.markers, each => each.time - startTime);
let end = this.basicFind(this.markers, each => each.time - endTime, start);
return this.markers.slice(start, end);
}
basicFind(array, cmp, offset = 0) {
let min = offset;
let max = array.length;
while (min < max) {
let mid = min + Math.floor((max - min) / 2);
let result = cmp(array[mid]);
if (result > 0) {
max = mid - 1;
} else {
min = mid + 1;
}
}
return min;
}
count(filter) {
return this.values.reduce((sum, each) => {
return sum + (filter(each) === true ? 1 : 0);
}, 0);
}
filter(predicate) {
return this.values.filter(predicate);
}
filterUniqueTransitions(filter) {
// Returns a list of Maps whose parent is not in the list.
return this.values.filter(map => {
if (filter(map) === false) return false;
let parent = map.parent();
if (parent === undefined) return true;
return filter(parent) === false;
});
}
depthHistogram() {
return this.values.histogram(each => each.depth);
}
fanOutHistogram() {
return this.values.histogram(each => each.children.length);
}
forEach(fn) {
return this.values.forEach(fn)
}
}
// ===========================================================================
class Chunk {
constructor(index, start, end, items, markers) {
this.index = index;
this.start = start;
this.end = end;
this.items = items;
this.markers = markers;
this.height = 0;
}
isEmpty() {
return this.items.length === 0;
}
last() {
return this.at(this.size() - 1);
}
first() {
return this.at(0);
}
at(index) {
return this.items[index];
}
size() {
return this.items.length;
}
yOffset(map) {
// items[0] == oldest map, displayed at the top of the chunk
// items[n-1] == youngest map, displayed at the bottom of the chunk
return (1 - (this.indexOf(map) + 0.5) / this.size()) * this.height;
}
indexOf(map) {
return this.items.indexOf(map);
}
has(map) {
if (this.isEmpty()) return false;
return this.first().time <= map.time && map.time <= this.last().time;
}
next(chunks) {
return this.findChunk(chunks, 1);
}
prev(chunks) {
return this.findChunk(chunks, -1);
}
findChunk(chunks, delta) {
let i = this.index + delta;
let chunk = chunks[i];
while (chunk && chunk.size() === 0) {
i += delta;
chunk = chunks[i]
}
return chunk;
}
getTransitionBreakdown() {
return BreakDown(this.items, map => map.getType())
}
getUniqueTransitions() {
// Filter out all the maps that have parents within the same chunk.
return this.items.filter(map => !map.parent() || !this.has(map.parent()));
}
}
// ===========================================================================
function BreakDown(list, map_fn) {
if (map_fn === void 0) {
map_fn = each => each;
}
let breakdown = {__proto__: null};
list.forEach(each => {
let type = map_fn(each);
let v = breakdown[type];
breakdown[type] = (v | 0) + 1
});
return Object.entries(breakdown).sort((a, b) => a[1] - b[1]);
}
// ===========================================================================
class ArgumentsProcessor extends BaseArgumentsProcessor {
getArgsDispatch() {
return {
'--range':
['range', 'auto,auto', 'Specify the range limit as [start],[end]'],
'--source-map': [
'sourceMap', null,
'Specify the source map that should be used for output'
]
};
}
getDefaultResults() {
return {
logFileName: 'v8.log',
range: 'auto,auto',
};
}
}

View File

@ -0,0 +1,114 @@
<!-- 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. -->
<style>
.timeline-panel {
background-color: #355EC2;
padding: 20px 20px 20px 20px ;
margin: auto;
}
#timeline-panel {
background-color: #232323;
padding: 10px 10px 10px 10px ;
margin: auto;
}
#timeline {
position: relative;
height: 300px;
overflow-y: hidden;
overflow-x: scroll;
user-select: none;
background-color: whitesmoke;
}
#timelineLabel {
transform: rotate(90deg);
transform-origin: left bottom 0;
position: absolute;
left: 0;
width: 250px;
text-align: center;
font-size: 10px;
opacity: 0.5;
}
#timelineChunks {
height: 250px;
position: absolute;
margin-right: 100px;
}
#timelineCanvas {
height: 250px;
position: relative;
overflow: visible;
pointer-events: none;
}
.chunk {
width: 6px;
border: 0px white solid;
border-width: 0 2px 0 2px;
position: absolute;
background-size: 100% 100%;
image-rendering: pixelated;
bottom: 0px;
}
.timestamp {
height: 250px;
width: 100px;
border-left: 1px black dashed;
padding-left: 4px;
position: absolute;
pointer-events: none;
font-size: 10px;
opacity: 0.5;
}
#timelineOverview {
width: 100%;
height: 50px;
position: relative;
margin-top: -50px;
margin-bottom: 10px;
background-size: 100% 100%;
border: 1px black solid;
border-width: 1px 0 1px 0;
overflow: hidden;
}
#timelineOverviewIndicator {
height: 100%;
position: absolute;
box-shadow: 0px 2px 20px -5px black inset;
top: 0px;
cursor: ew-resize;
}
#timelineOverviewIndicator .leftMask,
#timelineOverviewIndicator .rightMask {
background-color: rgba(200, 200, 200, 0.5);
width: 10000px;
height: 100%;
position: absolute;
top: 0px;
}
#timelineOverviewIndicator .leftMask {
right: 100%;
}
#timelineOverviewIndicator .rightMask {
left: 100%;
}
</style>
<div class="timeline-panel">
<section id="timeline-panel">
<h2>Timeline</h2>
<div id="timeline">
<div id="timelineLabel">Frequency</div>
<div id="timelineChunks"></div>
<canvas id="timelineCanvas"></canvas>
</div>
<div id="timelineOverview">
<div id="timelineOverviewIndicator">
<div class="leftMask"></div>
<div class="rightMask"></div>
</div>
</div>
</section>
</div>

View File

@ -0,0 +1,52 @@
// 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.
defineCustomElement('timeline-panel', (templateText) =>
class TimelinePanel extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this.timelineOverviewSelect.addEventListener(
'mousemove', e => this.handleTimelineIndicatorMove(e));
}
$(id) {
return this.shadowRoot.querySelector(id);
}
querySelectorAll(query) {
return this.shadowRoot.querySelectorAll(query);
}
get timelineOverviewSelect() {
return this.$('#timelineOverview');
}
get timelineOverviewIndicatorSelect() {
return this.$('#timelineOverviewIndicator');
}
get timelineCanvasSelect() {
return this.$('#timelineCanvas');
}
get timelineChunksSelect() {
return this.$('#timelineChunks');
}
get timelineSelect() {
return this.$('#timeline');
}
handleTimelineIndicatorMove(event) {
if (event.buttons == 0) return;
let timelineTotalWidth = this.timelineCanvasSelect.offsetWidth;
let factor = this.timelineOverviewSelect.offsetWidth / timelineTotalWidth;
this.timelineSelect.scrollLeft += event.movementX / factor;
}
});