diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/v8/tools/turbolizer | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-85-based.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/v8/tools/turbolizer')
-rw-r--r-- | chromium/v8/tools/turbolizer/README.md | 7 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/down-arrow.png | bin | 0 -> 3670 bytes | |||
-rw-r--r-- | chromium/v8/tools/turbolizer/index.html | 7 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/src/constants.ts | 3 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/src/graphmultiview.ts | 9 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/src/range-view.ts | 938 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/src/resizer.ts | 131 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/src/sequence-view.ts | 177 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/src/source-resolver.ts | 43 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/src/turbo-visualizer.ts | 5 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/src/util.ts | 7 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/turbo-visualizer-ranges.css | 238 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/turbo-visualizer.css | 28 | ||||
-rw-r--r-- | chromium/v8/tools/turbolizer/up-arrow.png | bin | 0 -> 3579 bytes |
14 files changed, 1543 insertions, 50 deletions
diff --git a/chromium/v8/tools/turbolizer/README.md b/chromium/v8/tools/turbolizer/README.md index c5ee729d64d..fa804f65e94 100644 --- a/chromium/v8/tools/turbolizer/README.md +++ b/chromium/v8/tools/turbolizer/README.md @@ -74,7 +74,6 @@ well as '--cpu' to specify which CPU to sample. Turbolizer build process ------------------------ -Turbolizer is currently migrating to TypeScript. The typescript sources reside in -tools/turbolizer/src, and the typescript compiler will put the JavaScript output -into tools/turbolizer/build/. The index.html file is set up to load the JavaScript -from that directory. +The typescript sources reside in tools/turbolizer/src, and the typescript +compiler will put the JavaScript output into tools/turbolizer/build/. The +index.html file is set up to load the JavaScript from that directory. diff --git a/chromium/v8/tools/turbolizer/down-arrow.png b/chromium/v8/tools/turbolizer/down-arrow.png Binary files differnew file mode 100644 index 00000000000..39339f289a3 --- /dev/null +++ b/chromium/v8/tools/turbolizer/down-arrow.png diff --git a/chromium/v8/tools/turbolizer/index.html b/chromium/v8/tools/turbolizer/index.html index 268e51e0200..ea1b0b74d27 100644 --- a/chromium/v8/tools/turbolizer/index.html +++ b/chromium/v8/tools/turbolizer/index.html @@ -8,6 +8,7 @@ code is governed by a BSD-style license that can be found in the LICENSE file. <meta charset="utf-8"> <title>V8 Turbolizer</title> <link rel="stylesheet" href="turbo-visualizer.css"> + <link rel="stylesheet" href="turbo-visualizer-ranges.css"> <link rel="stylesheet" href="tabs.css"> <link rel="icon" type="image/png" href="turbolizer.png"> </head> @@ -21,6 +22,12 @@ code is governed by a BSD-style license that can be found in the LICENSE file. <input id="upload-helper" type="file"> <input id="upload" type="image" title="load graph" class="button-input" src="upload-icon.png" alt="upload graph"> </div> + <div id="resizer-ranges" class="resizer" style="visibility:hidden;"></div> + <div id="ranges" class="content" style="visibility:hidden;"></div> + <div id="show-hide-ranges" class="show-hide-pane" style="visibility: hidden"> + <input id="ranges-expand" type="image" title="show ranges" src="up-arrow.png" class="button-input invisible"> + <input id="ranges-shrink" type="image" title="hide ranges" src="down-arrow.png" class="button-input"> + </div> </div> <div id="resizer-right" class="resizer"></div> <div id="right" class="content"></div> diff --git a/chromium/v8/tools/turbolizer/src/constants.ts b/chromium/v8/tools/turbolizer/src/constants.ts index ada39ae6b3b..47dee8547ff 100644 --- a/chromium/v8/tools/turbolizer/src/constants.ts +++ b/chromium/v8/tools/turbolizer/src/constants.ts @@ -14,6 +14,9 @@ export const GENERATED_PANE_ID = 'right'; export const DISASSEMBLY_PANE_ID = 'disassembly'; export const DISASSEMBLY_COLLAPSE_ID = 'disassembly-shrink'; export const DISASSEMBLY_EXPAND_ID = 'disassembly-expand'; +export const RANGES_PANE_ID = "ranges"; +export const RANGES_COLLAPSE_ID = "ranges-shrink"; +export const RANGES_EXPAND_ID = "ranges-expand"; export const UNICODE_BLOCK = '▋'; export const PROF_COLS = [ { perc: 0, col: { r: 255, g: 255, b: 255 } }, diff --git a/chromium/v8/tools/turbolizer/src/graphmultiview.ts b/chromium/v8/tools/turbolizer/src/graphmultiview.ts index 380f7df77db..4f8f6339199 100644 --- a/chromium/v8/tools/turbolizer/src/graphmultiview.ts +++ b/chromium/v8/tools/turbolizer/src/graphmultiview.ts @@ -38,6 +38,11 @@ export class GraphMultiView extends View { return pane; } + hide() { + this.hideCurrentPhase(); + super.hide(); + } + constructor(id, selectionBroker, sourceResolver) { super(id); const view = this; @@ -86,7 +91,9 @@ export class GraphMultiView extends View { } show() { - super.show(); + // Insert before is used so that the display is inserted before the + // resizer for the RangeView. + this.container.insertBefore(this.divNode, this.container.firstChild); this.initializeSelect(); const lastPhaseIndex = +window.sessionStorage.getItem("lastSelectedPhase"); const initialPhaseIndex = this.sourceResolver.repairPhaseId(lastPhaseIndex); diff --git a/chromium/v8/tools/turbolizer/src/range-view.ts b/chromium/v8/tools/turbolizer/src/range-view.ts new file mode 100644 index 00000000000..17058e4f3b2 --- /dev/null +++ b/chromium/v8/tools/turbolizer/src/range-view.ts @@ -0,0 +1,938 @@ +// 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 { createElement } from "../src/util"; +import { SequenceView } from "../src/sequence-view"; +import { RegisterAllocation, Range, ChildRange, Interval } from "../src/source-resolver"; + +class Constants { + // Determines how many rows each div group holds for the purposes of + // hiding by syncHidden. + static readonly ROW_GROUP_SIZE = 20; + static readonly POSITIONS_PER_INSTRUCTION = 4; + static readonly FIXED_REGISTER_LABEL_WIDTH = 6; + + static readonly INTERVAL_TEXT_FOR_NONE = "none"; + static readonly INTERVAL_TEXT_FOR_CONST = "const"; + static readonly INTERVAL_TEXT_FOR_STACK = "stack:"; +} + +// This class holds references to the HTMLElements that represent each cell. +class Grid { + elements: Array<Array<HTMLElement>>; + + constructor() { + this.elements = []; + } + + setRow(row: number, elementsRow: Array<HTMLElement>) { + this.elements[row] = elementsRow; + } + + getCell(row: number, column: number) { + return this.elements[row][column]; + } + + getInterval(row: number, column: number) { + // The cell is within an inner wrapper div which is within the interval div. + return this.getCell(row, column).parentElement.parentElement; + } +} + +// This class is used as a wrapper to hide the switch between the +// two different Grid objects used, one for each phase, +// before and after register allocation. +class GridAccessor { + sequenceView: SequenceView; + grids: Map<number, Grid>; + + constructor(sequenceView: SequenceView) { + this.sequenceView = sequenceView; + this.grids = new Map<number, Grid>(); + } + + private currentGrid() { + return this.grids.get(this.sequenceView.currentPhaseIndex); + } + + getAnyGrid() { + return this.grids.values().next().value; + } + + hasGrid() { + return this.grids.has(this.sequenceView.currentPhaseIndex); + } + + addGrid(grid: Grid) { + if (this.hasGrid()) console.warn("Overwriting existing Grid."); + this.grids.set(this.sequenceView.currentPhaseIndex, grid); + } + + getCell(row: number, column: number) { + return this.currentGrid().getCell(row, column); + } + + getInterval(row: number, column: number) { + return this.currentGrid().getInterval(row, column); + } +} + +// This class is used as a wrapper to access the interval HTMLElements +class IntervalElementsAccessor { + sequenceView: SequenceView; + map: Map<number, Array<HTMLElement>>; + + constructor(sequenceView: SequenceView) { + this.sequenceView = sequenceView; + this.map = new Map<number, Array<HTMLElement>>(); + } + + private currentIntervals() { + const intervals = this.map.get(this.sequenceView.currentPhaseIndex); + if (intervals == undefined) { + this.map.set(this.sequenceView.currentPhaseIndex, new Array<HTMLElement>()); + return this.currentIntervals(); + } + return intervals; + } + + addInterval(interval: HTMLElement) { + this.currentIntervals().push(interval); + } + + forEachInterval(callback: (phase: number, interval: HTMLElement) => void) { + for (const phase of this.map.keys()) { + for (const interval of this.map.get(phase)) { + callback(phase, interval); + } + } + } +} + +// A simple class used to hold two Range objects. This is used to allow the two fixed register live +// ranges of normal and deferred to be easily combined into a single row. +class RangePair { + ranges: [Range, Range]; + + constructor(ranges: [Range, Range]) { + this.ranges = ranges; + } + + forEachRange(callback: (range: Range) => void) { + this.ranges.forEach((range: Range) => { if (range) callback(range); }); + } +} + +// A number of css variables regarding dimensions of HTMLElements are required by RangeView. +class CSSVariables { + positionWidth: number; + blockBorderWidth: number; + + constructor() { + const getNumberValue = varName => { + return parseFloat(getComputedStyle(document.body) + .getPropertyValue(varName).match(/[+-]?\d+(\.\d+)?/g)[0]); + }; + this.positionWidth = getNumberValue("--range-position-width"); + this.blockBorderWidth = getNumberValue("--range-block-border"); + } +} + +// Store the required data from the blocks JSON. +class BlocksData { + blockBorders: Set<number>; + blockInstructionCountMap: Map<number, number>; + + constructor(blocks: Array<any>) { + this.blockBorders = new Set<number>(); + this.blockInstructionCountMap = new Map<number, number>(); + for (const block of blocks) { + this.blockInstructionCountMap.set(block.id, block.instructions.length); + const maxInstructionInBlock = block.instructions[block.instructions.length - 1].id; + this.blockBorders.add(maxInstructionInBlock); + } + } + + isInstructionBorder(position: number) { + return ((position + 1) % Constants.POSITIONS_PER_INSTRUCTION) == 0; + } + + isBlockBorder(position: number) { + return this.isInstructionBorder(position) + && this.blockBorders.has(Math.floor(position / Constants.POSITIONS_PER_INSTRUCTION)); + } +} + +class Divs { + // Already existing. + container: HTMLElement; + resizerBar: HTMLElement; + snapper: HTMLElement; + + // Created by constructor. + content: HTMLElement; + // showOnLoad contains all content that may change depending on the JSON. + showOnLoad: HTMLElement; + xAxisLabel: HTMLElement; + yAxisLabel: HTMLElement; + registerHeaders: HTMLElement; + registers: HTMLElement; + + // Assigned from RangeView. + wholeHeader: HTMLElement; + positionHeaders: HTMLElement; + yAxis: HTMLElement; + grid: HTMLElement; + + constructor() { + this.container = document.getElementById("ranges"); + this.resizerBar = document.getElementById("resizer-ranges"); + this.snapper = document.getElementById("show-hide-ranges"); + + this.content = document.createElement("div"); + this.content.appendChild(this.elementForTitle()); + + this.showOnLoad = document.createElement("div"); + this.showOnLoad.style.visibility = "hidden"; + this.content.appendChild(this.showOnLoad); + + this.xAxisLabel = createElement("div", "range-header-label-x"); + this.xAxisLabel.innerText = "Blocks, Instructions, and Positions"; + this.showOnLoad.appendChild(this.xAxisLabel); + this.yAxisLabel = createElement("div", "range-header-label-y"); + this.yAxisLabel.innerText = "Registers"; + this.showOnLoad.appendChild(this.yAxisLabel); + + this.registerHeaders = createElement("div", "range-register-labels"); + this.registers = createElement("div", "range-registers"); + this.registerHeaders.appendChild(this.registers); + } + + elementForTitle() { + const titleEl = createElement("div", "range-title-div"); + const titleBar = createElement("div", "range-title"); + titleBar.appendChild(createElement("div", "", "Live Ranges")); + const titleHelp = createElement("div", "range-title-help", "?"); + titleHelp.title = "Each row represents a single TopLevelLiveRange (or two if deferred exists)." + + "\nEach interval belongs to a LiveRange contained within that row's TopLevelLiveRange." + + "\nAn interval is identified by i, the index of the LiveRange within the TopLevelLiveRange," + + "\nand j, the index of the interval within the LiveRange, to give i:j."; + titleEl.appendChild(titleBar); + titleEl.appendChild(titleHelp); + return titleEl; + } +} + +class Helper { + static virtualRegisterName(registerIndex: string) { + return "v" + registerIndex; + } + + static fixedRegisterName(range: Range) { + return range.child_ranges[0].op.text; + } + + static getPositionElementsFromInterval(interval: HTMLElement) { + return interval.children[1].children; + } + + static forEachFixedRange(source: RegisterAllocation, row: number, + callback: (registerIndex: string, row: number, registerName: string, + ranges: RangePair) => void) { + + const forEachRangeInMap = (rangeMap: Map<string, Range>) => { + // There are two fixed live ranges for each register, one for normal, another for deferred. + // These are combined into a single row. + const fixedRegisterMap = new Map<string, {ranges: [Range, Range], registerIndex: number}>(); + for (const [registerIndex, range] of rangeMap) { + const registerName = this.fixedRegisterName(range); + if (fixedRegisterMap.has(registerName)) { + const entry = fixedRegisterMap.get(registerName); + entry.ranges[1] = range; + // Only use the deferred register index if no normal index exists. + if (!range.is_deferred) { + entry.registerIndex = parseInt(registerIndex, 10); + } + } else { + fixedRegisterMap.set(registerName, {ranges: [range, undefined], + registerIndex: parseInt(registerIndex, 10)}); + } + } + // Sort the registers by number. + const sortedMap = new Map([...fixedRegisterMap.entries()].sort(([nameA, _], [nameB, __]) => { + // Larger numbers create longer strings. + if (nameA.length > nameB.length) return 1; + if (nameA.length < nameB.length) return -1; + // Sort lexicographically if same length. + if (nameA > nameB) return 1; + if (nameA < nameB) return -1; + return 0; + })); + for (const [registerName, {ranges, registerIndex}] of sortedMap) { + callback("" + (-registerIndex - 1), row, registerName, new RangePair(ranges)); + ++row; + } + }; + + forEachRangeInMap(source.fixedLiveRanges); + forEachRangeInMap(source.fixedDoubleLiveRanges); + + return row; + } +} + +class RowConstructor { + view: RangeView; + + constructor(view: RangeView) { + this.view = view; + } + + // Constructs the row of HTMLElements for grid while providing a callback for each position + // depending on whether that position is the start of an interval or not. + // RangePair is used to allow the two fixed register live ranges of normal and deferred to be + // easily combined into a single row. + construct(grid: Grid, row: number, registerIndex: string, ranges: RangePair, + getElementForEmptyPosition: (position: number) => HTMLElement, + callbackForInterval: (position: number, interval: HTMLElement) => void) { + const positionArray = new Array<HTMLElement>(this.view.numPositions); + // Construct all of the new intervals. + const intervalMap = this.elementsForIntervals(registerIndex, ranges); + for (let position = 0; position < this.view.numPositions; ++position) { + const interval = intervalMap.get(position); + if (interval == undefined) { + positionArray[position] = getElementForEmptyPosition(position); + } else { + callbackForInterval(position, interval); + this.view.intervalsAccessor.addInterval(interval); + const intervalPositionElements = Helper.getPositionElementsFromInterval(interval); + for (let j = 0; j < intervalPositionElements.length; ++j) { + // Point positionsArray to the new elements. + positionArray[position + j] = (intervalPositionElements[j] as HTMLElement); + } + position += intervalPositionElements.length - 1; + } + } + grid.setRow(row, positionArray); + ranges.forEachRange((range: Range) => this.setUses(grid, row, range)); + } + + // This is the main function used to build new intervals. + // Returns a map of LifeTimePositions to intervals. + private elementsForIntervals(registerIndex: string, ranges: RangePair) { + const intervalMap = new Map<number, HTMLElement>(); + let tooltip = ""; + ranges.forEachRange((range: Range) => { + for (const childRange of range.child_ranges) { + switch (childRange.type) { + case "none": + tooltip = Constants.INTERVAL_TEXT_FOR_NONE; + break; + case "spill_range": + tooltip = Constants.INTERVAL_TEXT_FOR_STACK + registerIndex; + break; + default: + if (childRange.op.type == "constant") { + tooltip = Constants.INTERVAL_TEXT_FOR_CONST; + } else { + if (childRange.op.text) { + tooltip = childRange.op.text; + } else { + tooltip = childRange.op; + } + } + break; + } + childRange.intervals.forEach((intervalNums, index) => { + const interval = new Interval(intervalNums); + const intervalEl = this.elementForInterval(childRange, interval, tooltip, + index, range.is_deferred); + intervalMap.set(interval.start, intervalEl); + }); + } + }); + return intervalMap; + } + + private elementForInterval(childRange: ChildRange, interval: Interval, + tooltip: string, index: number, isDeferred: boolean): HTMLElement { + const intervalEl = createElement("div", "range-interval"); + const title = childRange.id + ":" + index + " " + tooltip; + intervalEl.setAttribute("title", isDeferred ? "deferred: " + title : title); + this.setIntervalColor(intervalEl, tooltip); + const intervalInnerWrapper = createElement("div", "range-interval-wrapper"); + intervalEl.style.gridColumn = (interval.start + 1) + " / " + (interval.end + 1); + intervalInnerWrapper.style.gridTemplateColumns = "repeat(" + (interval.end - interval.start) + + ",calc(" + this.view.cssVariables.positionWidth + "ch + " + + this.view.cssVariables.blockBorderWidth + "px)"; + const intervalTextEl = this.elementForIntervalString(tooltip, interval.end - interval.start); + intervalEl.appendChild(intervalTextEl); + for (let i = interval.start; i < interval.end; ++i) { + const classes = "range-position range-interval-position range-empty" + + (this.view.blocksData.isBlockBorder(i) ? " range-block-border" : + this.view.blocksData.isInstructionBorder(i) ? " range-instr-border" : ""); + const positionEl = createElement("div", classes, "_"); + positionEl.style.gridColumn = (i - interval.start + 1) + ""; + intervalInnerWrapper.appendChild(positionEl); + } + intervalEl.appendChild(intervalInnerWrapper); + return intervalEl; + } + + private setIntervalColor(interval: HTMLElement, tooltip: string) { + if (tooltip.includes(Constants.INTERVAL_TEXT_FOR_NONE)) return; + if (tooltip.includes(Constants.INTERVAL_TEXT_FOR_STACK + "-")) { + interval.style.backgroundColor = "rgb(250, 158, 168)"; + } else if (tooltip.includes(Constants.INTERVAL_TEXT_FOR_STACK)) { + interval.style.backgroundColor = "rgb(250, 158, 100)"; + } else if (tooltip.includes(Constants.INTERVAL_TEXT_FOR_CONST)) { + interval.style.backgroundColor = "rgb(153, 158, 230)"; + } else { + interval.style.backgroundColor = "rgb(153, 220, 168)"; + } + } + + private elementForIntervalString(tooltip: string, numCells: number) { + const spanEl = createElement("span", "range-interval-text"); + this.setIntervalString(spanEl, tooltip, numCells); + return spanEl; + } + + // Each interval displays a string of information about it. + private setIntervalString(spanEl: HTMLElement, tooltip: string, numCells: number) { + const spacePerCell = this.view.cssVariables.positionWidth; + // One character space is removed to accommodate for padding. + const spaceAvailable = (numCells * spacePerCell) - 0.5; + let str = tooltip + ""; + const length = tooltip.length; + spanEl.style.width = null; + let paddingLeft = null; + // Add padding if possible + if (length <= spaceAvailable) { + paddingLeft = (length == spaceAvailable) ? "0.5ch" : "1ch"; + } else { + str = ""; + } + spanEl.style.paddingTop = null; + spanEl.style.paddingLeft = paddingLeft; + spanEl.innerHTML = str; + } + + private setUses(grid: Grid, row: number, range: Range) { + for (const liveRange of range.child_ranges) { + if (liveRange.uses) { + for (const use of liveRange.uses) { + grid.getCell(row, use).classList.toggle("range-use", true); + } + } + } + } +} + +class RangeViewConstructor { + view: RangeView; + gridTemplateColumns: string; + grid: Grid; + + // Group the rows in divs to make hiding/showing divs more efficient. + currentGroup: HTMLElement; + currentPlaceholderGroup: HTMLElement; + + constructor(rangeView: RangeView) { + this.view = rangeView; + } + + construct() { + this.gridTemplateColumns = "repeat(" + this.view.numPositions + + ",calc(" + this.view.cssVariables.positionWidth + "ch + " + + this.view.cssVariables.blockBorderWidth + "px)"; + + this.grid = new Grid(); + this.view.gridAccessor.addGrid(this.grid); + + this.view.divs.wholeHeader = this.elementForHeader(); + this.view.divs.showOnLoad.appendChild(this.view.divs.wholeHeader); + + const gridContainer = document.createElement("div"); + this.view.divs.grid = this.elementForGrid(); + this.view.divs.yAxis = createElement("div", "range-y-axis"); + this.view.divs.yAxis.appendChild(this.view.divs.registerHeaders); + this.view.divs.yAxis.onscroll = () => { + this.view.scrollHandler.syncScroll(ToSync.TOP, this.view.divs.yAxis, this.view.divs.grid); + this.view.scrollHandler.saveScroll(); + }; + gridContainer.appendChild(this.view.divs.yAxis); + gridContainer.appendChild(this.view.divs.grid); + this.view.divs.showOnLoad.appendChild(gridContainer); + + this.resetGroups(); + let row = 0; + row = this.addVirtualRanges(row); + this.addFixedRanges(row); + } + + // The following three functions are for constructing the groups which the rows are contained + // within and which make up the grid. This is so as to allow groups of rows to easily be displayed + // and hidden for performance reasons. As rows are constructed, they are added to the currentGroup + // div. Each row in currentGroup is matched with an equivalent placeholder row in + // currentPlaceholderGroup that will be shown when currentGroup is hidden so as to maintain the + // dimensions and scroll positions of the grid. + + private resetGroups () { + this.currentGroup = createElement("div", "range-positions-group range-hidden"); + this.currentPlaceholderGroup = createElement("div", "range-positions-group"); + } + + private appendGroupsToGrid() { + this.view.divs.grid.appendChild(this.currentPlaceholderGroup); + this.view.divs.grid.appendChild(this.currentGroup); + } + + private addRowToGroup(row: number, rowEl: HTMLElement) { + this.currentGroup.appendChild(rowEl); + this.currentPlaceholderGroup + .appendChild(createElement("div", "range-positions range-positions-placeholder", "_")); + if ((row + 1) % Constants.ROW_GROUP_SIZE == 0) { + this.appendGroupsToGrid(); + this.resetGroups(); + } + } + + private addVirtualRanges(row: number) { + const source = this.view.sequenceView.sequence.register_allocation; + for (const [registerIndex, range] of source.liveRanges) { + const registerName = Helper.virtualRegisterName(registerIndex); + const registerEl = this.elementForVirtualRegister(registerName); + this.addRowToGroup(row, this.elementForRow(row, registerIndex, + new RangePair([range, undefined]))); + this.view.divs.registers.appendChild(registerEl); + ++row; + } + return row; + } + + private addFixedRanges(row: number) { + row = Helper.forEachFixedRange(this.view.sequenceView.sequence.register_allocation, row, + (registerIndex: string, row: number, + registerName: string, ranges: RangePair) => { + const registerEl = this.elementForFixedRegister(registerName); + this.addRowToGroup(row, this.elementForRow(row, registerIndex, ranges)); + this.view.divs.registers.appendChild(registerEl); + }); + if (row % Constants.ROW_GROUP_SIZE != 0) { + this.appendGroupsToGrid(); + } + } + + // Each row of positions and intervals associated with a register is contained in a single + // HTMLElement. RangePair is used to allow the two fixed register live ranges of normal and + // deferred to be easily combined into a single row. + private elementForRow(row: number, registerIndex: string, ranges: RangePair) { + const rowEl = createElement("div", "range-positions"); + rowEl.style.gridTemplateColumns = this.gridTemplateColumns; + + const getElementForEmptyPosition = (position: number) => { + const blockBorder = this.view.blocksData.isBlockBorder(position); + const classes = "range-position range-empty " + + (blockBorder ? "range-block-border" : + this.view.blocksData.isInstructionBorder(position) ? "range-instr-border" + : "range-position-border"); + const positionEl = createElement("div", classes, "_"); + positionEl.style.gridColumn = (position + 1) + ""; + rowEl.appendChild(positionEl); + return positionEl; + }; + + const callbackForInterval = (_, interval: HTMLElement) => { + rowEl.appendChild(interval); + }; + + this.view.rowConstructor.construct(this.grid, row, registerIndex, ranges, + getElementForEmptyPosition, callbackForInterval); + return rowEl; + } + + private elementForVirtualRegister(registerName: string) { + const regEl = createElement("div", "range-reg", registerName); + regEl.setAttribute("title", registerName); + return regEl; + } + + private elementForFixedRegister(registerName: string) { + let text = registerName; + const span = "".padEnd(Constants.FIXED_REGISTER_LABEL_WIDTH - text.length, "_"); + text = "HW - <span class='range-transparent'>" + span + "</span>" + text; + const regEl = createElement("div", "range-reg"); + regEl.innerHTML = text; + regEl.setAttribute("title", registerName); + return regEl; + } + + // The header element contains the three headers for the LifeTimePosition axis. + private elementForHeader() { + const headerEl = createElement("div", "range-header"); + this.view.divs.positionHeaders = createElement("div", "range-position-labels"); + + this.view.divs.positionHeaders.appendChild(this.elementForBlockHeader()); + this.view.divs.positionHeaders.appendChild(this.elementForInstructionHeader()); + this.view.divs.positionHeaders.appendChild(this.elementForPositionHeader()); + + headerEl.appendChild(this.view.divs.positionHeaders); + headerEl.onscroll = () => { + this.view.scrollHandler.syncScroll(ToSync.LEFT, + this.view.divs.wholeHeader, this.view.divs.grid); + this.view.scrollHandler.saveScroll(); + }; + return headerEl; + } + + // The LifeTimePosition axis shows three headers, for positions, instructions, and blocks. + + private elementForBlockHeader() { + const headerEl = createElement("div", "range-block-ids"); + headerEl.style.gridTemplateColumns = this.gridTemplateColumns; + + const elementForBlockIndex = (index: number, firstInstruction: number, instrCount: number) => { + const str = "B" + index; + const element = + createElement("div", "range-block-id range-header-element range-block-border", str); + element.setAttribute("title", str); + const firstGridCol = (firstInstruction * Constants.POSITIONS_PER_INSTRUCTION) + 1; + const lastGridCol = firstGridCol + (instrCount * Constants.POSITIONS_PER_INSTRUCTION); + element.style.gridColumn = firstGridCol + " / " + lastGridCol; + return element; + }; + + let blockIndex = 0; + for (let i = 0; i < this.view.sequenceView.numInstructions;) { + const instrCount = this.view.blocksData.blockInstructionCountMap.get(blockIndex); + headerEl.appendChild(elementForBlockIndex(blockIndex, i, instrCount)); + ++blockIndex; + i += instrCount; + } + return headerEl; + } + + private elementForInstructionHeader() { + const headerEl = createElement("div", "range-instruction-ids"); + headerEl.style.gridTemplateColumns = this.gridTemplateColumns; + + const elementForInstructionIndex = (index: number, isBlockBorder: boolean) => { + const classes = "range-instruction-id range-header-element " + + (isBlockBorder ? "range-block-border" : "range-instr-border"); + const element = createElement("div", classes, "" + index); + element.setAttribute("title", "" + index); + const firstGridCol = (index * Constants.POSITIONS_PER_INSTRUCTION) + 1; + element.style.gridColumn = firstGridCol + " / " + + (firstGridCol + Constants.POSITIONS_PER_INSTRUCTION); + return element; + }; + + for (let i = 0; i < this.view.sequenceView.numInstructions; ++i) { + const blockBorder = this.view.blocksData.blockBorders.has(i); + headerEl.appendChild(elementForInstructionIndex(i, blockBorder)); + } + return headerEl; + } + + private elementForPositionHeader() { + const headerEl = createElement("div", "range-positions range-positions-header"); + headerEl.style.gridTemplateColumns = this.gridTemplateColumns; + + const elementForPositionIndex = (index: number, isBlockBorder: boolean) => { + const classes = "range-position range-header-element " + + (isBlockBorder ? "range-block-border" + : this.view.blocksData.isInstructionBorder(index) ? "range-instr-border" + : "range-position-border"); + const element = createElement("div", classes, "" + index); + element.setAttribute("title", "" + index); + return element; + }; + + for (let i = 0; i < this.view.numPositions; ++i) { + headerEl.appendChild(elementForPositionIndex(i, this.view.blocksData.isBlockBorder(i))); + } + return headerEl; + } + + private elementForGrid() { + const gridEl = createElement("div", "range-grid"); + gridEl.onscroll = () => { + this.view.scrollHandler.syncScroll(ToSync.TOP, this.view.divs.grid, this.view.divs.yAxis); + this.view.scrollHandler.syncScroll(ToSync.LEFT, + this.view.divs.grid, this.view.divs.wholeHeader); + this.view.scrollHandler.saveScroll(); + }; + return gridEl; + } +} + +// Handles the work required when the phase is changed. +// Between before and after register allocation for example. +class PhaseChangeHandler { + view: RangeView; + + constructor(view: RangeView) { + this.view = view; + } + + // Called when the phase view is switched between before and after register allocation. + phaseChange() { + if (!this.view.gridAccessor.hasGrid()) { + // If this phase view has not been seen yet then the intervals need to be constructed. + this.addNewIntervals(); + } + // Show all intervals pertaining to the current phase view. + this.view.intervalsAccessor.forEachInterval((phase, interval) => { + interval.classList.toggle("range-hidden", phase != this.view.sequenceView.currentPhaseIndex); + }); + } + + private addNewIntervals() { + // All Grids should point to the same HTMLElement for empty cells in the grid, + // so as to avoid duplication. The current Grid is used to retrieve these elements. + const currentGrid = this.view.gridAccessor.getAnyGrid(); + const newGrid = new Grid(); + this.view.gridAccessor.addGrid(newGrid); + const source = this.view.sequenceView.sequence.register_allocation; + let row = 0; + for (const [registerIndex, range] of source.liveRanges) { + this.addnewIntervalsInRange(currentGrid, newGrid, row, registerIndex, + new RangePair([range, undefined])); + ++row; + } + Helper.forEachFixedRange(this.view.sequenceView.sequence.register_allocation, row, + (registerIndex, row, _, ranges) => { + this.addnewIntervalsInRange(currentGrid, newGrid, row, registerIndex, ranges); + }); + } + + private addnewIntervalsInRange(currentGrid: Grid, newGrid: Grid, row: number, + registerIndex: string, ranges: RangePair) { + const numReplacements = new Map<HTMLElement, number>(); + + const getElementForEmptyPosition = (position: number) => { + return currentGrid.getCell(row, position); + }; + + // Inserts new interval beside existing intervals. + const callbackForInterval = (position: number, interval: HTMLElement) => { + // Overlapping intervals are placed beside each other and the relevant ones displayed. + let currentInterval = currentGrid.getInterval(row, position); + // The number of intervals already inserted is tracked so that the inserted intervals + // are ordered correctly. + const intervalsAlreadyInserted = numReplacements.get(currentInterval); + numReplacements.set(currentInterval, intervalsAlreadyInserted ? intervalsAlreadyInserted + 1 + : 1); + if (intervalsAlreadyInserted) { + for (let j = 0; j < intervalsAlreadyInserted; ++j) { + currentInterval = (currentInterval.nextElementSibling as HTMLElement); + } + } + interval.classList.add("range-hidden"); + currentInterval.insertAdjacentElement('afterend', interval); + }; + + this.view.rowConstructor.construct(newGrid, row, registerIndex, ranges, + getElementForEmptyPosition, callbackForInterval); + } +} + +enum ToSync { LEFT, TOP } + +// Handles saving and syncing the scroll positions of the grid. +class ScrollHandler { + divs: Divs; + scrollTop: number; + scrollLeft: number; + scrollTopTimeout: NodeJS.Timeout; + scrollLeftTimeout: NodeJS.Timeout; + scrollTopFunc: (this: GlobalEventHandlers, ev: Event) => any; + scrollLeftFunc: (this: GlobalEventHandlers, ev: Event) => any; + + constructor(divs: Divs) { + this.divs = divs; + } + + // This function is used to hide the rows which are not currently in view and + // so reduce the performance cost of things like hit tests and scrolling. + syncHidden() { + + const getOffset = (rowEl: HTMLElement, placeholderRowEl: HTMLElement, isHidden: boolean) => { + return isHidden ? placeholderRowEl.offsetTop : rowEl.offsetTop; + }; + + const toHide = new Array<[HTMLElement, HTMLElement]>(); + + const sampleCell = this.divs.registers.children[1] as HTMLElement; + const buffer = 2 * sampleCell.clientHeight; + const min = this.divs.grid.offsetTop + this.divs.grid.scrollTop - buffer; + const max = min + this.divs.grid.clientHeight + buffer; + + // The rows are grouped by being contained within a group div. This is so as to allow + // groups of rows to easily be displayed and hidden with less of a performance cost. + // Each row in the mainGroup div is matched with an equivalent placeholder row in + // the placeholderGroup div that will be shown when mainGroup is hidden so as to maintain + // the dimensions and scroll positions of the grid. + + const rangeGroups = this.divs.grid.children; + for (let i = 1; i < rangeGroups.length; i += 2) { + const mainGroup = rangeGroups[i] as HTMLElement; + const placeholderGroup = rangeGroups[i - 1] as HTMLElement; + const isHidden = mainGroup.classList.contains("range-hidden"); + // The offsets are used to calculate whether the group is in view. + const offsetMin = getOffset(mainGroup.firstChild as HTMLElement, + placeholderGroup.firstChild as HTMLElement, isHidden); + const offsetMax = getOffset(mainGroup.lastChild as HTMLElement, + placeholderGroup.lastChild as HTMLElement, isHidden); + if (offsetMax > min && offsetMin < max) { + if (isHidden) { + // Show the rows, hide the placeholders. + mainGroup.classList.toggle("range-hidden", false); + placeholderGroup.classList.toggle("range-hidden", true); + } + } else if (!isHidden) { + // Only hide the rows once the new rows are shown so that scrollLeft is not lost. + toHide.push([mainGroup, placeholderGroup]); + } + } + for (const [mainGroup, placeholderGroup] of toHide) { + // Hide the rows, show the placeholders. + mainGroup.classList.toggle("range-hidden", true); + placeholderGroup.classList.toggle("range-hidden", false); + } + } + + // This function is required to keep the axes labels in line with the grid + // content when scrolling. + syncScroll(toSync: ToSync, source: HTMLElement, target: HTMLElement) { + // Continually delay timeout until scrolling has stopped. + toSync == ToSync.TOP ? clearTimeout(this.scrollTopTimeout) + : clearTimeout(this.scrollLeftTimeout); + if (target.onscroll) { + if (toSync == ToSync.TOP) this.scrollTopFunc = target.onscroll; + else this.scrollLeftFunc = target.onscroll; + } + // Clear onscroll to prevent the target syncing back with the source. + target.onscroll = null; + + if (toSync == ToSync.TOP) target.scrollTop = source.scrollTop; + else target.scrollLeft = source.scrollLeft; + + // Only show / hide the grid content once scrolling has stopped. + if (toSync == ToSync.TOP) { + this.scrollTopTimeout = setTimeout(() => { + target.onscroll = this.scrollTopFunc; + this.syncHidden(); + }, 500); + } else { + this.scrollLeftTimeout = setTimeout(() => { + target.onscroll = this.scrollLeftFunc; + this.syncHidden(); + }, 500); + } + } + + saveScroll() { + this.scrollLeft = this.divs.grid.scrollLeft; + this.scrollTop = this.divs.grid.scrollTop; + } + + restoreScroll() { + if (this.scrollLeft) { + this.divs.grid.scrollLeft = this.scrollLeft; + this.divs.grid.scrollTop = this.scrollTop; + } + } +} + +// RangeView displays the live range data as passed in by SequenceView. +// The data is displayed in a grid format, with the fixed and virtual registers +// along one axis, and the LifeTimePositions along the other. Each LifeTimePosition +// is part of an Instruction in SequenceView, which itself is part of an Instruction +// Block. The live ranges are displayed as intervals, each belonging to a register, +// and spanning across a certain range of LifeTimePositions. +// When the phase being displayed changes between before register allocation and +// after register allocation, only the intervals need to be changed. +export class RangeView { + sequenceView: SequenceView; + + initialized: boolean; + isShown: boolean; + numPositions: number; + cssVariables: CSSVariables; + divs: Divs; + rowConstructor: RowConstructor; + phaseChangeHandler: PhaseChangeHandler; + scrollHandler: ScrollHandler; + blocksData: BlocksData; + intervalsAccessor: IntervalElementsAccessor; + gridAccessor: GridAccessor; + + constructor(sequence: SequenceView) { + this.initialized = false; + this.isShown = false; + this.sequenceView = sequence; + } + + initializeContent(blocks: Array<any>) { + if (!this.initialized) { + this.gridAccessor = new GridAccessor(this.sequenceView); + this.intervalsAccessor = new IntervalElementsAccessor(this.sequenceView); + this.cssVariables = new CSSVariables(); + this.blocksData = new BlocksData(blocks); + this.divs = new Divs(); + this.scrollHandler = new ScrollHandler(this.divs); + this.numPositions = this.sequenceView.numInstructions * Constants.POSITIONS_PER_INSTRUCTION; + this.rowConstructor = new RowConstructor(this); + const constructor = new RangeViewConstructor(this); + constructor.construct(); + this.phaseChangeHandler = new PhaseChangeHandler(this); + this.initialized = true; + } else { + // If the RangeView has already been initialized then the phase must have + // been changed. + this.phaseChangeHandler.phaseChange(); + } + } + + show() { + if (!this.isShown) { + this.isShown = true; + this.divs.container.appendChild(this.divs.content); + this.divs.resizerBar.style.visibility = "visible"; + this.divs.container.style.visibility = "visible"; + this.divs.snapper.style.visibility = "visible"; + // Dispatch a resize event to ensure that the + // panel is shown. + window.dispatchEvent(new Event('resize')); + + setTimeout(() => { + this.scrollHandler.restoreScroll(); + this.scrollHandler.syncHidden(); + this.divs.showOnLoad.style.visibility = "visible"; + }, 100); + } + } + + hide() { + if (this.initialized) { + this.isShown = false; + this.divs.container.removeChild(this.divs.content); + this.divs.resizerBar.style.visibility = "hidden"; + this.divs.container.style.visibility = "hidden"; + this.divs.snapper.style.visibility = "hidden"; + this.divs.showOnLoad.style.visibility = "hidden"; + } else { + window.document.getElementById('ranges').style.visibility = "hidden"; + } + // Dispatch a resize event to ensure that the + // panel is hidden. + window.dispatchEvent(new Event('resize')); + } + + onresize() { + if (this.isShown) this.scrollHandler.syncHidden(); + } +} diff --git a/chromium/v8/tools/turbolizer/src/resizer.ts b/chromium/v8/tools/turbolizer/src/resizer.ts index 4bd771f7313..ce0519398bb 100644 --- a/chromium/v8/tools/turbolizer/src/resizer.ts +++ b/chromium/v8/tools/turbolizer/src/resizer.ts @@ -11,6 +11,8 @@ class Snapper { sourceCollapse: HTMLElement; disassemblyExpand: HTMLElement; disassemblyCollapse: HTMLElement; + rangesExpand: HTMLElement; + rangesCollapse: HTMLElement; constructor(resizer: Resizer) { this.resizer = resizer; @@ -18,6 +20,8 @@ class Snapper { this.sourceCollapse = document.getElementById(C.SOURCE_COLLAPSE_ID); this.disassemblyExpand = document.getElementById(C.DISASSEMBLY_EXPAND_ID); this.disassemblyCollapse = document.getElementById(C.DISASSEMBLY_COLLAPSE_ID); + this.rangesExpand = document.getElementById(C.RANGES_EXPAND_ID); + this.rangesCollapse = document.getElementById(C.RANGES_COLLAPSE_ID); document.getElementById("show-hide-source").addEventListener("click", () => { this.resizer.resizerLeft.classed("snapped", !this.resizer.resizerLeft.classed("snapped")); @@ -29,13 +33,20 @@ class Snapper { this.setDisassemblyExpanded(!this.disassemblyExpand.classList.contains("invisible")); this.resizer.updatePanes(); }); + document.getElementById("show-hide-ranges").addEventListener("click", () => { + this.resizer.resizerRanges.classed("snapped", !this.resizer.resizerRanges.classed("snapped")); + this.setRangesExpanded(!this.rangesExpand.classList.contains("invisible")); + this.resizer.updatePanes(); + }); } restoreExpandedState(): void { this.resizer.resizerLeft.classed("snapped", window.sessionStorage.getItem("expandedState-source") == "false"); this.resizer.resizerRight.classed("snapped", window.sessionStorage.getItem("expandedState-disassembly") == "false"); + this.resizer.resizerRanges.classed("snapped", window.sessionStorage.getItem("expandedState-ranges") == "false"); this.setSourceExpanded(this.getLastExpandedState("source", true)); this.setDisassemblyExpanded(this.getLastExpandedState("disassembly", true)); + this.setRangesExpanded(this.getLastExpandedState("ranges", true)); } getLastExpandedState(type: string, defaultState: boolean): boolean { @@ -48,6 +59,7 @@ class Snapper { window.sessionStorage.setItem("expandedState-source", `${isSourceExpanded}`); this.sourceExpand.classList.toggle("invisible", isSourceExpanded); this.sourceCollapse.classList.toggle("invisible", !isSourceExpanded); + document.getElementById("show-hide-ranges").style.marginLeft = isSourceExpanded ? null : "40px"; } setSourceExpanded(isSourceExpanded: boolean): void { @@ -65,30 +77,53 @@ class Snapper { this.disassemblyUpdate(isDisassemblyExpanded); this.resizer.updateRightWidth(); } + + rangesUpdate(isRangesExpanded: boolean): void { + window.sessionStorage.setItem("expandedState-ranges", `${isRangesExpanded}`); + this.rangesExpand.classList.toggle("invisible", isRangesExpanded); + this.rangesCollapse.classList.toggle("invisible", !isRangesExpanded); + } + + setRangesExpanded(isRangesExpanded: boolean): void { + this.rangesUpdate(isRangesExpanded); + this.resizer.updateRanges(); + } } export class Resizer { snapper: Snapper; deadWidth: number; + deadHeight: number; left: HTMLElement; right: HTMLElement; + ranges: HTMLElement; + middle: HTMLElement; sepLeft: number; sepRight: number; + sepRangesHeight: number; panesUpdatedCallback: () => void; resizerRight: d3.Selection<HTMLDivElement, any, any, any>; resizerLeft: d3.Selection<HTMLDivElement, any, any, any>; + resizerRanges: d3.Selection<HTMLDivElement, any, any, any>; private readonly SOURCE_PANE_DEFAULT_PERCENT = 1 / 4; private readonly DISASSEMBLY_PANE_DEFAULT_PERCENT = 3 / 4; + private readonly RANGES_PANE_HEIGHT_DEFAULT_PERCENT = 3 / 4; + private readonly RESIZER_RANGES_HEIGHT_BUFFER_PERCENTAGE = 5; + private readonly RESIZER_SIZE = document.getElementById("resizer-ranges").offsetHeight; - constructor(panesUpdatedCallback: () => void, deadWidth: number) { + constructor(panesUpdatedCallback: () => void, deadWidth: number, deadHeight: number) { const resizer = this; resizer.panesUpdatedCallback = panesUpdatedCallback; resizer.deadWidth = deadWidth; + resizer.deadHeight = deadHeight; resizer.left = document.getElementById(C.SOURCE_PANE_ID); resizer.right = document.getElementById(C.GENERATED_PANE_ID); + resizer.ranges = document.getElementById(C.RANGES_PANE_ID); + resizer.middle = document.getElementById("middle"); resizer.resizerLeft = d3.select('#resizer-left'); resizer.resizerRight = d3.select('#resizer-right'); + resizer.resizerRanges = d3.select('#resizer-ranges'); // Set default sizes, if they weren't set. if (window.sessionStorage.getItem("source-pane-percent") === null) { window.sessionStorage.setItem("source-pane-percent", `${this.SOURCE_PANE_DEFAULT_PERCENT}`); @@ -96,8 +131,11 @@ export class Resizer { if (window.sessionStorage.getItem("disassembly-pane-percent") === null) { window.sessionStorage.setItem("disassembly-pane-percent", `${this.DISASSEMBLY_PANE_DEFAULT_PERCENT}`); } + if (window.sessionStorage.getItem("ranges-pane-height-percent") === null) { + window.sessionStorage.setItem("ranges-pane-height-percent", `${this.RANGES_PANE_HEIGHT_DEFAULT_PERCENT}`); + } - this.updateWidths(); + this.updateSizes(); const dragResizeLeft = d3.drag() .on('drag', function () { @@ -151,8 +189,35 @@ export class Resizer { resizer.resizerRight.classed("dragged", false); }); resizer.resizerRight.call(dragResizeRight); + + const dragResizeRanges = d3.drag() + .on('drag', function () { + const y = d3.mouse(this.parentElement)[1]; + resizer.sepRangesHeight = Math.max(100, Math.min(y, window.innerHeight) - resizer.RESIZER_RANGES_HEIGHT_BUFFER_PERCENTAGE); + resizer.updatePanes(); + }) + .on('start', function () { + resizer.resizerRanges.classed("dragged", true); + }) + .on('end', function () { + // If the panel is close enough to the bottom, treat it as if it was pulled all the way to the bottom. + const y = d3.mouse(this.parentElement)[1]; + if (y >= (window.innerHeight - deadHeight)) { + resizer.sepRangesHeight = window.innerHeight; + resizer.updatePanes(); + } + // Snap if dragged all the way to the bottom. + resizer.resizerRanges.classed("snapped", resizer.sepRangesHeight >= window.innerHeight - 1); + if (!resizer.isRangesSnapped()) { + window.sessionStorage.setItem("ranges-pane-height-percent", `${resizer.sepRangesHeight / window.innerHeight}`); + } + resizer.snapper.setRangesExpanded(!resizer.isRangesSnapped()); + resizer.resizerRanges.classed("dragged", false); + }); + resizer.resizerRanges.call(dragResizeRanges); + window.onresize = function () { - resizer.updateWidths(); + resizer.updateSizes(); resizer.updatePanes(); }; resizer.snapper = new Snapper(resizer); @@ -167,15 +232,70 @@ export class Resizer { return this.resizerRight.classed("snapped"); } + isRangesSnapped() { + return this.resizerRanges.classed("snapped"); + } + + updateRangesPane() { + const clientHeight = window.innerHeight; + const rangesIsHidden = this.ranges.style.visibility == "hidden"; + let resizerSize = this.RESIZER_SIZE; + if (rangesIsHidden) { + resizerSize = 0; + this.sepRangesHeight = clientHeight; + } + + const rangeHeight = clientHeight - this.sepRangesHeight; + this.ranges.style.height = rangeHeight + 'px'; + const panelWidth = this.sepRight - this.sepLeft - (2 * resizerSize); + this.ranges.style.width = panelWidth + 'px'; + const multiview = document.getElementById("multiview"); + if (multiview && multiview.style) { + multiview.style.height = (this.sepRangesHeight - resizerSize) + 'px'; + multiview.style.width = panelWidth + 'px'; + } + + // Resize the range grid and labels. + const rangeGrid = (this.ranges.getElementsByClassName("range-grid")[0] as HTMLElement); + if (rangeGrid) { + const yAxis = (this.ranges.getElementsByClassName("range-y-axis")[0] as HTMLElement); + const rangeHeader = (this.ranges.getElementsByClassName("range-header")[0] as HTMLElement); + + const gridWidth = panelWidth - yAxis.clientWidth; + rangeGrid.style.width = Math.floor(gridWidth - 1) + 'px'; + // Take live ranges' right scrollbar into account. + rangeHeader.style.width = (gridWidth - rangeGrid.offsetWidth + rangeGrid.clientWidth - 1) + 'px'; + // Set resizer to horizontal. + this.resizerRanges.style('width', panelWidth + 'px'); + + const rangeTitle = (this.ranges.getElementsByClassName("range-title-div")[0] as HTMLElement); + const rangeHeaderLabel = (this.ranges.getElementsByClassName("range-header-label-x")[0] as HTMLElement); + const gridHeight = rangeHeight - rangeHeader.clientHeight - rangeTitle.clientHeight - rangeHeaderLabel.clientHeight; + rangeGrid.style.height = gridHeight + 'px'; + // Take live ranges' bottom scrollbar into account. + yAxis.style.height = (gridHeight - rangeGrid.offsetHeight + rangeGrid.clientHeight) + 'px'; + } + this.resizerRanges.style('ranges', this.ranges.style.height); + } + updatePanes() { this.left.style.width = this.sepLeft + 'px'; this.resizerLeft.style('left', this.sepLeft + 'px'); this.right.style.width = (document.body.getBoundingClientRect().width - this.sepRight) + 'px'; this.resizerRight.style('right', (document.body.getBoundingClientRect().width - this.sepRight - 1) + 'px'); - + this.updateRangesPane(); this.panesUpdatedCallback(); } + updateRanges() { + if (this.isRangesSnapped()) { + this.sepRangesHeight = window.innerHeight; + } else { + const sepRangesHeight = window.sessionStorage.getItem("ranges-pane-height-percent"); + this.sepRangesHeight = window.innerHeight * Number.parseFloat(sepRangesHeight); + } + } + updateLeftWidth() { if (this.isLeftSnapped()) { this.sepLeft = 0; @@ -194,8 +314,9 @@ export class Resizer { } } - updateWidths() { + updateSizes() { this.updateLeftWidth(); this.updateRightWidth(); + this.updateRanges(); } } diff --git a/chromium/v8/tools/turbolizer/src/sequence-view.ts b/chromium/v8/tools/turbolizer/src/sequence-view.ts index 49b7e9f7b2a..187b162b1cd 100644 --- a/chromium/v8/tools/turbolizer/src/sequence-view.ts +++ b/chromium/v8/tools/turbolizer/src/sequence-view.ts @@ -3,12 +3,21 @@ // found in the LICENSE file. import { Sequence } from "../src/source-resolver"; -import { isIterable } from "../src/util"; +import { createElement } from "../src/util"; import { TextView } from "../src/text-view"; +import { RangeView } from "../src/range-view"; export class SequenceView extends TextView { sequence: Sequence; searchInfo: Array<any>; + phaseSelect: HTMLSelectElement; + numInstructions: number; + currentPhaseIndex: number; + phaseIndexes: Set<number>; + isShown: boolean; + rangeView: RangeView; + showRangeView: boolean; + toggleRangeViewEl: HTMLElement; createViewElement() { const pane = document.createElement('div'); @@ -20,6 +29,12 @@ export class SequenceView extends TextView { constructor(parentId, broker) { super(parentId, broker); + this.numInstructions = 0; + this.phaseIndexes = new Set<number>(); + this.isShown = false; + this.showRangeView = false; + this.rangeView = null; + this.toggleRangeViewEl = this.elementForToggleRangeView(); } attachSelection(s) { @@ -37,34 +52,58 @@ export class SequenceView extends TextView { return this.selection.detachSelection(); } + show() { + this.currentPhaseIndex = this.phaseSelect.selectedIndex; + if (!this.isShown) { + this.isShown = true; + this.phaseIndexes.add(this.currentPhaseIndex); + this.container.appendChild(this.divNode); + this.container.getElementsByClassName("graph-toolbox")[0].appendChild(this.toggleRangeViewEl); + } + if (this.showRangeView) this.rangeView.show(); + } + + hide() { + // A single SequenceView object is used for two phases (i.e before and after + // register allocation), tracking the indexes lets the redundant hides and + // shows be avoided when switching between the two. + this.currentPhaseIndex = this.phaseSelect.selectedIndex; + if (!this.phaseIndexes.has(this.currentPhaseIndex)) { + this.isShown = false; + this.container.removeChild(this.divNode); + this.container.getElementsByClassName("graph-toolbox")[0].removeChild(this.toggleRangeViewEl); + if (this.showRangeView) this.rangeView.hide(); + } + } + + onresize() { + if (this.showRangeView) this.rangeView.onresize(); + } + initializeContent(data, rememberedSelection) { this.divNode.innerHTML = ''; this.sequence = data.sequence; this.searchInfo = []; - this.divNode.addEventListener('click', (e: MouseEvent) => { + this.divNode.onclick = (e: MouseEvent) => { if (!(e.target instanceof HTMLElement)) return; const instructionId = Number.parseInt(e.target.dataset.instructionId, 10); if (!instructionId) return; if (!e.shiftKey) this.broker.broadcastClear(null); this.broker.broadcastInstructionSelect(null, [instructionId], true); - }); + }; + this.phaseSelect = (document.getElementById('phase-select') as HTMLSelectElement); + this.currentPhaseIndex = this.phaseSelect.selectedIndex; + this.addBlocks(this.sequence.blocks); + const lastBlock = this.sequence.blocks[this.sequence.blocks.length - 1]; + this.numInstructions = lastBlock.instructions[lastBlock.instructions.length - 1].id + 1; + this.addRangeView(); this.attachSelection(rememberedSelection); this.show(); } elementForBlock(block) { const view = this; - function createElement(tag: string, cls: string | Array<string>, content?: string) { - const el = document.createElement(tag); - if (isIterable(cls)) { - for (const c of cls) el.classList.add(c); - } else { - el.classList.add(cls); - } - if (content != undefined) el.innerHTML = content; - return el; - } function mkLinkHandler(id, handler) { return function (e) { @@ -84,16 +123,33 @@ export class SequenceView extends TextView { return mkLinkHandler(text, view.selectionHandler); } + function elementForOperandWithSpan(span, text, searchInfo, isVirtual) { + const selectionText = isVirtual ? "virt_" + text : text; + span.onclick = mkOperandLinkHandler(selectionText); + searchInfo.push(text); + view.addHtmlElementForNodeId(selectionText, span); + const container = createElement("div", ""); + container.appendChild(span); + return container; + } + function elementForOperand(operand, searchInfo) { - const text = operand.text; - const operandEl = createElement("div", ["parameter", "tag", "clickable", operand.type], text); + let isVirtual = false; + let className = "parameter tag clickable " + operand.type; + if (operand.text[0] == 'v' && !(operand.tooltip && operand.tooltip.includes("Float"))) { + isVirtual = true; + className += " virtual-reg"; + } + const span = createElement("span", className, operand.text); if (operand.tooltip) { - operandEl.setAttribute("title", operand.tooltip); + span.setAttribute("title", operand.tooltip); } - operandEl.onclick = mkOperandLinkHandler(text); - searchInfo.push(text); - view.addHtmlElementForNodeId(text, operandEl); - return operandEl; + return elementForOperandWithSpan(span, operand.text, searchInfo, isVirtual); + } + + function elementForPhiOperand(text, searchInfo) { + const span = createElement("span", "parameter tag clickable virtual-reg", text); + return elementForOperandWithSpan(span, text, searchInfo, true); } function elementForInstruction(instruction, searchInfo) { @@ -115,7 +171,7 @@ export class SequenceView extends TextView { const gapEl = createElement("div", "gap", "gap"); let hasGaps = false; for (const gap of instruction.gaps) { - const moves = createElement("div", ["comma-sep-list", "gap-move"]); + const moves = createElement("div", "comma-sep-list gap-move"); for (const move of gap) { hasGaps = true; const moveEl = createElement("div", "move"); @@ -137,7 +193,7 @@ export class SequenceView extends TextView { instContentsEl.appendChild(instEl); if (instruction.outputs.length > 0) { - const outputs = createElement("div", ["comma-sep-list", "input-output-list"]); + const outputs = createElement("div", "comma-sep-list input-output-list"); for (const output of instruction.outputs) { const outputEl = elementForOperand(output, searchInfo); outputs.appendChild(outputEl); @@ -147,8 +203,8 @@ export class SequenceView extends TextView { instEl.appendChild(assignEl); } - let text = instruction.opcode + instruction.flags; - const instLabel = createElement("div", "node-label", text) + const text = instruction.opcode + instruction.flags; + const instLabel = createElement("div", "node-label", text); if (instruction.opcode == "ArchNop" && instruction.outputs.length == 1 && instruction.outputs[0].tooltip) { instLabel.innerText = instruction.outputs[0].tooltip; } @@ -158,7 +214,7 @@ export class SequenceView extends TextView { instEl.appendChild(instLabel); if (instruction.inputs.length > 0) { - const inputs = createElement("div", ["comma-sep-list", "input-output-list"]); + const inputs = createElement("div", "comma-sep-list input-output-list"); for (const input of instruction.inputs) { const inputEl = elementForOperand(input, searchInfo); inputs.appendChild(inputEl); @@ -167,7 +223,7 @@ export class SequenceView extends TextView { } if (instruction.temps.length > 0) { - const temps = createElement("div", ["comma-sep-list", "input-output-list", "temps"]); + const temps = createElement("div", "comma-sep-list input-output-list temps"); for (const temp of instruction.temps) { const tempEl = elementForOperand(temp, searchInfo); temps.appendChild(tempEl); @@ -181,12 +237,12 @@ export class SequenceView extends TextView { const sequenceBlock = createElement("div", "schedule-block"); sequenceBlock.classList.toggle("deferred", block.deferred); - const blockId = createElement("div", ["block-id", "com", "clickable"], block.id); + const blockId = createElement("div", "block-id com clickable", block.id); blockId.onclick = mkBlockLinkHandler(block.id); sequenceBlock.appendChild(blockId); - const blockPred = createElement("div", ["predecessor-list", "block-list", "comma-sep-list"]); + const blockPred = createElement("div", "predecessor-list block-list comma-sep-list"); for (const pred of block.predecessors) { - const predEl = createElement("div", ["block-id", "com", "clickable"], pred); + const predEl = createElement("div", "block-id com clickable", pred); predEl.onclick = mkBlockLinkHandler(pred); blockPred.appendChild(predEl); } @@ -211,7 +267,7 @@ export class SequenceView extends TextView { phiEl.appendChild(assignEl); for (const input of phi.operands) { - const inputEl = createElement("div", ["parameter", "tag", "clickable"], input); + const inputEl = elementForPhiOperand(input, this.searchInfo); phiEl.appendChild(inputEl); } } @@ -221,9 +277,9 @@ export class SequenceView extends TextView { instructions.appendChild(elementForInstruction(instruction, this.searchInfo)); } sequenceBlock.appendChild(instructions); - const blockSucc = createElement("div", ["successor-list", "block-list", "comma-sep-list"]); + const blockSucc = createElement("div", "successor-list block-list comma-sep-list"); for (const succ of block.successors) { - const succEl = createElement("div", ["block-id", "com", "clickable"], succ); + const succEl = createElement("div", "block-id com clickable", succ); succEl.onclick = mkBlockLinkHandler(succ); blockSucc.appendChild(succEl); } @@ -239,6 +295,63 @@ export class SequenceView extends TextView { } } + addRangeView() { + const preventRangeView = reason => { + const toggleRangesInput = this.toggleRangeViewEl.firstChild as HTMLInputElement; + if (this.rangeView) { + toggleRangesInput.checked = false; + this.toggleRangeView(toggleRangesInput); + } + toggleRangesInput.disabled = true; + this.toggleRangeViewEl.style.textDecoration = "line-through"; + this.toggleRangeViewEl.setAttribute("title", reason); + }; + + if (this.sequence.register_allocation) { + if (!this.rangeView) { + this.rangeView = new RangeView(this); + } + const source = this.sequence.register_allocation; + if (source.fixedLiveRanges.size == 0 && source.liveRanges.size == 0) { + preventRangeView("No live ranges to show"); + } else if (this.numInstructions >= 249) { + // This is due to the css grid-column being limited to 1000 columns. + // Performance issues would otherwise impose some limit. + // TODO(george.wort@arm.com): Allow the user to specify an instruction range + // to display that spans less than 249 instructions. + preventRangeView( + "Live range display is only supported for sequences with less than 249 instructions"); + } + if (this.showRangeView) { + this.rangeView.initializeContent(this.sequence.blocks); + } + } else { + preventRangeView("No live range data provided"); + } + } + + elementForToggleRangeView() { + const toggleRangeViewEl = createElement("label", "", "show live ranges"); + const toggleRangesInput = createElement("input", "range-toggle-show") as HTMLInputElement; + toggleRangesInput.setAttribute("type", "checkbox"); + toggleRangesInput.oninput = () => this.toggleRangeView(toggleRangesInput); + toggleRangeViewEl.insertBefore(toggleRangesInput, toggleRangeViewEl.firstChild); + return toggleRangeViewEl; + } + + toggleRangeView(toggleRangesInput: HTMLInputElement) { + toggleRangesInput.disabled = true; + this.showRangeView = toggleRangesInput.checked; + if (this.showRangeView) { + this.rangeView.initializeContent(this.sequence.blocks); + this.rangeView.show(); + } else { + this.rangeView.hide(); + } + window.dispatchEvent(new Event('resize')); + toggleRangesInput.disabled = false; + } + searchInputAction(searchBar, e) { e.stopPropagation(); this.selectionHandler.clear(); diff --git a/chromium/v8/tools/turbolizer/src/source-resolver.ts b/chromium/v8/tools/turbolizer/src/source-resolver.ts index 588eea5b995..085b44f3a75 100644 --- a/chromium/v8/tools/turbolizer/src/source-resolver.ts +++ b/chromium/v8/tools/turbolizer/src/source-resolver.ts @@ -83,7 +83,7 @@ interface InstructionsPhase { instructionOffsetToPCOffset?: any; blockIdtoInstructionRange?: any; nodeIdToInstructionRange?: any; - codeOffsetsInfo?: CodeOffsetsInfo + codeOffsetsInfo?: CodeOffsetsInfo; } interface GraphPhase { @@ -100,8 +100,44 @@ export interface Schedule { nodes: Array<any>; } +export class Interval { + start: number; + end: number; + + constructor(numbers: [number, number]) { + this.start = numbers[0]; + this.end = numbers[1]; + } +} + +export interface ChildRange { + id: string; + type: string; + op: any; + intervals: Array<[number, number]>; + uses: Array<number>; +} + +export interface Range { + child_ranges: Array<ChildRange>; + is_deferred: boolean; +} + +export class RegisterAllocation { + fixedDoubleLiveRanges: Map<string, Range>; + fixedLiveRanges: Map<string, Range>; + liveRanges: Map<string, Range>; + + constructor(registerAllocation) { + this.fixedDoubleLiveRanges = new Map<string, Range>(Object.entries(registerAllocation.fixed_double_live_ranges)); + this.fixedLiveRanges = new Map<string, Range>(Object.entries(registerAllocation.fixed_live_ranges)); + this.liveRanges = new Map<string, Range>(Object.entries(registerAllocation.live_ranges)); + } +} + export interface Sequence { blocks: Array<any>; + register_allocation: RegisterAllocation; } class CodeOffsetsInfo { @@ -720,8 +756,11 @@ export class SourceResolver { phase.schedule = state; return phase; } + parseSequence(phase) { - phase.sequence = { blocks: phase.blocks }; + phase.sequence = { blocks: phase.blocks, + register_allocation: phase.register_allocation ? new RegisterAllocation(phase.register_allocation) + : undefined }; return phase; } } diff --git a/chromium/v8/tools/turbolizer/src/turbo-visualizer.ts b/chromium/v8/tools/turbolizer/src/turbo-visualizer.ts index 22753cdda50..2dd01c28f74 100644 --- a/chromium/v8/tools/turbolizer/src/turbo-visualizer.ts +++ b/chromium/v8/tools/turbolizer/src/turbo-visualizer.ts @@ -18,7 +18,7 @@ window.onload = function () { let sourceViews: Array<CodeView> = []; let selectionBroker: SelectionBroker = null; let sourceResolver: SourceResolver = null; - const resizer = new Resizer(panesUpdatedCallback, 75); + const resizer = new Resizer(panesUpdatedCallback, 75, 75); const sourceTabsContainer = document.getElementById(C.SOURCE_PANE_ID); const sourceTabs = new Tabs(sourceTabsContainer); sourceTabs.addTab("+").classList.add("last-tab", "persistent-tab"); @@ -48,6 +48,9 @@ window.onload = function () { sourceViews.forEach(sv => sv.hide()); if (multiview) multiview.hide(); multiview = null; + document.getElementById("ranges").innerHTML = ''; + document.getElementById('ranges').style.visibility = "hidden"; + document.getElementById('show-hide-ranges').style.visibility = "hidden"; if (disassemblyView) disassemblyView.hide(); sourceViews = []; sourceResolver = new SourceResolver(); diff --git a/chromium/v8/tools/turbolizer/src/util.ts b/chromium/v8/tools/turbolizer/src/util.ts index d9c8dcdce05..8d2fc845115 100644 --- a/chromium/v8/tools/turbolizer/src/util.ts +++ b/chromium/v8/tools/turbolizer/src/util.ts @@ -91,3 +91,10 @@ export function measureText(text: string) { export function interpolate(val: number, max: number, start: number, end: number) { return start + (end - start) * (val / max); } + +export function createElement(tag: string, cls: string, content?: string) { + const el = document.createElement(tag); + el.className = cls; + if (content != undefined) el.innerText = content; + return el; +} diff --git a/chromium/v8/tools/turbolizer/turbo-visualizer-ranges.css b/chromium/v8/tools/turbolizer/turbo-visualizer-ranges.css new file mode 100644 index 00000000000..03976e2ec54 --- /dev/null +++ b/chromium/v8/tools/turbolizer/turbo-visualizer-ranges.css @@ -0,0 +1,238 @@ +/* CSS specific to the live ranges div associated with + the RangeView typescript class in src/range-view.ts. */ + +:root { + --range-y-axis-width: 18ch; + --range-position-width: 3.5ch; + --range-block-border: 6px; + --range-instr-border: 3px; + --range-position-border: 1px; +} + +.range-bold { + font-weight: bold; + color: black; +} + +#ranges { + font-family: monospace; + min-height: auto; + overflow: hidden; +} + +#resizer-ranges { + height: 10px; +} + +.range-title-div { + padding: 2ch 2ch 2ch 2ch; + white-space: nowrap; + overflow: auto; +} + +.range-title { + text-decoration: underline; + font-weight: bold; + font-size: large; + display: inline-block; +} + +.range-title-help { + margin-left: 2ch; + width: 1ch; + padding: 0 0.25ch; + border: 1px dotted black; + color: slategray; + display: inline-block; +} + +input.range-toggle-show { + vertical-align: middle; +} + +.range-header-label-x { + text-align: center; + margin-left: 13ch; +} + +.range-header-label-y { + width: 11ch; + float: left; + white-space: pre-wrap; + word-wrap: break-word; + margin-left: 6ch; + margin-top: 4ch; +} + +.range-y-axis { + display: inline-block; + width: var(--range-y-axis-width); + overflow: hidden; + white-space: nowrap; + vertical-align: top; +} + +.range-header { + display: flex; + overflow: hidden; + height: 8ch; + margin-left: var(--range-y-axis-width); +} + +.range-position-labels, +.range-register-labels { + background-color: lightgray; +} + +.range-register-labels { + float: right; +} + +.range-position-labels { + margin-top: auto; +} + +.range-registers { + float: right; + overflow: hidden; + text-align: right; +} + +.range-positions-header, +.range-instruction-ids, +.range-block-ids { + overflow: hidden; + white-space: nowrap; + display: grid; + grid-gap: 0; +} + +.range-reg { + width: 13ch; + text-align: right; +} + +.range-reg::after { + content: ":"; +} + +.range-grid { + overflow: auto; + display: inline-block; + white-space: nowrap; +} + +.range-block-id { + display: inline-block; + text-align: center; +} + +.range-instruction-id { + display: inline-block; + text-align: center; +} + +.range-position { + display: inline-block; + text-align: center; + z-index: 1; +} + +.range-transparent, +.range-position.range-empty { + color: transparent; +} + +.range-block-id:hover, +.range-instruction-id:hover, +.range-reg:hover, +.range-position:hover { + background-color: rgba(0, 0, 255, 0.10); +} + +.range-position.range-header-element { + border-bottom: 2px solid rgb(109, 107, 107); +} + +.range-block-id, +.range-instruction-id, +.range-reg, +.range-interval, +.range-position { + position: relative; + border: var(--range-position-border) solid rgb(109, 107, 107); +} + +.range-block-id, +.range-instruction-id, +.range-interval, +.range-position { + border-left: 0; +} + +.range-block-ids > .range-block-id:first-child, +.range-instruction-ids > .range-instruction-id:first-child, +.range-positions > .range-position:first-child { + border-left: var(--range-position-border) solid rgb(109, 107, 107); +} + +.range-position.range-interval-position { + border: none; +} + +.range-interval-text { + position: absolute; + padding-left: 0.5ch; + z-index: 2; + pointer-events: none +} + +.range-position.range-use { + border-left: var(--range-instr-border) solid red; +} + +.range-block-border, +.range-block-border.range-position.range-interval-position:last-child { + border-right: var(--range-block-border) solid rgb(109, 107, 107); +} + +.range-block-border.range-position.range-interval-position { + border-right: var(--range-block-border) solid transparent; +} + +.range-instr-border, +.range-instr-border.range-position.range-interval-position:last-child { + border-right: var(--range-instr-border) solid rgb(109, 107, 107); +} + +.range-instr-border.range-position.range-interval-position { + border-right: var(--range-instr-border) solid transparent; +} + +.range, +.range-interval, +.range-interval-wrapper, +.range-positions { + white-space: nowrap; + display: inline-block; +} + +.range-interval-wrapper, +.range-positions { + display: grid; + grid-gap: 0; +} + +.range-interval { + background-color: rgb(153, 158, 168); +} + +.range-hidden { + display: none !important; +} + +.range-positions-placeholder { + width: 100%; + border: var(--range-position-border) solid transparent; + color: transparent; +}
\ No newline at end of file diff --git a/chromium/v8/tools/turbolizer/turbo-visualizer.css b/chromium/v8/tools/turbolizer/turbo-visualizer.css index 6fb6da3b794..c7da769eb5d 100644 --- a/chromium/v8/tools/turbolizer/turbo-visualizer.css +++ b/chromium/v8/tools/turbolizer/turbo-visualizer.css @@ -342,6 +342,13 @@ input:hover, background-color: #F8F8F8; user-select: none; flex: 1; + z-index: 7; +} + +#middle.display-inline-flex, +#middle.display-inline-flex #multiview, +#middle.display-inline-flex #ranges { + display: inline-flex; } .viewpane { @@ -351,11 +358,6 @@ input:hover, flex-direction: column; } -.multiview { - width: 100%; -} - - #show-hide-disassembly { right: 0; } @@ -423,6 +425,10 @@ text { dominant-baseline: text-before-edge; } +.tab-content { + z-index: 6; +} + .resizer { z-index: 10; width: 10px; @@ -595,6 +601,10 @@ text { padding-right: .5ex; } +.instruction span { + padding-right: 0; +} + .phi-label, .instruction-id { display: inline-block; @@ -626,6 +636,10 @@ text { display: inline-block; } +.phi span { + padding-right: 0; +} + .gap .gap-move { padding-left: .5ex; padding-right: .5ex; @@ -639,6 +653,10 @@ text { content: ")"; } +.virtual-reg { + outline: 1px dotted blue; +} + .parameter.constant { outline: 1px dotted red; } diff --git a/chromium/v8/tools/turbolizer/up-arrow.png b/chromium/v8/tools/turbolizer/up-arrow.png Binary files differnew file mode 100644 index 00000000000..68cb14e80b3 --- /dev/null +++ b/chromium/v8/tools/turbolizer/up-arrow.png |