• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2020 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import { createElement } from "../src/util";
6import { SequenceView } from "../src/sequence-view";
7import { RegisterAllocation, Range, ChildRange, Interval } from "../src/source-resolver";
8
9class Constants {
10  // Determines how many rows each div group holds for the purposes of
11  // hiding by syncHidden.
12  static readonly ROW_GROUP_SIZE = 20;
13  static readonly POSITIONS_PER_INSTRUCTION = 4;
14  static readonly FIXED_REGISTER_LABEL_WIDTH = 6;
15
16  static readonly INTERVAL_TEXT_FOR_NONE = "none";
17  static readonly INTERVAL_TEXT_FOR_CONST = "const";
18  static readonly INTERVAL_TEXT_FOR_STACK = "stack:";
19}
20
21// This class holds references to the HTMLElements that represent each cell.
22class Grid {
23  elements: Array<Array<HTMLElement>>;
24
25  constructor() {
26    this.elements = [];
27  }
28
29  setRow(row: number, elementsRow: Array<HTMLElement>) {
30    this.elements[row] = elementsRow;
31  }
32
33  getCell(row: number, column: number) {
34    return this.elements[row][column];
35  }
36
37  getInterval(row: number, column: number) {
38    // The cell is within an inner wrapper div which is within the interval div.
39    return this.getCell(row, column).parentElement.parentElement;
40  }
41}
42
43// This class is used as a wrapper to hide the switch between the
44// two different Grid objects used, one for each phase,
45// before and after register allocation.
46class GridAccessor {
47  sequenceView: SequenceView;
48  grids: Map<number, Grid>;
49
50  constructor(sequenceView: SequenceView) {
51    this.sequenceView = sequenceView;
52    this.grids = new Map<number, Grid>();
53  }
54
55  private currentGrid() {
56    return this.grids.get(this.sequenceView.currentPhaseIndex);
57  }
58
59  getAnyGrid() {
60    return this.grids.values().next().value;
61  }
62
63  hasGrid() {
64    return this.grids.has(this.sequenceView.currentPhaseIndex);
65  }
66
67  addGrid(grid: Grid) {
68    if (this.hasGrid()) console.warn("Overwriting existing Grid.");
69    this.grids.set(this.sequenceView.currentPhaseIndex, grid);
70  }
71
72  getCell(row: number, column: number) {
73    return this.currentGrid().getCell(row, column);
74  }
75
76  getInterval(row: number, column: number) {
77    return this.currentGrid().getInterval(row, column);
78  }
79}
80
81// This class is used as a wrapper to access the interval HTMLElements
82class IntervalElementsAccessor {
83  sequenceView: SequenceView;
84  map: Map<number, Array<HTMLElement>>;
85
86  constructor(sequenceView: SequenceView) {
87    this.sequenceView = sequenceView;
88    this.map = new Map<number, Array<HTMLElement>>();
89  }
90
91  private currentIntervals() {
92    const intervals = this.map.get(this.sequenceView.currentPhaseIndex);
93    if (intervals == undefined) {
94      this.map.set(this.sequenceView.currentPhaseIndex, new Array<HTMLElement>());
95      return this.currentIntervals();
96    }
97    return intervals;
98  }
99
100  addInterval(interval: HTMLElement) {
101    this.currentIntervals().push(interval);
102  }
103
104  forEachInterval(callback: (phase: number, interval: HTMLElement) => void) {
105    for (const phase of this.map.keys()) {
106      for (const interval of this.map.get(phase)) {
107        callback(phase, interval);
108      }
109    }
110  }
111}
112
113// A simple class used to hold two Range objects. This is used to allow the two fixed register live
114// ranges of normal and deferred to be easily combined into a single row.
115class RangePair {
116  ranges: [Range, Range];
117
118  constructor(ranges: [Range, Range]) {
119    this.ranges = ranges;
120  }
121
122  forEachRange(callback: (range: Range) => void) {
123    this.ranges.forEach((range: Range) => { if (range) callback(range); });
124  }
125}
126
127// A number of css variables regarding dimensions of HTMLElements are required by RangeView.
128class CSSVariables {
129  positionWidth: number;
130  blockBorderWidth: number;
131
132  constructor() {
133    const getNumberValue = varName => {
134      return parseFloat(getComputedStyle(document.body)
135             .getPropertyValue(varName).match(/[+-]?\d+(\.\d+)?/g)[0]);
136    };
137    this.positionWidth = getNumberValue("--range-position-width");
138    this.blockBorderWidth = getNumberValue("--range-block-border");
139  }
140}
141
142// Store the required data from the blocks JSON.
143class BlocksData {
144  blockBorders: Set<number>;
145  blockInstructionCountMap: Map<number, number>;
146
147  constructor(blocks: Array<any>) {
148    this.blockBorders = new Set<number>();
149    this.blockInstructionCountMap = new Map<number, number>();
150    for (const block of blocks) {
151      this.blockInstructionCountMap.set(block.id, block.instructions.length);
152      const maxInstructionInBlock = block.instructions[block.instructions.length - 1].id;
153      this.blockBorders.add(maxInstructionInBlock);
154    }
155  }
156
157  isInstructionBorder(position: number) {
158    return ((position + 1) % Constants.POSITIONS_PER_INSTRUCTION) == 0;
159  }
160
161  isBlockBorder(position: number) {
162    return this.isInstructionBorder(position)
163        && this.blockBorders.has(Math.floor(position / Constants.POSITIONS_PER_INSTRUCTION));
164  }
165}
166
167class Divs {
168  // Already existing.
169  container: HTMLElement;
170  resizerBar: HTMLElement;
171  snapper: HTMLElement;
172
173  // Created by constructor.
174  content: HTMLElement;
175  // showOnLoad contains all content that may change depending on the JSON.
176  showOnLoad: HTMLElement;
177  xAxisLabel: HTMLElement;
178  yAxisLabel: HTMLElement;
179  registerHeaders: HTMLElement;
180  registers: HTMLElement;
181
182  // Assigned from RangeView.
183  wholeHeader: HTMLElement;
184  positionHeaders: HTMLElement;
185  yAxis: HTMLElement;
186  grid: HTMLElement;
187
188  constructor() {
189    this.container = document.getElementById("ranges");
190    this.resizerBar = document.getElementById("resizer-ranges");
191    this.snapper = document.getElementById("show-hide-ranges");
192
193    this.content = document.createElement("div");
194    this.content.appendChild(this.elementForTitle());
195
196    this.showOnLoad = document.createElement("div");
197    this.showOnLoad.style.visibility = "hidden";
198    this.content.appendChild(this.showOnLoad);
199
200    this.xAxisLabel = createElement("div", "range-header-label-x");
201    this.xAxisLabel.innerText = "Blocks, Instructions, and Positions";
202    this.showOnLoad.appendChild(this.xAxisLabel);
203    this.yAxisLabel = createElement("div", "range-header-label-y");
204    this.yAxisLabel.innerText = "Registers";
205    this.showOnLoad.appendChild(this.yAxisLabel);
206
207    this.registerHeaders = createElement("div", "range-register-labels");
208    this.registers = createElement("div", "range-registers");
209    this.registerHeaders.appendChild(this.registers);
210  }
211
212  elementForTitle() {
213    const titleEl = createElement("div", "range-title-div");
214    const titleBar = createElement("div", "range-title");
215    titleBar.appendChild(createElement("div", "", "Live Ranges"));
216    const titleHelp = createElement("div", "range-title-help", "?");
217    titleHelp.title = "Each row represents a single TopLevelLiveRange (or two if deferred exists)."
218      + "\nEach interval belongs to a LiveRange contained within that row's TopLevelLiveRange."
219      + "\nAn interval is identified by i, the index of the LiveRange within the TopLevelLiveRange,"
220      + "\nand j, the index of the interval within the LiveRange, to give i:j.";
221    titleEl.appendChild(titleBar);
222    titleEl.appendChild(titleHelp);
223    return titleEl;
224  }
225}
226
227class Helper {
228  static virtualRegisterName(registerIndex: string) {
229    return "v" + registerIndex;
230  }
231
232  static fixedRegisterName(range: Range) {
233    return range.child_ranges[0].op.text;
234  }
235
236  static getPositionElementsFromInterval(interval: HTMLElement) {
237    return interval.children[1].children;
238  }
239
240  static forEachFixedRange(source: RegisterAllocation, row: number,
241                           callback: (registerIndex: string, row: number, registerName: string,
242                                      ranges: RangePair) => void) {
243
244    const forEachRangeInMap = (rangeMap: Map<string, Range>) => {
245      // There are two fixed live ranges for each register, one for normal, another for deferred.
246      // These are combined into a single row.
247      const fixedRegisterMap = new Map<string, {ranges: [Range, Range], registerIndex: number}>();
248      for (const [registerIndex, range] of rangeMap) {
249        const registerName = this.fixedRegisterName(range);
250        if (fixedRegisterMap.has(registerName)) {
251          const entry = fixedRegisterMap.get(registerName);
252          entry.ranges[1] = range;
253          // Only use the deferred register index if no normal index exists.
254          if (!range.is_deferred) {
255            entry.registerIndex = parseInt(registerIndex, 10);
256          }
257        } else {
258          fixedRegisterMap.set(registerName, {ranges: [range, undefined],
259                                              registerIndex: parseInt(registerIndex, 10)});
260        }
261      }
262      // Sort the registers by number.
263      const sortedMap = new Map([...fixedRegisterMap.entries()].sort(([nameA, _], [nameB, __]) => {
264        // Larger numbers create longer strings.
265        if (nameA.length > nameB.length) return 1;
266        if (nameA.length < nameB.length) return -1;
267        // Sort lexicographically if same length.
268        if (nameA > nameB) return 1;
269        if (nameA < nameB) return -1;
270        return 0;
271      }));
272      for (const [registerName, {ranges, registerIndex}] of sortedMap) {
273        callback("" + (-registerIndex - 1), row, registerName, new RangePair(ranges));
274        ++row;
275      }
276    };
277
278    forEachRangeInMap(source.fixedLiveRanges);
279    forEachRangeInMap(source.fixedDoubleLiveRanges);
280
281    return row;
282  }
283}
284
285class RowConstructor {
286  view: RangeView;
287
288  constructor(view: RangeView) {
289    this.view = view;
290  }
291
292  // Constructs the row of HTMLElements for grid while providing a callback for each position
293  // depending on whether that position is the start of an interval or not.
294  // RangePair is used to allow the two fixed register live ranges of normal and deferred to be
295  // easily combined into a single row.
296  construct(grid: Grid, row: number, registerIndex: string, ranges: RangePair,
297            getElementForEmptyPosition: (position: number) => HTMLElement,
298            callbackForInterval: (position: number, interval: HTMLElement) => void) {
299    const positionArray = new Array<HTMLElement>(this.view.numPositions);
300    // Construct all of the new intervals.
301    const intervalMap = this.elementsForIntervals(registerIndex, ranges);
302    for (let position = 0; position < this.view.numPositions; ++position) {
303      const interval = intervalMap.get(position);
304      if (interval == undefined) {
305        positionArray[position] = getElementForEmptyPosition(position);
306      } else {
307        callbackForInterval(position, interval);
308        this.view.intervalsAccessor.addInterval(interval);
309        const intervalPositionElements = Helper.getPositionElementsFromInterval(interval);
310        for (let j = 0; j < intervalPositionElements.length; ++j) {
311          // Point positionsArray to the new elements.
312          positionArray[position + j] = (intervalPositionElements[j] as HTMLElement);
313        }
314        position += intervalPositionElements.length - 1;
315      }
316    }
317    grid.setRow(row, positionArray);
318    ranges.forEachRange((range: Range) => this.setUses(grid, row, range));
319  }
320
321  // This is the main function used to build new intervals.
322  // Returns a map of LifeTimePositions to intervals.
323  private elementsForIntervals(registerIndex: string, ranges: RangePair) {
324    const intervalMap = new Map<number, HTMLElement>();
325    let tooltip = "";
326    ranges.forEachRange((range: Range) => {
327      for (const childRange of range.child_ranges) {
328        switch (childRange.type) {
329          case "none":
330            tooltip = Constants.INTERVAL_TEXT_FOR_NONE;
331            break;
332          case "spill_range":
333            tooltip = Constants.INTERVAL_TEXT_FOR_STACK + registerIndex;
334            break;
335          default:
336            if (childRange.op.type == "constant") {
337              tooltip = Constants.INTERVAL_TEXT_FOR_CONST;
338            } else {
339              if (childRange.op.text) {
340                tooltip = childRange.op.text;
341              } else {
342                tooltip = childRange.op;
343              }
344            }
345            break;
346        }
347        childRange.intervals.forEach((intervalNums, index) => {
348          const interval = new Interval(intervalNums);
349          const intervalEl = this.elementForInterval(childRange, interval, tooltip,
350                                                     index, range.is_deferred);
351          intervalMap.set(interval.start, intervalEl);
352        });
353      }
354    });
355    return intervalMap;
356  }
357
358  private elementForInterval(childRange: ChildRange, interval: Interval,
359                             tooltip: string, index: number, isDeferred: boolean): HTMLElement {
360    const intervalEl = createElement("div", "range-interval");
361    const title = childRange.id + ":" + index + " " + tooltip;
362    intervalEl.setAttribute("title", isDeferred ? "deferred: " + title : title);
363    this.setIntervalColor(intervalEl, tooltip);
364    const intervalInnerWrapper = createElement("div", "range-interval-wrapper");
365    intervalEl.style.gridColumn = (interval.start + 1) + " / " + (interval.end + 1);
366    intervalInnerWrapper.style.gridTemplateColumns = "repeat(" + (interval.end - interval.start)
367                                        + ",calc(" + this.view.cssVariables.positionWidth + "ch + "
368                                        + this.view.cssVariables.blockBorderWidth + "px)";
369    const intervalTextEl = this.elementForIntervalString(tooltip, interval.end - interval.start);
370    intervalEl.appendChild(intervalTextEl);
371    for (let i = interval.start; i < interval.end; ++i) {
372      const classes = "range-position range-interval-position range-empty"
373                    + (this.view.blocksData.isBlockBorder(i) ? " range-block-border" :
374                      this.view.blocksData.isInstructionBorder(i) ? " range-instr-border" : "");
375      const positionEl = createElement("div", classes, "_");
376      positionEl.style.gridColumn = (i - interval.start + 1) + "";
377      intervalInnerWrapper.appendChild(positionEl);
378    }
379    intervalEl.appendChild(intervalInnerWrapper);
380    return intervalEl;
381  }
382
383  private setIntervalColor(interval: HTMLElement, tooltip: string) {
384    if (tooltip.includes(Constants.INTERVAL_TEXT_FOR_NONE)) return;
385    if (tooltip.includes(Constants.INTERVAL_TEXT_FOR_STACK + "-")) {
386      interval.style.backgroundColor = "rgb(250, 158, 168)";
387    } else if (tooltip.includes(Constants.INTERVAL_TEXT_FOR_STACK)) {
388      interval.style.backgroundColor = "rgb(250, 158, 100)";
389    } else if (tooltip.includes(Constants.INTERVAL_TEXT_FOR_CONST)) {
390      interval.style.backgroundColor = "rgb(153, 158, 230)";
391    } else {
392      interval.style.backgroundColor = "rgb(153, 220, 168)";
393    }
394  }
395
396  private elementForIntervalString(tooltip: string, numCells: number) {
397    const spanEl = createElement("span", "range-interval-text");
398    this.setIntervalString(spanEl, tooltip, numCells);
399    return spanEl;
400  }
401
402  // Each interval displays a string of information about it.
403  private setIntervalString(spanEl: HTMLElement, tooltip: string, numCells: number) {
404    const spacePerCell = this.view.cssVariables.positionWidth;
405    // One character space is removed to accommodate for padding.
406    const spaceAvailable = (numCells * spacePerCell) - 0.5;
407    let str = tooltip + "";
408    const length = tooltip.length;
409    spanEl.style.width = null;
410    let paddingLeft = null;
411    // Add padding if possible
412    if (length <= spaceAvailable) {
413      paddingLeft = (length == spaceAvailable) ? "0.5ch" : "1ch";
414    } else {
415      str = "";
416    }
417    spanEl.style.paddingTop = null;
418    spanEl.style.paddingLeft = paddingLeft;
419    spanEl.innerHTML = str;
420  }
421
422  private setUses(grid: Grid, row: number, range: Range) {
423    for (const liveRange of range.child_ranges) {
424      if (liveRange.uses) {
425        for (const use of liveRange.uses) {
426          grid.getCell(row, use).classList.toggle("range-use", true);
427        }
428      }
429    }
430  }
431}
432
433class RangeViewConstructor {
434  view: RangeView;
435  gridTemplateColumns: string;
436  grid: Grid;
437
438  // Group the rows in divs to make hiding/showing divs more efficient.
439  currentGroup: HTMLElement;
440  currentPlaceholderGroup: HTMLElement;
441
442  constructor(rangeView: RangeView) {
443    this.view = rangeView;
444  }
445
446  construct() {
447    this.gridTemplateColumns = "repeat(" + this.view.numPositions
448                             + ",calc(" + this.view.cssVariables.positionWidth + "ch + "
449                             + this.view.cssVariables.blockBorderWidth + "px)";
450
451    this.grid = new Grid();
452    this.view.gridAccessor.addGrid(this.grid);
453
454    this.view.divs.wholeHeader = this.elementForHeader();
455    this.view.divs.showOnLoad.appendChild(this.view.divs.wholeHeader);
456
457    const gridContainer = document.createElement("div");
458    this.view.divs.grid = this.elementForGrid();
459    this.view.divs.yAxis = createElement("div", "range-y-axis");
460    this.view.divs.yAxis.appendChild(this.view.divs.registerHeaders);
461    this.view.divs.yAxis.onscroll = () => {
462      this.view.scrollHandler.syncScroll(ToSync.TOP, this.view.divs.yAxis, this.view.divs.grid);
463      this.view.scrollHandler.saveScroll();
464    };
465    gridContainer.appendChild(this.view.divs.yAxis);
466    gridContainer.appendChild(this.view.divs.grid);
467    this.view.divs.showOnLoad.appendChild(gridContainer);
468
469    this.resetGroups();
470    let row = 0;
471    row = this.addVirtualRanges(row);
472    this.addFixedRanges(row);
473  }
474
475  // The following three functions are for constructing the groups which the rows are contained
476  // within and which make up the grid. This is so as to allow groups of rows to easily be displayed
477  // and hidden for performance reasons. As rows are constructed, they are added to the currentGroup
478  // div. Each row in currentGroup is matched with an equivalent placeholder row in
479  // currentPlaceholderGroup that will be shown when currentGroup is hidden so as to maintain the
480  // dimensions and scroll positions of the grid.
481
482  private resetGroups () {
483    this.currentGroup = createElement("div", "range-positions-group range-hidden");
484    this.currentPlaceholderGroup = createElement("div", "range-positions-group");
485  }
486
487  private appendGroupsToGrid() {
488    this.view.divs.grid.appendChild(this.currentPlaceholderGroup);
489    this.view.divs.grid.appendChild(this.currentGroup);
490  }
491
492  private addRowToGroup(row: number, rowEl: HTMLElement) {
493    this.currentGroup.appendChild(rowEl);
494    this.currentPlaceholderGroup
495        .appendChild(createElement("div", "range-positions range-positions-placeholder", "_"));
496    if ((row + 1) % Constants.ROW_GROUP_SIZE == 0) {
497      this.appendGroupsToGrid();
498      this.resetGroups();
499    }
500  }
501
502  private addVirtualRanges(row: number) {
503    const source = this.view.sequenceView.sequence.register_allocation;
504    for (const [registerIndex, range] of source.liveRanges) {
505      const registerName = Helper.virtualRegisterName(registerIndex);
506      const registerEl = this.elementForVirtualRegister(registerName);
507      this.addRowToGroup(row, this.elementForRow(row, registerIndex,
508                                                 new RangePair([range, undefined])));
509      this.view.divs.registers.appendChild(registerEl);
510      ++row;
511    }
512    return row;
513  }
514
515  private addFixedRanges(row: number) {
516    row = Helper.forEachFixedRange(this.view.sequenceView.sequence.register_allocation, row,
517                                   (registerIndex: string, row: number,
518                                    registerName: string, ranges: RangePair) => {
519      const registerEl = this.elementForFixedRegister(registerName);
520      this.addRowToGroup(row, this.elementForRow(row, registerIndex, ranges));
521      this.view.divs.registers.appendChild(registerEl);
522    });
523    if (row % Constants.ROW_GROUP_SIZE != 0) {
524      this.appendGroupsToGrid();
525    }
526  }
527
528  // Each row of positions and intervals associated with a register is contained in a single
529  // HTMLElement. RangePair is used to allow the two fixed register live ranges of normal and
530  // deferred to be easily combined into a single row.
531  private elementForRow(row: number, registerIndex: string, ranges: RangePair) {
532    const rowEl = createElement("div", "range-positions");
533    rowEl.style.gridTemplateColumns = this.gridTemplateColumns;
534
535    const getElementForEmptyPosition = (position: number) => {
536      const blockBorder = this.view.blocksData.isBlockBorder(position);
537      const classes = "range-position range-empty "
538                    + (blockBorder ? "range-block-border" :
539                    this.view.blocksData.isInstructionBorder(position) ? "range-instr-border"
540                                                                       : "range-position-border");
541      const positionEl = createElement("div", classes, "_");
542      positionEl.style.gridColumn = (position + 1) + "";
543      rowEl.appendChild(positionEl);
544      return positionEl;
545    };
546
547    const callbackForInterval = (_, interval: HTMLElement) => {
548      rowEl.appendChild(interval);
549    };
550
551    this.view.rowConstructor.construct(this.grid, row, registerIndex, ranges,
552                                       getElementForEmptyPosition, callbackForInterval);
553    return rowEl;
554  }
555
556  private elementForVirtualRegister(registerName: string) {
557    const regEl = createElement("div", "range-reg", registerName);
558    regEl.setAttribute("title", registerName);
559    return regEl;
560  }
561
562  private elementForFixedRegister(registerName: string) {
563    let text = registerName;
564    const span = "".padEnd(Constants.FIXED_REGISTER_LABEL_WIDTH - text.length, "_");
565    text = "HW - <span class='range-transparent'>" + span + "</span>" + text;
566    const regEl = createElement("div", "range-reg");
567    regEl.innerHTML = text;
568    regEl.setAttribute("title", registerName);
569    return regEl;
570  }
571
572  // The header element contains the three headers for the LifeTimePosition axis.
573  private elementForHeader() {
574    const headerEl = createElement("div", "range-header");
575    this.view.divs.positionHeaders = createElement("div", "range-position-labels");
576
577    this.view.divs.positionHeaders.appendChild(this.elementForBlockHeader());
578    this.view.divs.positionHeaders.appendChild(this.elementForInstructionHeader());
579    this.view.divs.positionHeaders.appendChild(this.elementForPositionHeader());
580
581    headerEl.appendChild(this.view.divs.positionHeaders);
582    headerEl.onscroll = () => {
583      this.view.scrollHandler.syncScroll(ToSync.LEFT,
584                                         this.view.divs.wholeHeader, this.view.divs.grid);
585      this.view.scrollHandler.saveScroll();
586    };
587    return headerEl;
588  }
589
590  // The LifeTimePosition axis shows three headers, for positions, instructions, and blocks.
591
592  private elementForBlockHeader() {
593    const headerEl = createElement("div", "range-block-ids");
594    headerEl.style.gridTemplateColumns = this.gridTemplateColumns;
595
596    const elementForBlockIndex = (index: number, firstInstruction: number, instrCount: number) => {
597      const str = "B" + index;
598      const element =
599        createElement("div", "range-block-id range-header-element range-block-border", str);
600      element.setAttribute("title", str);
601      const firstGridCol = (firstInstruction * Constants.POSITIONS_PER_INSTRUCTION) + 1;
602      const lastGridCol = firstGridCol + (instrCount * Constants.POSITIONS_PER_INSTRUCTION);
603      element.style.gridColumn = firstGridCol + " / " + lastGridCol;
604      return element;
605    };
606
607    let blockIndex = 0;
608    for (let i = 0; i < this.view.sequenceView.numInstructions;) {
609      const instrCount = this.view.blocksData.blockInstructionCountMap.get(blockIndex);
610      headerEl.appendChild(elementForBlockIndex(blockIndex, i, instrCount));
611      ++blockIndex;
612      i += instrCount;
613    }
614    return headerEl;
615  }
616
617  private elementForInstructionHeader() {
618    const headerEl = createElement("div", "range-instruction-ids");
619    headerEl.style.gridTemplateColumns = this.gridTemplateColumns;
620
621    const elementForInstructionIndex = (index: number, isBlockBorder: boolean) => {
622      const classes = "range-instruction-id range-header-element "
623                    + (isBlockBorder ? "range-block-border" : "range-instr-border");
624      const element = createElement("div", classes, "" + index);
625      element.setAttribute("title", "" + index);
626      const firstGridCol = (index * Constants.POSITIONS_PER_INSTRUCTION) + 1;
627      element.style.gridColumn = firstGridCol + " / "
628                               + (firstGridCol + Constants.POSITIONS_PER_INSTRUCTION);
629      return element;
630    };
631
632    for (let i = 0; i < this.view.sequenceView.numInstructions; ++i) {
633      const blockBorder = this.view.blocksData.blockBorders.has(i);
634      headerEl.appendChild(elementForInstructionIndex(i, blockBorder));
635    }
636    return headerEl;
637  }
638
639  private elementForPositionHeader() {
640    const headerEl = createElement("div", "range-positions range-positions-header");
641    headerEl.style.gridTemplateColumns = this.gridTemplateColumns;
642
643    const elementForPositionIndex = (index: number, isBlockBorder: boolean) => {
644      const classes = "range-position range-header-element " +
645        (isBlockBorder ? "range-block-border"
646                       : this.view.blocksData.isInstructionBorder(index) ? "range-instr-border"
647                                                                         : "range-position-border");
648      const element = createElement("div", classes, "" + index);
649      element.setAttribute("title", "" + index);
650      return element;
651    };
652
653    for (let i = 0; i < this.view.numPositions; ++i) {
654      headerEl.appendChild(elementForPositionIndex(i, this.view.blocksData.isBlockBorder(i)));
655    }
656    return headerEl;
657  }
658
659  private elementForGrid() {
660    const gridEl = createElement("div", "range-grid");
661    gridEl.onscroll = () => {
662      this.view.scrollHandler.syncScroll(ToSync.TOP, this.view.divs.grid, this.view.divs.yAxis);
663      this.view.scrollHandler.syncScroll(ToSync.LEFT,
664                                         this.view.divs.grid, this.view.divs.wholeHeader);
665      this.view.scrollHandler.saveScroll();
666    };
667    return gridEl;
668  }
669}
670
671// Handles the work required when the phase is changed.
672// Between before and after register allocation for example.
673class PhaseChangeHandler {
674  view: RangeView;
675
676  constructor(view: RangeView) {
677    this.view = view;
678  }
679
680  // Called when the phase view is switched between before and after register allocation.
681  phaseChange() {
682    if (!this.view.gridAccessor.hasGrid()) {
683      // If this phase view has not been seen yet then the intervals need to be constructed.
684      this.addNewIntervals();
685    }
686    // Show all intervals pertaining to the current phase view.
687    this.view.intervalsAccessor.forEachInterval((phase, interval) => {
688      interval.classList.toggle("range-hidden", phase != this.view.sequenceView.currentPhaseIndex);
689    });
690  }
691
692  private addNewIntervals() {
693    // All Grids should point to the same HTMLElement for empty cells in the grid,
694    // so as to avoid duplication. The current Grid is used to retrieve these elements.
695    const currentGrid = this.view.gridAccessor.getAnyGrid();
696    const newGrid = new Grid();
697    this.view.gridAccessor.addGrid(newGrid);
698    const source = this.view.sequenceView.sequence.register_allocation;
699    let row = 0;
700    for (const [registerIndex, range] of source.liveRanges) {
701      this.addnewIntervalsInRange(currentGrid, newGrid, row, registerIndex,
702                                  new RangePair([range, undefined]));
703      ++row;
704    }
705    Helper.forEachFixedRange(this.view.sequenceView.sequence.register_allocation, row,
706                             (registerIndex, row, _, ranges) => {
707      this.addnewIntervalsInRange(currentGrid, newGrid, row, registerIndex, ranges);
708    });
709  }
710
711  private addnewIntervalsInRange(currentGrid: Grid, newGrid: Grid, row: number,
712                                 registerIndex: string, ranges: RangePair) {
713    const numReplacements = new Map<HTMLElement, number>();
714
715    const getElementForEmptyPosition = (position: number) => {
716      return currentGrid.getCell(row, position);
717    };
718
719    // Inserts new interval beside existing intervals.
720    const callbackForInterval = (position: number, interval: HTMLElement) => {
721      // Overlapping intervals are placed beside each other and the relevant ones displayed.
722      let currentInterval = currentGrid.getInterval(row, position);
723      // The number of intervals already inserted is tracked so that the inserted intervals
724      // are ordered correctly.
725      const intervalsAlreadyInserted = numReplacements.get(currentInterval);
726      numReplacements.set(currentInterval, intervalsAlreadyInserted ? intervalsAlreadyInserted + 1
727                                                                    : 1);
728      if (intervalsAlreadyInserted) {
729        for (let j = 0; j < intervalsAlreadyInserted; ++j) {
730          currentInterval = (currentInterval.nextElementSibling as HTMLElement);
731        }
732      }
733      interval.classList.add("range-hidden");
734      currentInterval.insertAdjacentElement('afterend', interval);
735    };
736
737    this.view.rowConstructor.construct(newGrid, row, registerIndex, ranges,
738                                          getElementForEmptyPosition, callbackForInterval);
739  }
740}
741
742enum ToSync { LEFT, TOP }
743
744// Handles saving and syncing the scroll positions of the grid.
745class ScrollHandler {
746  divs: Divs;
747  scrollTop: number;
748  scrollLeft: number;
749  scrollTopTimeout: NodeJS.Timeout;
750  scrollLeftTimeout: NodeJS.Timeout;
751  scrollTopFunc: (this: GlobalEventHandlers, ev: Event) => any;
752  scrollLeftFunc: (this: GlobalEventHandlers, ev: Event) => any;
753
754  constructor(divs: Divs) {
755    this.divs = divs;
756  }
757
758  // This function is used to hide the rows which are not currently in view and
759  // so reduce the performance cost of things like hit tests and scrolling.
760  syncHidden() {
761
762    const getOffset = (rowEl: HTMLElement, placeholderRowEl: HTMLElement, isHidden: boolean) => {
763      return isHidden ? placeholderRowEl.offsetTop : rowEl.offsetTop;
764    };
765
766    const toHide = new Array<[HTMLElement, HTMLElement]>();
767
768    const sampleCell = this.divs.registers.children[1] as HTMLElement;
769    const buffer = 2 * sampleCell.clientHeight;
770    const min = this.divs.grid.offsetTop + this.divs.grid.scrollTop - buffer;
771    const max = min + this.divs.grid.clientHeight + buffer;
772
773    // The rows are grouped by being contained within a group div. This is so as to allow
774    // groups of rows to easily be displayed and hidden with less of a performance cost.
775    // Each row in the mainGroup div is matched with an equivalent placeholder row in
776    // the placeholderGroup div that will be shown when mainGroup is hidden so as to maintain
777    // the dimensions and scroll positions of the grid.
778
779    const rangeGroups = this.divs.grid.children;
780    for (let i = 1; i < rangeGroups.length; i += 2) {
781      const mainGroup = rangeGroups[i] as HTMLElement;
782      const placeholderGroup = rangeGroups[i - 1] as HTMLElement;
783      const isHidden = mainGroup.classList.contains("range-hidden");
784      // The offsets are used to calculate whether the group is in view.
785      const offsetMin = getOffset(mainGroup.firstChild as HTMLElement,
786                                  placeholderGroup.firstChild as HTMLElement, isHidden);
787      const offsetMax = getOffset(mainGroup.lastChild as HTMLElement,
788                                  placeholderGroup.lastChild as HTMLElement, isHidden);
789      if (offsetMax > min && offsetMin < max) {
790        if (isHidden) {
791          // Show the rows, hide the placeholders.
792          mainGroup.classList.toggle("range-hidden", false);
793          placeholderGroup.classList.toggle("range-hidden", true);
794        }
795      } else if (!isHidden) {
796        // Only hide the rows once the new rows are shown so that scrollLeft is not lost.
797        toHide.push([mainGroup, placeholderGroup]);
798      }
799    }
800    for (const [mainGroup, placeholderGroup] of toHide) {
801      // Hide the rows, show the placeholders.
802      mainGroup.classList.toggle("range-hidden", true);
803      placeholderGroup.classList.toggle("range-hidden", false);
804    }
805  }
806
807  // This function is required to keep the axes labels in line with the grid
808  // content when scrolling.
809  syncScroll(toSync: ToSync, source: HTMLElement, target: HTMLElement) {
810    // Continually delay timeout until scrolling has stopped.
811    toSync == ToSync.TOP ? clearTimeout(this.scrollTopTimeout)
812                         : clearTimeout(this.scrollLeftTimeout);
813    if (target.onscroll) {
814      if (toSync == ToSync.TOP) this.scrollTopFunc = target.onscroll;
815      else this.scrollLeftFunc = target.onscroll;
816    }
817    // Clear onscroll to prevent the target syncing back with the source.
818    target.onscroll = null;
819
820    if (toSync == ToSync.TOP) target.scrollTop = source.scrollTop;
821    else target.scrollLeft = source.scrollLeft;
822
823    // Only show / hide the grid content once scrolling has stopped.
824    if (toSync == ToSync.TOP) {
825      this.scrollTopTimeout = setTimeout(() => {
826        target.onscroll = this.scrollTopFunc;
827        this.syncHidden();
828      }, 500);
829    } else {
830      this.scrollLeftTimeout = setTimeout(() => {
831        target.onscroll = this.scrollLeftFunc;
832        this.syncHidden();
833      }, 500);
834    }
835  }
836
837  saveScroll() {
838    this.scrollLeft = this.divs.grid.scrollLeft;
839    this.scrollTop = this.divs.grid.scrollTop;
840  }
841
842  restoreScroll() {
843    if (this.scrollLeft) {
844      this.divs.grid.scrollLeft = this.scrollLeft;
845      this.divs.grid.scrollTop = this.scrollTop;
846    }
847  }
848}
849
850// RangeView displays the live range data as passed in by SequenceView.
851// The data is displayed in a grid format, with the fixed and virtual registers
852// along one axis, and the LifeTimePositions along the other. Each LifeTimePosition
853// is part of an Instruction in SequenceView, which itself is part of an Instruction
854// Block. The live ranges are displayed as intervals, each belonging to a register,
855// and spanning across a certain range of LifeTimePositions.
856// When the phase being displayed changes between before register allocation and
857// after register allocation, only the intervals need to be changed.
858export class RangeView {
859  sequenceView: SequenceView;
860
861  initialized: boolean;
862  isShown: boolean;
863  numPositions: number;
864  cssVariables: CSSVariables;
865  divs: Divs;
866  rowConstructor: RowConstructor;
867  phaseChangeHandler: PhaseChangeHandler;
868  scrollHandler: ScrollHandler;
869  blocksData: BlocksData;
870  intervalsAccessor: IntervalElementsAccessor;
871  gridAccessor: GridAccessor;
872
873  constructor(sequence: SequenceView) {
874    this.initialized = false;
875    this.isShown = false;
876    this.sequenceView = sequence;
877  }
878
879  initializeContent(blocks: Array<any>) {
880    if (!this.initialized) {
881      this.gridAccessor = new GridAccessor(this.sequenceView);
882      this.intervalsAccessor = new IntervalElementsAccessor(this.sequenceView);
883      this.cssVariables = new CSSVariables();
884      this.blocksData = new BlocksData(blocks);
885      this.divs = new Divs();
886      this.scrollHandler = new ScrollHandler(this.divs);
887      this.numPositions = this.sequenceView.numInstructions * Constants.POSITIONS_PER_INSTRUCTION;
888      this.rowConstructor = new RowConstructor(this);
889      const constructor = new RangeViewConstructor(this);
890      constructor.construct();
891      this.phaseChangeHandler = new PhaseChangeHandler(this);
892      this.initialized = true;
893    } else {
894      // If the RangeView has already been initialized then the phase must have
895      // been changed.
896      this.phaseChangeHandler.phaseChange();
897    }
898  }
899
900  show() {
901    if (!this.isShown) {
902      this.isShown = true;
903      this.divs.container.appendChild(this.divs.content);
904      this.divs.resizerBar.style.visibility = "visible";
905      this.divs.container.style.visibility = "visible";
906      this.divs.snapper.style.visibility = "visible";
907      // Dispatch a resize event to ensure that the
908      // panel is shown.
909      window.dispatchEvent(new Event('resize'));
910
911      setTimeout(() => {
912        this.scrollHandler.restoreScroll();
913        this.scrollHandler.syncHidden();
914        this.divs.showOnLoad.style.visibility = "visible";
915      }, 100);
916    }
917  }
918
919  hide() {
920    if (this.initialized) {
921      this.isShown = false;
922      this.divs.container.removeChild(this.divs.content);
923      this.divs.resizerBar.style.visibility = "hidden";
924      this.divs.container.style.visibility = "hidden";
925      this.divs.snapper.style.visibility = "hidden";
926      this.divs.showOnLoad.style.visibility = "hidden";
927    } else {
928      window.document.getElementById('ranges').style.visibility = "hidden";
929    }
930    // Dispatch a resize event to ensure that the
931    // panel is hidden.
932    window.dispatchEvent(new Event('resize'));
933  }
934
935  onresize() {
936    if (this.isShown) this.scrollHandler.syncHidden();
937  }
938}
939