mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
perf(codegen): skip escaping harmless < tokens (#25564)
## Summary - Detect actual case-insensitive `</script` sequences in the initial string-escape scan. - Keep strings containing only harmless `<` on the bulk-write fast path. - Add regression coverage for script closers, controls, paired and lone surrogates, and template quasis. ## Benchmark methodology No permanent microbenchmark is added. Measurements used release bundles built with `pnpm --filter oxc-codegen run build` on Node v26.5.0, a one-off focused harness over representative string-literal ASTs, and the existing `packages/codegen/bench.bench.ts` parsed-AST fixtures. The focused harness used ten warmup batches and 20 measured batches per case; Vitest fixtures used 1s warmup and 3s measurement windows (50/20 iteration minima). The removed one-off harness is retained for reference at [bench.string.bench.ts](https://github.com/oxc-project/oxc/blob/49e7a9d43bdc55a8f01075363bb8701ca5a9380c/packages/codegen/bench.string.bench.ts). Focused release results (ms/print, mean ± standard deviation): | Case | Before | After | Change | | --- | ---: | ---: | ---: | | Short ordinary string | 0.00003 ± 0.00000 | 0.00003 ± 0.00000 | no material change | | Long string without `<` | 0.02048 ± 0.00114 | 0.01906 ± 0.00038 | within noise / slightly faster | | Dense harmless HTML/SVG-like `<` (15,360 chars) | 0.05625 ± 0.00152 | 0.01283 ± 0.00036 | 77% faster | | Mixed text with harmless `<` | 0.04766 ± 0.00210 | 0.02371 ± 0.00072 | 50% faster | | Frequent mixed-case `</script` closers | 0.04350 ± 0.00074 | 0.04366 ± 0.00079 | within noise | Existing parsed-AST benchmark fixtures (Vitest RME 0.15–0.79%): Radix 0.0028 -> 0.0027 ms, React development 0.0981 -> 0.0976 ms. App.tsx varied from 0.6261 to 0.6657 ms after versus 0.6415 ms before, so this PR makes no throughput claim for that fixture and shows no consistent regression. A release CPU profile of the dense harmless case reduced `printEscapedStringContents` from 1,099 of 1,459 samples, plus 124 `^</script` regex samples, to zero; the after profile spends 321 of 383 samples in the single initial scan.
This commit is contained in:
1 parent
ffa31535db
commit
5444cbfaf8
2 files changed
+33
-3
No files matched your search
@@ -7,20 +7,20 @@ import type { State } from "../state.ts";
|
||||
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
||||
|
||||
/**
|
||||
* Characters which may need escaping or special handling.
|
||||
* Characters or sequences which may need escaping or special handling.
|
||||
*
|
||||
* The quote is fixed to `"` in pretty mode, so `'` and `` ` `` are deliberately absent -
|
||||
* they are never escaped, and must not drag a string onto the slow path.
|
||||
*/
|
||||
const STRING_ESCAPE_REGEX =
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/[\0\x07\b\v\f\n\r\x1B\\"<\u2028\u2029\xA0\uD800-\uDFFF]/;
|
||||
/[\0\x07\b\v\f\n\r\x1B\\"\u2028\u2029\xA0\uD800-\uDFFF]|<\/script/i;
|
||||
|
||||
/**
|
||||
* Print a string literal, quotes and all.
|
||||
*
|
||||
* Pretty mode always uses double quotes, matching Oxc's default options, so there is no quote to choose.
|
||||
* Almost no string needs escaping, so the scan for characters which do is all that happens on the common path.
|
||||
* Almost no string needs escaping, so the scan for characters or sequences which do is all that happens on the common path.
|
||||
*/
|
||||
export function printString(state: State, value: string, node: ESTree.Node): void {
|
||||
// Quote is fixed - double, matching `oxc_codegen`'s default option
|
||||
|
||||
@@ -214,6 +214,36 @@ describe("numbers", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
// String printing has a deliberately broad fast path. In particular, a `<` by itself is harmless;
|
||||
// only the case-insensitive `</script` sequence must be broken up for safe inline-script output.
|
||||
describe("strings", () => {
|
||||
checkCases([
|
||||
["harmless less-than", e(str("a < b; <div> is text")), '("a < b; <div> is text");\n'],
|
||||
["script close tag", e(str("</script")), '("<\\/script");\n'],
|
||||
["mixed-case script close tag", e(str("</ScRiPt")), '("<\\/ScRiPt");\n'],
|
||||
// This is a prefix check, matching the HTML parser behavior and the previous slow-path test.
|
||||
["script close tag prefix", e(str("</scripture")), '("<\\/scripture");\n'],
|
||||
[
|
||||
"quote and controls",
|
||||
e(str('"\\\0' + "1\n\r\u0007\u000b\f\u001b\u00a0\u2028\u2029")),
|
||||
'("\\\"\\\\\\x001\\n\\r\\x07\\v\\f\\x1B\\xA0\\u2028\\u2029");\n',
|
||||
],
|
||||
["paired surrogate", e(str("\ud83d\ude00")), '("😀");\n'],
|
||||
["lone high surrogate", e(str("\ud800")), '("\\ud800");\n'],
|
||||
["lone low surrogate", e(str("\udc00")), '("\\udc00");\n'],
|
||||
]);
|
||||
|
||||
test("template literal quasis also escape script close tags", () => {
|
||||
const { program: parsed, errors } = parseSync(
|
||||
"fixture.js",
|
||||
"const value = `before </ScRiPt> after`;",
|
||||
PARSE_OPTIONS,
|
||||
);
|
||||
if (errors.length > 0) throw new Error(`fixture parse failed: ${errors[0].message}`);
|
||||
expect(printSync(parsed)).toBe("const value = `before <\\/ScRiPt> after`;\n");
|
||||
});
|
||||
});
|
||||
|
||||
// A negative bigint is parenthesized where the position binds tighter than a prefix operator, the
|
||||
// same rule `printNumericLiteral` follows. `-1n` parses as a unary minus around a positive literal,
|
||||
// so a `BigIntLiteral` whose text starts with `-` never comes from a parser and this whole branch
|
||||
|
||||
Reference in new issue
Block a user