mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
07a07933c7
Continuation of #26108. Previously we built the whole output as a single rope, and then flattened it in one go at the end. This is fine for smaller files, but for larger files it has 2 problems: 1. Each `output += segment` append allocates a 32-byte cons string cell. When printing a large AST, so many of these are allocated that they can fill "new space" and trigger garbage collection. When that happens, all these little cons strings have to be copied into the other half of new space, which is slow. Even worse, if they survive 2 rounds of GC, they get promoted into "old space" where (a) they may get scattered across memory, ruining cache coherence, and (b) they sit there, taking up memory, until the next major GC round. 2. Flattening a rope string which contains any segments which are 2-byte strings (non-Latin1 characters) is way more expensive than flattening a rope which is pure Latin1/ASCII. If a single segment contains Unicode, the entire flatten operation deopts and becomes several times slower. This PR attacks both these problems by flattening a large output in chunks. - When output reaches 16 KiB, flatten that as a chunk and store it in an array. - This leaves all the little cons string cells unreferenced, so garbage collection will discard them all, instead of copying them across to keep them alive. - If file contains some non-Latin1 characters, that does still heavily impact the performance of flattening the chunk they're in, but only that chunk - other chunks which are pure ASCII still take the fast path. The 16 KiB limit is a heuristic, chosen after experimentation with different values. ### Benchmarks The effect of this change is minimal on small files, but dramatic on large ones (negative is faster): | Fixture | Bytes | Change | |:---| ---:| ---:| | `tiny.js` | 26 | \+3.2% | | `RadixUIAdoptionSection.jsx` | 2,424 | \-0.1% | | `react.development.js` | 50,496 | \+2.0% | | `binder.ts` | 126,212 | \-1.9% | | `lodash.js` | 182,262 | \-32.5% | | `App.tsx` | 298,130 | \-26.4% | | `kitchen-sink.tsx` | 662,560 | \-14.2% | | `antd.js` | 5,100,647 | \-28.4% | ### Total cost of flattening After this PR, compared to where we were prior to #26106 (positive is slower): | Fixture | Bytes | Change | |:---| ---:| ---:| | `tiny.js` | 26 | \+51.5% | | `RadixUIAdoptionSection.jsx` | 2,424 | \+134.2% | | `react.development.js` | 50,496 | \+35.5% | | `binder.ts` | 126,212 | \+39.0% | | `lodash.js` | 182,262 | \+45.3% | | `App.tsx` | 298,130 | \+55.0% | | `kitchen-sink.tsx` | 662,560 | \+51.3% | | `antd.js` | 5,100,647 | \+2.2% | The effect of flattening is still very negative, but (as mentioned in #26106) it was always a cost, just one that we were ignoring. At least we've now managed to mitigate it. In the case of really large files (`antd.js`), benchmark is pretty much unchanged from before flattening came into the picture. But now our measure _includes_ the large flattening cost which was previously _on top of_ the cost of printing - so the real cost for the user is much reduced from where it was previously. ### Sourcemap builds Printing with sourcemaps on large files actually gets faster after this change. It was already paying the cost of string flattening, now it pays less. Printing `lodash.js` with sourcemaps is about 15% faster now.
73 lines
3.4 KiB
TypeScript
73 lines
3.4 KiB
TypeScript
// A fast JavaScript code generator from an ESTree-compliant AST.
|
|
//
|
|
// A faithful port of Oxc's Rust `oxc_codegen` crate (pretty-printing mode).
|
|
// Output is byte-identical to `oxc_codegen` with default options (tab indentation, double quotes, comments off).
|
|
//
|
|
// The printer is one function per construct, split across this directory by area.
|
|
// `printStatement` in `statement.ts` and `printExpression` in `expression.ts` are the two dispatch
|
|
// points the rest hangs off.
|
|
//
|
|
// Reference: `oxc/crates/oxc_codegen/src/{gen.rs,lib.rs,str.rs,binary_expr_visitor.rs}`
|
|
|
|
import { debugAssert, typeAssertIs } from "../asserts.ts";
|
|
import { flattenString } from "./flatten.ts";
|
|
import { generateSourceMap } from "./source_map.ts";
|
|
import { printProgram, printStatement } from "./statement.ts";
|
|
|
|
import type { CodegenResult, Options } from "./options.ts";
|
|
import type { State } from "../state.ts";
|
|
import type * as ESTree from "../../../../npm/oxc-types/types.d.ts";
|
|
|
|
/**
|
|
* Print `node`, returning an object including the generated code.
|
|
*
|
|
* One of these exists per build. The package entry point picks the build and calls it -
|
|
* callers of the package never reach this directly.
|
|
*
|
|
* The result is an object so that further outputs (a source map) can be added without a breaking change.
|
|
*
|
|
* @param state - Created by the entry point, so that all 4 builds share one class and so one object shape
|
|
* @param options - The same options `state` was created from, for the parts only this build acts on
|
|
* @returns Object holding the generated code
|
|
*/
|
|
export function printSync(node: ESTree.Node, state: State, options: Options): CodegenResult {
|
|
if (node.type === "Program") {
|
|
printProgram(node, state);
|
|
} else {
|
|
typeAssertIs<ESTree.Statement>(node);
|
|
printStatement(node, state);
|
|
}
|
|
|
|
// Flatten the output before handing it on - see `flatten.ts` for why, and why this way.
|
|
//
|
|
// A large print has already flattened most of its output into `state.outputChunks` chunk by chunk (see `flatten.ts`),
|
|
// leaving `state.output` as the tail. Those flat chunks and the tail are joined here, which is a straight copy,
|
|
// and the result is a proper flat string.
|
|
const { outputChunks } = state;
|
|
let output;
|
|
if (outputChunks === null) {
|
|
output = state.output;
|
|
flattenString(output);
|
|
} else {
|
|
// `state.output` may be empty, which `join` ignores. A lone chunk comes back as-is, and it is already flat.
|
|
outputChunks.push(state.output);
|
|
output = outputChunks.join("");
|
|
// The chunks are now redundant. Drop them now rather than when `state` dies, so a GC during `generateSourceMap`
|
|
// collects them, instead of pointlessly retaining and copying them
|
|
state.outputChunks = null;
|
|
}
|
|
|
|
// This is removed by minifier in non-sourcemap builds.
|
|
//
|
|
// The `skipSourcemapGeneration` check exists only in benchmark builds -
|
|
// everywhere else `BENCHMARKS` is `false` and the condition folds back to `SOURCEMAPS` alone.
|
|
// The option is deliberately not in `Options` - types are documentation, and it is not public API.
|
|
// @ts-expect-error `skipSourcemapGeneration` is benchmarks-only, so is not in `Options`
|
|
if (SOURCEMAPS && (!BENCHMARKS || !options.skipSourcemapGeneration)) {
|
|
debugAssert(options.sourcemap === true, "`options.sourcemap` should be true in a maps build");
|
|
return { code: output, map: generateSourceMap(state, output, options) };
|
|
}
|
|
|
|
return { code: output, map: null };
|
|
}
|