current commit
250502.3 · deps: update V8 to 13.6.233.8
// 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 {SelectRelatedEvent} from './events.mjs';
import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs';

DOM.defineCustomElement(
    'view/code-panel',
    (templateText) => class CodePanel extends CollapsableElement {
      _timeline;
      _selectedEntries;
      _entry;

      constructor() {
        super(templateText);
        this._propertiesNode = this.$('#properties');
        this._codeSelectNode = this.$('#codeSelect');
        this._disassemblyNode = this.$('#disassembly');
        this._feedbackVectorNode = this.$('#feedbackVector');
        this._selectionHandler = new SelectionHandler(this._disassemblyNode);

        this._codeSelectNode.onchange = this._handleSelectCode.bind(this);
        this.$('#selectedRelatedButton').onclick =
            this._handleSelectRelated.bind(this)
      }

      set timeline(timeline) {
        this._timeline = timeline;
        this.$('.panel').style.display =
            timeline.isEmpty() ? 'none' : 'inherit';
        this.requestUpdate();
      }

      set selectedEntries(entries) {
        this._selectedEntries = entries;
        this.entry = entries.first();
      }

      set entry(entry) {
        this._entry = entry;
        if (!entry) {
          this._propertiesNode.propertyDict = {};
        } else {
          this._propertiesNode.propertyDict = {
            '__this__': entry,
            functionName: entry.functionName,
            size: formatBytes(entry.size),
            creationTime: formatMicroSeconds(entry.time / 1000),
            sourcePosition: entry.sourcePosition,
            script: entry.script,
            type: entry.type,
            kind: entry.kindName,
            variants: entry.variants.length > 1 ?
                [undefined, ...entry.variants] :
                undefined,
          };
        }
        this.requestUpdate();
      }

      _update() {
        this._updateSelect();
        this._updateDisassembly();
        this._updateFeedbackVector();
      }

      _updateFeedbackVector() {
        if (!this._entry?.feedbackVector) {
          this._feedbackVectorNode.propertyDict = {};
        } else {
          const dict = this._entry.feedbackVector.toolTipDict;
          delete dict.title;
          delete dict.code;
          this._feedbackVectorNode.propertyDict = dict;
        }
      }

      _updateDisassembly() {
        this._disassemblyNode.innerText = '';
        if (!this._entry?.code) return;
        try {
          this._disassemblyNode.appendChild(
              new AssemblyFormatter(this._entry).fragment);
        } catch (e) {
          console.error(e);
          this._disassemblyNode.innerText = this._entry.code;
        }
      }

      _updateSelect() {
        const select = this._codeSelectNode;
        if (select.data === this._selectedEntries) return;
        select.data = this._selectedEntries;
        select.options.length = 0;
        const sorted =
            this._selectedEntries.slice().sort((a, b) => a.time - b.time);
        for (const code of this._selectedEntries) {
          const option = DOM.element('option');
          option.text = this._entrySummary(code);
          option.data = code;
          select.add(option);
        }
      }
      _entrySummary(code) {
        if (code.isBuiltinKind) {
          return `${code.functionName}(...) t=${
              formatMicroSeconds(code.time)} size=${formatBytes(code.size)}`;
        }
        return `${code.functionName}(...) t=${
            formatMicroSeconds(code.time)} size=${
            formatBytes(code.size)} script=${code.script?.toString()}`;
      }

      _handleSelectCode() {
        this.entry = this._codeSelectNode.selectedOptions[0].data;
      }

      _handleSelectRelated(e) {
        if (!this._entry) return;
        this.dispatchEvent(new SelectRelatedEvent(this._entry));
      }
    });

const kRegisters = ['rsp', 'rbp', 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi'];
// Make sure we dont match register on bytecode: Star1 or Star2
const kAvoidBytecodeOpsRegexpSource = '(.*?[^a-zA-Z])'
// Look for registers in strings like:  movl rbx,[rcx-0x30]
const kRegisterRegexpSource = `(?<register>${kRegisters.join('|')}|r[0-9]+)`
const kRegisterSplitRegexp =
    new RegExp(`${kAvoidBytecodeOpsRegexpSource}${kRegisterRegexpSource}`)
const kIsRegisterRegexp = new RegExp(`^${kRegisterRegexpSource}$`);

const kFullAddressRegexp = /(0x[0-9a-f]{8,})/;
const kRelativeAddressRegexp = /([+-]0x[0-9a-f]+)/;
const kAnyAddressRegexp = /(?<address>[+-]?0x[0-9a-f]+)/;

const kJmpRegexp = new RegExp(`jmp ${kRegisterRegexpSource}`);
const kMovRegexp =
    new RegExp(`mov. ${kRegisterRegexpSource},${kAnyAddressRegexp.source}`);

class AssemblyFormatter {
  constructor(codeLogEntry) {
    this._fragment = new DocumentFragment();
    this._entry = codeLogEntry;
    this._lines = new Map();
    this._previousLine = undefined;
    this._parseLines();
    this._format();
  }

  get fragment() {
    return this._fragment;
  }

  _format() {
    let block = DOM.div(['basicBlock', 'header']);
    this._lines.forEach(line => {
      if (!block || line.isBlockStart) {
        this._fragment.appendChild(block);
        block = DOM.div('basicBlock');
      }
      block.appendChild(line.format())
    });
    this._fragment.appendChild(block);
  }

  _parseLines() {
    this._entry.code.split('\n').forEach(each => this._parseLine(each));
    this._findBasicBlocks();
  }

  _parseLine(line) {
    const parts = line.split(' ');
    // Use unique placeholder for address:
    let lineAddress = -this._lines.size;
    for (let part of parts) {
      if (kFullAddressRegexp.test(part)) {
        lineAddress = parseInt(part);
        break;
      }
    }
    const newLine = new AssemblyLine(lineAddress, parts);
    // special hack for: mov reg 0x...; jmp reg;
    if (lineAddress <= 0 && this._previousLine) {
      const jmpMatch = line.match(kJmpRegexp);
      if (jmpMatch) {
        const register = jmpMatch.groups.register;
        const movMatch = this._previousLine.line.match(kMovRegexp);
        if (movMatch.groups.register === register) {
          newLine.outgoing.push(movMatch.groups.address);
        }
      }
    }
    this._lines.set(lineAddress, newLine);
    this._previousLine = newLine;
  }

  _findBasicBlocks() {
    const lines = Array.from(this._lines.values());
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      let forceBasicBlock = i == 0;
      if (i > 0 && i < lines.length - 1) {
        const prevHasAddress = lines[i - 1].address > 0;
        const currentHasAddress = lines[i].address > 0;
        const nextHasAddress = lines[i + 1].address > 0;
        if (prevHasAddress !== currentHasAddress &&
            currentHasAddress == nextHasAddress) {
          forceBasicBlock = true;
        }
      }
      if (forceBasicBlock) {
        // Add fake-incoming address to mark a block start.
        line.addIncoming(0);
      }
      line.outgoing.forEach(address => {
        const outgoing = this._lines.get(address);
        if (outgoing) outgoing.addIncoming(line.address);
      })
    }
  }
}

