diff --git a/.gitignore b/.gitignore index ffbf9ce355..77543b2892 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ .cproject .gclient_entries .gdb_history +.jslint-cache .landmines .project .pydevproject diff --git a/PRESUBMIT.py b/PRESUBMIT.py index eba4158d81..91ba6c534b 100644 --- a/PRESUBMIT.py +++ b/PRESUBMIT.py @@ -80,6 +80,7 @@ def _V8PresubmitChecks(input_api, output_api): sys.path.append(input_api.os_path.join( input_api.PresubmitLocalPath(), 'tools')) from v8_presubmit import CppLintProcessor + from v8_presubmit import JSLintProcessor from v8_presubmit import TorqueLintProcessor from v8_presubmit import SourceProcessor from v8_presubmit import StatusFilesProcessor @@ -95,6 +96,11 @@ def _V8PresubmitChecks(input_api, output_api): affected_file, files_to_check=(r'.+\.tq')) + def FilterJSFile(affected_file): + return input_api.FilterSourceFile( + affected_file, + white_list=(r'.+\.m?js')) + results = [] if not CppLintProcessor().RunOnFiles( input_api.AffectedFiles(file_filter=FilterFile, include_deletes=False)): @@ -103,6 +109,10 @@ def _V8PresubmitChecks(input_api, output_api): input_api.AffectedFiles(file_filter=FilterTorqueFile, include_deletes=False)): results.append(output_api.PresubmitError("Torque format check failed")) + if not JSLintProcessor().RunOnFiles( + input_api.AffectedFiles(file_filter=FilterJSFile, + include_deletes=False)): + results.append(output_api.PresubmitError("JS format check failed")) if not SourceProcessor().RunOnFiles( input_api.AffectedFiles(include_deletes=False)): results.append(output_api.PresubmitError( diff --git a/tools/system-analyzer/app-model.mjs b/tools/system-analyzer/app-model.mjs index 554a3aa47a..a0b176c170 100644 --- a/tools/system-analyzer/app-model.mjs +++ b/tools/system-analyzer/app-model.mjs @@ -3,7 +3,7 @@ // found in the LICENSE file. class State { - _timeSelection = { start: 0, end: Infinity }; + _timeSelection = {start: 0, end: Infinity}; _map; _ic; _selectedMapLogEntries; @@ -60,11 +60,11 @@ class State { this._deoptTimeline = timeline; } set chunks(value) { - //TODO(zcankara) split up between maps and ics, and every timeline track + // TODO(zcankara) split up between maps and ics, and every timeline track this._chunks = value; } get chunks() { - //TODO(zcankara) split up between maps and ics, and every timeline track + // TODO(zcankara) split up between maps and ics, and every timeline track return this._chunks; } get nofChunks() { @@ -74,20 +74,20 @@ class State { this._nofChunks = count; } get map() { - //TODO(zcankara) rename as selectedMapEvents, array of selected events + // TODO(zcankara) rename as selectedMapEvents, array of selected events return this._map; } set map(value) { - //TODO(zcankara) rename as selectedMapEvents, array of selected events + // TODO(zcankara) rename as selectedMapEvents, array of selected events if (!value) return; this._map = value; } get ic() { - //TODO(zcankara) rename selectedICEvents, array of selected events + // TODO(zcankara) rename selectedICEvents, array of selected events return this._ic; } set ic(value) { - //TODO(zcankara) rename selectedIcEvents, array of selected events + // TODO(zcankara) rename selectedIcEvents, array of selected events if (!value) return; this._ic = value; } @@ -122,4 +122,4 @@ class State { } } -export { State }; +export {State}; diff --git a/tools/system-analyzer/events.mjs b/tools/system-analyzer/events.mjs index ff71336b4c..69529233b4 100644 --- a/tools/system-analyzer/events.mjs +++ b/tools/system-analyzer/events.mjs @@ -4,44 +4,48 @@ class SelectionEvent extends CustomEvent { // TODO: turn into static class fields once Safari supports it. - static get name() { return "showentries"; } + static get name() { + return 'showentries'; + } constructor(entries) { - super(SelectionEvent.name, { bubbles: true, composed: true }); + super(SelectionEvent.name, {bubbles: true, composed: true}); if (!Array.isArray(entries) || entries.length == 0) { - throw new Error("No valid entries selected!"); + throw new Error('No valid entries selected!'); } this.entries = entries; } } class FocusEvent extends CustomEvent { - static get name() { return "showentrydetail"; } + static get name() { + return 'showentrydetail'; + } constructor(entry) { - super(FocusEvent.name, { bubbles: true, composed: true }); + super(FocusEvent.name, {bubbles: true, composed: true}); this.entry = entry; } } class SelectTimeEvent extends CustomEvent { - static get name() { return 'timerangeselect'; } + static get name() { + return 'timerangeselect'; + } constructor(start, end) { - super(SelectTimeEvent.name, { bubbles: true, composed: true }); + super(SelectTimeEvent.name, {bubbles: true, composed: true}); this.start = start; this.end = end; } } class SynchronizeSelectionEvent extends CustomEvent { - static get name() { return 'syncselection'; } + static get name() { + return 'syncselection'; + } constructor(start, end) { - super(SynchronizeSelectionEvent.name, { bubbles: true, composed: true }); + super(SynchronizeSelectionEvent.name, {bubbles: true, composed: true}); this.start = start; this.end = end; } } - -export { - SelectionEvent, FocusEvent, SelectTimeEvent, - SynchronizeSelectionEvent -}; +export {SelectionEvent, FocusEvent, SelectTimeEvent, SynchronizeSelectionEvent}; diff --git a/tools/system-analyzer/helper.mjs b/tools/system-analyzer/helper.mjs index 3ace703d70..8a2fd0e318 100644 --- a/tools/system-analyzer/helper.mjs +++ b/tools/system-analyzer/helper.mjs @@ -144,7 +144,7 @@ class DOM { if (className) node.classList.add(className); return node; } - + static tr(className) { const node = document.createElement('tr'); if (className) node.classList.add(className); @@ -162,12 +162,13 @@ class DOM { } static defineCustomElement(path, generator) { - let name = path.substring(path.lastIndexOf("/") + 1, path.length); + let name = path.substring(path.lastIndexOf('/') + 1, path.length); path = path + '-template.html'; fetch(path) - .then(stream => stream.text()) - .then( - templateText => customElements.define(name, generator(templateText))); + .then(stream => stream.text()) + .then( + templateText => + customElements.define(name, generator(templateText))); } } @@ -178,7 +179,7 @@ function $(id) { class V8CustomElement extends HTMLElement { constructor(templateText) { super(); - const shadowRoot = this.attachShadow({ mode: 'open' }); + const shadowRoot = this.attachShadow({mode: 'open'}); shadowRoot.innerHTML = templateText; } $(id) { @@ -190,7 +191,6 @@ class V8CustomElement extends HTMLElement { } } - class LazyTable { constructor(table, rowData, rowElementCreator) { this._table = table; @@ -198,7 +198,7 @@ class LazyTable { this._rowElementCreator = rowElementCreator; const tbody = table.querySelector('tbody'); table.replaceChild(document.createElement('tbody'), tbody); - table.querySelector("tfoot td").onclick = (e) => this._addMoreRows(); + table.querySelector('tfoot td').onclick = (e) => this._addMoreRows(); this._addMoreRows(); } @@ -216,7 +216,6 @@ class LazyTable { } } - class LazyTable { constructor(table, rowData, rowElementCreator) { this._table = table; @@ -224,7 +223,7 @@ class LazyTable { this._rowElementCreator = rowElementCreator; const tbody = table.querySelector('tbody'); table.replaceChild(document.createElement('tbody'), tbody); - table.querySelector("tfoot td").onclick = (e) => this._addMoreRows(); + table.querySelector('tfoot td').onclick = (e) => this._addMoreRows(); this._addMoreRows(); } @@ -247,6 +246,12 @@ function delay(time) { } export { - DOM, $, V8CustomElement, formatBytes, - typeToColor, CSSColor, delay, LazyTable, + DOM, + $, + V8CustomElement, + formatBytes, + typeToColor, + CSSColor, + delay, + LazyTable, }; diff --git a/tools/system-analyzer/ic-model.mjs b/tools/system-analyzer/ic-model.mjs index f09718679a..2bb40b6853 100644 --- a/tools/system-analyzer/ic-model.mjs +++ b/tools/system-analyzer/ic-model.mjs @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { IcLogEntry } from "./log/ic.mjs"; +import {IcLogEntry} from './log/ic.mjs'; // For compatibility with console scripts: print = console.log; @@ -51,8 +51,7 @@ export class Group { group.percentage = Math.round(group.count / length * 100 * 100) / 100; result.push(group); } - result.sort((a, b) => { return b.count - a.count }); + result.sort((a, b) => {return b.count - a.count}); return result; } - } diff --git a/tools/system-analyzer/ic-panel.mjs b/tools/system-analyzer/ic-panel.mjs index 8210b78819..a6e27ec638 100644 --- a/tools/system-analyzer/ic-panel.mjs +++ b/tools/system-analyzer/ic-panel.mjs @@ -2,194 +2,194 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { Group } from './ic-model.mjs'; -import { MapLogEntry } from "./log/map.mjs"; -import { FocusEvent, SelectTimeEvent, SelectionEvent } from './events.mjs'; -import { DOM, V8CustomElement, delay } from './helper.mjs'; -import { IcLogEntry } from './log/ic.mjs'; +import {FocusEvent, SelectionEvent, SelectTimeEvent} from './events.mjs'; +import {delay, DOM, V8CustomElement} from './helper.mjs'; +import {Group} from './ic-model.mjs'; +import {IcLogEntry} from './log/ic.mjs'; +import {MapLogEntry} from './log/map.mjs'; -DOM.defineCustomElement('ic-panel', (templateText) => - class ICPanel extends V8CustomElement { - _selectedLogEntries; - _timeline; - constructor() { - super(templateText); - this.initGroupKeySelect(); - this.groupKey.addEventListener( - 'change', e => this.updateTable(e)); - } - set timeline(value) { - console.assert(value !== undefined, "timeline undefined!"); - this._timeline = value; - this.selectedLogEntries = this._timeline.all; - this.updateCount(); - } - get groupKey() { - return this.$('#group-key'); - } - - get table() { - return this.$('#table'); - } - - get tableBody() { - return this.$('#table-body'); - } - - get count() { - return this.$('#count'); - } - - get spanSelectAll() { - return this.querySelectorAll("span"); - } - - set selectedLogEntries(value) { - this._selectedLogEntries = value; - this.update(); - } - - async update() { - await delay(1); - this.updateCount(); - this.updateTable(); - } - - updateCount() { - this.count.innerHTML = "length=" + this._selectedLogEntries.length; - } - - updateTable(event) { - let select = this.groupKey; - let key = select.options[select.selectedIndex].text; - let tableBody = this.tableBody; - DOM.removeAllChildren(tableBody); - let groups = Group.groupBy(this._selectedLogEntries, key, true); - this.render(groups, tableBody); - } - - escapeHtml(unsafe) { - if (!unsafe) return ""; - return unsafe.toString() - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - handleMapClick(e) { - const group = e.target.parentNode.entry; - const id = group.key; - const selectedMapLogEntries = - this.searchIcLogEntryToMapLogEntry(id, group.entries); - this.dispatchEvent(new SelectionEvent(selectedMapLogEntries)); - } - - searchIcLogEntryToMapLogEntry(id, icLogEntries) { - // searches for mapLogEntries using the id, time - const selectedMapLogEntriesSet = new Set(); - for (const icLogEntry of icLogEntries) { - const time = icLogEntry.time; - const selectedMap = MapLogEntry.get(id, time); - selectedMapLogEntriesSet.add(selectedMap); +DOM.defineCustomElement( + 'ic-panel', (templateText) => class ICPanel extends V8CustomElement { + _selectedLogEntries; + _timeline; + constructor() { + super(templateText); + this.initGroupKeySelect(); + this.groupKey.addEventListener('change', e => this.updateTable(e)); + } + set timeline(value) { + console.assert(value !== undefined, 'timeline undefined!'); + this._timeline = value; + this.selectedLogEntries = this._timeline.all; + this.updateCount(); + } + get groupKey() { + return this.$('#group-key'); } - return Array.from(selectedMapLogEntriesSet); - } - //TODO(zcankara) Handle in the processor for events with source positions. - handleFilePositionClick(e) { - const tr = e.target.parentNode; - const sourcePosition = tr.group.entries[0].sourcePosition; - this.dispatchEvent(new FocusEvent(sourcePosition)); - } + get table() { + return this.$('#table'); + } - render(groups, parent) { - const fragment = document.createDocumentFragment(); - const max = Math.min(1000, groups.length) - const detailsClickHandler = this.handleDetailsClick.bind(this); - const mapClickHandler = this.handleMapClick.bind(this); - const fileClickHandler = this.handleFilePositionClick.bind(this); - for (let i = 0; i < max; i++) { - const group = groups[i]; - const tr = DOM.tr(); - tr.group = group; - const details = tr.appendChild(DOM.td('', 'toggle')); - details.onclick = detailsClickHandler; - tr.appendChild(DOM.td(group.percentage + "%", 'percentage')); - tr.appendChild(DOM.td(group.count, 'count')); - const valueTd = tr.appendChild(DOM.td(group.key, 'key')); - if (group.property === "map") { - valueTd.onclick = mapClickHandler; - valueTd.classList.add('clickable'); - } else if (group.property == "filePosition") { - valueTd.classList.add('clickable'); - valueTd.onclick = fileClickHandler; + get tableBody() { + return this.$('#table-body'); + } + + get count() { + return this.$('#count'); + } + + get spanSelectAll() { + return this.querySelectorAll('span'); + } + + set selectedLogEntries(value) { + this._selectedLogEntries = value; + this.update(); + } + + async update() { + await delay(1); + this.updateCount(); + this.updateTable(); + } + + updateCount() { + this.count.innerHTML = 'length=' + this._selectedLogEntries.length; + } + + updateTable(event) { + let select = this.groupKey; + let key = select.options[select.selectedIndex].text; + let tableBody = this.tableBody; + DOM.removeAllChildren(tableBody); + let groups = Group.groupBy(this._selectedLogEntries, key, true); + this.render(groups, tableBody); + } + + escapeHtml(unsafe) { + if (!unsafe) return ''; + return unsafe.toString() + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + handleMapClick(e) { + const group = e.target.parentNode.entry; + const id = group.key; + const selectedMapLogEntries = + this.searchIcLogEntryToMapLogEntry(id, group.entries); + this.dispatchEvent(new SelectionEvent(selectedMapLogEntries)); + } + + searchIcLogEntryToMapLogEntry(id, icLogEntries) { + // searches for mapLogEntries using the id, time + const selectedMapLogEntriesSet = new Set(); + for (const icLogEntry of icLogEntries) { + const time = icLogEntry.time; + const selectedMap = MapLogEntry.get(id, time); + selectedMapLogEntriesSet.add(selectedMap); } - fragment.appendChild(tr); + return Array.from(selectedMapLogEntriesSet); } - const omitted = groups.length - max; - if (omitted > 0) { - const tr = DOM.tr(); - const tdNode = - tr.appendChild(DOM.td('Omitted ' + omitted + " entries.")); - tdNode.colSpan = 4; - fragment.appendChild(tr); - } - parent.appendChild(fragment); - } - handleDetailsClick(event) { - const tr = event.target.parentNode; - const group = tr.group; - // Create subgroup in-place if the don't exist yet. - if (group.groups === undefined) { - group.createSubGroups(); - this.renderDrilldown(group, tr); + // TODO(zcankara) Handle in the processor for events with source + // positions. + handleFilePositionClick(e) { + const tr = e.target.parentNode; + const sourcePosition = tr.group.entries[0].sourcePosition; + this.dispatchEvent(new FocusEvent(sourcePosition)); } - let detailsTr = tr.nextSibling; - if (tr.classList.contains("open")) { - tr.classList.remove("open"); - detailsTr.style.display = "none"; - } else { - tr.classList.add("open"); - detailsTr.style.display = "table-row"; - } - } - renderDrilldown(group, previousSibling) { - let tr = DOM.tr("entry-details"); - tr.style.display = "none"; - // indent by one td. - tr.appendChild(DOM.td()); - let td = DOM.td(); - td.colSpan = 3; - for (let key in group.groups) { - this.renderDrilldownGroup(td, group.groups[key], key); + render(groups, parent) { + const fragment = document.createDocumentFragment(); + const max = Math.min(1000, groups.length) + const detailsClickHandler = this.handleDetailsClick.bind(this); + const mapClickHandler = this.handleMapClick.bind(this); + const fileClickHandler = this.handleFilePositionClick.bind(this); + for (let i = 0; i < max; i++) { + const group = groups[i]; + const tr = DOM.tr(); + tr.group = group; + const details = tr.appendChild(DOM.td('', 'toggle')); + details.onclick = detailsClickHandler; + tr.appendChild(DOM.td(group.percentage + '%', 'percentage')); + tr.appendChild(DOM.td(group.count, 'count')); + const valueTd = tr.appendChild(DOM.td(group.key, 'key')); + if (group.property === 'map') { + valueTd.onclick = mapClickHandler; + valueTd.classList.add('clickable'); + } else if (group.property == 'filePosition') { + valueTd.classList.add('clickable'); + valueTd.onclick = fileClickHandler; + } + fragment.appendChild(tr); + } + const omitted = groups.length - max; + if (omitted > 0) { + const tr = DOM.tr(); + const tdNode = + tr.appendChild(DOM.td('Omitted ' + omitted + ' entries.')); + tdNode.colSpan = 4; + fragment.appendChild(tr); + } + parent.appendChild(fragment); } - tr.appendChild(td); - // Append the new TR after previousSibling. - previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling) - } - renderDrilldownGroup(td, children, key) { - const max = 20; - const div = DOM.div('drilldown-group-title'); - div.textContent = - `Grouped by ${key} [top ${max} out of ${children.length}]`; - td.appendChild(div); - const table = DOM.table(); - this.render(children.slice(0, max), table, false) - td.appendChild(table); - } - - initGroupKeySelect() { - const select = this.groupKey; - select.options.length = 0; - for (const propertyName of IcLogEntry.propertyNames) { - const option = document.createElement("option"); - option.text = propertyName; - select.add(option); + handleDetailsClick(event) { + const tr = event.target.parentNode; + const group = tr.group; + // Create subgroup in-place if the don't exist yet. + if (group.groups === undefined) { + group.createSubGroups(); + this.renderDrilldown(group, tr); + } + let detailsTr = tr.nextSibling; + if (tr.classList.contains('open')) { + tr.classList.remove('open'); + detailsTr.style.display = 'none'; + } else { + tr.classList.add('open'); + detailsTr.style.display = 'table-row'; + } } - } - }); + + renderDrilldown(group, previousSibling) { + let tr = DOM.tr('entry-details'); + tr.style.display = 'none'; + // indent by one td. + tr.appendChild(DOM.td()); + let td = DOM.td(); + td.colSpan = 3; + for (let key in group.groups) { + this.renderDrilldownGroup(td, group.groups[key], key); + } + tr.appendChild(td); + // Append the new TR after previousSibling. + previousSibling.parentNode.insertBefore(tr, previousSibling.nextSibling) + } + + renderDrilldownGroup(td, children, key) { + const max = 20; + const div = DOM.div('drilldown-group-title'); + div.textContent = + `Grouped by ${key} [top ${max} out of ${children.length}]`; + td.appendChild(div); + const table = DOM.table(); + this.render(children.slice(0, max), table, false) + td.appendChild(table); + } + + initGroupKeySelect() { + const select = this.groupKey; + select.options.length = 0; + for (const propertyName of IcLogEntry.propertyNames) { + const option = document.createElement('option'); + option.text = propertyName; + select.add(option); + } + } + }); diff --git a/tools/system-analyzer/index.mjs b/tools/system-analyzer/index.mjs index 00ac33a040..dfc858e5d6 100644 --- a/tools/system-analyzer/index.mjs +++ b/tools/system-analyzer/index.mjs @@ -2,22 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { SelectionEvent, FocusEvent, SelectTimeEvent } from "./events.mjs"; -import { State } from "./app-model.mjs"; -import { MapLogEntry } from "./log/map.mjs"; -import { IcLogEntry } from "./log/ic.mjs"; -import { Processor } from "./processor.mjs"; -import { SourcePosition } from "../profile.mjs"; -import { $ } from "./helper.mjs"; +import {SourcePosition} from '../profile.mjs'; +import {State} from './app-model.mjs'; +import {FocusEvent, SelectionEvent, SelectTimeEvent} from './events.mjs'; +import {$} from './helper.mjs'; +import {IcLogEntry} from './log/ic.mjs'; +import {MapLogEntry} from './log/map.mjs'; +import {Processor} from './processor.mjs'; class App { _state; _view; _navigation; _startupPromise; - constructor(fileReaderId, mapPanelId, mapStatsPanelId, timelinePanelId, - icPanelId, mapTrackId, icTrackId, deoptTrackId, sourcePanelId) { + constructor( + fileReaderId, mapPanelId, mapStatsPanelId, timelinePanelId, icPanelId, + mapTrackId, icTrackId, deoptTrackId, sourcePanelId) { this._view = { __proto__: null, logFileReader: $(fileReaderId), @@ -31,32 +32,30 @@ class App { sourcePanel: $(sourcePanelId) }; this.toggleSwitch = $('.theme-switch input[type="checkbox"]'); - this.toggleSwitch.addEventListener("change", (e) => this.switchTheme(e)); - this._view.logFileReader.addEventListener("fileuploadstart", (e) => - this.handleFileUploadStart(e) - ); - this._view.logFileReader.addEventListener("fileuploadend", (e) => - this.handleFileUploadEnd(e) - ); + this.toggleSwitch.addEventListener('change', (e) => this.switchTheme(e)); + this._view.logFileReader.addEventListener( + 'fileuploadstart', (e) => this.handleFileUploadStart(e)); + this._view.logFileReader.addEventListener( + 'fileuploadend', (e) => this.handleFileUploadEnd(e)); this._startupPromise = this.runAsyncInitialize(); } async runAsyncInitialize() { await Promise.all([ - import("./ic-panel.mjs"), - import("./timeline-panel.mjs"), - import("./stats-panel.mjs"), - import("./map-panel.mjs"), - import("./source-panel.mjs"), - ]); - document.addEventListener('keydown', - e => this._navigation?.handleKeyDown(e)); - document.addEventListener(SelectionEvent.name, - e => this.handleShowEntries(e)); - document.addEventListener(FocusEvent.name, - e => this.handleShowEntryDetail(e)); - document.addEventListener(SelectTimeEvent.name, - e => this.handleTimeRangeSelect(e)); + import('./ic-panel.mjs'), + import('./timeline-panel.mjs'), + import('./stats-panel.mjs'), + import('./map-panel.mjs'), + import('./source-panel.mjs'), + ]); + document.addEventListener( + 'keydown', e => this._navigation?.handleKeyDown(e)); + document.addEventListener( + SelectionEvent.name, e => this.handleShowEntries(e)); + document.addEventListener( + FocusEvent.name, e => this.handleShowEntryDetail(e)); + document.addEventListener( + SelectTimeEvent.name, e => this.handleTimeRangeSelect(e)); } handleShowEntries(e) { @@ -67,7 +66,7 @@ class App { } else if (e.entries[0] instanceof SourcePosition) { this.showSourcePositionEntries(e.entries); } else { - throw new Error("Unknown selection type!"); + throw new Error('Unknown selection type!'); } e.stopPropagation(); } @@ -84,7 +83,7 @@ class App { this._state.selectedDeoptLogEntries = entries; } showSourcePositionEntries(entries) { - //TODO: Handle multiple source position selection events + // TODO: Handle multiple source position selection events this._view.sourcePanel.selectedSourcePositions = entries } @@ -98,7 +97,7 @@ class App { this.showMapEntries(this._state.mapTimeline.selection); this.showIcEntries(this._state.icTimeline.selection); this.showDeoptEntries(this._state.deoptTimeline.selection); - this._view.timelinePanel.timeSelection = {start,end}; + this._view.timelinePanel.timeSelection = {start, end}; } handleShowEntryDetail(e) { @@ -109,7 +108,7 @@ class App { } else if (e.entry instanceof SourcePosition) { this.selectSourcePosition(e.entry); } else { - throw new Error("Unknown selection type!"); + throw new Error('Unknown selection type!'); } e.stopPropagation(); } @@ -129,7 +128,7 @@ class App { handleFileUploadStart(e) { this.restartApp(); - $("#container").className = "initial"; + $('#container').className = 'initial'; } restartApp() { @@ -150,17 +149,18 @@ class App { // Transitions must be set before timeline for stats panel. this._view.mapPanel.timeline = mapTimeline; this._view.mapTrack.data = mapTimeline; - this._view.mapStatsPanel.transitions = this._state.mapTimeline.transitions; + this._view.mapStatsPanel.transitions = + this._state.mapTimeline.transitions; this._view.mapStatsPanel.timeline = mapTimeline; this._view.icPanel.timeline = icTimeline; this._view.icTrack.data = icTimeline; this._view.deoptTrack.data = deoptTimeline; this._view.sourcePanel.data = processor.scripts - } catch(e) { - this._view.logFileReader.error = "Log file contains errors!" - throw(e); + } catch (e) { + this._view.logFileReader.error = 'Log file contains errors!' + throw (e); } finally { - $("#container").className = "loaded"; + $('#container').className = 'loaded'; this.fileLoaded = true; } } @@ -172,9 +172,8 @@ class App { } switchTheme(event) { - document.documentElement.dataset.theme = event.target.checked - ? "light" - : "dark"; + document.documentElement.dataset.theme = + event.target.checked ? 'light' : 'dark'; if (this.fileLoaded) { this.refreshTimelineTrackView(); } @@ -268,7 +267,7 @@ class Navigation { } handleKeyDown(event) { switch (event.key) { - case "ArrowUp": + case 'ArrowUp': event.preventDefault(); if (event.shiftKey) { this.selectPrevEdge(); @@ -276,7 +275,7 @@ class Navigation { this.moveInChunk(-1); } return false; - case "ArrowDown": + case 'ArrowDown': event.preventDefault(); if (event.shiftKey) { this.selectNextEdge(); @@ -284,20 +283,20 @@ class Navigation { this.moveInChunk(1); } return false; - case "ArrowLeft": + case 'ArrowLeft': this.moveInChunks(false); break; - case "ArrowRight": + case 'ArrowRight': this.moveInChunks(true); break; - case "+": + case '+': this.increaseTimelineResolution(); break; - case "-": + case '-': this.decreaseTimelineResolution(); break; } } } -export { App }; +export {App}; diff --git a/tools/system-analyzer/log-file-reader.mjs b/tools/system-analyzer/log-file-reader.mjs index 1dd726751f..353e907760 100644 --- a/tools/system-analyzer/log-file-reader.mjs +++ b/tools/system-analyzer/log-file-reader.mjs @@ -1,82 +1,84 @@ // 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 { DOM, V8CustomElement } from './helper.mjs'; +import {DOM, V8CustomElement} from './helper.mjs'; -DOM.defineCustomElement('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)); +DOM.defineCustomElement('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})); + var 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'; + const reader = new FileReader(); + reader.onload = (e) => this.handleFileLoad(e, file); + // Delay the loading a bit to allow for CSS animations to happen. + setTimeout(() => reader.readAsText(file), 0); + } - 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 })); - var 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'; - const reader = new FileReader(); - reader.onload = (e) => this.handleFileLoad(e, file); - // Delay the loading a bit to allow for CSS animations to happen. - setTimeout(() => reader.readAsText(file), 0); - } - - handleFileLoad(e, file) { - const chunk = e.target.result; - this.updateLabel('Finished loading \'' + file.name + '\'.'); - this.dispatchEvent(new CustomEvent( - 'fileuploadend', { - bubbles: true, - composed: true, - detail: chunk, - })); - this.root.className = 'done'; - } - }); + handleFileLoad(e, file) { + const chunk = e.target.result; + this.updateLabel('Finished loading \'' + file.name + '\'.'); + this.dispatchEvent(new CustomEvent('fileuploadend', { + bubbles: true, + composed: true, + detail: chunk, + })); + this.root.className = 'done'; + } +}); diff --git a/tools/system-analyzer/log/deopt.mjs b/tools/system-analyzer/log/deopt.mjs index 549e9aa59a..f3ff1a71a2 100644 --- a/tools/system-analyzer/log/deopt.mjs +++ b/tools/system-analyzer/log/deopt.mjs @@ -1,7 +1,7 @@ // Copyright 2020 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { LogEntry } from './log.mjs'; +import {LogEntry} from './log.mjs'; export class DeoptLogEntry extends LogEntry { constructor(type, time) { diff --git a/tools/system-analyzer/log/ic.mjs b/tools/system-analyzer/log/ic.mjs index 727cf0576f..b6c7ec5553 100644 --- a/tools/system-analyzer/log/ic.mjs +++ b/tools/system-analyzer/log/ic.mjs @@ -1,12 +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 { LogEntry } from './log.mjs'; +import {LogEntry} from './log.mjs'; export class IcLogEntry extends LogEntry { constructor( - type, fn_file, time, line, column, key, oldState, newState, map, reason, - script, modifier, additional) { + type, fn_file, time, line, column, key, oldState, newState, map, reason, + script, modifier, additional) { super(type, time); this.category = 'other'; if (this.type.indexOf('Store') !== -1) { @@ -55,18 +55,11 @@ export class IcLogEntry extends LogEntry { this.file = parts[offset]; return offset; } - + static get propertyNames() { return [ - 'type', - 'category', - 'functionName', - 'filePosition', - 'state', - 'key', - 'map', - 'reason', - 'file' + 'type', 'category', 'functionName', 'filePosition', 'state', 'key', 'map', + 'reason', 'file' ]; } } diff --git a/tools/system-analyzer/log/log.mjs b/tools/system-analyzer/log/log.mjs index 1c1545a1d1..69195d7853 100644 --- a/tools/system-analyzer/log/log.mjs +++ b/tools/system-analyzer/log/log.mjs @@ -6,7 +6,7 @@ export class LogEntry { _time; _type; constructor(type, time) { - //TODO(zcankara) remove type and add empty getters to override + // TODO(zcankara) remove type and add empty getters to override this._time = time; this._type = type; } @@ -18,6 +18,6 @@ export class LogEntry { } // Returns an Array of all possible #type values. static get allTypes() { - throw new Error("Not implemented."); + throw new Error('Not implemented.'); } } \ No newline at end of file diff --git a/tools/system-analyzer/log/map.mjs b/tools/system-analyzer/log/map.mjs index dc2b92e648..977b877de2 100644 --- a/tools/system-analyzer/log/map.mjs +++ b/tools/system-analyzer/log/map.mjs @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { LogEntry } from './log.mjs'; +import {LogEntry} from './log.mjs'; // =========================================================================== // Map Log Events @@ -11,10 +11,10 @@ const kChunkHeight = 200; const kChunkWidth = 10; function define(prototype, name, fn) { - Object.defineProperty(prototype, name, { value: fn, enumerable: false }); + Object.defineProperty(prototype, name, {value: fn, enumerable: false}); } -define(Array.prototype, 'max', function (fn) { +define(Array.prototype, 'max', function(fn) { if (this.length === 0) return undefined; if (fn === undefined) fn = (each) => each; let max = fn(this[0]); @@ -23,10 +23,10 @@ define(Array.prototype, 'max', function (fn) { } return max; }) -define(Array.prototype, 'first', function () { +define(Array.prototype, 'first', function() { return this[0] }); -define(Array.prototype, 'last', function () { +define(Array.prototype, 'last', function() { return this[this.length - 1] }); @@ -282,9 +282,8 @@ class Edge { return this.type + ' ' + this.symbol() + this.name; } return this.type + ' ' + (this.reason ? this.reason : '') + ' ' + - (this.name ? this.name : '') + (this.name ? this.name : '') } } - -export { MapLogEntry, Edge, kChunkWidth, kChunkHeight }; +export {MapLogEntry, Edge, kChunkWidth, kChunkHeight}; diff --git a/tools/system-analyzer/map-panel.mjs b/tools/system-analyzer/map-panel.mjs index 146b6e306a..e47db3e634 100644 --- a/tools/system-analyzer/map-panel.mjs +++ b/tools/system-analyzer/map-panel.mjs @@ -1,77 +1,76 @@ // Copyright 2020 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import "./stats-panel.mjs"; -import "./map-panel/map-details.mjs"; -import "./map-panel/map-transitions.mjs"; -import { FocusEvent } from './events.mjs'; -import { MapLogEntry } from "./log/map.mjs"; -import { DOM, V8CustomElement } from './helper.mjs'; +import './stats-panel.mjs'; +import './map-panel/map-details.mjs'; +import './map-panel/map-transitions.mjs'; -DOM.defineCustomElement('map-panel', (templateText) => - class MapPanel extends V8CustomElement { - _map; - constructor() { - super(templateText); - this.searchBarBtn.addEventListener( - 'click', e => this.handleSearchBar(e)); - this.addEventListener( - FocusEvent.name, e => this.handleUpdateMapDetails(e)); - } +import {FocusEvent} from './events.mjs'; +import {DOM, V8CustomElement} from './helper.mjs'; +import {MapLogEntry} from './log/map.mjs'; - handleUpdateMapDetails(e) { - if (e.entry instanceof MapLogEntry) { - this.mapDetailsPanel.mapDetails = e.entry; - } - } +DOM.defineCustomElement('map-panel', + (templateText) => + class MapPanel extends V8CustomElement { + _map; + constructor() { + super(templateText); + this.searchBarBtn.addEventListener('click', e => this.handleSearchBar(e)); + this.addEventListener(FocusEvent.name, e => this.handleUpdateMapDetails(e)); + } - get mapTransitionsPanel() { - return this.$('#map-transitions'); + handleUpdateMapDetails(e) { + if (e.entry instanceof MapLogEntry) { + this.mapDetailsPanel.mapDetails = e.entry; } + } - get mapDetailsPanel() { - return this.$('#map-details'); - } + get mapTransitionsPanel() { + return this.$('#map-transitions'); + } - get searchBarBtn() { - return this.$('#searchBarBtn'); - } + get mapDetailsPanel() { + return this.$('#map-details'); + } - get searchBar() { - return this.$('#searchBar'); - } + get searchBarBtn() { + return this.$('#searchBarBtn'); + } - get mapDetails() { - return this.mapDetailsPanel.mapDetails; - } + get searchBar() { + return this.$('#searchBar'); + } - set timeline(timeline) { - this._timeline = timeline; - } + get mapDetails() { + return this.mapDetailsPanel.mapDetails; + } - set map(value) { - this._map = value; - this.mapTransitionsPanel.map = this._map; - } + set timeline(timeline) { + this._timeline = timeline; + } - handleSearchBar(e) { - let searchBar = this.$('#searchBarInput'); - let searchBarInput = searchBar.value; - //access the map from model cache - let selectedMap = MapLogEntry.get(parseInt(searchBarInput)); - if (selectedMap) { - searchBar.className = "success"; - } else { - searchBar.className = "failure"; - } - this.dispatchEvent(new FocusEvent(selectedMap)); - } + set map(value) { + this._map = value; + this.mapTransitionsPanel.map = this._map; + } - set selectedMapLogEntries(list) { - this.mapTransitionsPanel.selectedMapLogEntries = list; - } - get selectedMapLogEntries() { - return this.mapTransitionsPanel.selectedMapLogEntries; + handleSearchBar(e) { + let searchBar = this.$('#searchBarInput'); + let searchBarInput = searchBar.value; + // access the map from model cache + let selectedMap = MapLogEntry.get(parseInt(searchBarInput)); + if (selectedMap) { + searchBar.className = 'success'; + } else { + searchBar.className = 'failure'; } + this.dispatchEvent(new FocusEvent(selectedMap)); + } - }); + set selectedMapLogEntries(list) { + this.mapTransitionsPanel.selectedMapLogEntries = list; + } + get selectedMapLogEntries() { + return this.mapTransitionsPanel.selectedMapLogEntries; + } +}); diff --git a/tools/system-analyzer/map-panel/map-details.mjs b/tools/system-analyzer/map-panel/map-details.mjs index 92f267d9bb..0854f08981 100644 --- a/tools/system-analyzer/map-panel/map-details.mjs +++ b/tools/system-analyzer/map-panel/map-details.mjs @@ -1,26 +1,24 @@ // 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 { V8CustomElement, DOM} from "../helper.mjs"; -import { FocusEvent } from "../events.mjs"; +import {FocusEvent} from '../events.mjs'; +import {DOM, V8CustomElement} from '../helper.mjs'; DOM.defineCustomElement( - "./map-panel/map-details", - (templateText) => - class MapDetails extends V8CustomElement { + './map-panel/map-details', + (templateText) => class MapDetails extends V8CustomElement { constructor() { super(templateText); - this._filePositionNode.addEventListener("click", e => - this.handleFilePositionClick(e) - ); + this._filePositionNode.addEventListener( + 'click', e => this.handleFilePositionClick(e)); this.selectedMap = undefined; } get mapDetails() { - return this.$("#mapDetails"); + return this.$('#mapDetails'); } get _filePositionNode() { - return this.$("#filePositionNode"); + return this.$('#filePositionNode'); } setSelectedMap(value) { @@ -28,21 +26,20 @@ DOM.defineCustomElement( } set mapDetails(map) { - let details = ""; - let clickableDetails = ""; + let details = ''; + let clickableDetails = ''; if (map) { - clickableDetails += "ID: " + map.id; - clickableDetails += "\nSource location: " + map.filePosition; - details += "\n" + map.description; + clickableDetails += 'ID: ' + map.id; + clickableDetails += '\nSource location: ' + map.filePosition; + details += '\n' + map.description; this.setSelectedMap(map); } this._filePositionNode.innerText = clickableDetails; - this._filePositionNode.classList.add("clickable"); + this._filePositionNode.classList.add('clickable'); this.mapDetails.innerText = details; } handleFilePositionClick() { this.dispatchEvent(new FocusEvent(this.selectedMap.sourcePosition)); } - } -); + }); diff --git a/tools/system-analyzer/map-panel/map-transitions.mjs b/tools/system-analyzer/map-panel/map-transitions.mjs index d209ee2099..709f4a19bb 100644 --- a/tools/system-analyzer/map-panel/map-transitions.mjs +++ b/tools/system-analyzer/map-panel/map-transitions.mjs @@ -1,197 +1,188 @@ // 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 { V8CustomElement, DOM, typeToColor } from "../helper.mjs"; -import { FocusEvent, SelectionEvent } from "../events.mjs"; +import {FocusEvent, SelectionEvent} from '../events.mjs'; +import {DOM, typeToColor, V8CustomElement} from '../helper.mjs'; -DOM.defineCustomElement( - "./map-panel/map-transitions", - (templateText) => - class MapTransitions extends V8CustomElement { - _map; - _selectedMapLogEntries; - _displayedMapsInTree; - _showMapsUpdateId; - constructor() { - super(templateText); - this.transitionView.addEventListener("mousemove", (e) => - this.handleTransitionViewChange(e) - ); - this.currentNode = this.transitionView; - this.currentMap = undefined; - } +DOM.defineCustomElement('./map-panel/map-transitions', + (templateText) => + class MapTransitions extends V8CustomElement { + _map; + _selectedMapLogEntries; + _displayedMapsInTree; + _showMapsUpdateId; + constructor() { + super(templateText); + this.transitionView.addEventListener( + 'mousemove', (e) => this.handleTransitionViewChange(e)); + this.currentNode = this.transitionView; + this.currentMap = undefined; + } - get transitionView() { - return this.$("#transitionView"); - } + get transitionView() { + return this.$('#transitionView'); + } - get tooltip() { - return this.$("#tooltip"); - } + get tooltip() { + return this.$('#tooltip'); + } - get tooltipContents() { - return this.$("#tooltipContents"); - } + get tooltipContents() { + return this.$('#tooltipContents'); + } - set map(value) { - this._map = value; - this.showMap(); - } + set map(value) { + this._map = value; + this.showMap(); + } - handleTransitionViewChange(e) { - this.tooltip.style.left = e.pageX + "px"; - this.tooltip.style.top = e.pageY + "px"; - let map = e.target.map; - if (map) { - this.tooltipContents.innerText = map.description; - } - } - - selectMap(map) { - this.dispatchEvent(new SelectionEvent([map])); - } - - showMap() { - if (this.currentMap === this._map) return; - this.currentMap = this._map; - this.selectedMapLogEntries = [this._map]; - this.showMaps(); - } - - showMaps() { - clearTimeout(this._showMapsUpdateId); - this._showMapsUpdateId = setTimeout(() => this._showMaps(), 250); - } - _showMaps() { - this.transitionView.style.display = "none"; - DOM.removeAllChildren(this.transitionView); - this._displayedMapsInTree = new Set(); - // Limit view to 200 maps for performance reasons. - this.selectedMapLogEntries.slice(0, 200).forEach((map) => - this.addMapAndParentTransitions(map)); - this._displayedMapsInTree = undefined; - this.transitionView.style.display = ""; - } - - set selectedMapLogEntries(list) { - this._selectedMapLogEntries = list; - this.showMaps(); - } - - get selectedMapLogEntries() { - return this._selectedMapLogEntries; - } - - addMapAndParentTransitions(map) { - if (map === void 0) return; - if (this._displayedMapsInTree.has(map)) return; - this._displayedMapsInTree.add(map); - this.currentNode = this.transitionView; - 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 - ); - } - } - - 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"]; - let edge = DOM.div(classes); - edge.style.backgroundColor = typeToColor(map.edge); - let labelNode = DOM.div("transitionLabel"); - labelNode.innerText = map.edge.toString(); - edge.appendChild(labelNode); - return edge; - } - - addTransitionTo(map) { - // transition[ transitions[ transition[...], transition[...], ...]]; - this._displayedMapsInTree?.add(map); - let transition = DOM.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 = DOM.div("transitions"); - transition.appendChild(subtree); - - this.currentNode.appendChild(transition); - this.currentNode = subtree; - - return mapNode; - } - - addMapNode(map) { - let node = DOM.div("map"); - if (map.edge) node.style.backgroundColor = typeToColor(map.edge); - node.map = map; - node.addEventListener("click", () => this.selectMap(map)); - if (map.children.length > 1) { - node.innerText = map.children.length; - let showSubtree = DOM.div("showSubtransitions"); - showSubtree.addEventListener("click", (e) => - this.toggleSubtree(e, node) - ); - node.appendChild(showSubtree); - } else if (map.children.length == 0) { - node.innerHTML = "●"; - } - this.currentNode.appendChild(node); - return node; - } - - - 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]); - } + handleTransitionViewChange(e) { + this.tooltip.style.left = e.pageX + 'px'; + this.tooltip.style.top = e.pageY + 'px'; + let map = e.target.map; + if (map) { + this.tooltipContents.innerText = map.description; + } + } + + selectMap(map) { + this.dispatchEvent(new SelectionEvent([map])); + } + + showMap() { + if (this.currentMap === this._map) return; + this.currentMap = this._map; + this.selectedMapLogEntries = [this._map]; + this.showMaps(); + } + + showMaps() { + clearTimeout(this._showMapsUpdateId); + this._showMapsUpdateId = setTimeout(() => this._showMaps(), 250); + } + _showMaps() { + this.transitionView.style.display = 'none'; + DOM.removeAllChildren(this.transitionView); + this._displayedMapsInTree = new Set(); + // Limit view to 200 maps for performance reasons. + this.selectedMapLogEntries.slice(0, 200).forEach( + (map) => this.addMapAndParentTransitions(map)); + this._displayedMapsInTree = undefined; + this.transitionView.style.display = ''; + } + + set selectedMapLogEntries(list) { + this._selectedMapLogEntries = list; + this.showMaps(); + } + + get selectedMapLogEntries() { + return this._selectedMapLogEntries; + } + + addMapAndParentTransitions(map) { + if (map === void 0) return; + if (this._displayedMapsInTree.has(map)) return; + this._displayedMapsInTree.add(map); + this.currentNode = this.transitionView; + 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); + } + } + + 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']; + let edge = DOM.div(classes); + edge.style.backgroundColor = typeToColor(map.edge); + let labelNode = DOM.div('transitionLabel'); + labelNode.innerText = map.edge.toString(); + edge.appendChild(labelNode); + return edge; + } + + addTransitionTo(map) { + // transition[ transitions[ transition[...], transition[...], ...]]; + this._displayedMapsInTree?.add(map); + let transition = DOM.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 = DOM.div('transitions'); + transition.appendChild(subtree); + + this.currentNode.appendChild(transition); + this.currentNode = subtree; + + return mapNode; + } + + addMapNode(map) { + let node = DOM.div('map'); + if (map.edge) node.style.backgroundColor = typeToColor(map.edge); + node.map = map; + node.addEventListener('click', () => this.selectMap(map)); + if (map.children.length > 1) { + node.innerText = map.children.length; + let showSubtree = DOM.div('showSubtransitions'); + showSubtree.addEventListener('click', (e) => this.toggleSubtree(e, node)); + node.appendChild(showSubtree); + } else if (map.children.length == 0) { + node.innerHTML = '●'; + } + this.currentNode.appendChild(node); + return node; + } + + 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]); } } -); + } +}); diff --git a/tools/system-analyzer/processor.mjs b/tools/system-analyzer/processor.mjs index ef0cdea53c..925490d560 100644 --- a/tools/system-analyzer/processor.mjs +++ b/tools/system-analyzer/processor.mjs @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { MapLogEntry, Edge } from "./log/map.mjs"; -import { IcLogEntry } from "./log/ic.mjs"; -import { DeoptLogEntry } from "./log/deopt.mjs"; -import { Timeline } from "./timeline.mjs"; -import { LogReader, parseString, parseVarArgs } from "../logreader.mjs"; -import { Profile } from "../profile.mjs"; +import {LogReader, parseString, parseVarArgs} from '../logreader.mjs'; +import {Profile} from '../profile.mjs'; + +import {DeoptLogEntry} from './log/deopt.mjs'; +import {IcLogEntry} from './log/ic.mjs'; +import {Edge, MapLogEntry} from './log/map.mjs'; +import {Timeline} from './timeline.mjs'; // =========================================================================== @@ -43,7 +44,8 @@ export class Processor extends LogReader { }, 'v8-version': { parsers: [ - parseInt, parseInt, + parseInt, + parseInt, ], processor: this.processV8Version }, @@ -52,12 +54,12 @@ export class Processor extends LogReader { processor: this.processScriptSource }, 'code-move': - { parsers: [parseInt, parseInt], processor: this.processCodeMove }, - 'code-delete': { parsers: [parseInt], processor: this.processCodeDelete }, + {parsers: [parseInt, parseInt], processor: this.processCodeMove}, + 'code-delete': {parsers: [parseInt], processor: this.processCodeDelete}, 'sfi-move': - { parsers: [parseInt, parseInt], processor: this.processFunctionMove }, + {parsers: [parseInt, parseInt], processor: this.processFunctionMove}, 'map-create': - { parsers: [parseInt, parseString], processor: this.processMapCreate }, + {parsers: [parseInt, parseString], processor: this.processMapCreate}, 'map': { parsers: [ parseString, parseInt, parseString, parseString, parseInt, parseInt, @@ -140,8 +142,8 @@ export class Processor extends LogReader { } } catch (e) { console.error( - 'Error occurred during parsing line ' + i + - ', trying to continue: ' + e); + 'Error occurred during parsing line ' + i + + ', trying to continue: ' + e); } this.finalize(); } @@ -184,24 +186,25 @@ export class Processor extends LogReader { let funcAddr = parseInt(maybe_func[0]); let state = this.parseState(maybe_func[1]); this._profile.addFuncCode( - type, name, timestamp, start, size, funcAddr, state); + type, name, timestamp, start, size, funcAddr, state); } else { this._profile.addCode(type, name, timestamp, start, size); } } - processCodeDeopt(timestamp, codeSize, instructionStart, inliningId, - scriptOffset, deoptKind, deoptLocation, deoptReason) { + processCodeDeopt( + timestamp, codeSize, instructionStart, inliningId, scriptOffset, + deoptKind, deoptLocation, deoptReason) { this._deoptTimeline.push(new DeoptLogEntry(deoptKind, timestamp)); } processV8Version(majorVersion, minorVersion) { - if ( - (majorVersion == this.MAJOR_VERSION && minorVersion <= this.MINOR_VERSION) - || (majorVersion < this.MAJOR_VERSION)) { + if ((majorVersion == this.MAJOR_VERSION && + minorVersion <= this.MINOR_VERSION) || + (majorVersion < this.MAJOR_VERSION)) { window.alert( - `Unsupported version ${majorVersion}.${minorVersion}. \n` + - `Please use the matching tool for given the V8 version.`); + `Unsupported version ${majorVersion}.${minorVersion}. \n` + + `Please use the matching tool for given the V8 version.`); } } @@ -231,16 +234,16 @@ export class Processor extends LogReader { } processPropertyIC( - type, pc, time, line, column, old_state, new_state, map, key, modifier, - slow_reason) { + type, pc, time, line, column, old_state, new_state, map, key, modifier, + slow_reason) { let fnName = this.functionName(pc); let parts = fnName.split(' '); - let fileName = parts[parts.length-1]; + let fileName = parts[parts.length - 1]; let script = this.getScript(fileName); // TODO: Use SourcePosition here directly let entry = new IcLogEntry( - type, fnName, time, line, column, key, old_state, new_state, map, - slow_reason, script, modifier); + type, fnName, time, line, column, key, old_state, new_state, map, + slow_reason, script, modifier); if (script) { entry.sourcePosition = script.addSourcePosition(line, column, entry); } @@ -254,9 +257,9 @@ export class Processor extends LogReader { formatPC(pc, line, column) { let entry = this._profile.findEntry(pc); if (!entry) return '' - if (entry.type === 'Builtin') { - return entry.name; - } + if (entry.type === 'Builtin') { + return entry.name; + } let name = entry.func.getName(); let array = this._formatPCRegexp.exec(name); if (array === null) { @@ -272,7 +275,7 @@ export class Processor extends LogReader { // Try to handle urls with file positions: https://foo.bar.com/:17:330" filePositionLine = filePositionLine.split(' '); let parts = filePositionLine[1].split(':'); - if (parts[0].length <= 5) return parts[0] + ':' + parts[1] + if (parts[0].length <= 5) return parts[0] + ':' + parts[1]; return parts[1]; } diff --git a/tools/system-analyzer/source-panel.mjs b/tools/system-analyzer/source-panel.mjs index 32014c0827..70650902c5 100644 --- a/tools/system-analyzer/source-panel.mjs +++ b/tools/system-analyzer/source-panel.mjs @@ -1,138 +1,133 @@ // 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 { V8CustomElement, DOM, delay, formatBytes} from "./helper.mjs"; -import { SelectionEvent, FocusEvent } from "./events.mjs"; -import { MapLogEntry } from "./log/map.mjs"; -import { IcLogEntry } from "./log/ic.mjs"; +import {FocusEvent, SelectionEvent} from './events.mjs'; +import {delay, DOM, formatBytes, V8CustomElement} from './helper.mjs'; +import {IcLogEntry} from './log/ic.mjs'; +import {MapLogEntry} from './log/map.mjs'; -DOM.defineCustomElement( - "source-panel", - (templateText) => - class SourcePanel extends V8CustomElement { - _selectedSourcePositions = []; - _sourcePositionsToMarkNodes; - _scripts = []; - _script; - constructor() { - super(templateText); - this.scriptDropdown.addEventListener( - 'change', e => this._handleSelectScript(e)); - } +DOM.defineCustomElement('source-panel', + (templateText) => + class SourcePanel extends V8CustomElement { + _selectedSourcePositions = []; + _sourcePositionsToMarkNodes; + _scripts = []; + _script; + constructor() { + super(templateText); + this.scriptDropdown.addEventListener( + 'change', e => this._handleSelectScript(e)); + } - get script() { - return this.$('#script'); - } + get script() { + return this.$('#script'); + } - get scriptNode() { - return this.$('.scriptNode'); - } + get scriptNode() { + return this.$('.scriptNode'); + } - set script(script) { - if (this._script === script) return; - this._script = script; - this._renderSourcePanel(); - this._updateScriptDropdownSelection(); - } + set script(script) { + if (this._script === script) return; + this._script = script; + this._renderSourcePanel(); + this._updateScriptDropdownSelection(); + } - set selectedSourcePositions(sourcePositions) { - this._selectedSourcePositions = sourcePositions; - // TODO: highlight multiple scripts - this.script = sourcePositions[0]?.script; - this._focusSelectedMarkers(); - } + set selectedSourcePositions(sourcePositions) { + this._selectedSourcePositions = sourcePositions; + // TODO: highlight multiple scripts + this.script = sourcePositions[0]?.script; + this._focusSelectedMarkers(); + } - set data(scripts) { - this._scripts = scripts; - this._initializeScriptDropdown(); - } + set data(scripts) { + this._scripts = scripts; + this._initializeScriptDropdown(); + } - get scriptDropdown() { - return this.$("#script-dropdown"); - } + get scriptDropdown() { + return this.$('#script-dropdown'); + } - _initializeScriptDropdown() { - this._scripts.sort((a, b) => a.name.localeCompare(b.name)); - let select = this.scriptDropdown; - select.options.length = 0; - for (const script of this._scripts) { - const option = document.createElement("option"); - const size = formatBytes(script.source.length); - option.text = `${script.name} (id=${script.id} size=${size})`; - option.script = script; - select.add(option); - } - } - _updateScriptDropdownSelection() { - this.scriptDropdown.selectedIndex = - this._script ? this._scripts.indexOf(this._script) : -1; - } + _initializeScriptDropdown() { + this._scripts.sort((a, b) => a.name.localeCompare(b.name)); + let select = this.scriptDropdown; + select.options.length = 0; + for (const script of this._scripts) { + const option = document.createElement('option'); + const size = formatBytes(script.source.length); + option.text = `${script.name} (id=${script.id} size=${size})`; + option.script = script; + select.add(option); + } + } + _updateScriptDropdownSelection() { + this.scriptDropdown.selectedIndex = + this._script ? this._scripts.indexOf(this._script) : -1; + } - async _renderSourcePanel() { - let scriptNode; - if (this._script) { - await delay(1); - const builder = new LineBuilder( - this, this._script, this._selectedSourcePositions); - scriptNode = builder.createScriptNode(); - this._sourcePositionsToMarkNodes = builder.sourcePositionToMarkers; - } else { - scriptNode = document.createElement("pre"); - this._selectedMarkNodes = undefined; - } - const oldScriptNode = this.script.childNodes[1]; - this.script.replaceChild(scriptNode, oldScriptNode); - } + async _renderSourcePanel() { + let scriptNode; + if (this._script) { + await delay(1); + const builder = + new LineBuilder(this, this._script, this._selectedSourcePositions); + scriptNode = builder.createScriptNode(); + this._sourcePositionsToMarkNodes = builder.sourcePositionToMarkers; + } else { + scriptNode = document.createElement('pre'); + this._selectedMarkNodes = undefined; + } + const oldScriptNode = this.script.childNodes[1]; + this.script.replaceChild(scriptNode, oldScriptNode); + } - async _focusSelectedMarkers() { - await delay(100); - // Remove all marked nodes. - for (let markNode of this._sourcePositionsToMarkNodes.values()) { - markNode.className = ""; - } - for (let sourcePosition of this._selectedSourcePositions) { - this._sourcePositionsToMarkNodes - .get(sourcePosition).className = "marked"; - } - const sourcePosition = this._selectedSourcePositions[0]; - if (!sourcePosition) return; - const markNode = this._sourcePositionsToMarkNodes.get(sourcePosition); - markNode.scrollIntoView({ - behavior: "smooth", block: "nearest", inline: "center" - }); - } + async _focusSelectedMarkers() { + await delay(100); + // Remove all marked nodes. + for (let markNode of this._sourcePositionsToMarkNodes.values()) { + markNode.className = ''; + } + for (let sourcePosition of this._selectedSourcePositions) { + this._sourcePositionsToMarkNodes.get(sourcePosition).className = 'marked'; + } + const sourcePosition = this._selectedSourcePositions[0]; + if (!sourcePosition) return; + const markNode = this._sourcePositionsToMarkNodes.get(sourcePosition); + markNode.scrollIntoView( + {behavior: 'smooth', block: 'nearest', inline: 'center'}); + } - _handleSelectScript(e) { - const option = - this.scriptDropdown.options[this.scriptDropdown.selectedIndex]; - this.script = option.script; - this.selectLogEntries(this._script.entries()); - } + _handleSelectScript(e) { + const option = + this.scriptDropdown.options[this.scriptDropdown.selectedIndex]; + this.script = option.script; + this.selectLogEntries(this._script.entries()); + } - handleSourcePositionClick(e) { - this.selectLogEntries(e.target.sourcePosition.entries) - } + handleSourcePositionClick(e) { + this.selectLogEntries(e.target.sourcePosition.entries) + } - selectLogEntries(logEntries) { - let icLogEntries = []; - let mapLogEntries = []; - for (const entry of logEntries) { - if (entry instanceof MapLogEntry) { - mapLogEntries.push(entry); - } else if (entry instanceof IcLogEntry) { - icLogEntries.push(entry); - } - } - if (icLogEntries.length > 0 ) { - this.dispatchEvent(new SelectionEvent(icLogEntries)); - } - if (mapLogEntries.length > 0) { - this.dispatchEvent(new SelectionEvent(mapLogEntries)); - } + selectLogEntries(logEntries) { + let icLogEntries = []; + let mapLogEntries = []; + for (const entry of logEntries) { + if (entry instanceof MapLogEntry) { + mapLogEntries.push(entry); + } else if (entry instanceof IcLogEntry) { + icLogEntries.push(entry); } } -); - + if (icLogEntries.length > 0) { + this.dispatchEvent(new SelectionEvent(icLogEntries)); + } + if (mapLogEntries.length > 0) { + this.dispatchEvent(new SelectionEvent(mapLogEntries)); + } + } +}); class SourcePositionIterator { _entries; @@ -141,16 +136,16 @@ class SourcePositionIterator { this._entries = sourcePositions; } - *forLine(lineIndex) { + * forLine(lineIndex) { this._findStart(lineIndex); - while(!this._done() && this._current().line === lineIndex) { + while (!this._done() && this._current().line === lineIndex) { yield this._current(); this._next(); } } _findStart(lineIndex) { - while(!this._done() && this._current().line < lineIndex) { + while (!this._done() && this._current().line < lineIndex) { this._next(); } } @@ -168,11 +163,11 @@ class SourcePositionIterator { } } -function * lineIterator(source) { +function* lineIterator(source) { let current = 0; let line = 1; - while(current < source.length) { - const next = source.indexOf("\n", current); + while (current < source.length) { + const next = source.indexOf('\n', current); if (next === -1) break; yield [line, source.substring(current, next)]; line++; @@ -181,7 +176,6 @@ function * lineIterator(source) { if (current < source.length) yield [line, source.substring(current)]; } - class LineBuilder { _script; _clickHandler; @@ -194,12 +188,12 @@ class LineBuilder { this._selection = new Set(highlightPositions); this._clickHandler = panel.handleSourcePositionClick.bind(panel); // TODO: sort on script finalization. - script.sourcePositions.sort((a, b) => { - if (a.line === b.line) return a.column - b.column; - return a.line - b.line; - }) - this._sourcePositions - = new SourcePositionIterator(script.sourcePositions); + script.sourcePositions + .sort((a, b) => { + if (a.line === b.line) return a.column - b.column; + return a.line - b.line; + }) this._sourcePositions = + new SourcePositionIterator(script.sourcePositions); } get sourcePositionToMarkers() { @@ -207,7 +201,7 @@ class LineBuilder { } createScriptNode() { - const scriptNode = document.createElement("pre"); + const scriptNode = document.createElement('pre'); scriptNode.classList.add('scriptNode'); for (let [lineIndex, line] of lineIterator(this._script.source)) { scriptNode.appendChild(this._createLineNode(lineIndex, line)); @@ -216,13 +210,12 @@ class LineBuilder { } _createLineNode(lineIndex, line) { - const lineNode = document.createElement("span"); - let columnIndex = 0; + const lineNode = document.createElement('span'); + let columnIndex = 0; for (const sourcePosition of this._sourcePositions.forLine(lineIndex)) { const nextColumnIndex = sourcePosition.column - 1; - lineNode.appendChild( - document.createTextNode( - line.substring(columnIndex, nextColumnIndex))); + lineNode.appendChild(document.createTextNode( + line.substring(columnIndex, nextColumnIndex))); columnIndex = nextColumnIndex; lineNode.appendChild( @@ -230,12 +223,12 @@ class LineBuilder { columnIndex++; } lineNode.appendChild( - document.createTextNode(line.substring(columnIndex) + "\n")); + document.createTextNode(line.substring(columnIndex) + '\n')); return lineNode; } _createMarkerNode(text, sourcePosition) { - const marker = document.createElement("mark"); + const marker = document.createElement('mark'); this._sourcePositionToMarkers.set(sourcePosition, marker); marker.textContent = text; marker.sourcePosition = sourcePosition; diff --git a/tools/system-analyzer/stats-panel.mjs b/tools/system-analyzer/stats-panel.mjs index 8c8057aec0..2ffb4af425 100644 --- a/tools/system-analyzer/stats-panel.mjs +++ b/tools/system-analyzer/stats-panel.mjs @@ -1,14 +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 { V8CustomElement, DOM} from "./helper.mjs"; -import { SelectionEvent } from "./events.mjs"; -import { delay, LazyTable } from "./helper.mjs"; +import {SelectionEvent} from './events.mjs'; +import {DOM, V8CustomElement} from './helper.mjs'; +import {delay, LazyTable} from './helper.mjs'; DOM.defineCustomElement( - "stats-panel", - (templateText) => - class StatsPanel extends V8CustomElement { + 'stats-panel', (templateText) => class StatsPanel extends V8CustomElement { _timeline; _transitions; _selectedLogEntries; @@ -17,7 +15,7 @@ DOM.defineCustomElement( } get stats() { - return this.$("#stats"); + return this.$('#stats'); } set timeline(timeline) { @@ -51,40 +49,40 @@ DOM.defineCustomElement( } updateGeneralStats() { - console.assert(this._timeline !== undefined, "Timeline not set yet!"); + console.assert(this._timeline !== undefined, 'Timeline not set yet!'); let pairs = [ - ["Transitions", "primary", (e) => e.edge && e.edge.isTransition()], - ["Fast to Slow", "violet", (e) => e.edge && e.edge.isFastToSlow()], - ["Slow to Fast", "orange", (e) => e.edge && e.edge.isSlowToFast()], - ["Initial Map", "yellow", (e) => e.edge && e.edge.isInitial()], + ['Transitions', 'primary', (e) => e.edge && e.edge.isTransition()], + ['Fast to Slow', 'violet', (e) => e.edge && e.edge.isFastToSlow()], + ['Slow to Fast', 'orange', (e) => e.edge && e.edge.isSlowToFast()], + ['Initial Map', 'yellow', (e) => e.edge && e.edge.isInitial()], [ - "Replace Descriptors", - "red", + 'Replace Descriptors', + 'red', (e) => e.edge && e.edge.isReplaceDescriptors(), ], [ - "Copy as Prototype", - "red", + 'Copy as Prototype', + 'red', (e) => e.edge && e.edge.isCopyAsPrototype(), ], [ - "Optimize as Prototype", + 'Optimize as Prototype', null, (e) => e.edge && e.edge.isOptimizeAsPrototype(), ], - ["Deprecated", null, (e) => e.isDeprecated()], - ["Bootstrapped", "green", (e) => e.isBootstrapped()], - ["Total", null, (e) => true], + ['Deprecated', null, (e) => e.isDeprecated()], + ['Bootstrapped', 'green', (e) => e.isBootstrapped()], + ['Total', null, (e) => true], ]; - let tbody = document.createElement("tbody"); + let tbody = document.createElement('tbody'); let total = this._selectedLogEntries.length; pairs.forEach(([name, color, filter]) => { let row = DOM.tr(); if (color !== null) { - row.appendChild(DOM.td(DOM.div(["colorbox", color]))); + row.appendChild(DOM.td(DOM.div(['colorbox', color]))); } else { - row.appendChild(DOM.td("")); + row.appendChild(DOM.td('')); } row.classList.add('clickable'); row.onclick = (e) => { @@ -99,10 +97,10 @@ DOM.defineCustomElement( let count = this.count(filter); row.appendChild(DOM.td(count)); let percent = Math.round((count / total) * 1000) / 10; - row.appendChild(DOM.td(percent.toFixed(1) + "%")); + row.appendChild(DOM.td(percent.toFixed(1) + '%')); tbody.appendChild(row); }); - this.$("#typeTable").replaceChild(tbody, this.$("#typeTable tbody")); + this.$('#typeTable').replaceChild(tbody, this.$('#typeTable tbody')); } count(filter) { @@ -116,21 +114,17 @@ DOM.defineCustomElement( updateNamedTransitionsStats() { let rowData = Array.from(this._transitions.entries()); rowData.sort((a, b) => b[1].length - a[1].length); - new LazyTable(this.$("#nameTable"), rowData, ([name, maps]) => { - let row = DOM.tr(); - row.maps = maps; - row.classList.add('clickable'); - row.addEventListener("click", (e) => - this.dispatchEvent( - new SelectionEvent( - e.target.parentNode.maps.map((map) => map.to) - ) - ) - ); - row.appendChild(DOM.td(maps.length)); - row.appendChild(DOM.td(name)); - return row; - }); + new LazyTable(this.$('#nameTable'), rowData, ([name, maps]) => { + let row = DOM.tr(); + row.maps = maps; + row.classList.add('clickable'); + row.addEventListener( + 'click', + (e) => this.dispatchEvent(new SelectionEvent( + e.target.parentNode.maps.map((map) => map.to)))); + row.appendChild(DOM.td(maps.length)); + row.appendChild(DOM.td(name)); + return row; + }); } - } -); + }); diff --git a/tools/system-analyzer/timeline-panel.mjs b/tools/system-analyzer/timeline-panel.mjs index a2be11403b..91c71e4528 100644 --- a/tools/system-analyzer/timeline-panel.mjs +++ b/tools/system-analyzer/timeline-panel.mjs @@ -2,53 +2,55 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { DOM, V8CustomElement } from './helper.mjs'; -import { SynchronizeSelectionEvent } from './events.mjs'; import './timeline/timeline-track.mjs'; -DOM.defineCustomElement('timeline-panel', (templateText) => - class TimelinePanel extends V8CustomElement { - constructor() { - super(templateText); - this.addEventListener( - 'scrolltrack', e => this.handleTrackScroll(e)); - this.addEventListener( - SynchronizeSelectionEvent.name, - e => this.handleSelectionSyncronization(e)); - } +import {SynchronizeSelectionEvent} from './events.mjs'; +import {DOM, V8CustomElement} from './helper.mjs'; - set nofChunks(count) { - for (const track of this.timelineTracks) { - track.nofChunks = count; +DOM.defineCustomElement( + 'timeline-panel', + (templateText) => class TimelinePanel extends V8CustomElement { + constructor() { + super(templateText); + this.addEventListener('scrolltrack', e => this.handleTrackScroll(e)); + this.addEventListener( + SynchronizeSelectionEvent.name, + e => this.handleSelectionSyncronization(e)); } - } - get nofChunks() { - return this.timelineTracks[0].nofChunks; - } - - get timelineTracks() { - return this.$("slot").assignedNodes().filter( - node => node.nodeType === Node.ELEMENT_NODE); - } - - handleTrackScroll(event) { - //TODO(zcankara) add forEachTrack helper method - for (const track of this.timelineTracks) { - track.scrollLeft = event.detail; + set nofChunks(count) { + for (const track of this.timelineTracks) { + track.nofChunks = count; + } } - } - handleSelectionSyncronization(event) { - this.timeSelection = {start:event.start, end:event.end}; - } + get nofChunks() { + return this.timelineTracks[0].nofChunks; + } - set timeSelection(timeSelection) { - if (timeSelection.start > timeSelection.end) { - throw new Error("Invalid time range"); + get timelineTracks() { + return this.$('slot').assignedNodes().filter( + node => node.nodeType === Node.ELEMENT_NODE); } - for (const track of this.timelineTracks) { - track.timeSelection = timeSelection;; + + handleTrackScroll(event) { + // TODO(zcankara) add forEachTrack helper method + for (const track of this.timelineTracks) { + track.scrollLeft = event.detail; + } } - } - }); + + handleSelectionSyncronization(event) { + this.timeSelection = {start: event.start, end: event.end}; + } + + set timeSelection(timeSelection) { + if (timeSelection.start > timeSelection.end) { + throw new Error('Invalid time range'); + } + for (const track of this.timelineTracks) { + track.timeSelection = timeSelection; + ; + } + } + }); diff --git a/tools/system-analyzer/timeline.mjs b/tools/system-analyzer/timeline.mjs index 0f7e42e36c..996f108b6a 100644 --- a/tools/system-analyzer/timeline.mjs +++ b/tools/system-analyzer/timeline.mjs @@ -35,17 +35,16 @@ class Timeline { } selectTimeRange(start, end) { - this._selection = this.filter( - e => e.time >= start && e.time <= end); + this._selection = this.filter(e => e.time >= start && e.time <= end); } getChunks(windowSizeMs) { - //TODO(zcankara) Fill this one + // TODO(zcankara) Fill this one return this.chunkSizes(windowSizeMs); } get values() { - //TODO(zcankara) Not to break something delete later + // TODO(zcankara) Not to break something delete later return this._values; } @@ -253,7 +252,7 @@ class Chunk { if (event_fn === void 0) { event_fn = each => each; } - let breakdown = { __proto__: null }; + let breakdown = {__proto__: null}; this.items.forEach(each => { const type = event_fn(each); const v = breakdown[type]; @@ -265,7 +264,6 @@ class Chunk { filter() { return this.items.filter(map => !map.parent() || !this.has(map.parent())); } - } -export { Timeline, Chunk }; +export {Timeline, Chunk}; diff --git a/tools/system-analyzer/timeline/timeline-track.mjs b/tools/system-analyzer/timeline/timeline-track.mjs index 9893f52d9c..ce25142fb6 100644 --- a/tools/system-analyzer/timeline/timeline-track.mjs +++ b/tools/system-analyzer/timeline/timeline-track.mjs @@ -2,12 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import { DOM, V8CustomElement, CSSColor, delay } from '../helper.mjs'; -import { kChunkWidth, kChunkHeight } from "../log/map.mjs"; -import { - SelectionEvent, FocusEvent, SelectTimeEvent, - SynchronizeSelectionEvent -} from '../events.mjs'; +import {FocusEvent, SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent} from '../events.mjs'; +import {CSSColor, delay, DOM, V8CustomElement} from '../helper.mjs'; +import {kChunkHeight, kChunkWidth} from '../log/map.mjs'; const kColors = [ CSSColor.green, @@ -21,500 +18,499 @@ const kColors = [ CSSColor.secondaryColor, ]; -DOM.defineCustomElement('./timeline/timeline-track', (templateText) => - class TimelineTrack extends V8CustomElement { - // TODO turn into static field once Safari supports it. - static get SELECTION_OFFSET() { return 10 }; - _timeline; - _nofChunks = 400; - _chunks; - _selectedEntry; - _timeToPixel; - _timeSelection = { start: -1, end: Infinity }; - _timeStartOffset; - _selectionOriginTime; - _typeToColor; - constructor() { - super(templateText); - this.timeline.addEventListener("scroll", - e => this.handleTimelineScroll(e)); - this.timeline.addEventListener("mousedown", - e => this.handleTimeSelectionMouseDown(e)); - this.timeline.addEventListener("mouseup", - e => this.handleTimeSelectionMouseUp(e)); - this.timeline.addEventListener("mousemove", - e => this.handleTimeSelectionMouseMove(e)); - this.backgroundCanvas = document.createElement('canvas'); - this.isLocked = false; - } +DOM.defineCustomElement('./timeline/timeline-track', + (templateText) => + class TimelineTrack extends V8CustomElement { + // TODO turn into static field once Safari supports it. + static get SELECTION_OFFSET() { + return 10 + }; + _timeline; + _nofChunks = 400; + _chunks; + _selectedEntry; + _timeToPixel; + _timeSelection = {start: -1, end: Infinity}; + _timeStartOffset; + _selectionOriginTime; + _typeToColor; + constructor() { + super(templateText); + this.timeline.addEventListener('scroll', e => this.handleTimelineScroll(e)); + this.timeline.addEventListener( + 'mousedown', e => this.handleTimeSelectionMouseDown(e)); + this.timeline.addEventListener( + 'mouseup', e => this.handleTimeSelectionMouseUp(e)); + this.timeline.addEventListener( + 'mousemove', e => this.handleTimeSelectionMouseMove(e)); + this.backgroundCanvas = document.createElement('canvas'); + this.isLocked = false; + } - handleTimeSelectionMouseDown(e) { - let xPosition = e.clientX - // Update origin time in case we click on a handle. - if (this.isOnLeftHandle(xPosition)) { - xPosition = this.rightHandlePosX; - } else if (this.isOnRightHandle(xPosition)) { - xPosition = this.leftHandlePosX; - } - this._selectionOriginTime = this.positionToTime(xPosition); + handleTimeSelectionMouseDown(e) { + let xPosition = e.clientX + // Update origin time in case we click on a handle. + if (this.isOnLeftHandle(xPosition)) { + xPosition = this.rightHandlePosX; } - - isOnLeftHandle(posX) { - return (Math.abs(this.leftHandlePosX - posX) - <= TimelineTrack.SELECTION_OFFSET); + else if (this.isOnRightHandle(xPosition)) { + xPosition = this.leftHandlePosX; } + this._selectionOriginTime = this.positionToTime(xPosition); + } - isOnRightHandle(posX) { - return (Math.abs(this.rightHandlePosX - posX) - <= TimelineTrack.SELECTION_OFFSET); - } + isOnLeftHandle(posX) { + return ( + Math.abs(this.leftHandlePosX - posX) <= TimelineTrack.SELECTION_OFFSET); + } - handleTimeSelectionMouseMove(e) { - if (!this._isSelecting) return; - const currentTime = this.positionToTime(e.clientX); - this.dispatchEvent(new SynchronizeSelectionEvent( + isOnRightHandle(posX) { + return ( + Math.abs(this.rightHandlePosX - posX) <= + TimelineTrack.SELECTION_OFFSET); + } + + handleTimeSelectionMouseMove(e) { + if (!this._isSelecting) return; + const currentTime = this.positionToTime(e.clientX); + this.dispatchEvent(new SynchronizeSelectionEvent( Math.min(this._selectionOriginTime, currentTime), Math.max(this._selectionOriginTime, currentTime))); - } + } - handleTimeSelectionMouseUp(e) { - this._selectionOriginTime = -1; - const delta = this._timeSelection.end - this._timeSelection.start; - if (delta <= 1 || isNaN(delta)) return; - this.dispatchEvent(new SelectTimeEvent(this._timeSelection.start, - this._timeSelection.end)); - } + handleTimeSelectionMouseUp(e) { + this._selectionOriginTime = -1; + const delta = this._timeSelection.end - this._timeSelection.start; + if (delta <= 1 || isNaN(delta)) return; + this.dispatchEvent(new SelectTimeEvent( + this._timeSelection.start, this._timeSelection.end)); + } - set timeSelection(selection) { - this._timeSelection.start = selection.start; - this._timeSelection.end= selection.end; - this.updateSelection(); - } + set timeSelection(selection) { + this._timeSelection.start = selection.start; + this._timeSelection.end = selection.end; + this.updateSelection(); + } - get _isSelecting() { - return this._selectionOriginTime >= 0; - } + get _isSelecting() { + return this._selectionOriginTime >= 0; + } - updateSelection() { - const startPosition = this.timeToPosition(this._timeSelection.start); - const endPosition = this.timeToPosition(this._timeSelection.end); - const delta = endPosition - startPosition; - this.leftHandle.style.left = startPosition + "px"; - this.selection.style.left = startPosition + "px"; - this.rightHandle.style.left = endPosition + "px"; - this.selection.style.width = delta + "px"; - } + updateSelection() { + const startPosition = this.timeToPosition(this._timeSelection.start); + const endPosition = this.timeToPosition(this._timeSelection.end); + const delta = endPosition - startPosition; + this.leftHandle.style.left = startPosition + 'px'; + this.selection.style.left = startPosition + 'px'; + this.rightHandle.style.left = endPosition + 'px'; + this.selection.style.width = delta + 'px'; + } - get leftHandlePosX() { - return this.leftHandle.getBoundingClientRect().x; - } + get leftHandlePosX() { + return this.leftHandle.getBoundingClientRect().x; + } - get rightHandlePosX() { - return this.rightHandle.getBoundingClientRect().x; - } + get rightHandlePosX() { + return this.rightHandle.getBoundingClientRect().x; + } - // Maps the clicked x position to the x position on timeline canvas - positionOnTimeline(posX) { - let rect = this.timeline.getBoundingClientRect(); - let posClickedX = posX - rect.left + this.timeline.scrollLeft; - return posClickedX; - } + // Maps the clicked x position to the x position on timeline canvas + positionOnTimeline(posX) { + let rect = this.timeline.getBoundingClientRect(); + let posClickedX = posX - rect.left + this.timeline.scrollLeft; + return posClickedX; + } - positionToTime(posX) { - let posTimelineX = this.positionOnTimeline(posX) + this._timeStartOffset; - return posTimelineX / this._timeToPixel; - } + positionToTime(posX) { + let posTimelineX = this.positionOnTimeline(posX) + this._timeStartOffset; + return posTimelineX / this._timeToPixel; + } - timeToPosition(time) { - let posX = time * this._timeToPixel; - posX -= this._timeStartOffset - return posX; - } + timeToPosition(time) { + let posX = time * this._timeToPixel; + posX -= this._timeStartOffset + return posX; + } - get leftHandle() { - return this.$('.leftHandle'); - } + get leftHandle() { + return this.$('.leftHandle'); + } - get rightHandle() { - return this.$('.rightHandle'); - } + get rightHandle() { + return this.$('.rightHandle'); + } - get selection() { - return this.$('.selection'); - } + get selection() { + return this.$('.selection'); + } - get timelineCanvas() { - return this.$('#timelineCanvas'); - } + get timelineCanvas() { + return this.$('#timelineCanvas'); + } - get timelineChunks() { - return this.$('#timelineChunks'); - } + get timelineChunks() { + return this.$('#timelineChunks'); + } - get timeline() { - return this.$('#timeline'); - } + get timeline() { + return this.$('#timeline'); + } - get timelineLegend() { - return this.$('#legend'); - } + get timelineLegend() { + return this.$('#legend'); + } - get timelineLegendContent() { - return this.$('#legendContent'); - } + get timelineLegendContent() { + return this.$('#legendContent'); + } - set data(value) { - this._timeline = value; - this._resetTypeToColorCache(); - this.updateChunks(); - this.updateTimeline(); - this.renderLegend(); - } + set data(value) { + this._timeline = value; + this._resetTypeToColorCache(); + this.updateChunks(); + this.updateTimeline(); + this.renderLegend(); + } - _resetTypeToColorCache() { - this._typeToColor = new Map(); - let lastIndex = 0; - for (const type of this.data.uniqueTypes.keys()) { - this._typeToColor.set(type, kColors[lastIndex++]); - } - } - - get data() { - return this._timeline; - } - - set nofChunks(count) { - this._nofChunks = count; - this.updateChunks(); - this.updateTimeline(); - } - - get nofChunks() { - return this._nofChunks; - } - - updateChunks() { - this._chunks = this.data.chunks(this.nofChunks); - } - - get chunks() { - return this._chunks; - } - - set selectedEntry(value) { - this._selectedEntry = value; - if (value.edge) this.redraw(); - } - - get selectedEntry() { - return this._selectedEntry; - } - - set scrollLeft(offset) { - this.timeline.scrollLeft = offset; - } - - typeToColor(type) { - return this._typeToColor.get(type); - } - - renderLegend() { - let timelineLegend = this.timelineLegend; - let timelineLegendContent = this.timelineLegendContent; - DOM.removeAllChildren(timelineLegendContent); - this._timeline.uniqueTypes.forEach((entries, type) => { - let row = DOM.tr("clickable"); - row.entries = entries; - row.addEventListener('dblclick', e => this.handleEntryTypeDblClick(e)); - let color = this.typeToColor(type); - if (color !== null) { - let div = DOM.div("colorbox"); - div.style.backgroundColor = color; - row.appendChild(DOM.td(div)); - } else { - row.appendChild(DOM.td()); - } - let td = DOM.td(type); - row.appendChild(td); - row.appendChild(DOM.td(entries.length)); - let percent = (entries.length / this.data.all.length) * 100; - row.appendChild(DOM.td(percent.toFixed(1) + "%")); - timelineLegendContent.appendChild(row); - }); - // Add Total row. - let row = DOM.tr(); - row.appendChild(DOM.td("")); - row.appendChild(DOM.td("All")); - row.appendChild(DOM.td(this.data.all.length)); - row.appendChild(DOM.td("100%")); - timelineLegendContent.appendChild(row); - timelineLegend.appendChild(timelineLegendContent); - } - - handleEntryTypeDblClick(e) { - this.dispatchEvent(new SelectionEvent(e.target.parentNode.entries)); - } - - timelineIndicatorMove(offset) { - this.timeline.scrollLeft += offset; - } - - handleTimelineScroll(e) { - let horizontal = e.currentTarget.scrollLeft; - this.dispatchEvent(new CustomEvent( - 'scrolltrack', { - bubbles: true, composed: true, - detail: horizontal - })); - } - - async setChunkBackgrounds(backgroundTodo) { - const kMaxDuration = 50; - let lastTime = 0; - for (let [chunk, node] of backgroundTodo) { - const current = performance.now(); - if (current - lastTime > kMaxDuration) { - await delay(25); - lastTime = current; - } - this.setChunkBackground(chunk, node); - } - } - - setChunkBackground(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.getBreakdown(map => map.type).forEach(([type, count]) => { - ctx.fillStyle = this.typeToColor(type); - let height = count / total * kHeight; - ctx.fillRect(0, y, kWidth, y + height); - y += height; - }); - } else { - chunk.items.forEach(map => { - ctx.fillStyle = this.typeToColor(map.type); - 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 + ')'; - } - - updateTimeline() { - let chunksNode = this.timelineChunks; - DOM.removeAllChildren(chunksNode); - let chunks = this.chunks; - let max = chunks.max(each => each.size()); - let start = this.data.startTime; - let end = this.data.endTime; - let duration = end - start; - this._timeToPixel = chunks.length * kChunkWidth / duration; - this._timeStartOffset = start * this._timeToPixel; - let addTimestamp = (time, name) => { - let timeNode = DOM.div('timestamp'); - timeNode.innerText = name; - timeNode.style.left = ((time - start) * this._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 = DOM.div(); - node.className = 'chunk'; - node.style.left = - ((chunks[i].start - start) * this._timeToPixel) + '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); - } - this.setChunkBackgrounds(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.redraw(); - } - - handleChunkMouseMove(event) { - if (this.isLocked) return false; - if (this._isSelecting) 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.dispatchEvent(new FocusEvent(map)); - } - - handleChunkClick(event) { - this.isLocked = !this.isLocked; - } - - handleChunkDoubleClick(event) { - let chunk = event.target.chunk; - if (!chunk) return; - this.dispatchEvent(new SelectTimeEvent(chunk.start, chunk.end)); - } - - redraw() { - let canvas = this.timelineCanvas; - canvas.width = (this.chunks.length + 1) * kChunkWidth; - canvas.height = kChunkHeight; - let ctx = canvas.getContext('2d'); - ctx.clearRect(0, 0, canvas.width, kChunkHeight); - if (!this.selectedEntry || !this.selectedEntry.edge) return; - this.drawEdges(ctx); - } - setMapStyle(map, ctx) { - ctx.fillStyle = map.edge && map.edge.from ? - CSSColor.onBackgroundColor : CSSColor.onPrimaryColor; - } - - setEdgeStyle(edge, ctx) { - let color = this.typeToColor(edge.type); - ctx.strokeStyle = color; - ctx.fillStyle = color; - } - - markMap(ctx, map) { - let [x, y] = map.position(this.chunks); - ctx.beginPath(); - this.setMapStyle(map, ctx); - ctx.arc(x, y, 3, 0, 2 * Math.PI); - ctx.fill(); - ctx.beginPath(); - ctx.fillStyle = CSSColor.onBackgroundColor; - ctx.arc(x, y, 2, 0, 2 * Math.PI); - ctx.fill(); - } - - markSelectedMap(ctx, map) { - let [x, y] = map.position(this.chunks); - ctx.beginPath(); - this.setMapStyle(map, ctx); - ctx.arc(x, y, 6, 0, 2 * Math.PI); - ctx.strokeStyle = CSSColor.onBackgroundColor; - 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.selectedEntry; - while (current && nofEdges < kMaxOutgoingEdges) { - nofEdges += current.children.length; - stack.push(current); - current = current.parent(); - } - ctx.save(); - this.drawOutgoingEdges(ctx, this.selectedEntry, 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.selectedEntry); - } - - 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.fillStyle = this.typeToColor(edge.type); - 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 = CSSColor.timelineBackgroundColor; - 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); - } + _resetTypeToColorCache() { + this._typeToColor = new Map(); + let lastIndex = 0; + for (const type of this.data.uniqueTypes.keys()) { + this._typeToColor.set(type, kColors[lastIndex++]); } } -); + + get data() { + return this._timeline; + } + + set nofChunks(count) { + this._nofChunks = count; + this.updateChunks(); + this.updateTimeline(); + } + + get nofChunks() { + return this._nofChunks; + } + + updateChunks() { + this._chunks = this.data.chunks(this.nofChunks); + } + + get chunks() { + return this._chunks; + } + + set selectedEntry(value) { + this._selectedEntry = value; + if (value.edge) this.redraw(); + } + + get selectedEntry() { + return this._selectedEntry; + } + + set scrollLeft(offset) { + this.timeline.scrollLeft = offset; + } + + typeToColor(type) { + return this._typeToColor.get(type); + } + + renderLegend() { + let timelineLegend = this.timelineLegend; + let timelineLegendContent = this.timelineLegendContent; + DOM.removeAllChildren(timelineLegendContent); + this._timeline.uniqueTypes.forEach((entries, type) => { + let row = DOM.tr('clickable'); + row.entries = entries; + row.addEventListener('dblclick', e => this.handleEntryTypeDblClick(e)); + let color = this.typeToColor(type); + if (color !== null) { + let div = DOM.div('colorbox'); + div.style.backgroundColor = color; + row.appendChild(DOM.td(div)); + } else { + row.appendChild(DOM.td()); + } + let td = DOM.td(type); + row.appendChild(td); + row.appendChild(DOM.td(entries.length)); + let percent = (entries.length / this.data.all.length) * 100; + row.appendChild(DOM.td(percent.toFixed(1) + '%')); + timelineLegendContent.appendChild(row); + }); + // Add Total row. + let row = DOM.tr(); + row.appendChild(DOM.td('')); + row.appendChild(DOM.td('All')); + row.appendChild(DOM.td(this.data.all.length)); + row.appendChild(DOM.td('100%')); + timelineLegendContent.appendChild(row); + timelineLegend.appendChild(timelineLegendContent); + } + + handleEntryTypeDblClick(e) { + this.dispatchEvent(new SelectionEvent(e.target.parentNode.entries)); + } + + timelineIndicatorMove(offset) { + this.timeline.scrollLeft += offset; + } + + handleTimelineScroll(e) { + let horizontal = e.currentTarget.scrollLeft; + this.dispatchEvent(new CustomEvent( + 'scrolltrack', {bubbles: true, composed: true, detail: horizontal})); + } + + async setChunkBackgrounds(backgroundTodo) { + const kMaxDuration = 50; + let lastTime = 0; + for (let [chunk, node] of backgroundTodo) { + const current = performance.now(); + if (current - lastTime > kMaxDuration) { + await delay(25); + lastTime = current; + } + this.setChunkBackground(chunk, node); + } + } + + setChunkBackground(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.getBreakdown(map => map.type).forEach(([type, count]) => { + ctx.fillStyle = this.typeToColor(type); + let height = count / total * kHeight; + ctx.fillRect(0, y, kWidth, y + height); + y += height; + }); + } else { + chunk.items.forEach(map => { + ctx.fillStyle = this.typeToColor(map.type); + 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 + ')'; + } + + updateTimeline() { + let chunksNode = this.timelineChunks; + DOM.removeAllChildren(chunksNode); + let chunks = this.chunks; + let max = chunks.max(each => each.size()); + let start = this.data.startTime; + let end = this.data.endTime; + let duration = end - start; + this._timeToPixel = chunks.length * kChunkWidth / duration; + this._timeStartOffset = start * this._timeToPixel; + let addTimestamp = (time, name) => { + let timeNode = DOM.div('timestamp'); + timeNode.innerText = name; + timeNode.style.left = ((time - start) * this._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 = DOM.div(); + node.className = 'chunk'; + node.style.left = ((chunks[i].start - start) * this._timeToPixel) + '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); + } + this.setChunkBackgrounds(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.redraw(); + } + + handleChunkMouseMove(event) { + if (this.isLocked) return false; + if (this._isSelecting) 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.dispatchEvent(new FocusEvent(map)); + } + + handleChunkClick(event) { + this.isLocked = !this.isLocked; + } + + handleChunkDoubleClick(event) { + let chunk = event.target.chunk; + if (!chunk) return; + this.dispatchEvent(new SelectTimeEvent(chunk.start, chunk.end)); + } + + redraw() { + let canvas = this.timelineCanvas; + canvas.width = (this.chunks.length + 1) * kChunkWidth; + canvas.height = kChunkHeight; + let ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, kChunkHeight); + if (!this.selectedEntry || !this.selectedEntry.edge) return; + this.drawEdges(ctx); + } + setMapStyle(map, ctx) { + ctx.fillStyle = map.edge && map.edge.from ? CSSColor.onBackgroundColor : + CSSColor.onPrimaryColor; + } + + setEdgeStyle(edge, ctx) { + let color = this.typeToColor(edge.type); + ctx.strokeStyle = color; + ctx.fillStyle = color; + } + + markMap(ctx, map) { + let [x, y] = map.position(this.chunks); + ctx.beginPath(); + this.setMapStyle(map, ctx); + ctx.arc(x, y, 3, 0, 2 * Math.PI); + ctx.fill(); + ctx.beginPath(); + ctx.fillStyle = CSSColor.onBackgroundColor; + ctx.arc(x, y, 2, 0, 2 * Math.PI); + ctx.fill(); + } + + markSelectedMap(ctx, map) { + let [x, y] = map.position(this.chunks); + ctx.beginPath(); + this.setMapStyle(map, ctx); + ctx.arc(x, y, 6, 0, 2 * Math.PI); + ctx.strokeStyle = CSSColor.onBackgroundColor; + 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.selectedEntry; + while (current && nofEdges < kMaxOutgoingEdges) { + nofEdges += current.children.length; + stack.push(current); + current = current.parent(); + } + ctx.save(); + this.drawOutgoingEdges(ctx, this.selectedEntry, 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.selectedEntry); + } + + 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.fillStyle = this.typeToColor(edge.type); + 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 = CSSColor.timelineBackgroundColor; + 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); + } + } +}); diff --git a/tools/v8_presubmit.py b/tools/v8_presubmit.py index 6fbc3ad2ed..5f94be6568 100755 --- a/tools/v8_presubmit.py +++ b/tools/v8_presubmit.py @@ -131,6 +131,31 @@ def TorqueLintWorker(command): print('Error running format-torque.py') process.kill() +def JSLintWorker(command): + try: + file_name = command[-1] + with open(file_name, "r") as file_handle: + contents = file_handle.read() + + process = subprocess.Popen(command, stdout=PIPE, stderr=subprocess.PIPE) + output, err = process.communicate() + rc = process.returncode + if rc != 0: + sys.stdout.write("error code " + str(rc) + " running clang-format.\n") + return rc + + if output != contents: + sys.stdout.write(file_name + " requires formatting.\n") + return 1 + + return 0 + except KeyboardInterrupt: + process.kill() + except Exception: + print('Error running clang-format. Please make sure you have depot_tools' + + ' in your $PATH. Lint check skipped.') + process.kill() + class FileContentsCache(object): def __init__(self, sums_file_name): @@ -392,6 +417,33 @@ class TorqueLintProcessor(CacheableSourceFileProcessor): return None, arguments +class JSLintProcessor(CacheableSourceFileProcessor): + """ + Check .{m}js file to verify they follow the JS Style guide. + """ + def __init__(self, use_cache=True): + super(JSLintProcessor, self).__init__( + use_cache=use_cache, cache_file_path='.jslint-cache', + file_type='JavaScript') + + def IsRelevant(self, name): + return name.endswith('.js') or name.endswith('.mjs') + + def GetPathsToSearch(self): + return ['tools/system-analyzer'] + + def GetProcessorWorker(self): + return JSLintWorker + + def GetProcessorScript(self): + for path in [TOOLS_PATH] + os.environ["PATH"].split(os.pathsep): + path = path.strip('"') + clang_format = os.path.join(path, 'clang_format.py') + if os.path.isfile(clang_format): + return clang_format, [] + + return None, [] + COPYRIGHT_HEADER_PATTERN = re.compile( r'Copyright [\d-]*20[0-2][0-9] the V8 project authors. All rights reserved.') @@ -708,6 +760,9 @@ def Main(): print("Running Torque formatting check...") success &= TorqueLintProcessor(use_cache=use_linter_cache).RunOnPath( workspace) + print("Running JavaScript formatting check...") + success &= JSLintProcessor(use_cache=use_linter_cache).RunOnPath( + workspace) print("Running copyright header, trailing whitespaces and " \ "two empty lines between declarations check...") success &= SourceProcessor().RunOnPath(workspace)