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 {SelectRelatedEvent} from './events.mjs';
6import {CollapsableElement, DOM, formatBytes, formatMicroSeconds} from './helper.mjs';
7
8DOM.defineCustomElement('view/code-panel',
9                        (templateText) =>
10                            class CodePanel extends CollapsableElement {
11  _timeline;
12  _selectedEntries;
13  _entry;
14
15  constructor() {
16    super(templateText);
17    this._propertiesNode = this.$('#properties');
18    this._codeSelectNode = this.$('#codeSelect');
19    this._disassemblyNode = this.$('#disassembly');
20    this._feedbackVectorNode = this.$('#feedbackVector');
21    this._selectionHandler = new SelectionHandler(this._disassemblyNode);
22
23    this._codeSelectNode.onchange = this._handleSelectCode.bind(this);
24    this.$('#selectedRelatedButton').onclick =
25        this._handleSelectRelated.bind(this)
26  }
27
28  set timeline(timeline) {
29    this._timeline = timeline;
30    this.$('.panel').style.display = timeline.isEmpty() ? 'none' : 'inherit';
31    this.requestUpdate();
32  }
33
34  set selectedEntries(entries) {
35    this._selectedEntries = entries;
36    this.entry = entries.first();
37  }
38
39  set entry(entry) {
40    this._entry = entry;
41    if (!entry) {
42      this._propertiesNode.propertyDict = {};
43    } else {
44      this._propertiesNode.propertyDict = {
45        '__this__': entry,
46        functionName: entry.functionName,
47        size: formatBytes(entry.size),
48        creationTime: formatMicroSeconds(entry.time / 1000),
49        sourcePosition: entry.sourcePosition,
50        script: entry.script,
51        type: entry.type,
52        kind: entry.kindName,
53        variants: entry.variants.length > 1 ? [undefined, ...entry.variants] :
54                                              undefined,
55      };
56    }
57    this.requestUpdate();
58  }
59
60  _update() {
61    this._updateSelect();
62    this._updateDisassembly();
63    this._updateFeedbackVector();
64  }
65
66  _updateFeedbackVector() {
67    if (!this._entry?.feedbackVector) {
68      this._feedbackVectorNode.propertyDict = {};
69    } else {
70      const dict = this._entry.feedbackVector.toolTipDict;
71      delete dict.title;
72      delete dict.code;
73      this._feedbackVectorNode.propertyDict = dict;
74    }
75  }
76
77  _updateDisassembly() {
78    this._disassemblyNode.innerText = '';
79    if (!this._entry?.code) return;
80    try {
81      this._disassemblyNode.appendChild(
82          new AssemblyFormatter(this._entry).fragment);
83    } catch (e) {
84      console.error(e);
85      this._disassemblyNode.innerText = this._entry.code;
86    }
87  }
88
89  _updateSelect() {
90    const select = this._codeSelectNode;
91    if (select.data === this._selectedEntries) return;
92    select.data = this._selectedEntries;
93    select.options.length = 0;
94    const sorted =
95        this._selectedEntries.slice().sort((a, b) => a.time - b.time);
96    for (const code of this._selectedEntries) {
97      const option = DOM.element('option');
98      option.text = this._entrySummary(code);
99      option.data = code;
100      select.add(option);
101    }
102  }
103  _entrySummary(code) {
104    if (code.isBuiltinKind) {
105      return `${code.functionName}(...) t=${
106          formatMicroSeconds(code.time)} size=${formatBytes(code.size)}`;
107    }
108    return `${code.functionName}(...) t=${formatMicroSeconds(code.time)} size=${
109        formatBytes(code.size)} script=${code.script?.toString()}`;
110  }
111
112  _handleSelectCode() {
113    this.entry = this._codeSelectNode.selectedOptions[0].data;
114  }
115
116  _handleSelectRelated(e) {
117    if (!this._entry) return;
118    this.dispatchEvent(new SelectRelatedEvent(this._entry));
119  }
120});
121
122const kRegisters = ['rsp', 'rbp', 'rax', 'rbx', 'rcx', 'rdx', 'rsi', 'rdi'];
123// Make sure we dont match register on bytecode: Star1 or Star2
124const kAvoidBytecodeOpsRegexpSource = '(.*?[^a-zA-Z])'
125// Look for registers in strings like:  movl rbx,[rcx-0x30]
126const kRegisterRegexpSource = `(?<register>${kRegisters.join('|')}|r[0-9]+)`
127const kRegisterSplitRegexp =
128    new RegExp(`${kAvoidBytecodeOpsRegexpSource}${kRegisterRegexpSource}`)
129const kIsRegisterRegexp = new RegExp(`^${kRegisterRegexpSource}$`);
130
131const kFullAddressRegexp = /(0x[0-9a-f]{8,})/;
132const kRelativeAddressRegexp = /([+-]0x[0-9a-f]+)/;
133const kAnyAddressRegexp = /(?<address>[+-]?0x[0-9a-f]+)/;
134
135const kJmpRegexp = new RegExp(`jmp ${kRegisterRegexpSource}`);
136const kMovRegexp =
137    new RegExp(`mov. ${kRegisterRegexpSource},${kAnyAddressRegexp.source}`);
138
139class AssemblyFormatter {
140  constructor(codeLogEntry) {
141    this._fragment = new DocumentFragment();
142    this._entry = codeLogEntry;
143    this._lines = new Map();
144    this._previousLine = undefined;
145    this._parseLines();
146    this._format();
147  }
148
149  get fragment() {
150    return this._fragment;
151  }
152
153  _format() {
154    let block = DOM.div(['basicBlock', 'header']);
155    this._lines.forEach(line => {
156      if (!block || line.isBlockStart) {
157        this._fragment.appendChild(block);
158        block = DOM.div('basicBlock');
159      }
160      block.appendChild(line.format())
161    });
162    this._fragment.appendChild(block);
163  }
164
165  _parseLines() {
166    this._entry.code.split('\n').forEach(each => this._parseLine(each));
167    this._findBasicBlocks();
168  }
169
170  _parseLine(line) {
171    const parts = line.split(' ');
172    // Use unique placeholder for address:
173    let lineAddress = -this._lines.size;
174    for (let part of parts) {
175      if (kFullAddressRegexp.test(part)) {
176        lineAddress = parseInt(part);
177        break;
178      }
179    }
180    const newLine = new AssemblyLine(lineAddress, parts);
181    // special hack for: mov reg 0x...; jmp reg;
182    if (lineAddress <= 0 && this._previousLine) {
183      const jmpMatch = line.match(kJmpRegexp);
184      if (jmpMatch) {
185        const register = jmpMatch.groups.register;
186        const movMatch = this._previousLine.line.match(kMovRegexp);
187        if (movMatch.groups.register === register) {
188          newLine.outgoing.push(movMatch.groups.address);
189        }
190      }
191    }
192    this._lines.set(lineAddress, newLine);
193    this._previousLine = newLine;
194  }
195
196  _findBasicBlocks() {
197    const lines = Array.from(this._lines.values());
198    for (let i = 0; i < lines.length; i++) {
199      const line = lines[i];
200      let forceBasicBlock = i == 0;
201      if (i > 0 && i < lines.length - 1) {
202        const prevHasAddress = lines[i - 1].address > 0;
203        const currentHasAddress = lines[i].address > 0;
204        const nextHasAddress = lines[i + 1].address > 0;
205        if (prevHasAddress !== currentHasAddress &&
206            currentHasAddress == nextHasAddress) {
207          forceBasicBlock = true;
208        }
209      }
210      if (forceBasicBlock) {
211        // Add fake-incoming address to mark a block start.
212        line.addIncoming(0);
213      }
214      line.outgoing.forEach(address => {
215        const outgoing = this._lines.get(address);
216        if (outgoing) outgoing.addIncoming(line.address);
217      })
218    }
219  }
220}
221
222class AssemblyLine {
223  constructor(address, parts) {
224    this.address = address;
225    this.outgoing = [];
226    this.incoming = [];
227    parts.forEach(part => {
228      const fullMatch = part.match(kFullAddressRegexp);
229      if (fullMatch) {
230        let inlineAddress = parseInt(fullMatch[0]);
231        if (inlineAddress != this.address) this.outgoing.push(inlineAddress);
232        if (Number.isNaN(inlineAddress)) throw 'invalid address';
233      } else if (kRelativeAddressRegexp.test(part)) {
234        this.outgoing.push(this._toAbsoluteAddress(part));
235      }
236    });
237    this.line = parts.join(' ');
238  }
239
240  get isBlockStart() {
241    return this.incoming.length > 0;
242  }
243
244  addIncoming(address) {
245    this.incoming.push(address);
246  }
247
248  format() {
249    const content = DOM.span({textContent: this.line + '\n'});
250    let formattedCode = content.innerHTML.split(kRegisterSplitRegexp)
251                            .map(part => this._formatRegisterPart(part))
252                            .join('');
253    formattedCode =
254        formattedCode.split(kAnyAddressRegexp)
255            .map((part, index) => this._formatAddressPart(part, index))
256            .join('');
257    // Let's replace the base-address since it doesn't add any value.
258    // TODO
259    content.innerHTML = formattedCode;
260    return content;
261  }
262
263  _formatRegisterPart(part) {
264    if (!kIsRegisterRegexp.test(part)) return part;
265    return `<span class="reg ${part}">${part}</span>`
266  }
267
268  _formatAddressPart(part, index) {
269    if (kFullAddressRegexp.test(part)) {
270      // The first or second address must be the line address
271      if (index <= 1) {
272        return `<span class="addr line" data-addr="${part}">${part}</span>`;
273      }
274      return `<span class=addr data-addr="${part}">${part}</span>`;
275    } else if (kRelativeAddressRegexp.test(part)) {
276      return `<span class=addr data-addr="0x${
277          this._toAbsoluteAddress(part).toString(16)}">${part}</span>`;
278    } else {
279      return part;
280    }
281  }
282
283  _toAbsoluteAddress(part) {
284    return this.address + parseInt(part);
285  }
286}
287
288class SelectionHandler {
289  _currentRegisterHovered;
290  _currentRegisterClicked;
291
292  constructor(node) {
293    this._node = node;
294    this._node.onmousemove = this._handleMouseMove.bind(this);
295    this._node.onclick = this._handleClick.bind(this);
296  }
297
298  $(query) {
299    return this._node.querySelectorAll(query);
300  }
301
302  _handleClick(event) {
303    const target = event.target;
304    if (target.classList.contains('addr')) {
305      return this._handleClickAddress(target);
306    } else if (target.classList.contains('reg')) {
307      this._handleClickRegister(target);
308    } else {
309      this._clearRegisterSelection();
310    }
311  }
312
313  _handleClickAddress(target) {
314    let targetAddress = target.getAttribute('data-addr') ?? target.innerText;
315    // Clear any selection
316    for (let addrNode of this.$('.addr.selected')) {
317      addrNode.classList.remove('selected');
318    }
319    // Highlight all matching addresses
320    let lineAddrNode;
321    for (let addrNode of this.$(`.addr[data-addr="${targetAddress}"]`)) {
322      addrNode.classList.add('selected');
323      if (addrNode.classList.contains('line') && lineAddrNode == undefined) {
324        lineAddrNode = addrNode;
325      }
326    }
327    // Jump to potential target address.
328    if (lineAddrNode) {
329      lineAddrNode.scrollIntoView({behavior: 'smooth', block: 'nearest'});
330    }
331  }
332
333  _handleClickRegister(target) {
334    this._setRegisterSelection(target.innerText);
335    this._currentRegisterClicked = this._currentRegisterHovered;
336  }
337
338  _handleMouseMove(event) {
339    if (this._currentRegisterClicked) return;
340    const target = event.target;
341    if (!target.classList.contains('reg')) {
342      this._clearRegisterSelection();
343    } else {
344      this._setRegisterSelection(target.innerText);
345    }
346  }
347
348  _clearRegisterSelection() {
349    if (!this._currentRegisterHovered) return;
350    for (let node of this.$('.reg.selected')) {
351      node.classList.remove('selected');
352    }
353    this._currentRegisterClicked = undefined;
354    this._currentRegisterHovered = undefined;
355  }
356
357  _setRegisterSelection(register) {
358    if (register == this._currentRegisterHovered) return;
359    this._clearRegisterSelection();
360    this._currentRegisterHovered = register;
361    for (let node of this.$(`.reg.${register}`)) {
362      node.classList.add('selected');
363    }
364  }
365}
366