|  | // 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(); | 
|  | } | 
|  | } |