perf: avoid rescanning emitted source in analysis guards (#18920)

This commit is contained in:
Neil
2026-09-05 20:03:14 -07:00
committed by GitHub
parent 4e8e14424d
commit 388e9fb776
3 changed files with 96 additions and 3 deletions
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { stripTypeScriptTypes } from 'node:module'
import { performance } from 'node:perf_hooks'
import { blankStringContents as after } from '../../src/shared/source-scan/source-tree-scan.ts'
const ref = process.argv[2]
if (!ref) {
throw new Error('Usage: node config/scripts/source-string-blanking-benchmark.mjs <baseline-ref>')
}
const source = execFileSync('git', ['show', `${ref}:src/shared/source-scan/source-tree-scan.ts`], {
encoding: 'utf8'
})
const { blankStringContents: before } = await import(
`data:text/javascript;base64,${Buffer.from(stripTypeScriptTypes(source)).toString('base64')}`
)
const tokens = [
'a',
'/',
'*',
' ',
'\n',
'\r',
'\t',
'\u00a0',
'\u2028',
'"',
"'",
'`',
'${',
'}',
'{',
'\\',
'(',
')',
'[',
']',
'=',
'+',
'-',
';'
]
let seed = 173
for (let sample = 0; sample < 3000; sample++) {
let input = ''
for (let token = 0; token < 40; token++) {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
input += tokens[seed % tokens.length]
}
assert.equal(after(input), before(input), JSON.stringify(input))
assert.equal(after(input, true), before(input, true), JSON.stringify(input))
}
function measure(fn, input) {
const samples = []
for (let run = 0; run < 3; run++) {
const start = performance.now()
fn(input)
samples.push(performance.now() - start)
}
return samples.sort((a, b) => a - b)[1]
}
const results = []
for (const lines of [100, 1000, 5000, 10000]) {
const input = 'const x = value / 2;\n'.repeat(lines)
assert.equal(after(input), before(input))
results.push({
lines,
bytes: Buffer.byteLength(input),
beforeMs: measure(before, input),
afterMs: measure(after, input)
})
}
console.log(
JSON.stringify(
{ node: process.version, platform: process.platform, differentialCases: 3000, results },
null,
2
)
)
@@ -47,6 +47,13 @@ describe('stripComments', () => {
})
describe('blankStringContents', () => {
it('does not rescan the accumulated source for each division operator', () => {
const source = 'const x = value / 2;\n'.repeat(10000)
const started = performance.now()
expect(blankStringContents(source)).toBe(source)
expect(performance.now() - started).toBeLessThan(200)
})
it('neutralises parentheses inside a string so a call is matched whole', () => {
// A shell script embedded as a string closed the call early, so the options
// object fell outside the match and its flags read as absent.
+10 -3
View File
@@ -179,8 +179,7 @@ export function blankStringContentsDesynced(source: string): boolean {
* each also has a prefix reading: `!` (non-null assertion vs `!/re/.test(x)`),
* `+` `-` `*` `%` `^` `~` (postfix `--`/`++`), and `>` `}` (JSX close).
*/
function startsRegexLiteral(emitted: string): boolean {
const prev = emitted.replace(/\s+$/, '').at(-1)
function startsRegexLiteral(prev: string | undefined): boolean {
return prev === undefined || '(,=:[&|?;'.includes(prev)
}
@@ -209,6 +208,7 @@ function findRegexLiteralEnd(source: string, start: number): number {
export function blankStringContents(source: string, reportDesync = false): string {
let out = ''
let lastSignificantChar: string | undefined
let index = 0
let quote: string | null = null
// Brace depth per interpolation, so a `}` inside `${ { a: 1 } }` does not
@@ -220,6 +220,7 @@ export function blankStringContents(source: string, reportDesync = false): strin
templates.push(0)
quote = null
out += '${'
lastSignificantChar = '{'
index += 2
continue
}
@@ -232,6 +233,7 @@ export function blankStringContents(source: string, reportDesync = false): strin
templates.pop()
quote = '`'
out += char
lastSignificantChar = char
index += 1
continue
}
@@ -256,6 +258,7 @@ export function blankStringContents(source: string, reportDesync = false): strin
if (char === quote) {
quote = null
out += char
lastSignificantChar = char
} else {
out += char === '\n' ? char : ' '
}
@@ -270,10 +273,11 @@ export function blankStringContents(source: string, reportDesync = false): strin
// comments first, but this runs standalone too, and at index 0 a file
// starting with a banner comment read as one giant regex.
const next = source[index + 1]
if (char === '/' && next !== '/' && next !== '*' && startsRegexLiteral(out)) {
if (char === '/' && next !== '/' && next !== '*' && startsRegexLiteral(lastSignificantChar)) {
const end = findRegexLiteralEnd(source, index)
if (end !== -1) {
out += `/${' '.repeat(end - index - 1)}`
lastSignificantChar = '/'
index = end
continue
}
@@ -282,6 +286,9 @@ export function blankStringContents(source: string, reportDesync = false): strin
quote = char
}
out += char
if (/\S/.test(char)) {
lastSignificantChar = char
}
index += 1
}
if (reportDesync) {