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