class AssemblyLine {
  constructor(address, parts) {
    this.address = address;
    this.outgoing = [];
    this.incoming = [];
    parts.forEach(part => {
      const fullMatch = part.match(kFullAddressRegexp);
      if (fullMatch) {
        let inlineAddress = parseInt(fullMatch[0]);
        if (inlineAddress != this.address) this.outgoing.push(inlineAddress);
        if (Number.isNaN(inlineAddress)) throw 'invalid address';
      } else if (kRelativeAddressRegexp.test(part)) {
        this.outgoing.push(this._toAbsoluteAddress(part));
      }
    });
    this.line = parts.join(' ');
  }

  get isBlockStart() {
    return this.incoming.length > 0;
  }

  addIncoming(address) {
    this.incoming.push(address);
  }

  format() {
    const content = DOM.span({textContent: this.line + '\n'});
    let formattedCode = content.innerHTML.split(kRegisterSplitRegexp)
                            .map(part => this._formatRegisterPart(part))
                            .join('');
    formattedCode =
        formattedCode.split(kAnyAddressRegexp)
            .map((part, index) => this._formatAddressPart(part, index))
            .join('');
    // Let's replace the base-address since it doesn't add any value.
    // TODO
    content.innerHTML = formattedCode;
    return content;
  }

  _formatRegisterPart(part) {
    if (!kIsRegisterRegexp.test(part)) return part;
    return `<span class="reg ${part}">${part}</span>`
  }

  _formatAddressPart(part, index) {
    if (kFullAddressRegexp.test(part)) {
      // The first or second address must be the line address
      if (index <= 1) {
        return `<span class="addr line" data-addr="${part}">${part}</span>`;
      }
      return `<span class=addr data-addr="${part}">${part}</span>`;
    } else if (kRelativeAddressRegexp.test(part)) {
      return `<span class=addr data-addr="0x${
          this._toAbsoluteAddress(part).toString(16)}">${part}</span>`;
    } else {
      return part;
    }
  }

  _toAbsoluteAddress(part) {
    return this.address + parseInt(part);
  }
}

class SelectionHandler {
  _currentRegisterHovered;
  _currentRegisterClicked;

  constructor(node) {
    this._node = node;
    this._node.onmousemove = this._handleMouseMove.bind(this);
    this._node.onclick = this._handleClick.bind(this);
  }

  $(query) {
    return this._node.querySelectorAll(query);
  }

  _handleClick(event) {
    const target = event.target;
    if (target.classList.contains('addr')) {
      return this._handleClickAddress(target);
    } else if (target.classList.contains('reg')) {
      this._handleClickRegister(target);
    } else {
      this._clearRegisterSelection();
    }
  }

  _handleClickAddress(target) {
    let targetAddress = target.getAttribute('data-addr') ?? target.innerText;
    // Clear any selection
    for (let addrNode of this.$('.addr.selected')) {
      addrNode.classList.remove('selected');
    }
    // Highlight all matching addresses
    let lineAddrNode;
    for (let addrNode of this.$(`.addr[data-addr="${targetAddress}"]`)) {
      addrNode.classList.add('selected');
      if (addrNode.classList.contains('line') && lineAddrNode == undefined) {
        lineAddrNode = addrNode;
      }
    }
    // Jump to potential target address.
    if (lineAddrNode) {
      lineAddrNode.scrollIntoView({behavior: 'smooth', block: 'nearest'});
    }
  }

  _handleClickRegister(target) {
    this._setRegisterSelection(target.innerText);
    this._currentRegisterClicked = this._currentRegisterHovered;
  }

  _handleMouseMove(event) {
    if (this._currentRegisterClicked) return;
    const target = event.target;
    if (!target.classList.contains('reg')) {
      this._clearRegisterSelection();
    } else {
      this._setRegisterSelection(target.innerText);
    }
  }

  _clearRegisterSelection() {
    if (!this._currentRegisterHovered) return;
    for (let node of this.$('.reg.selected')) {
      node.classList.remove('selected');
    }
    this._currentRegisterClicked = undefined;
    this._currentRegisterHovered = undefined;
  }

  _setRegisterSelection(register) {
    if (register == this._currentRegisterHovered) return;
    this._clearRegisterSelection();
    this._currentRegisterHovered = register;
    for (let node of this.$(`.reg.${register}`)) {
      node.classList.add('selected');
    }
  }
}