[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: Camillo Bruni <cbruni@chromium.org> Commit-Queue: Shiyu Zhang <shiyu.zhang@intel.com> Cr-Commit-Position: refs/heads/main@{#78428}
This commit is contained in:
parent
0dbcfe1fde
commit
257b0a43ac
@ -21,6 +21,7 @@ group("v8_mjsunit") {
|
||||
"../../tools/consarray.mjs",
|
||||
"../../tools/csvparser.mjs",
|
||||
"../../tools/dumpcpp.mjs",
|
||||
"../../tools/js/helper.mjs",
|
||||
"../../tools/logreader.mjs",
|
||||
"../../tools/profile.mjs",
|
||||
"../../tools/profile_view.mjs",
|
||||
|
14
tools/heap-layout/heap-layout-viewer-template.html
Normal file
14
tools/heap-layout/heap-layout-viewer-template.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!-- 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>
|
225
tools/heap-layout/heap-layout-viewer.mjs
Normal file
225
tools/heap-layout/heap-layout-viewer.mjs
Normal file
@ -0,0 +1,225 @@
|
||||
// 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;
|
||||
}
|
||||
});
|
14
tools/heap-layout/heap-size-trend-viewer-template.html
Normal file
14
tools/heap-layout/heap-size-trend-viewer-template.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!-- 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>
|
266
tools/heap-layout/heap-size-trend-viewer.mjs
Normal file
266
tools/heap-layout/heap-size-trend-viewer.mjs
Normal file
@ -0,0 +1,266 @@
|
||||
// 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);
|
||||
}
|
||||
});
|
24
tools/heap-layout/index.css
Normal file
24
tools/heap-layout/index.css
Normal file
@ -0,0 +1,24 @@
|
||||
: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;
|
||||
}
|
72
tools/heap-layout/index.html
Normal file
72
tools/heap-layout/index.html
Normal file
@ -0,0 +1,72 @@
|
||||
<!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>
|
32
tools/heap-layout/space-categories.mjs
Normal file
32
tools/heap-layout/space-categories.mjs
Normal file
@ -0,0 +1,32 @@
|
||||
// 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];
|
||||
}
|
110
tools/heap-layout/trace-file-reader.mjs
Normal file
110
tools/heap-layout/trace-file-reader.mjs
Normal file
@ -0,0 +1,110 @@
|
||||
// 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 {
|
||||
<dt><a href="./heap-stats/index.html">Heap Stats</a></dt>
|
||||
<dd>Visualize heap memory usage.</dd>
|
||||
</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">
|
||||
<dt><a href="./parse-processor.html">Parse Processor</a></dt>
|
||||
<dd>Analyse parse, compile and first-execution.</dd>
|
||||
|
53
tools/js/helper.mjs
Normal file
53
tools/js/helper.mjs
Normal file
@ -0,0 +1,53 @@
|
||||
// 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;
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
<!-- 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
|
||||
found in the LICENSE file. -->
|
||||
|
||||
<head>
|
||||
<link href="./index.css" rel="stylesheet">
|
||||
<link href="./index.css" rel="stylesheet" />
|
||||
</head>
|
||||
<style>
|
||||
#fileReader {
|
||||
@ -13,6 +13,8 @@ found in the LICENSE file. -->
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease-in-out;
|
||||
background-color: var(--surface-color);
|
||||
border: solid 1px var(--file-reader-border-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#fileReader:hover {
|
||||
@ -20,7 +22,7 @@ found in the LICENSE file. -->
|
||||
color: var(--on-primary-color);
|
||||
}
|
||||
|
||||
.done #fileReader{
|
||||
.done #fileReader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -32,7 +34,7 @@ found in the LICENSE file. -->
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
#fileReader>input {
|
||||
#fileReader > input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -79,11 +81,11 @@ found in the LICENSE file. -->
|
||||
}
|
||||
</style>
|
||||
<div id="root">
|
||||
<div id="fileReader" class="panel" tabindex=1>
|
||||
<div id="fileReader" class="panel" 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">
|
||||
<input id="file" type="file" name="file" />
|
||||
</div>
|
||||
<div id="loader">
|
||||
<div id="spinner"></div>
|
258
tools/js/web-api-helper.mjs
Normal file
258
tools/js/web-api-helper.mjs
Normal file
@ -0,0 +1,258 @@
|
||||
// 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)));
|
||||
}
|
||||
}
|
@ -2,48 +2,6 @@
|
||||
// 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 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) {
|
||||
return new Promise(resolver => setTimeout(resolver, time));
|
||||
}
|
||||
@ -105,3 +63,5 @@ export function groupBy(array, keyFunction, collect = false) {
|
||||
// Sort by length
|
||||
return groups.sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
export * from '../js/helper.mjs'
|
@ -13,6 +13,7 @@
|
||||
--map-background-color: #5e5454;
|
||||
--timeline-background-color: #1f1f1f;
|
||||
--file-reader-background-color: #ffffff80;
|
||||
--file-reader-border-color: #ffffff;
|
||||
--red: #dc6eae;
|
||||
--green: #aedc6e;
|
||||
--yellow: #eeff41;
|
||||
|
@ -11,7 +11,7 @@ found in the LICENSE file. -->
|
||||
<link rel="modulepreload" href="./helper.mjs" >
|
||||
<link rel="modulepreload" href="./view/log-file-reader.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">
|
||||
// Force instatiating the log-reader before anything else.
|
||||
import "./view/log-file-reader.mjs";
|
||||
|
@ -122,117 +122,7 @@ export class CSSColor {
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
import {DOM} from '../../js/web-api-helper.mjs';
|
||||
|
||||
const SVGNamespace = 'http://www.w3.org/2000/svg';
|
||||
export class SVG {
|
||||
@ -259,45 +149,7 @@ export function $(id) {
|
||||
return document.querySelector(id)
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
import {V8CustomElement} from '../../js/web-api-helper.mjs'
|
||||
|
||||
export class CollapsableElement extends V8CustomElement {
|
||||
constructor(templateText) {
|
||||
@ -468,3 +320,4 @@ export function gradientStopsFromGroups(
|
||||
}
|
||||
|
||||
export * from '../helper.mjs';
|
||||
export * from '../../js/web-api-helper.mjs'
|
@ -1,110 +1,12 @@
|
||||
// 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 {delay} from '../helper.mjs';
|
||||
import {DOM, V8CustomElement} from './helper.mjs';
|
||||
import {DOM, FileReader} from './helper.mjs';
|
||||
|
||||
DOM.defineCustomElement('view/log-file-reader',
|
||||
(templateText) =>
|
||||
class LogFileReader extends V8CustomElement {
|
||||
constructor() {
|
||||
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,
|
||||
}));
|
||||
}
|
||||
});
|
||||
DOM.defineCustomElement(
|
||||
'../../js/log-file-reader',
|
||||
(templateText) => class LogFileReader extends FileReader {
|
||||
constructor() {
|
||||
super(templateText);
|
||||
}
|
||||
});
|
||||
|
@ -441,7 +441,7 @@ class JSLintProcessor(CacheableSourceFileProcessor):
|
||||
return name.endswith('.js') or name.endswith('.mjs')
|
||||
|
||||
def GetPathsToSearch(self):
|
||||
return ['tools/system-analyzer']
|
||||
return ['tools/system-analyzer', 'tools/heap-layout', 'tools/js']
|
||||
|
||||
def GetProcessorWorker(self):
|
||||
return JSLintWorker
|
||||
|
Loading…
Reference in New Issue
Block a user