Commit 257b0a43 authored by JianxiaoLuIntel's avatar JianxiaoLuIntel Committed by V8 LUCI CQ

[tool] heap layout trace file visualization tool

Design doc:
https://docs.google.com/document/d/1rxM3sDd-ZiOLznqw7MvYraulAPWJSVqC_CztO4YpUTQ/edit

Change-Id: I471ff31f32b7bdd22cb03005c1dcc18aa485ad77
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3313793
Auto-Submit: Jianxiao Lu <jianxiao.lu@intel.com>
Reviewed-by: 's avatarCamillo Bruni <cbruni@chromium.org>
Commit-Queue: Shiyu Zhang <shiyu.zhang@intel.com>
Cr-Commit-Position: refs/heads/main@{#78428}
parent 0dbcfe1f
...@@ -21,6 +21,7 @@ group("v8_mjsunit") { ...@@ -21,6 +21,7 @@ group("v8_mjsunit") {
"../../tools/consarray.mjs", "../../tools/consarray.mjs",
"../../tools/csvparser.mjs", "../../tools/csvparser.mjs",
"../../tools/dumpcpp.mjs", "../../tools/dumpcpp.mjs",
"../../tools/js/helper.mjs",
"../../tools/logreader.mjs", "../../tools/logreader.mjs",
"../../tools/profile.mjs", "../../tools/profile.mjs",
"../../tools/profile_view.mjs", "../../tools/profile_view.mjs",
......
<!-- Copyright 2021 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>
#chart {
width: 100%;
height: 600px;
}
</style>
<div id="container" style="display: none;">
<h2>V8 Heap Layout</h2>
<div id="chart"></div>
</div>
\ No newline at end of file
// Copyright 2021 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 {GB, MB} from '../js/helper.mjs';
import {DOM} from '../js/web-api-helper.mjs';
import {getColorFromSpaceName, kSpaceNames} from './space-categories.mjs';
DOM.defineCustomElement('heap-layout-viewer',
(templateText) =>
class HeapLayoutViewer extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this.chart = echarts.init(this.$('#chart'), null, {
renderer: 'canvas',
});
window.addEventListener('resize', () => {
this.chart.resize();
});
this.currentIndex = 0;
}
$(id) {
return this.shadowRoot.querySelector(id);
}
set data(value) {
this._data = value;
this.stateChanged();
}
get data() {
return this._data;
}
hide() {
this.$('#container').style.display = 'none';
}
show() {
this.$('#container').style.display = 'block';
}
stateChanged() {
this.drawChart(0);
}
getChartTitle(index) {
return this.data[index].header;
}
getSeriesData(pageinfos) {
let ret = [];
for (let pageinfo of pageinfos) {
ret.push({value: pageinfo});
}
return ret;
}
getChartSeries(index) {
const snapshot = this.data[index];
let series = [];
for (const [space_name, pageinfos] of Object.entries(snapshot.data)) {
let space_series = {
name: space_name,
type: 'custom',
renderItem(params, api) {
const addressBegin = api.value(1);
const addressEnd = api.value(2);
const allocated = api.value(3);
const start = api.coord([addressBegin, 0]);
const end = api.coord([addressEnd, 0]);
const allocatedRate = allocated / (addressEnd - addressBegin);
const unAllocatedRate = 1 - allocatedRate;
const standardH = api.size([0, 1])[1];
const standardY = start[1] - standardH / 2;
const allocatedY = standardY + standardH * unAllocatedRate;
const allocatedH = standardH * allocatedRate;
const unAllocatedY = standardY;
const unAllocatedH = standardH - allocatedH;
const allocatedShape = echarts.graphic.clipRectByRect(
{
x: start[0],
y: allocatedY,
width: end[0] - start[0],
height: allocatedH,
},
{
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height,
});
const unAllocatedShape = echarts.graphic.clipRectByRect(
{
x: start[0],
y: unAllocatedY,
width: end[0] - start[0],
height: unAllocatedH,
},
{
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height,
});
const ret = {
type: 'group',
children: [
{
type: 'rect',
shape: allocatedShape,
style: api.style(),
},
{
type: 'rect',
shape: unAllocatedShape,
style: {
fill: '#000000',
},
},
],
};
return ret;
},
data: this.getSeriesData(pageinfos),
encode: {
x: [1, 2],
},
itemStyle: {
color: getColorFromSpaceName(space_name),
},
};
series.push(space_series);
}
return series;
}
drawChart(index) {
if (index >= this.data.length || index < 0) {
console.error('Invalid index:', index);
return;
}
const option = {
tooltip: {
formatter(params) {
const ret = params.marker + params.value[0] + '<br>' +
'address:' + (params.value[1] / MB).toFixed(3) + 'MB' +
'<br>' +
'size:' + ((params.value[2] - params.value[1]) / MB).toFixed(3) +
'MB' +
'<br>' +
'allocated:' + (params.value[3] / MB).toFixed(3) + 'MB' +
'<br>' +
'wasted:' + params.value[4] + 'B';
return ret;
},
},
grid: {
bottom: 120,
top: 120,
},
dataZoom: [
{
type: 'slider',
filterMode: 'weakFilter',
showDataShadow: true,
labelFormatter: '',
},
{
type: 'inside',
filterMode: 'weakFilter',
},
],
legend: {
show: true,
data: kSpaceNames,
top: '6%',
type: 'scroll',
},
title: {
text: this.getChartTitle(index),
left: 'center',
},
xAxis: {
name: 'Address offset in heap(MB)',
nameLocation: 'center',
nameTextStyle: {
fontSize: 25,
padding: [30, 0, 50, 0],
},
type: 'value',
min: 0,
max: 4 * GB,
axisLabel: {
rotate: 0,
formatter(value, index) {
value = value / MB;
value = value.toFixed(3);
return value;
},
},
},
yAxis: {
data: ['Page'],
},
series: this.getChartSeries(index),
};
this.show();
this.chart.resize();
this.chart.setOption(option);
this.currentIndex = index;
}
});
<!-- Copyright 2021 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>
#chart {
width: 100%;
height: 400px;
}
</style>
<div id="container" style="display: none;">
<h2>V8 Heap Space Size Trend</h2>
<div id="chart"></div>
</div>
\ No newline at end of file
// Copyright 2021 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 {MB} from '../js/helper.mjs';
import {DOM} from '../js/web-api-helper.mjs';
import {getColorFromSpaceName, kSpaceNames} from './space-categories.mjs';
class TrendLineHelper {
static re_gc_count = /(?<=(Before|After) GC:)\d+(?=,)/;
static re_allocated = /allocated/;
static re_space_name = /^[a-z_]+_space/;
static snapshotHeaderToXLabel(header) {
const gc_count = this.re_gc_count.exec(header)[0];
const alpha = header[0];
return alpha + gc_count;
}
static getLineSymbolFromTrendLineName(trend_line_name) {
const is_allocated_line = this.re_allocated.test(trend_line_name);
if (is_allocated_line) {
return 'emptyTriangle';
}
return 'emptyCircle';
}
static getSizeTrendLineName(space_name) {
return space_name + ' size';
}
static getAllocatedTrendSizeName(space_name) {
return space_name + ' allocated';
}
static getSpaceNameFromTrendLineName(trend_line_name) {
const space_name = this.re_space_name.exec(trend_line_name)[0];
return space_name;
}
}
DOM.defineCustomElement('heap-size-trend-viewer',
(templateText) =>
class HeapSizeTrendViewer extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
this.chart = echarts.init(this.$('#chart'), null, {
renderer: 'canvas',
});
this.chart.getZr().on('click', 'series.line', (params) => {
const pointInPixel = [params.offsetX, params.offsetY];
const pointInGrid =
this.chart.convertFromPixel({seriesIndex: 0}, pointInPixel);
const xIndex = pointInGrid[0];
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: xIndex,
}));
this.setXMarkLine(xIndex);
});
this.chartXAxisData = null;
this.chartSeriesData = null;
this.currentIndex = 0;
window.addEventListener('resize', () => {
this.chart.resize();
});
}
$(id) {
return this.shadowRoot.querySelector(id);
}
set data(value) {
this._data = value;
this.stateChanged();
}
get data() {
return this._data;
}
hide() {
this.$('#container').style.display = 'none';
}
show() {
this.$('#container').style.display = 'block';
}
stateChanged() {
this.initTrendLineNames();
this.initXAxisDataAndSeries();
this.drawChart();
}
initTrendLineNames() {
this.trend_line_names = [];
for (const space_name of kSpaceNames) {
this.trend_line_names.push(
TrendLineHelper.getSizeTrendLineName(space_name));
this.trend_line_names.push(
TrendLineHelper.getAllocatedTrendSizeName(space_name));
}
}
// X axis represent the moment before or after nth GC : [B1,A1,...Bn,An].
initXAxisDataAndSeries() {
this.chartXAxisData = [];
this.chartSeriesData = [];
let trend_line_name_data_dict = {};
for (const trend_line_name of this.trend_line_names) {
trend_line_name_data_dict[trend_line_name] = [];
}
// Init x axis data and trend line series.
for (const snapshot of this.data) {
this.chartXAxisData.push(
TrendLineHelper.snapshotHeaderToXLabel(snapshot.header));
for (const [space_name, pageinfos] of Object.entries(snapshot.data)) {
const size_trend_line_name =
TrendLineHelper.getSizeTrendLineName(space_name);
const allocated_trend_line_name =
TrendLineHelper.getAllocatedTrendSizeName(space_name);
let size_sum = 0;
let allocated_sum = 0;
for (const pageinfo of pageinfos) {
size_sum += pageinfo[2] - pageinfo[1];
allocated_sum += pageinfo[3];
}
trend_line_name_data_dict[size_trend_line_name].push(size_sum);
trend_line_name_data_dict[allocated_trend_line_name].push(
allocated_sum);
}
}
// Init mark line series as the first series
const markline_series = {
name: 'mark-line',
type: 'line',
markLine: {
silent: true,
symbol: 'none',
label: {
show: false,
},
lineStyle: {
color: '#333',
},
data: [
{
xAxis: 0,
},
],
},
};
this.chartSeriesData.push(markline_series);
for (const [trend_line_name, trend_line_data] of Object.entries(
trend_line_name_data_dict)) {
const color = getColorFromSpaceName(
TrendLineHelper.getSpaceNameFromTrendLineName(trend_line_name));
const trend_line_series = {
name: trend_line_name,
type: 'line',
data: trend_line_data,
lineStyle: {
color: color,
},
itemStyle: {
color: color,
},
symbol: TrendLineHelper.getLineSymbolFromTrendLineName(trend_line_name),
symbolSize: 8,
};
this.chartSeriesData.push(trend_line_series);
}
}
setXMarkLine(index) {
if (index < 0 || index >= this.data.length) {
console.error('Invalid index:', index);
return;
}
// Set the mark-line series
this.chartSeriesData[0].markLine.data[0].xAxis = index;
this.chart.setOption({
series: this.chartSeriesData,
});
this.currentIndex = index;
}
drawChart() {
const option = {
dataZoom: [
{
type: 'inside',
filterMode: 'weakFilter',
},
{
type: 'slider',
filterMode: 'weakFilter',
labelFormatter: '',
},
],
title: {
text: 'Size Trend',
left: 'center',
},
tooltip: {
trigger: 'axis',
position(point, params, dom, rect, size) {
let ret_x = point[0] + 10;
if (point[0] > size.viewSize[0] * 0.7) {
ret_x = point[0] - dom.clientWidth - 10;
}
return [ret_x, '85%'];
},
formatter(params) {
const colorSpan = (color) =>
'<span style="display:inline-block;margin-right:1px;border-radius:5px;width:9px;height:9px;background-color:' +
color + '"></span>';
let result = '<p>' + params[0].axisValue + '</p>';
params.forEach((item) => {
const xx = '<p style="margin:0;">' + colorSpan(item.color) + ' ' +
item.seriesName + ': ' + (item.data / MB).toFixed(2) + 'MB' +
'</p>';
result += xx;
});
return result;
},
},
legend: {
data: this.trend_line_names,
top: '6%',
type: 'scroll',
},
xAxis: {
minInterval: 1,
type: 'category',
boundaryGap: false,
data: this.chartXAxisData,
},
yAxis: {
type: 'value',
axisLabel: {
formatter(value, index) {
return (value / MB).toFixed(3) + 'MB';
},
},
},
series: this.chartSeriesData,
};
this.show();
this.chart.resize();
this.chart.setOption(option);
}
});
:root {
--surface-color: #ffffff;
--primary-color: #bb86fc;
--on-primary-color: #000000;
--error-color: #cf6679;
--file-reader-background-color: #ffffff80;
--file-reader-border-color: #000000;
}
body {
font-family: "Roboto", sans-serif;
margin-left: 5%;
margin-right: 5%;
}
.button-container {
text-align: center;
display: none;
}
button {
height: 50px;
width: 100px;
}
<!DOCTYPE html>
<!-- Copyright 2021 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>V8 Heap Layout</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.2.2/echarts.min.js"></script>
<script type="module" src="heap-layout-viewer.mjs"></script>
<script type="module" src="heap-size-trend-viewer.mjs"></script>
<script type="module" src="trace-file-reader.mjs"></script>
<link rel="stylesheet" type="text/css" href="./index.css">
<script>
'use strict';
function $(id) { return document.querySelector(id); }
function globalDataChanged(e) {
$('#heap-layout-viewer').data = e.detail;
$('#heap-size-trend-viewer').data = e.detail;
$('.button-container').style.display = 'block';
}
function selectSnapshotAtIndex(e) {
const index = e.detail;
$('#heap-layout-viewer').drawChart(index);
}
function OnPrevClick() {
const heap_size_trend_viewer = $('#heap-size-trend-viewer');
const heap_layout_viewer = $('#heap-layout-viewer');
heap_size_trend_viewer.setXMarkLine(heap_size_trend_viewer.currentIndex - 1);
heap_layout_viewer.drawChart(heap_layout_viewer.currentIndex - 1);
}
function OnNextClick() {
const heap_size_trend_viewer = $('#heap-size-trend-viewer');
const heap_layout_viewer = $('#heap-layout-viewer');
heap_size_trend_viewer.setXMarkLine(heap_size_trend_viewer.currentIndex + 1);
heap_layout_viewer.drawChart(heap_layout_viewer.currentIndex + 1);
}
</script>
</head>
<body>
<h1>V8 Heap Layout</h1>
<trace-file-reader onchange="globalDataChanged(event)"></trace-file-reader>
<heap-size-trend-viewer id="heap-size-trend-viewer" onchange="selectSnapshotAtIndex(event)"></heap-size-trend-viewer>
<heap-layout-viewer id="heap-layout-viewer"></heap-layout-viewer>
<div class="button-container">
<button id="button_prev" type="button" onclick="OnPrevClick()">Prev</button>
<button id="button_next" type="button" onclick="OnNextClick()">Next</button>
</div>
<p>Heap layout is a HTML-based tool for visualizing V8-internal page layout.</p>
<p>Visualize page layout that have been gathered using</p>
<ul>
<li><code>--trace-gc-page-layout</code> on V8</li>
</ul>
</body>
</html>
\ No newline at end of file
// Copyright 2021 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.
export const kSpaceNames = [
'to_space',
'from_space',
'old_space',
'map_space',
'code_space',
'large_object_space',
'new_large_object_space',
'code_large_object_space',
'ro_space',
];
const kSpaceColors = [
'#5b8ff9',
'#5ad8a6',
'#5d7092',
'#f6bd16',
'#e8684a',
'#6dc8ec',
'#9270ca',
'#ff9d4d',
'#269a99',
];
export function getColorFromSpaceName(space_name) {
const index = kSpaceNames.indexOf(space_name);
return kSpaceColors[index];
}
// Copyright 2021 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 {calcOffsetInVMCage} from '../js/helper.mjs';
import {DOM, FileReader,} from '../js/web-api-helper.mjs';
import {kSpaceNames} from './space-categories.mjs';
class TraceLogParseHelper {
static re_gc_header = /(Before|After) GC:\d/;
static re_page_info =
/\{owner:.+,address:.+,size:.+,allocated_bytes:.+,wasted_memory:.+\}/;
static re_owner = /(?<=owner:)[a-z_]+_space/;
static re_address = /(?<=address:)0x[a-f0-9]+(?=,)/;
static re_size = /(?<=size:)\d+(?=,)/;
static re_allocated_bytes = /(?<=allocated_bytes:)\d+(?=,)/;
static re_wasted_memory = /(?<=wasted_memory:)\d+(?=})/;
static matchGCHeader(content) {
return this.re_gc_header.test(content);
}
static matchPageInfo(content) {
return this.re_page_info.test(content);
}
static parsePageInfo(content) {
const owner = this.re_owner.exec(content)[0];
const address =
calcOffsetInVMCage(BigInt(this.re_address.exec(content)[0], 16));
const size = parseInt(this.re_size.exec(content)[0]);
const allocated_bytes = parseInt(this.re_allocated_bytes.exec(content)[0]);
const wasted_memory = parseInt(this.re_wasted_memory.exec(content)[0]);
const info = [
owner,
address,
address + size,
allocated_bytes,
wasted_memory,
];
return info;
}
// Create a empty snapshot.
static createSnapShotData() {
let snapshot = {header: null, data: {}};
for (let space_name of kSpaceNames) {
snapshot.data[space_name] = [];
}
return snapshot;
}
static createModelFromV8TraceFile(contents) {
let snapshots = [];
let snapshot = this.createSnapShotData();
// Fill data info a snapshot, then push it into snapshots.
for (let content of contents) {
if (this.matchGCHeader(content)) {
if (snapshot.header != null) {
snapshots.push(snapshot);
}
snapshot = this.createSnapShotData();
snapshot.header = content;
continue;
}
if (this.matchPageInfo(content)) {
let pageinfo = this.parsePageInfo(content);
try {
snapshot.data[pageinfo[0]].push(pageinfo);
} catch (e) {
console.error(e);
}
}
}
// EOL, push the last.
if (snapshot.header != null) {
snapshots.push(snapshot);
}
return snapshots;
}
}
DOM.defineCustomElement('../js/log-file-reader', 'trace-file-reader',
(templateText) =>
class TraceFileReader extends FileReader {
constructor() {
super(templateText);
this.fullDataFromFile = '';
this.addEventListener('fileuploadchunk', (e) => this.handleLoadChunk(e));
this.addEventListener('fileuploadend', (e) => this.handleLoadEnd(e));
}
handleLoadChunk(event) {
this.fullDataFromFile += event.detail;
}
handleLoadEnd(event) {
let contents = this.fullDataFromFile.split('\n');
let snapshots = TraceLogParseHelper.createModelFromV8TraceFile(contents);
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: snapshots,
}));
}
});
...@@ -80,6 +80,10 @@ dd, dt { ...@@ -80,6 +80,10 @@ dd, dt {
<dt><a href="./heap-stats/index.html">Heap Stats</a></dt> <dt><a href="./heap-stats/index.html">Heap Stats</a></dt>
<dd>Visualize heap memory usage.</dd> <dd>Visualize heap memory usage.</dd>
</div> </div>
<div class="card">
<dt><a href="./heap-layout/index.html">Heap Layout</a></dt>
<dd>Visualize heap memory layout.</dd>
</div>
<div class="card"> <div class="card">
<dt><a href="./parse-processor.html">Parse Processor</a></dt> <dt><a href="./parse-processor.html">Parse Processor</a></dt>
<dd>Analyse parse, compile and first-execution.</dd> <dd>Analyse parse, compile and first-execution.</dd>
......
// Copyright 2021 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.
export const KB = 1024;
export const MB = KB * KB;
export const GB = MB * KB;
export const kMillis2Seconds = 1 / 1000;
export const kMicro2Milli = 1 / 1000;
export 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];
}
export function formatMicroSeconds(micro) {
return (micro * kMicro2Milli).toFixed(1) + 'ms';
}
export function formatDurationMicros(micros, secondsDigits = 3) {
return formatDurationMillis(micros * kMicro2Milli, secondsDigits);
}
export function formatDurationMillis(millis, secondsDigits = 3) {
if (millis < 1000) {
if (millis < 1) {
return (millis / kMicro2Milli).toFixed(1) + 'ns';
}
return millis.toFixed(2) + 'ms';
}
let seconds = millis / 1000;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
seconds = seconds % 60;
let buffer = '';
if (hours > 0) buffer += hours + 'h ';
if (hours > 0 || minutes > 0) buffer += minutes + 'm ';
buffer += seconds.toFixed(secondsDigits) + 's';
return buffer;
}
// Get the offset in the 4GB virtual memory cage.
export function calcOffsetInVMCage(address) {
let mask = (1n << 32n) - 1n;
let ret = Number(address & mask);
return ret;
}
<!-- Copyright 2020 the V8 project authors. All rights reserved. <!-- Copyright 2021 the V8 project authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. --> found in the LICENSE file. -->
<head> <head>
<link href="./index.css" rel="stylesheet"> <link href="./index.css" rel="stylesheet" />
</head> </head>
<style> <style>
#fileReader { #fileReader {
...@@ -13,6 +13,8 @@ found in the LICENSE file. --> ...@@ -13,6 +13,8 @@ found in the LICENSE file. -->
cursor: pointer; cursor: pointer;
transition: all 0.5s ease-in-out; transition: all 0.5s ease-in-out;
background-color: var(--surface-color); background-color: var(--surface-color);
border: solid 1px var(--file-reader-border-color);
border-radius: 5px;
} }
#fileReader:hover { #fileReader:hover {
...@@ -20,7 +22,7 @@ found in the LICENSE file. --> ...@@ -20,7 +22,7 @@ found in the LICENSE file. -->
color: var(--on-primary-color); color: var(--on-primary-color);
} }
.done #fileReader{ .done #fileReader {
display: none; display: none;
} }
...@@ -32,7 +34,7 @@ found in the LICENSE file. --> ...@@ -32,7 +34,7 @@ found in the LICENSE file. -->
cursor: wait; cursor: wait;
} }
#fileReader>input { #fileReader > input {
display: none; display: none;
} }
...@@ -79,11 +81,11 @@ found in the LICENSE file. --> ...@@ -79,11 +81,11 @@ found in the LICENSE file. -->
} }
</style> </style>
<div id="root"> <div id="root">
<div id="fileReader" class="panel" tabindex=1> <div id="fileReader" class="panel" tabindex="1">
<span id="label"> <span id="label">
Drag and drop a v8.log file into this area, or click to choose from disk. Drag and drop a v8.log file into this area, or click to choose from disk.
</span> </span>
<input id="file" type="file" name="file"> <input id="file" type="file" name="file" />
</div> </div>
<div id="loader"> <div id="loader">
<div id="spinner"></div> <div id="spinner"></div>
......
// Copyright 2021 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.
export class V8CustomElement extends HTMLElement {
_updateTimeoutId;
_updateCallback = this.forceUpdate.bind(this);
constructor(templateText) {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
}
$(id) {
return this.shadowRoot.querySelector(id);
}
querySelectorAll(query) {
return this.shadowRoot.querySelectorAll(query);
}
requestUpdate(useAnimation = false) {
if (useAnimation) {
window.cancelAnimationFrame(this._updateTimeoutId);
this._updateTimeoutId =
window.requestAnimationFrame(this._updateCallback);
} else {
// Use timeout tasks to asynchronously update the UI without blocking.
clearTimeout(this._updateTimeoutId);
const kDelayMs = 5;
this._updateTimeoutId = setTimeout(this._updateCallback, kDelayMs);
}
}
forceUpdate() {
this._update();
}
_update() {
throw Error('Subclass responsibility');
}
}
export class FileReader extends V8CustomElement {
constructor(templateText) {
super(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));
}
set error(message) {
this._updateLabel(message);
this.root.className = 'fail';
}
_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();
this.dispatchEvent(
new CustomEvent('fileuploadstart', {bubbles: true, composed: true}));
const host = event.dataTransfer ? event.dataTransfer : event.target;
this.readFile(host.files[0]);
}
handleDragOver(event) {
event.preventDefault();
}
connectedCallback() {
this.fileReader.focus();
}
get fileReader() {
return this.$('#fileReader');
}
get root() {
return this.$('#root');
}
readFile(file) {
if (!file) {
this.error = 'Failed to load file.';
return;
}
this.fileReader.blur();
this.root.className = 'loading';
// Delay the loading a bit to allow for CSS animations to happen.
window.requestAnimationFrame(() => this.asyncReadFile(file));
}
async asyncReadFile(file) {
const decoder = globalThis.TextDecoderStream;
if (decoder) {
await this._streamFile(file, decoder);
} else {
await this._readFullFile(file);
}
this._updateLabel(`Finished loading '${file.name}'.`);
this.dispatchEvent(
new CustomEvent('fileuploadend', {bubbles: true, composed: true}));
this.root.className = 'done';
}
async _readFullFile(file) {
const text = await file.text();
this._handleFileChunk(text);
}
async _streamFile(file, decoder) {
const stream = file.stream().pipeThrough(new decoder());
const reader = stream.getReader();
let chunk, readerDone;
do {
const readResult = await reader.read();
chunk = readResult.value;
readerDone = readResult.done;
if (chunk) this._handleFileChunk(chunk);
} while (!readerDone);
}
_handleFileChunk(chunk) {
this.dispatchEvent(new CustomEvent('fileuploadchunk', {
bubbles: true,
composed: true,
detail: chunk,
}));
}
}
export class DOM {
static element(type, options) {
const node = document.createElement(type);
if (options !== undefined) {
if (typeof options === 'string') {
// Old behaviour: options = class string
node.className = options;
} else if (Array.isArray(options)) {
// Old behaviour: options = class array
DOM.addClasses(node, options);
} else {
// New behaviour: options = attribute dict
for (const [key, value] of Object.entries(options)) {
if (key == 'className') {
node.className = value;
} else if (key == 'classList') {
node.classList = value;
} else if (key == 'textContent') {
node.textContent = value;
} else if (key == 'children') {
for (const child of value) {
node.appendChild(child);
}
} else {
node.setAttribute(key, value);
}
}
}
}
return node;
}
static addClasses(node, classes) {
const classList = node.classList;
if (typeof classes === 'string') {
classList.add(classes);
} else {
for (let i = 0; i < classes.length; i++) {
classList.add(classes[i]);
}
}
return node;
}
static text(string) {
return document.createTextNode(string);
}
static button(label, clickHandler) {
const button = DOM.element('button');
button.innerText = label;
button.onclick = clickHandler;
return button;
}
static div(options) {
return this.element('div', options);
}
static span(options) {
return this.element('span', options);
}
static table(options) {
return this.element('table', options);
}
static tbody(options) {
return this.element('tbody', options);
}
static td(textOrNode, className) {
const node = this.element('td');
if (typeof textOrNode === 'object') {
node.appendChild(textOrNode);
} else if (textOrNode) {
node.innerText = textOrNode;
}
if (className) node.className = className;
return node;
}
static tr(classes) {
return this.element('tr', classes);
}
static removeAllChildren(node) {
let range = document.createRange();
range.selectNodeContents(node);
range.deleteContents();
}
static defineCustomElement(
path, nameOrGenerator, maybeGenerator = undefined) {
let generator = nameOrGenerator;
let name = nameOrGenerator;
if (typeof nameOrGenerator == 'function') {
console.assert(maybeGenerator === undefined);
name = path.substring(path.lastIndexOf('/') + 1, path.length);
} else {
console.assert(typeof nameOrGenerator == 'string');
generator = maybeGenerator;
}
path = path + '-template.html';
fetch(path)
.then(stream => stream.text())
.then(
templateText =>
customElements.define(name, generator(templateText)));
}
}
\ No newline at end of file
...@@ -2,48 +2,6 @@ ...@@ -2,48 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
export const KB = 1024;
export const MB = KB * KB;
export const GB = MB * KB;
export const kMicro2Milli = 1 / 1000;
export 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];
}
export function formatMicroSeconds(micro) {
return (micro * kMicro2Milli).toFixed(1) + 'ms';
}
export function formatDurationMicros(micros, secondsDigits = 3) {
return formatDurationMillis(micros * kMicro2Milli, secondsDigits);
}
export function formatDurationMillis(millis, secondsDigits = 3) {
if (millis < 1000) {
if (millis < 1) {
return (millis / kMicro2Milli).toFixed(1) + 'ns';
}
return millis.toFixed(2) + 'ms';
}
let seconds = millis / 1000;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
seconds = seconds % 60;
let buffer = ''
if (hours > 0) buffer += hours + 'h ';
if (hours > 0 || minutes > 0) buffer += minutes + 'm ';
buffer += seconds.toFixed(secondsDigits) + 's'
return buffer;
}
export function delay(time) { export function delay(time) {
return new Promise(resolver => setTimeout(resolver, time)); return new Promise(resolver => setTimeout(resolver, time));
} }
...@@ -105,3 +63,5 @@ export function groupBy(array, keyFunction, collect = false) { ...@@ -105,3 +63,5 @@ export function groupBy(array, keyFunction, collect = false) {
// Sort by length // Sort by length
return groups.sort((a, b) => b.length - a.length); return groups.sort((a, b) => b.length - a.length);
} }
export * from '../js/helper.mjs'
\ No newline at end of file
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
--map-background-color: #5e5454; --map-background-color: #5e5454;
--timeline-background-color: #1f1f1f; --timeline-background-color: #1f1f1f;
--file-reader-background-color: #ffffff80; --file-reader-background-color: #ffffff80;
--file-reader-border-color: #ffffff;
--red: #dc6eae; --red: #dc6eae;
--green: #aedc6e; --green: #aedc6e;
--yellow: #eeff41; --yellow: #eeff41;
......
...@@ -11,7 +11,7 @@ found in the LICENSE file. --> ...@@ -11,7 +11,7 @@ found in the LICENSE file. -->
<link rel="modulepreload" href="./helper.mjs" > <link rel="modulepreload" href="./helper.mjs" >
<link rel="modulepreload" href="./view/log-file-reader.mjs" > <link rel="modulepreload" href="./view/log-file-reader.mjs" >
<link rel="modulepreload" href="./view/helper.mjs" > <link rel="modulepreload" href="./view/helper.mjs" >
<link rel="preload" href="./view/log-file-reader-template.html" as="fetch" crossorigin="anonymous"> <link rel="preload" href="../js/log-file-reader-template.html" as="fetch" crossorigin="anonymous">
<script type="module"> <script type="module">
// Force instatiating the log-reader before anything else. // Force instatiating the log-reader before anything else.
import "./view/log-file-reader.mjs"; import "./view/log-file-reader.mjs";
......
...@@ -122,117 +122,7 @@ export class CSSColor { ...@@ -122,117 +122,7 @@ export class CSSColor {
} }
} }
export class DOM { import {DOM} from '../../js/web-api-helper.mjs';
static element(type, options) {
const node = document.createElement(type);
if (options !== undefined) {
if (typeof options === 'string') {
// Old behaviour: options = class string
node.className = options;
} else if (Array.isArray(options)) {
// Old behaviour: options = class array
DOM.addClasses(node, options);
} else {
// New behaviour: options = attribute dict
for (const [key, value] of Object.entries(options)) {
if (key == 'className') {
node.className = value;
} else if (key == 'classList') {
node.classList = value;
} else if (key == 'textContent') {
node.textContent = value;
} else if (key == 'children') {
for (const child of value) {
node.appendChild(child);
}
} else {
node.setAttribute(key, value);
}
}
}
}
return node;
}
static addClasses(node, classes) {
const classList = node.classList;
if (typeof classes === 'string') {
classList.add(classes);
} else {
for (let i = 0; i < classes.length; i++) {
classList.add(classes[i]);
}
}
return node;
}
static text(string) {
return document.createTextNode(string);
}
static button(label, clickHandler) {
const button = DOM.element('button');
button.innerText = label;
button.onclick = clickHandler;
return button;
}
static div(options) {
return this.element('div', options);
}
static span(options) {
return this.element('span', options);
}
static table(options) {
return this.element('table', options);
}
static tbody(options) {
return this.element('tbody', options);
}
static td(textOrNode, className) {
const node = this.element('td');
if (typeof textOrNode === 'object') {
node.appendChild(textOrNode);
} else if (textOrNode) {
node.innerText = textOrNode;
}
if (className) node.className = className;
return node;
}
static tr(classes) {
return this.element('tr', classes);
}
static removeAllChildren(node) {
let range = document.createRange();
range.selectNodeContents(node);
range.deleteContents();
}
static defineCustomElement(
path, nameOrGenerator, maybeGenerator = undefined) {
let generator = nameOrGenerator;
let name = nameOrGenerator;
if (typeof nameOrGenerator == 'function') {
console.assert(maybeGenerator === undefined);
name = path.substring(path.lastIndexOf('/') + 1, path.length);
} else {
console.assert(typeof nameOrGenerator == 'string');
generator = maybeGenerator;
}
path = path + '-template.html';
fetch(path)
.then(stream => stream.text())
.then(
templateText =>
customElements.define(name, generator(templateText)));
}
}
const SVGNamespace = 'http://www.w3.org/2000/svg'; const SVGNamespace = 'http://www.w3.org/2000/svg';
export class SVG { export class SVG {
...@@ -259,45 +149,7 @@ export function $(id) { ...@@ -259,45 +149,7 @@ export function $(id) {
return document.querySelector(id) return document.querySelector(id)
} }
export class V8CustomElement extends HTMLElement { import {V8CustomElement} from '../../js/web-api-helper.mjs'
_updateTimeoutId;
_updateCallback = this.forceUpdate.bind(this);
constructor(templateText) {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = templateText;
}
$(id) {
return this.shadowRoot.querySelector(id);
}
querySelectorAll(query) {
return this.shadowRoot.querySelectorAll(query);
}
requestUpdate(useAnimation = false) {
if (useAnimation) {
window.cancelAnimationFrame(this._updateTimeoutId);
this._updateTimeoutId =
window.requestAnimationFrame(this._updateCallback);
} else {
// Use timeout tasks to asynchronously update the UI without blocking.
clearTimeout(this._updateTimeoutId);
const kDelayMs = 5;
this._updateTimeoutId = setTimeout(this._updateCallback, kDelayMs);
}
}
forceUpdate() {
this._update();
}
_update() {
throw Error('Subclass responsibility');
}
}
export class CollapsableElement extends V8CustomElement { export class CollapsableElement extends V8CustomElement {
constructor(templateText) { constructor(templateText) {
...@@ -468,3 +320,4 @@ export function gradientStopsFromGroups( ...@@ -468,3 +320,4 @@ export function gradientStopsFromGroups(
} }
export * from '../helper.mjs'; export * from '../helper.mjs';
export * from '../../js/web-api-helper.mjs'
\ No newline at end of file
// Copyright 2020 the V8 project authors. All rights reserved. // Copyright 2020 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import {delay} from '../helper.mjs'; import {DOM, FileReader} from './helper.mjs';
import {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
DOM.defineCustomElement('view/log-file-reader', '../../js/log-file-reader',
(templateText) => (templateText) => class LogFileReader extends FileReader {
class LogFileReader extends V8CustomElement { constructor() {
constructor() { super(templateText);
super(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));
}
set error(message) {
this._updateLabel(message);
this.root.className = 'fail';
}
_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();
this.dispatchEvent(
new CustomEvent('fileuploadstart', {bubbles: true, composed: true}));
const host = event.dataTransfer ? event.dataTransfer : event.target;
this.readFile(host.files[0]);
}
handleDragOver(event) {
event.preventDefault();
}
connectedCallback() {
this.fileReader.focus();
}
get fileReader() {
return this.$('#fileReader');
}
get root() {
return this.$('#root');
}
readFile(file) {
if (!file) {
this.error = 'Failed to load file.';
return;
}
this.fileReader.blur();
this.root.className = 'loading';
// Delay the loading a bit to allow for CSS animations to happen.
window.requestAnimationFrame(() => this.asyncReadFile(file));
}
async asyncReadFile(file) {
const decoder = globalThis.TextDecoderStream;
if (decoder) {
await this._streamFile(file, decoder);
} else {
await this._readFullFile(file);
}
this._updateLabel(`Finished loading '${file.name}'.`);
this.dispatchEvent(
new CustomEvent('fileuploadend', {bubbles: true, composed: true}));
this.root.className = 'done';
}
async _readFullFile(file) {
const text = await file.text();
this._handleFileChunk(text)
}
async _streamFile(file, decoder) {
const stream = file.stream().pipeThrough(new decoder());
const reader = stream.getReader();
let chunk, readerDone;
do {
const readResult = await reader.read();
chunk = readResult.value;
readerDone = readResult.done;
if (chunk) this._handleFileChunk(chunk);
} while (!readerDone);
}
_handleFileChunk(chunk) {
this.dispatchEvent(new CustomEvent('fileuploadchunk', {
bubbles: true,
composed: true,
detail: chunk,
}));
}
});
...@@ -441,7 +441,7 @@ class JSLintProcessor(CacheableSourceFileProcessor): ...@@ -441,7 +441,7 @@ class JSLintProcessor(CacheableSourceFileProcessor):
return name.endswith('.js') or name.endswith('.mjs') return name.endswith('.js') or name.endswith('.mjs')
def GetPathsToSearch(self): def GetPathsToSearch(self):
return ['tools/system-analyzer'] return ['tools/system-analyzer', 'tools/heap-layout', 'tools/js']
def GetProcessorWorker(self): def GetProcessorWorker(self):
return JSLintWorker return JSLintWorker
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment