Commit Graph

5 Commits

Author SHA1 Message Date
overlookmotel 07a07933c7 perf(packages/codegen): flatten output in chunks (#26109)
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.
2026-08-26 17:51:49 +00:00
overlookmotel 77855831a6 perf(packages/codegen): ensure indent strings are flattened (#26107)
Codegen pre-prepares strings for writing indentation and caches them.

This PR ensure that these strings are properly flattened. They already were - `indent.charCodeAt(0)` does that. But that's only the case as long as `growIndents` doesn't get tiered up to TurboFan. If it does, TurboFan treats `indent.charCodeAt(0)` as a no-op, and removes it as dead code, which removes the flattening.

`growIndents` is a cold path, so it's unlikely to get tiered up, but make sure that even if it does, we don't lose the flattening by using `indent = [indent, indentString].join("");` instead.

No effect on benchmarks, because TurboFan wasn't kicking in here.
2026-08-26 17:51:48 +00:00
overlookmotel d50c985fc2 refactor(packages/codegen): split categories into separate file (#26083)
Pure refactor. Move the category consts (`CAT_IDENT` etc) into a separate file.

2 reasons:

1. `write.ts` had got too large.
2. `index.ts`, which is not part of printer builds, imports `CAT_OTHER`. Importing it from a file containing only consts avoids any problems with minifier not being able to recognise dead code and remove it.
2026-08-25 13:46:38 +00:00
camc314 5703d7a44f docs(codegen): correct stale printer comments (#25561)
## Summary

- Correct the three stale codegen comments identified in review.
2026-08-12 17:16:55 +00:00
overlookmotel a4478e960a feat(codegen): add oxc-codegen package (#25488)
Add `oxc-codegen` package - a printer written in TS.

### Features

- Prints JS, JSX, TS, TSX.
- Matches `oxc_codegen` exactly on all Test262, TypeScript, Acorn-JSX fixtures.
- Very fast.

### Deficiencies

- Pretty-printing only (no compact/minified output).
- No support for printing comments.
- Source map support is semi-there, but largely untested, and poor API (design doc has some ideas on how to improve it).

### This PR adds

- The printer code.
- Conformance testing infra.
- Further tests for edge cases.
- README.
- Design doc - how it differs from `oxc_codegen` and why, what produces the perf.

### This PR doesn't add

- Not integrated into CI or release pipeline.

### Notes

This started as a slop port of `oxc_codegen`. I've iterated on that base, cleaned it up a lot, and at least skimmed through all of it. I think it's in fairly decent shape.

The design doc is I think worth a read. It explains the rationale for the choices, in particular the "1 codebase, 4 builds" approach.

I've put it in a new top-level directory called `packages`. It's not an app, so didn't seem to me to fit alongside `apps/oxlint` and `apps/oxfmt`. It's pure JS, so not like e.g. `napi/parser`. And it felt too heavyweight to put into `npm` dir. But maybe there's a better place for it.

Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-12 13:41:31 +00:00