[turbolizer] Display live ranges with sequences

Display register allocation live ranges alongside sequences in
turbolizer.

The existing --trace-turbo flag now also outputs the register
allocation data as part of the json file alongside the
instruction sequence data that is already produced before and
after register allocation is performed. This data includes live
range intervals for each virtual and fixed register and the state
of their assignments.

This json data can now be displayed in turbolizer alongside the
instruction sequences. The information is presented as a grid,
with each grid cell representing a LifeTimePosition of a certain
virtual register, determined by the column and row indices
respectively. Each LifeTimePosition is shown to be part of an
instruction id which itself is shown to be part of a block id.
Each interval is shown as a coloured rectangle positioned over
the relevant cells, and displaying text to indicate the state of
their assignment.

The Resizer object has been extended to allow the grid's html
panel to be varied in size in the same manner that the left and
right panels can be. The size of the grid itself must also be
adjusted whenever the div container changes size.

The RangeView class is introduced and is created and held by the
single SequenceView object used to display the
InstructionSequence data before and after register allocation.
A checkbox allows the user to show/hide the range view, this is
disabled when register allocation data is not provided or more
than 249 instructions are in the sequence. The latter being
required due to the css grid-row-col limit of 1000 alond with
helping alleviate performance issues. The SequenceView object
tracks the phase index currently selected as well as whether or
not it is currently being shown. This ensures that the RangeView
is not hidden and shown when switching between before and after
register allocation, allowing for a smoother transition between
the two. The scroll position is also saved and restored for
convenience.

The data about the instruction sequence required for the display
is held by the RangeView object and reset whenever a new
instruction sequence is shown. The grid div must sync its scroll
with the headers and row labels so as to ensure a consistent
view. The register allocation data is extracted from the json,
with each register row showing all intervals within the relevant
ranges. When the view is switched between before and after
register allocation, the relevant intervals are swapped in.

