/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
deps/v8/tools/system-analyzer/view/script-panel.mjs
768 строк
26 KB
Michaël Zasso
deps: update V8 to 14.6.202.33
24 апр 2026, 19:01
Не верифицирован
24 апр 2026, 19:01
f1e0b83
Код
Авторство
О чём код?
// 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 {App} from '../index.mjs' import {SelectionEvent, SelectRelatedEvent, ToolTipEvent} from './events.mjs'; import {arrayEquals, CollapsableElement, CSSColor, defer, delay, DOM, formatBytes, gradientStopsFromGroups, groupBy, LazyTable} from './helper.mjs'; // A source mapping proxy for source maps that don't have CORS headers. // TODO(leszeks): Make this configurable. const sourceMapFetchPrefix = 'http://localhost:8080/'; DOM.defineCustomElement( 'view/script-panel', (templateText) => class SourcePanel extends CollapsableElement { _selectedSourcePositions = []; _scripts = []; _script; _lineHeight = 0; _charWidth = 0; _charsPerLine = 0; _lineOffsets = []; _cumulativeHeights = []; _renderedStartLine = -1; _renderedEndLine = -1; _renderedStartChunk = 0; _renderedEndChunk = 0; _observer; showToolTipEntriesHandler = this.handleShowToolTipEntries.bind(this); constructor() { super(templateText); this.scriptDropdown.addEventListener( 'change', e => this._handleSelectScript(e)); this.$('#selectedRelatedButton').onclick = this._handleSelectRelated.bind(this); this.script.addEventListener('scroll', this._handleScroll.bind(this)); this._observer = new ResizeObserver(this._handleResize.bind(this)); this._observer.observe(this.script); } get script() { return this.$('#script'); } get scriptNode() { return this.$('.scriptNode'); } set script(script) { if (this._script === script) return; this._script = script; this._lineOffsets = []; this._cumulativeHeights = []; this._renderedStartLine = -1; this._renderedEndLine = -1; this._renderedStartChunk = 0; this._renderedEndChunk = 0; if (script) { script.ensureSourceMapCalculated(sourceMapFetchPrefix); this._script.sourcePositions.sort((a, b) => { if (a.line === b.line) return a.column - b.column; return a.line - b.line; }); this._calculateLineOffsets(script.source); } this._selectedSourcePositions = this._selectedSourcePositions.filter( each => each.script === script); this.requestUpdate(); } set focusedSourcePositions(sourcePositions) { this.selectedSourcePositions = sourcePositions; } set selectedSourcePositions(sourcePositions) { if (arrayEquals(this._selectedSourcePositions, sourcePositions)) { this._scrollToFirstSourcePosition(); } else { this._selectedSourcePositions = sourcePositions; // TODO: highlight multiple scripts this.script = sourcePositions[0]?.script; this._scrollToFirstSourcePosition(); this._renderVisibleLines(); } } set scripts(scripts) { this._scripts = scripts; this._initializeScriptDropdown(); } get scriptDropdown() { return this.$('#script-dropdown'); } _update() { this._renderSourcePanel(); this._updateScriptDropdownSelection(); } _initializeScriptDropdown() { this._scripts.sort((a, b) => a.name?.localeCompare(b.name) ?? 0); let select = this.scriptDropdown; select.options.length = 0; for (const script of this._scripts) { const option = document.createElement('option'); const size = formatBytes(script.source.length); option.text = `${script.name} (id=${script.id} size=${size})`; option.script = script; select.add(option); } } _updateScriptDropdownSelection() { this.scriptDropdown.selectedIndex = this._script ? this._scripts.indexOf(this._script) : -1; } _calculateLineOffsets(source) { this._lineOffsets = [0]; for (let i = 0; i < source.length; i++) { if (source[i] === '\n') this._lineOffsets.push(i + 1); } } _measureFontMetrics() { const lineSpan = DOM.span('line'); lineSpan.textContent = 'M'; this.scriptNode.appendChild(lineSpan); this._lineHeight = lineSpan.offsetHeight; const computedStyle = getComputedStyle(lineSpan); const paddingLeft = parseFloat(computedStyle.paddingLeft); this.scriptNode.removeChild(lineSpan); const charSpan = DOM.span(); charSpan.textContent = 'M'; this.scriptNode.appendChild(charSpan); this._charWidth = charSpan.getBoundingClientRect().width; this.scriptNode.removeChild(charSpan); if (this._lineHeight === 0) this._lineHeight = 18; if (this._charWidth === 0) this._charWidth = 7; const clientWidth = this.script.clientWidth; const paddingWidth = paddingLeft || (3.5 * this._charWidth); this._charsPerLine = Math.max( 1, Math.floor((clientWidth - paddingWidth) / this._charWidth) - 1); } _calculateLayout() { if (!this._script || this._charsPerLine === 0) return; this._cumulativeHeights = [0]; let currentHeight = 0; const lineOffsets = this._lineOffsets; for (let i = 1; i < lineOffsets.length; i++) { const start = lineOffsets[i - 1]; const end = lineOffsets[i] - 1; // exclude newline const len = Math.max(0, end - start); const visualLines = Math.ceil(len / this._charsPerLine) || 1; currentHeight += visualLines * this._lineHeight; this._cumulativeHeights.push(currentHeight); } // Handle last line if no newline at EOF if (lineOffsets[lineOffsets.length - 1] <= this._script.source.length) { const start = lineOffsets[lineOffsets.length - 1]; const end = this._script.source.length; if (end > start) { const len = end - start; const visualLines = Math.ceil(len / this._charsPerLine) || 1; currentHeight += visualLines * this._lineHeight; this._cumulativeHeights.push(currentHeight); } } } async _renderSourcePanel() { if (!this._script) { this.scriptNode.innerHTML = ''; this.scriptNode.style.paddingTop = '0px'; this.scriptNode.style.paddingBottom = '0px'; return; } await delay(1); if (this._script !== this._script) return; this._measureFontMetrics(); this._calculateLayout(); this._renderVisibleLines(); } _handleScroll() { this._renderVisibleLines(); } _handleResize() { this._renderedStartLine = -1; this._renderedEndLine = -1; this._renderedStartChunk = 0; this._renderedEndChunk = 0; this._measureFontMetrics(); this._calculateLayout(); this._renderVisibleLines(); } _findLineForOffset(offset) { let left = 0; let right = this._cumulativeHeights.length - 1; while (left <= right) { const mid = Math.floor((left + right) / 2); if (this._cumulativeHeights[mid] <= offset) { left = mid + 1; } else { right = mid - 1; } } return left; } _renderVisibleLines() { if (!this._script || this._lineHeight === 0 || this._cumulativeHeights.length === 0) return; const container = this.script; const scrollTop = container.scrollTop; const viewportHeight = container.clientHeight; const scrollBottom = scrollTop + viewportHeight; // Find source line corresponding to scrollTop let startLine = this._findLineForOffset(scrollTop); const startLineHeightBefore = (startLine > 1) ? this._cumulativeHeights[startLine - 2] : 0; const startOffsetInLine = Math.max(0, scrollTop - startLineHeightBefore); let startChunk = Math.floor(startOffsetInLine / this._lineHeight); // Find source line corresponding to scrollTop + viewportHeight let endLine = this._findLineForOffset(scrollBottom); const endLineHeightBefore = (endLine > 1) ? this._cumulativeHeights[endLine - 2] : 0; const endOffsetInLine = Math.max(0, scrollBottom - endLineHeightBefore); let endChunk = Math.ceil(endOffsetInLine / this._lineHeight); const BUFFER_LINES = 5; // Expand buffer by lines if possible, falling back to chunk buffering // for large lines. if (startChunk > 10) { startChunk -= 10; } else { const originalStartLine = startLine; startLine = Math.max(1, startLine - BUFFER_LINES); if (startLine < originalStartLine) startChunk = 0; } if (endLine < this._lineOffsets.length) { const originalEndLine = endLine; endLine = Math.min(this._lineOffsets.length, endLine + BUFFER_LINES); if (endLine > originalEndLine) endChunk = Infinity; else endChunk += 10; } else { endChunk += 10; } const builder = new LineBuilder(this, this._script, this._charsPerLine); if (this._renderedStartLine === -1 || startLine > this._renderedEndLine || endLine < this._renderedStartLine || (startLine === this._renderedEndLine && startChunk >= this._renderedEndChunk) || // Gap between rendered and new (endLine === this._renderedStartLine && endChunk <= this._renderedStartChunk)) { const fragment = builder.createScriptFragment( startLine, startChunk, endLine, endChunk, this._lineOffsets); this.scriptNode.innerHTML = ''; this.scriptNode.appendChild(fragment); } else { // Remove from top while (this.scriptNode.firstElementChild) { const child = this.scriptNode.firstElementChild; const childLine = parseInt(child.dataset.line, 10); const childChunk = parseInt(child.dataset.chunk, 10); if (childLine < startLine || (childLine === startLine && childChunk < startChunk)) { this.scriptNode.removeChild(child); } else { break; } } // Remove from bottom while (this.scriptNode.lastElementChild) { const child = this.scriptNode.lastElementChild; const childLine = parseInt(child.dataset.line, 10); const childChunk = parseInt(child.dataset.chunk, 10); if (childLine > endLine || (childLine === endLine && childChunk >= endChunk)) { this.scriptNode.removeChild(child); } else { break; } } // Prepend new top lines/chunks if (startLine < this._renderedStartLine || (startLine === this._renderedStartLine && startChunk < this._renderedStartChunk)) { const gapEndLine = this._renderedStartLine; const gapEndChunk = this._renderedStartChunk; const fragment = builder.createScriptFragment( startLine, startChunk, gapEndLine, gapEndChunk, this._lineOffsets); this.scriptNode.insertBefore( fragment, this.scriptNode.firstElementChild); } // Append new bottom lines/chunks if (endLine > this._renderedEndLine || (endLine === this._renderedEndLine && endChunk > this._renderedEndChunk)) { const gapStartLine = this._renderedEndLine; const gapStartChunk = this._renderedEndChunk; const fragment = builder.createScriptFragment( gapStartLine, gapStartChunk, endLine, endChunk, this._lineOffsets); this.scriptNode.appendChild(fragment); } } this._renderedStartLine = startLine; this._renderedEndLine = endLine; this._renderedStartChunk = startChunk; this._renderedEndChunk = endChunk; // Padding Top calculation const heightBeforeStartLine = (startLine > 1) ? this._cumulativeHeights[startLine - 2] : 0; const paddingTop = heightBeforeStartLine + startChunk * this._lineHeight; // Padding Bottom calculation const totalHeight = this._cumulativeHeights[this._cumulativeHeights.length - 1]; let renderedBottom; if (endChunk === Infinity) { renderedBottom = this._cumulativeHeights[endLine - 1]; } else { const heightBeforeEndLine = (endLine > 1) ? this._cumulativeHeights[endLine - 2] : 0; renderedBottom = heightBeforeEndLine + endChunk * this._lineHeight; } const paddingBottom = Math.max(0, totalHeight - renderedBottom); this.scriptNode.style.paddingTop = `${paddingTop}px`; this.scriptNode.style.paddingBottom = `${paddingBottom}px`; this.scriptNode.style.counterReset = `sourceLineCounter ${startChunk > 0 ? startLine : startLine - 1}`; this._updateHighlights(); } _updateHighlights() { const marked = this.scriptNode.querySelectorAll('.marked'); for (const node of marked) { node.classList.remove('marked'); } const marks = this.scriptNode.querySelectorAll('mark'); for (const mark of marks) { if (this._selectedSourcePositions.includes(mark.sourcePosition)) { mark.classList.add('marked'); } } } _scrollToFirstSourcePosition() { const sourcePosition = this._selectedSourcePositions.find( each => each.script === this._script); if (!sourcePosition) return; // Approximate scroll position based on line number. // We could be more precise with _cumulativeHeights if we knew which // line index corresponds to sourcePosition.line sourcePosition.line is // 1-based source line index. const lineIndex = sourcePosition.line; if (lineIndex > 0 && lineIndex <= this._cumulativeHeights.length) { const targetOffset = this._cumulativeHeights[lineIndex - 1]; const targetScrollTop = targetOffset - (this.script.clientHeight / 2); this.script.scrollTo({top: targetScrollTop, behavior: 'smooth'}); } } _handleSelectScript(e) { const option = this.scriptDropdown.options[this.scriptDropdown.selectedIndex]; this.script = option.script; } _handleSelectRelated(e) { if (!this._script) return; this.dispatchEvent(new SelectRelatedEvent(this._script)); } setSelectedSourcePositionInternal(sourcePosition) { this._selectedSourcePositions = [sourcePosition]; } handleSourcePositionClick(e) { const sourcePosition = e.target.sourcePosition; this.setSelectedSourcePositionInternal(sourcePosition); this.dispatchEvent(new SelectRelatedEvent(sourcePosition)); this._renderVisibleLines(); } handleSourcePositionMouseOver(e) { const sourcePosition = e.target.sourcePosition; const entries = sourcePosition.entries; const toolTipContent = DOM.div(); toolTipContent.appendChild( new ToolTipTableBuilder(this, entries).tableNode); let sourceMapContent; switch (this._script.sourceMapState) { case 'loaded': { const originalPosition = sourcePosition.originalPosition; if (originalPosition.source === null) { sourceMapContent = DOM.element( 'i', {textContent: 'no source mapping for location'}); } else { sourceMapContent = DOM.element('a', { href: `${originalPosition.source}`, target: '_blank', textContent: `${originalPosition.source}:${ originalPosition.line}:${originalPosition.column}` }); } break; } case 'loading': sourceMapContent = DOM.element('i', {textContent: 'source map still loading...'}); break; case 'failed': sourceMapContent = DOM.element('i', {textContent: 'source map failed to load'}); break; case 'none': sourceMapContent = DOM.element('i', {textContent: 'no source map'}); break; default: break; } toolTipContent.appendChild(sourceMapContent); this.dispatchEvent( new ToolTipEvent(toolTipContent, e.target, e.ctrlKey)); } handleShowToolTipEntries(event) { let entries = event.currentTarget.data; const sourcePosition = entries[0].sourcePosition; // Add a source position entry so the current position stays focused. this.setSelectedSourcePositionInternal(sourcePosition); entries = entries.concat(this._selectedSourcePositions); this.dispatchEvent(new SelectionEvent(entries)); } }); class ToolTipTableBuilder { constructor(scriptPanel, entries) { this._scriptPanel = scriptPanel; this.tableNode = DOM.table(); const tr = DOM.tr(); tr.appendChild(DOM.td('Type')); tr.appendChild(DOM.td('Subtype')); tr.appendChild(DOM.td('Count')); this.tableNode.appendChild(document.createElement('thead')).appendChild(tr); groupBy(entries, each => each.constructor, true).forEach(group => { this.addRow(group.key.name, 'all', entries, false) groupBy(group.entries, each => each.type, true).forEach(group => { this.addRow('', group.key, group.entries, false) }) }) } addRow(name, subtypeName, entries) { const tr = DOM.tr(); tr.appendChild(DOM.td(name)); tr.appendChild(DOM.td(subtypeName)); tr.appendChild(DOM.td(entries.length)); const button = DOM.button('🔎', this._scriptPanel.showToolTipEntriesHandler); button.title = `Show all ${entries.length} ${name || subtypeName} entries.` button.data = entries; tr.appendChild(DOM.td(button)); this.tableNode.appendChild(tr); } } class SourcePositionIterator { _entries; _index = 0; constructor(sourcePositions) { this._entries = sourcePositions; } seekToLine(lineIndex) { let left = 0; let right = this._entries.length - 1; let result = -1; while (left <= right) { const mid = Math.floor((left + right) / 2); if (this._entries[mid].line >= lineIndex) { result = mid; right = mid - 1; } else { left = mid + 1; } } if (result !== -1) { this._index = result; // Backup to find first entry for this line while (this._index > 0 && this._entries[this._index - 1].line === lineIndex) { this._index--; } } else { this._index = this._entries.length; } } * forLine(lineIndex) { while (!this._done() && this._current().line === lineIndex) { yield this._current(); this._next(); } } _current() { return this._entries[this._index]; } _done() { return this._index >= this._entries.length; } _next() { this._index++; } } class LineBuilder { static _colorMap = (function() { const map = new Map(); let i = 0; for (let type of App.allEventTypes) { map.set(type, CSSColor.at(i++)); } return map; })(); static get colorMap() { return this._colorMap; } _script; _clickHandler; _mouseoverHandler; _sourcePositionToMarkers = new Map(); _charsPerLine; constructor(panel, script, charsPerLine) { this._script = script; this._clickHandler = panel.handleSourcePositionClick.bind(panel); this._mouseoverHandler = panel.handleSourcePositionMouseOver.bind(panel); this._charsPerLine = charsPerLine; } get sourcePositionToMarkers() { return this._sourcePositionToMarkers; } createScriptFragment(startLine, startChunk, endLine, endChunk, lineOffsets) { const fragment = new DocumentFragment(); const sourcePositionsIterator = new SourcePositionIterator(this._script.sourcePositions); sourcePositionsIterator.seekToLine(startLine); const source = this._script.source; // Use inclusive range for lines, as endLine might be partially rendered. // If endChunk is 0, endLine is effectively not rendered. let effectiveEndLine = endLine; if (endChunk === 0) effectiveEndLine--; for (let lineIndex = startLine; lineIndex <= effectiveEndLine; lineIndex++) { const start = lineOffsets[lineIndex - 1]; const end = (lineIndex < lineOffsets.length) ? lineOffsets[lineIndex] - 1 : source.length; let lineText = ''; if (start < source.length) { lineText = source.substring(start, end); } const currentStartChunk = (lineIndex === startLine) ? startChunk : 0; const currentEndChunk = (lineIndex === endLine) ? endChunk : Infinity; this._appendVisualLines( fragment, sourcePositionsIterator, lineIndex, lineText, currentStartChunk, currentEndChunk); } return fragment; } _appendVisualLines( fragment, sourcePositionsIterator, lineIndex, line, startChunk = 0, endChunk = Infinity) { const markers = []; for (const sourcePosition of sourcePositionsIterator.forLine(lineIndex)) { markers.push(sourcePosition); } let currentMarkerIndex = 0; let currentColumn = 0; let remainingText = line; let isFirstVisualLine = true; let chunkIndex = 0; // If empty line if (remainingText.length === 0) { // If startChunk > 0, we skip the only chunk, so render nothing. if (startChunk > 0) return; if (chunkIndex >= endChunk) return; const lineNode = DOM.span( isFirstVisualLine ? 'line line-start' : 'line line-continuation'); lineNode.dataset.line = lineIndex; lineNode.dataset.chunk = chunkIndex; lineNode.appendChild(document.createTextNode('\n')); fragment.appendChild(lineNode); return; } // Fast forward if startChunk > 0 if (startChunk > 0) { const skipChars = startChunk * this._charsPerLine; if (skipChars < remainingText.length) { // Advance text remainingText = remainingText.substring(skipChars); currentColumn += skipChars; chunkIndex = startChunk; isFirstVisualLine = (startChunk === 0); // Always false if startChunk > 0 // Advance markers // We need to skip markers that appear before currentColumn. // Markers are sorted by column. while (currentMarkerIndex < markers.length) { const marker = markers[currentMarkerIndex]; // Marker column is 1-based. if (marker.column - 1 < currentColumn) { currentMarkerIndex++; } else { break; } } } else { // Skipped everything return; } } while (remainingText.length > 0 && chunkIndex < endChunk) { const chunkLength = Math.min(this._charsPerLine, remainingText.length); const chunkEndColumn = currentColumn + chunkLength; const lineNode = DOM.span( isFirstVisualLine ? 'line line-start' : 'line line-continuation'); lineNode.dataset.line = lineIndex; lineNode.dataset.chunk = chunkIndex; isFirstVisualLine = false; let chunkText = remainingText.substring(0, chunkLength); let chunkColumn = 0; // Process markers within this chunk while (currentMarkerIndex < markers.length) { const marker = markers[currentMarkerIndex]; const markerColumn = marker.column - 1; // 0-based if (markerColumn >= chunkEndColumn) break; // Marker is in next chunk // Marker starts in this chunk // Append text before marker const textBeforeMarkerLen = markerColumn - (currentColumn + chunkColumn); if (textBeforeMarkerLen > 0) { lineNode.appendChild(document.createTextNode(chunkText.substring( chunkColumn, chunkColumn + textBeforeMarkerLen))); chunkColumn += textBeforeMarkerLen; } // Append marker (assumes 1 char width as per original code) lineNode.appendChild( this._createMarkerNode(chunkText[chunkColumn], marker)); chunkColumn++; currentMarkerIndex++; } // Append remaining text in this chunk if (chunkColumn < chunkText.length) { lineNode.appendChild( document.createTextNode(chunkText.substring(chunkColumn))); } fragment.appendChild(lineNode); remainingText = remainingText.substring(chunkLength); currentColumn += chunkLength; chunkIndex++; // Break if we processed all text and markers (safety check) if (remainingText.length === 0 && currentMarkerIndex >= markers.length) break; } } _createMarkerNode(text, sourcePosition) { const marker = document.createElement('mark'); this._sourcePositionToMarkers.set(sourcePosition, marker); marker.textContent = text; marker.sourcePosition = sourcePosition; marker.onclick = this._clickHandler; marker.onmouseover = this._mouseoverHandler; const entries = sourcePosition.entries; const groups = groupBy(entries, entry => entry.constructor); if (groups.length > 1) { const stops = gradientStopsFromGroups( entries.length, '%', groups, type => LineBuilder.colorMap.get(type)); marker.style.backgroundImage = `linear-gradient(0deg,${stops.join(',')})` } else { marker.style.backgroundColor = LineBuilder.colorMap.get(groups[0].key) } return marker; } }