blob: 17058e4f3b23e96fb3d1907828097be7fdd4047a [file] [log] [blame]
// 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();
}
}