Bug: v8:7327
Notry: true
Change-Id: I183535a2410a7d663382f387199885250fb98691
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2184232
Reviewed-by: Georg Neis <neis@chromium.org>
Reviewed-by: Santiago Aboy Solanes <solanes@chromium.org>
Commit-Queue: Santiago Aboy Solanes <solanes@chromium.org>
Cr-Commit-Position: refs/heads/master@{#68019}
This commit is contained in:
George Wort 2020-05-27 12:18:28 +01:00 committed by Commit Bot
parent 55616a2496
commit 0282737d83
16 changed files with 1649 additions and 52 deletions

View File

@ -964,6 +964,97 @@ void PrintScheduledGraph(std::ostream& os, const Schedule* schedule) {
} // namespace
std::ostream& operator<<(std::ostream& os,
const LiveRangeAsJSON& live_range_json) {
const LiveRange& range = live_range_json.range_;
os << "{\"id\":" << range.relative_id() << ",\"type\":";
if (range.HasRegisterAssigned()) {
const InstructionOperand op = range.GetAssignedOperand();
os << "\"assigned\",\"op\":"
<< InstructionOperandAsJSON{&op, &(live_range_json.code_)};
} else if (range.spilled() && !range.TopLevel()->HasNoSpillType()) {
const TopLevelLiveRange* top = range.TopLevel();
if (top->HasSpillOperand()) {
os << "\"assigned\",\"op\":"
<< InstructionOperandAsJSON{top->GetSpillOperand(),
&(live_range_json.code_)};
} else {
int index = top->GetSpillRange()->assigned_slot();
os << "\"spilled\",\"op\":";
if (IsFloatingPoint(top->representation())) {
os << "\"fp_stack:" << index << "\"";
} else {
os << "\"stack:" << index << "\"";
}
}
} else {
os << "\"none\"";
}
os << ",\"intervals\":[";
bool first = true;
for (const UseInterval* interval = range.first_interval();
interval != nullptr; interval = interval->next()) {
if (!first) os << ",";
first = false;
os << "[" << interval->start().value() << "," << interval->end().value()
<< "]";
}
os << "]}";
return os;
}
std::ostream& operator<<(
std::ostream& os,
const TopLevelLiveRangeAsJSON& top_level_live_range_json) {
int vreg = top_level_live_range_json.range_.vreg();
bool first = true;
os << "\"" << (vreg > 0 ? vreg : -vreg) << "\":{ \"child_ranges\":[";
for (const LiveRange* child = &(top_level_live_range_json.range_);
child != nullptr; child = child->next()) {
if (!top_level_live_range_json.range_.IsEmpty()) {
if (!first) os << ",";
first = false;
os << LiveRangeAsJSON{*child, top_level_live_range_json.code_};
}
}
os << "]";
if (top_level_live_range_json.range_.IsFixed()) {
os << ", \"is_deferred\": "
<< (top_level_live_range_json.range_.IsDeferredFixed() ? "true"
: "false");
}
os << "}";
return os;
}
void PrintTopLevelLiveRanges(std::ostream& os,
const ZoneVector<TopLevelLiveRange*> ranges,
const InstructionSequence& code) {
bool first = true;
os << "{";
for (const TopLevelLiveRange* range : ranges) {
if (range != nullptr && !range->IsEmpty()) {
if (!first) os << ",";
first = false;
os << TopLevelLiveRangeAsJSON{*range, code};
}
}
os << "}";
}
std::ostream& operator<<(std::ostream& os,
const RegisterAllocationDataAsJSON& ac) {
os << "\"fixed_double_live_ranges\": ";
PrintTopLevelLiveRanges(os, ac.data_.fixed_double_live_ranges(), ac.code_);
os << ",\"fixed_live_ranges\": ";
PrintTopLevelLiveRanges(os, ac.data_.fixed_live_ranges(), ac.code_);
os << ",\"live_ranges\": ";
PrintTopLevelLiveRanges(os, ac.data_.live_ranges(), ac.code_);
return os;
}
std::ostream& operator<<(std::ostream& os, const AsScheduledGraph& scheduled) {
PrintScheduledGraph(os, scheduled.schedule);
return os;
@ -1228,7 +1319,7 @@ std::ostream& operator<<(std::ostream& os, const InstructionBlockAsJSON& b) {
std::ostream& operator<<(std::ostream& os, const InstructionSequenceAsJSON& s) {
const InstructionSequence* code = s.sequence_;
os << "\"blocks\": [";
os << "[";
bool need_comma = false;
for (int i = 0; i < code->InstructionBlockCount(); i++) {

View File

@ -22,6 +22,8 @@ class SourcePosition;
namespace compiler {
class Graph;
class LiveRange;
class TopLevelLiveRange;
class Instruction;
class InstructionBlock;
class InstructionOperand;
@ -155,6 +157,30 @@ std::ostream& operator<<(std::ostream& os, const AsC1V& ac);
std::ostream& operator<<(std::ostream& os,
const AsC1VRegisterAllocationData& ac);
struct LiveRangeAsJSON {
const LiveRange& range_;
const InstructionSequence& code_;
};
std::ostream& operator<<(std::ostream& os,
const LiveRangeAsJSON& live_range_json);
struct TopLevelLiveRangeAsJSON {
const TopLevelLiveRange& range_;
const InstructionSequence& code_;
};
std::ostream& operator<<(
std::ostream& os, const TopLevelLiveRangeAsJSON& top_level_live_range_json);
struct RegisterAllocationDataAsJSON {
const RegisterAllocationData& data_;
const InstructionSequence& code_;
};
std::ostream& operator<<(std::ostream& os,
const RegisterAllocationDataAsJSON& ac);
struct InstructionOperandAsJSON {
const InstructionOperand* op_;
const InstructionSequence* code_;

View File

@ -3332,9 +3332,12 @@ void TraceSequence(OptimizedCompilationInfo* info, PipelineData* data,
if (info->trace_turbo_json_enabled()) {
AllowHandleDereference allow_deref;
TurboJsonFile json_of(info, std::ios_base::app);
json_of << "{\"name\":\"" << phase_name << "\",\"type\":\"sequence\",";
json_of << InstructionSequenceAsJSON{data->sequence()};
json_of << "},\n";
json_of << "{\"name\":\"" << phase_name << "\",\"type\":\"sequence\""
<< ",\"blocks\":" << InstructionSequenceAsJSON{data->sequence()}
<< ",\"register_allocation\":{"
<< RegisterAllocationDataAsJSON{*(data->register_allocation_data()),
*(data->sequence())}
<< "}},\n";
}
if (info->trace_turbo_graph_enabled()) {
AllowHandleDereference allow_deref;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -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>

View File

@ -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 = '&#9611;';
export const PROF_COLS = [
{ perc: 0, col: { r: 255, g: 255, b: 255 } },

View File

@ -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);

View File

@ -0,0 +1,927 @@
// 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);
}
// 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;
}
}
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();
}
}

View File

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

View File

@ -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 elementForOperand(operand, searchInfo) {
const text = operand.text;
const operandEl = createElement("div", ["parameter", "tag", "clickable", operand.type], text);
if (operand.tooltip) {
operandEl.setAttribute("title", operand.tooltip);
}
operandEl.onclick = mkOperandLinkHandler(text);
function elementForOperandWithSpan(span, text, searchInfo, isVirtual) {
const selectionText = isVirtual ? "virt_" + text : text;
span.onclick = mkOperandLinkHandler(selectionText);
searchInfo.push(text);
view.addHtmlElementForNodeId(text, operandEl);
return operandEl;
view.addHtmlElementForNodeId(selectionText, span);
const container = createElement("div", "");
container.appendChild(span);
return container;
}
function elementForOperand(operand, searchInfo) {
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) {
span.setAttribute("title", operand.tooltip);
}
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();

View File

@ -83,7 +83,7 @@ interface InstructionsPhase {
instructionOffsetToPCOffset?: any;
blockIdtoInstructionRange?: any;
nodeIdToInstructionRange?: any;
codeOffsetsInfo?: CodeOffsetsInfo
codeOffsetsInfo?: CodeOffsetsInfo;
}
interface GraphPhase {
@ -100,8 +100,43 @@ 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]>;
}
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 +755,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;
}
}

View File

@ -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("&#x2b;").classList.add("last-tab", "persistent-tab");
@ -48,6 +48,8 @@ window.onload = function () {
sourceViews.forEach(sv => sv.hide());
if (multiview) multiview.hide();
multiview = null;
document.getElementById("ranges").innerHTML = '';
document.getElementById('ranges').style.visibility = "hidden";
if (disassemblyView) disassemblyView.hide();
sourceViews = [];
sourceResolver = new SourceResolver();

View File

@ -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;
}

View File

@ -0,0 +1,234 @@
/* 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-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;
}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB