Skip to content

Stop re-parsing and re-mirroring the project on every type-aware parse - #246

Draft
wagenet wants to merge 4 commits into
ember-tooling:mainfrom
wagenet:wagenet/eep-replaceextensions-perf
Draft

Stop re-parsing and re-mirroring the project on every type-aware parse#246
wagenet wants to merge 4 commits into
ember-tooling:mainfrom
wagenet:wagenet/eep-replaceextensions-perf

Conversation

@wagenet

@wagenet wagenet commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Type-aware linting of a large Ember app spends more time in this parser than I expected. Two places in src/parser/ts-patch.js do work proportional to the size of the whole project, on inputs where the work can't change the result.

One deliberate behavior change falls out of this, on a pathological input. It's called out under §1 rather than buried.

1. replaceExtensions parses every .ts file looking for imports that aren't there

patchTs() wraps ts.sys.readFile, so every .ts source TypeScript pulls into the program goes through replaceExtensions, which runs a full ts.createSourceFile to look for .gts module specifiers.

In an Ember app basically no .ts file names a .gts; components get imported extensionless. The parse is thrown away for nearly every file, once per file, while the program is being built.

The function only rewrites .gts (.gjs specifiers are left alone today, and that's unchanged here), and every specifier the walk can successfully rewrite carries a literal .gts in the source text. So:

if (!code.includes('.gts')) return code;

The one behavior change. A specifier that spells the extension behind an escape or a line continuation has no literal .gts in its raw text, so the pre-check skips a file the walk used to reach:

import Foo from './foo.gts';

The walk never rewrote those either. It replaces the raw span with the cooked value, so the file came out shorter and tripped the length !== jsCode.length assertion. I'd assumed that throw was harmless because readFile catches it, but gjs-gts-parser.js calls replaceExtensions outside that try/catch, so it actually surfaced as Parsing error: bad replacement on a file that is valid TypeScript. Confirmed on main:

main:   threw -> bad replacement
branch: parses; TypeScript reports the unresolved module

I think that's an improvement, but it is a change, and it's tested rather than assumed.

2. syncMtsGtsSourceFiles re-mirrors the whole project on every parse

This runs after every type-aware parse and walks every file in the program, including lib.d.ts and every .d.ts reachable from node_modules. Two costs in there.

The per-file one: syncVirtualFile ran twice per source file and built up to four new RegExp(...) per call before working out that the file was an ordinary .ts it didn't care about. Now a single endsWith drops the uninteresting majority, and the two surviving suffix patterns are module constants.

The per-.gts one is bigger: mirroring a source onto its virtual twin copies about 50 properties with Object.assign, and it ran for every .gts in the program on every parse. An app with 8,000 .gts files paid 8,000 of those per linted file. The hosts used for type-aware linting hand back the same SourceFile object until a file's content changes and build a new one when it does, so a WeakMap from twin to source plus a version check identifies the copies already in place.

I instrumented that before touching it rather than assuming it was safe to skip. Over a full lint of a synthetic project, 89,999 of 90,000 mirror operations were re-copying an unchanged source file. The only values that ever differed on the twin were lineMap and modifierFlagsCache, both derived lazily from text, which is shared by reference.

Two things reviewers should know about the guard:

  • On the options.programs path the program comes from a plain ts.createProgram, which never sets version. There the version clause is undefined === undefined and object identity carries the guard by itself. That's sound as long as a user-supplied program doesn't mutate its SourceFiles in place, which none does, but it's weaker than it looks.
  • The old code also re-copied id on every pass, which aliased the .gts and .mts files into one nodeLinks slot in the checker from the second sync onward. They now keep separate ids. I believe that's the correct behavior rather than a regression, and the differential check below finds no observable difference, but it is a change in what the checker sees.

Numbers

pnpm bench:project:compare against main with BENCH_PROJECT_FILES=400 (400 .gts plus 800 .ts in one tsconfig), on an M4 Max, node 20.20.2, TypeScript 5.7.2, @typescript-eslint 8.46.4:

benchmark main this branch
project warm parse x150 311.5 ms 189.0 ms 1.65x
project parse+typed x150 349.2 ms 198.7 ms 1.76x
projectService warm parse x150 313.6 ms 219.6 ms 1.43x
projectService parse+typed x150 333.1 ms 240.1 ms 1.39x
replaceExtensions over 400 .ts sources 22.25 ms 62.31 µs 357x
replaceExtensions over 400 .ts sources importing .gts 22.50 ms 22.03 ms 1.02x

Repeat runs put the parse rows in a 1.2x to 1.8x band depending on how quiet the machine is. The ordering is stable, but don't read the third digit.

The last row is a no-op case that lands inside the noise floor, and it has come out on both sides of parity: 1.02x faster here, 1.09x slower on the CI runner. Treat it as "no measurable difference" rather than as either a win or a regression. That is the expected shape, since all the pre-check adds to a file that does contain .gts is one String.includes scan before the same parse as before.

The last two rows are new scenarios in tests/project.bench.mjs. Everything already there measures warm parses, which happen after the program is built, so none of it ever reached the patched readFile and that cost was invisible to the bench. The second of the two is the case the pre-check can't skip, included so the pair brackets the change instead of only measuring the half it was built to win.

The gains land hardest in classic project mode, which is where the profile that started this pointed. projectService keeps source files in its DocumentRegistry instead of re-reading them, so it hits the patched readFile far less often.

On a real app

The synthetic project tops out well below the scale where this hurts, so I also measured a private Ember app: about 23,500 files in a single program, 3,700 of them .gts, on typescript@6.0.3, @typescript-eslint/parser@8.65.0 and eslint@10.8.0. Both parser versions were resolved against that app's own dependency closure, so the only thing differing between the two runs is ts-patch.js. Parse only, no rules running.

main this branch
warm parse per linted file, run 1 30.71 ms 17.88 ms 1.72x
warm parse per linted file, run 2 33.41 ms 20.42 ms 1.64x
cold first parse (program build) ~33.8 s ~31.0 s ~2.8 s

About 13 ms per linted file, holding across two rounds with the run order reversed.

That lines up with the CPU profile that started this, which attributed 16.73 ms of self time per linted file to this parser in classic project mode, against a 57.9 ms per-file total for that mode: roughly 29% of type-aware lint time spent in the parser rather than in TypeScript. Recovering ~13 of those 16.73 ms is about what a parse-only measurement should show.

The cold row is the other half. That profile was taken by differencing runs at n=200 and n=800 to cancel one-off project load, which is exactly what hides program construction: replaceExtensions runs there, once per file in the tsconfig rather than once per linted file, so it contributed ~0 to the differenced per-file figure while still costing ~2.8 s of every lint process. Worth knowing if you go looking for it in a profile and can't find it.

Checks run

pnpm test, pnpm lint, pnpm --filter '*' test:check, pnpm --filter '*' test:fix (the gjs-types project lints under both project and projectService), and node ./scripts/eslint-plugin-ember-test.mjs (6,327 tests).

Two things beyond the suite, since "no behavior change" is most of the claim here:

The new syncMtsGtsSourceFiles tests were checked against a build with the memo guard deleted, and the pinning test fails there as it should. The replaceExtensions tests pass against main's implementation too, except the escaped-specifier ones, which is the divergence in §1.

A differential harness parses a synthetic project of 25 .gts plus 40 .ts files three times over in classic project mode (passes after the first are what exercise the skip), dumps checker.typeToString(getTypeAtLocation(...)) for every ESTree node with a TS mapping, and appends every whole-program semantic diagnostic. 8,495 lines, byte-identical between main's ts-patch.js and this branch. The diagnostics include 5097 errors on rewritten .mts specifiers, so the rewrite path is genuinely covered rather than skipped.


Investigated and written by Claude (Claude Code), at Peter Wagenet's request.

🤖 Generated with Claude Code

wagenet and others added 3 commits August 14, 2026 13:09
The patched ts.sys.readFile runs replaceExtensions over every .ts source
TypeScript pulls into the program, and replaceExtensions does a full
createSourceFile to look for .gts module specifiers. Ember apps import
components extensionless, so that parse is thrown away for nearly every
file, once per file, while the program is being built.

Only .gts is ever rewritten, and every specifier form that reaches the
rewrite carries a literal .gts in the source text: spellings that hide it
behind an escape sequence or a line continuation already fail the length
assertion and leave the file alone. A substring miss therefore means the
walk cannot change anything.

Over 400 app-sized .ts sources this drops from 38.6ms to 105us.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
syncMtsGtsSourceFiles runs after every type-aware parse and walks the
whole program, including lib.d.ts and every .d.ts reachable from
node_modules. Two costs there scaled with project size rather than with
the file being linted.

Per source file, syncVirtualFile ran twice and built up to four RegExps
per call before concluding the file was an ordinary .ts it did not care
about. A single endsWith now drops the uninteresting majority, and the
two surviving suffix patterns are module constants. The branches are
mutually exclusive: no path ends in both .gts and .mts, and only files
linked here ever carry a virtual flag.

Per .gts, mirroring a source onto its twin copies ~50 properties with
Object.assign, and it ran for every .gts in the program on every parse.
TypeScript hands back the same SourceFile object until a file's content
changes, at which point it builds a new one with a new version, so a
WeakMap from twin to source plus a version check identifies the copies
already in place. Instrumenting a full lint of a synthetic project found
89,999 of 90,000 mirror operations re-copying an unchanged source, with
only lineMap and modifierFlagsCache ever differing on the twin; both are
derived lazily from text, which is identical on both sides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The project-mode scenarios all measure warm parses, which happen after
the program is built, so none of them reach the patched ts.sys.readFile
and its per-file cost was invisible to the bench.

Sources are generated for this scenario rather than reused from the
project above: the project's helper modules are a few lines each, and
this cost tracks file size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🏎️ Benchmark Comparison

Parse

Benchmark Control (p50) Experiment (p50) Δ
🟢 gts small 937.21 ms 893.57 ms -4.7%
gts medium 1561.22 ms 1568.79 ms +0.5%
gts large 742.98 ms 751.25 ms +1.1%
gjs small 785.40 ms 771.57 ms -1.8%
gjs medium 1355.24 ms 1353.00 ms -0.2%
gjs large 612.33 ms 622.95 ms +1.7%
🟠 hbs small 133.03 ms 136.68 ms +2.7%
🟢 hbs medium 742.25 ms 717.03 ms -3.4%
hbs large 820.16 ms 809.70 ms -1.3%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.10 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.19.0 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
gts small (control)          969.73 ms/iter    1.01 s      █               
                       (816.05 ms … 1.25 s)    1.11 s ▅ ▅  █▅ ▅▅  ▅ ▅  ▅  ▅
                    ( 13.07 mb …  88.32 mb)  42.49 mb █▁█▁▁██▁██▁▁█▁█▁▁█▁▁█

gts small (experiment)       938.68 ms/iter 973.24 ms    █                 
                       (808.17 ms … 1.22 s)    1.09 s ▅ ▅█▅ ▅▅  ▅ ▅ ▅     ▅
                    ( 12.77 mb …  88.08 mb)  42.38 mb █▁███▁██▁▁█▁█▁█▁▁▁▁▁█

                             ┌                                            ┐
                              ╷          ┌───────────┬──────┐             ╷
         gts small (control)  ├──────────┤           │      ├─────────────┤
                              ╵          └───────────┴──────┘             ╵
                             ╷      ┌────────────┬────┐                 ╷
      gts small (experiment) ├──────┤            │    ├─────────────────┤
                             ╵      └────────────┴────┘                 ╵
                             └                                            ┘
                             808.17 ms           956.91 ms           1.11 s

summary
  gts small (experiment)
   1.03x faster than gts small (control)

------------------------------------------- -------------------------------
gts medium (control)            1.60 s/iter    1.62 s    █                 
                          (1.49 s … 2.01 s)    1.64 s ▅ ▅█ ▅    ▅   ▅▅ ▅ ▅▅
                    ( 79.94 mb …  99.80 mb)  84.29 mb █▁██▁█▁▁▁▁█▁▁▁██▁█▁██

gts medium (experiment)         1.62 s/iter    1.63 s             █        
                          (1.49 s … 2.05 s)    1.67 s ▅  ▅▅▅ ▅ ▅  █   ▅ ▅ ▅
                    ( 78.82 mb … 134.48 mb)  89.39 mb █▁▁███▁█▁█▁▁█▁▁▁█▁█▁█

                             ┌                                            ┐
                             ╷    ┌──────────────────────┬───┐     ╷
        gts medium (control) ├────┤                      │   ├─────┤
                             ╵    └──────────────────────┴───┘     ╵
                              ╷        ┌────────────────────┬───┐         ╷
     gts medium (experiment)  ├────────┤                    │   ├─────────┤
                              ╵        └────────────────────┴───┘         ╵
                             └                                            ┘
                             1.49 s              1.58 s              1.67 s

summary
  gts medium (control)
   1.01x faster than gts medium (experiment)

------------------------------------------- -------------------------------
gts large (control)          787.07 ms/iter 789.56 ms   █                  
                       (714.45 ms … 1.16 s) 811.30 ms ▅▅█ ▅ ▅ ▅▅      ▅  ▅▅
                    ( 14.98 mb …  87.14 mb)  38.32 mb ███▁█▁█▁██▁▁▁▁▁▁█▁▁██

gts large (experiment)       793.75 ms/iter 792.74 ms     █ █              
                       (710.38 ms … 1.12 s) 868.77 ms ▅ ▅▅█▅█   ▅    ▅    ▅
                    ( 14.99 mb …  86.94 mb)  38.08 mb █▁█████▁▁▁█▁▁▁▁█▁▁▁▁█

                             ┌                                            ┐
                              ╷  ┌─────────────────┬      ╷
         gts large (control)  ├──┤                 │──────┤
                              ╵  └─────────────────┴      ╵
                             ╷     ┌─────────────────┬                    ╷
      gts large (experiment) ├─────┤                 │────────────────────┤
                             ╵     └─────────────────┴                    ╵
                             └                                            ┘
                             710.38 ms         789.58 ms          868.77 ms

summary
  gts large (control)
   1.01x faster than gts large (experiment)

------------------------------------------- -------------------------------
gjs small (control)          817.06 ms/iter 812.87 ms      █ █             
                       (736.42 ms … 1.10 s) 896.19 ms ▅▅  ▅█▅█  ▅▅        ▅
                    ( 29.17 mb …  86.07 mb)  37.17 mb ██▁▁████▁▁██▁▁▁▁▁▁▁▁█

gjs small (experiment)       806.72 ms/iter 816.11 ms █ █   █    █         
                       (736.38 ms … 1.10 s) 882.61 ms █ █▅ ▅█    █        ▅
                    ( 29.47 mb …  86.13 mb)  36.98 mb █▁██▁██▁▁▁▁█▁▁▁▁▁▁▁▁█

                             ┌                                            ┐
                             ╷       ┌──────────────┬                     ╷
         gjs small (control) ├───────┤              │─────────────────────┤
                             ╵       └──────────────┴                     ╵
                             ╷  ┌────────────────┬─┐                  ╷
      gjs small (experiment) ├──┤                │ ├──────────────────┤
                             ╵  └────────────────┴─┘                  ╵
                             └                                            ┘
                             736.38 ms         816.29 ms          896.19 ms

summary
  gjs small (experiment)
   1.01x faster than gjs small (control)

------------------------------------------- -------------------------------
gjs medium (control)            1.40 s/iter    1.42 s      █  █            
                          (1.29 s … 1.80 s)    1.46 s ▅   ▅█  █▅   ▅ ▅  ▅ ▅
                    ( 20.98 mb …  90.63 mb)  71.37 mb █▁▁▁██▁▁██▁▁▁█▁█▁▁█▁█

gjs medium (experiment)         1.41 s/iter    1.42 s    █                 
                          (1.30 s … 1.79 s)    1.49 s ▅  █▅▅▅ ▅ ▅  ▅ ▅    ▅
                    ( 18.76 mb …  88.86 mb)  70.33 mb █▁▁████▁█▁█▁▁█▁█▁▁▁▁█

                             ┌                                            ┐
                             ╷         ┌───────────────┬──┐        ╷
        gjs medium (control) ├─────────┤               │  ├────────┤
                             ╵         └───────────────┴──┘        ╵
                               ╷      ┌─────────────────┬──┐              ╷
     gjs medium (experiment)   ├──────┤                 │  ├──────────────┤
                               ╵      └─────────────────┴──┘              ╵
                             └                                            ┘
                             1.29 s              1.39 s              1.49 s

summary
  gjs medium (control)
   1x faster than gjs medium (experiment)

------------------------------------------- -------------------------------
gjs large (control)          650.36 ms/iter 620.09 ms    █                 
                    (588.42 ms … 922.39 ms) 723.50 ms    █▅▅               
                    (389.82 kb …  70.38 mb)  43.96 mb ▇▁▁███▁▁▁▁▁▁▇▁▁▁▁▁▁▁▇

gjs large (experiment)       674.59 ms/iter 659.63 ms  █                   
                    (609.40 ms … 967.18 ms) 793.45 ms  █                   
                    (162.75 kb …  69.93 mb)  43.56 mb ▆█▆▆▁▆▁▁▁▆▁▁▁▁▁▁▁▁▁▁▆

                             ┌                                            ┐
                             ╷   ┌─────────┬               ╷
         gjs large (control) ├───┤         │───────────────┤
                             ╵   └─────────┴               ╵
                                  ╷ ┌───────────┬                         ╷
      gjs large (experiment)      ├─┤           │─────────────────────────┤
                                  ╵ └───────────┴                         ╵
                             └                                            ┘
                             588.42 ms         690.94 ms          793.45 ms

summary
  gjs large (control)
   1.04x faster than gjs large (experiment)

------------------------------------------- -------------------------------
hbs small (control)          147.60 ms/iter 154.72 ms     █                
                    (114.10 ms … 262.74 ms) 163.58 ms ▅ ▅ █ ▅ ▅ ▅    ▅▅▅  ▅
                    ( 51.32 mb …  57.14 mb)  53.87 mb █▁█▁█▁█▁█▁█▁▁▁▁███▁▁█

hbs small (experiment)       149.91 ms/iter 149.56 ms         █  █        █
                    (112.73 ms … 299.95 ms) 156.04 ms ▅  ▅  ▅ █  █   ▅ ▅  █
                    ( 51.24 mb …  56.09 mb)  53.79 mb █▁▁█▁▁█▁█▁▁█▁▁▁█▁█▁▁█

                             ┌                                            ┐
                              ╷        ┌────────────────────┬─────┐       ╷
         hbs small (control)  ├────────┤                    │     ├───────┤
                              ╵        └────────────────────┴─────┘       ╵
                             ╷          ┌─────────────────────┬    ╷
      hbs small (experiment) ├──────────┤                     │────┤
                             ╵          └─────────────────────┴    ╵
                             └                                            ┘
                             112.73 ms         138.16 ms          163.58 ms

summary
  hbs small (control)
   1.02x faster than hbs small (experiment)

------------------------------------------- -------------------------------
hbs medium (control)         778.37 ms/iter 761.11 ms ██ █                 
                       (730.79 ms … 1.04 s) 816.89 ms ██▅█▅  ▅           ▅▅
                    (  8.75 mb …  82.93 mb)  37.97 mb █████▁▁█▁▁▁▁▁▁▁▁▁▁▁██

hbs medium (experiment)      750.18 ms/iter 732.77 ms  █                   
                    (705.67 ms … 965.35 ms) 797.68 ms  ██                  
                    (  8.68 mb …  82.16 mb)  32.61 mb █████▁█▁▁▁▁▁▁▁▁▁▁▁█▁█

                             ┌                                            ┐
                                       ╷┌─────────────────┬               ╷
        hbs medium (control)           ├┤                 │───────────────┤
                                       ╵└─────────────────┴               ╵
                             ╷ ┌───────────────┬                  ╷
     hbs medium (experiment) ├─┤               │──────────────────┤
                             ╵ └───────────────┴                  ╵
                             └                                            ┘
                             705.67 ms         761.28 ms          816.89 ms

summary
  hbs medium (experiment)
   1.04x faster than hbs medium (control)

------------------------------------------- -------------------------------
hbs large (control)          850.73 ms/iter 852.79 ms   █                  
                       (811.19 ms … 1.06 s) 880.76 ms   ██                 
                    ( 53.36 mb …  64.85 mb)  55.46 mb █████▁▁▁▁▁▁▁█▁▁▁█▁▁▁█

hbs large (experiment)       831.24 ms/iter 817.76 ms  █                   
                       (800.15 ms … 1.03 s) 851.34 ms  █   █               
                    ( 53.41 mb …  64.54 mb)  55.41 mb ██▁███▁█▁█▁▁▁▁▁▁▁▁▁▁█

                             ┌                                            ┐
                                   ╷   ┌─────────────────┬┐               ╷
         hbs large (control)       ├───┤                 │├───────────────┤
                                   ╵   └─────────────────┴┘               ╵
                             ╷┌───────────────┬           ╷
      hbs large (experiment) ├┤               │───────────┤
                             ╵└───────────────┴           ╵
                             └                                            ┘
                             800.15 ms         840.45 ms          880.76 ms

summary
  hbs large (experiment)
   1.02x faster than hbs large (control)
### Project mode (type-aware)
Benchmark Control (p50) Experiment (p50) Δ
🟢 project warm parse x150 598.13 ms 441.17 ms -26.2%
🟢 project parse+typed x150 613.28 ms 489.37 ms -20.2%
🟢 projectService warm parse x150 607.66 ms 475.28 ms -21.8%
🟢 projectService parse+typed x150 650.42 ms 513.72 ms -21.0%
🟢 replaceExtensions over 200 .ts sources 81.51 ms 37.60 µs -100.0%
🔴 replaceExtensions over 200 .ts sources importing .gts 81.64 ms 86.42 ms +5.9%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.02 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.19.0 (x64-linux)

benchmark                                                         avg (min … max) p75 / p99    (min … top 1%)
--------------------------------------------------------------------------------- -------------------------------
project warm parse x150 (control)                                  601.48 ms/iter 611.71 ms                █     
                                                          (577.49 ms … 626.53 ms) 623.40 ms █▁█▁█▁▁███▁██▁▁█▁▁▁▁█
                                                        gc(288.92 ms … 297.40 ms) 112.85 mb (109.88 mb…114.88 mb)

project warm parse x150 (experiment)                               438.05 ms/iter 443.82 ms                  █  █
                                                          (423.48 ms … 452.87 ms) 446.01 ms █▁▁█▁█▁██▁▁▁▁▁▁▁███▁█
                                                        gc(288.16 ms … 317.20 ms) 114.14 mb (113.52 mb…114.83 mb)

                                                                   ┌                                            ┐
                                                                                                      ╷ ┌──┬─┐  ╷
                                 project warm parse x150 (control)                                    ├─┤  │ ├──┤
                                                                                                      ╵ └──┴─┘  ╵
                                                                   ╷┌─┬─┐
                              project warm parse x150 (experiment) ├┤ │ │
                                                                   ╵└─┴─┘
                                                                   └                                            ┘
                                                                   423.48 ms         523.44 ms          623.40 ms

summary
  project warm parse x150 (experiment)
   1.37x faster than project warm parse x150 (control)

--------------------------------------------------------------------------------- -------------------------------
project parse+typed x150 (control)                                 625.86 ms/iter 645.42 ms    ██                
                                                          (602.70 ms … 659.87 ms) 651.57 ms █▁███▁▁▁█▁▁█▁▁▁▁▁█▁██
                                                        gc(291.52 ms … 305.33 ms) 118.49 mb (115.50 mb…123.24 mb)

project parse+typed x150 (experiment)                              491.18 ms/iter 491.91 ms           █ █       █
                                                          (478.09 ms … 507.24 ms) 500.47 ms █▁▁█▁▁▁█▁████▁▁▁▁▁▁▁█
                                                        gc(301.28 ms … 311.92 ms)  92.22 mb ( 89.05 mb…117.53 mb)

                                                                   ┌                                            ┐
                                                                                                   ╷ ┌───┬────┐ ╷
                                project parse+typed x150 (control)                                 ├─┤   │    ├─┤
                                                                                                   ╵ └───┴────┘ ╵
                                                                   ╷ ┌┬┐ ╷
                             project parse+typed x150 (experiment) ├─┤│├─┤
                                                                   ╵ └┴┘ ╵
                                                                   └                                            ┘
                                                                   478.09 ms         564.83 ms          651.57 ms

summary
  project parse+typed x150 (experiment)
   1.27x faster than project parse+typed x150 (control)

--------------------------------------------------------------------------------- -------------------------------
projectService warm parse x150 (control)                           612.84 ms/iter 614.30 ms      █     ▄         
                                                          (590.01 ms … 661.14 ms) 635.99 ms ▅▁▁▁▁█▁▁█▁▁█▁▁▁▁▁▁▁▁▅
                                                        gc(298.22 ms … 318.76 ms) 115.26 mb (113.91 mb…116.51 mb)

projectService warm parse x150 (experiment)                        474.21 ms/iter 477.15 ms           ▃    █     
                                                          (459.37 ms … 496.47 ms) 480.95 ms ▆▁▆▁▁▁▁▁▆▁█▁▁▁▁█▆▁▁▆▆
                                                        gc(299.68 ms … 325.85 ms) 118.43 mb (114.81 mb…119.45 mb)

                                                                   ┌                                            ┐
                                                                                                    ╷  ┌──┬     ╷
                          projectService warm parse x150 (control)                                  ├──┤  │─────┤
                                                                                                    ╵  └──┴     ╵
                                                                   ╷ ┌─┬┐
                       projectService warm parse x150 (experiment) ├─┤ ││
                                                                   ╵ └─┴┘
                                                                   └                                            ┘
                                                                   459.37 ms         547.68 ms          635.99 ms

summary
  projectService warm parse x150 (experiment)
   1.29x faster than projectService warm parse x150 (control)

--------------------------------------------------------------------------------- -------------------------------
projectService parse+typed x150 (control)                          653.78 ms/iter 653.87 ms █      █             
                                                          (640.15 ms … 683.37 ms) 670.47 ms █▁███▁▁███▁▁▁▁▁▁▁▁▁██
                                                        gc(304.85 ms … 321.01 ms) 118.65 mb (114.45 mb…123.54 mb)

projectService parse+typed x150 (experiment)                       521.46 ms/iter 526.00 ms   █                  
                                                          (499.24 ms … 561.51 ms) 554.92 ms ███▁███▁█▁█▁▁▁▁█▁▁▁▁█
                                                        gc(311.74 ms … 325.40 ms) 103.02 mb ( 87.16 mb…124.00 mb)

                                                                   ┌                                            ┐
                                                                                                        ╷┌──┬   ╷
                         projectService parse+typed x150 (control)                                      ├┤  │───┤
                                                                                                        ╵└──┴   ╵
                                                                   ╷┌────┬┐       ╷
                      projectService parse+typed x150 (experiment) ├┤    │├───────┤
                                                                   ╵└────┴┘       ╵
                                                                   └                                            ┘
                                                                   499.24 ms         584.86 ms          670.47 ms

summary
  projectService parse+typed x150 (experiment)
   1.25x faster than projectService parse+typed x150 (control)

--------------------------------------------------------------------------------- -------------------------------
replaceExtensions over 200 .ts sources (control)                    84.30 ms/iter  86.41 ms   █    █    █        
                                                            (74.62 ms … 98.10 ms)  93.64 ms █▁█▁▁▁██▁▁▁██▁▁▁▁█▁▁█
                                                        gc(314.56 ms … 331.84 ms)  10.66 mb ( 10.61 mb… 10.91 mb)

replaceExtensions over 200 .ts sources (experiment)                 37.97 µs/iter  38.17 µs ▃    █    ▃          
                                                            (36.87 µs … 39.87 µs)  39.57 µs █▁▁▁▆█▆▁▁▁█▆▁▁▁▁▁▁▁▁▆
                                                        gc(306.38 ms … 340.63 ms) 272.75  b (  0.17  b…  3.19 kb)

                                                                   ┌                                            ┐
                                                                                                       ╷┌───┬┐  ╷
                  replaceExtensions over 200 .ts sources (control)                                     ├┤   │├──┤
                                                                                                       ╵└───┴┘  ╵
                                                                   ┬
               replaceExtensions over 200 .ts sources (experiment) │
                                                                   ┴
                                                                   └                                            ┘
                                                                   36.87 µs           46.84 ms           93.64 ms

summary
  replaceExtensions over 200 .ts sources (experiment)
   2219.9x faster than replaceExtensions over 200 .ts sources (control)

--------------------------------------------------------------------------------- -------------------------------
replaceExtensions over 200 .ts sources importing .gts (control)     82.17 ms/iter  82.68 ms █       █            
                                                            (79.29 ms … 85.95 ms)  85.14 ms █▁▁█▁▁▁██▁███▁▁▁█▁▁▁█
                                                        gc(311.42 ms … 330.80 ms)   3.16 mb (  1.35 mb…  3.77 mb)

replaceExtensions over 200 .ts sources importing .gts (experiment)  88.75 ms/iter  90.09 ms █    █  █            
                                                           (80.27 ms … 105.59 ms) 104.00 ms █▁████▁██▁▁▁▁▁▁▁▁▁▁▁█
                                                        gc(307.55 ms … 326.17 ms)  10.78 mb (  3.69 mb… 11.79 mb)

                                                                   ┌                                            ┐
                                                                   ╷ ┌──┬┐    ╷
   replaceExtensions over 200 .ts sources importing .gts (control) ├─┤  │├────┤
                                                                   ╵ └──┴┘    ╵
                                                                     ╷  ┌───────────┬──┐                        ╷
replaceExtensions over 200 .ts sources importing .gts (experiment)   ├──┤           │  ├────────────────────────┤
                                                                     ╵  └───────────┴──┘                        ╵
                                                                   └                                            ┘
                                                                   79.29 ms          91.64 ms           104.00 ms

summary
  replaceExtensions over 200 .ts sources importing .gts (control)
   1.08x faster than replaceExtensions over 200 .ts sources importing .gts (experiment)

The comment on the replaceExtensions pre-check claimed the escaped-
specifier case was already a no-op. It is not: gjs-gts-parser calls
replaceExtensions outside the readFile try/catch, so `bad replacement`
reached ESLint as a parsing error on an otherwise valid file. Skipping
the walk swallows that throw. Say so, and cover it with a test.

The memoization comment stated object-identity-plus-version as a general
TypeScript property. It holds for the hosts used in type-aware linting;
user-supplied `programs` never set a version, and there identity carries
the guard alone.

None of the new tests failed when the memo guard was deleted, because
Object.assign does not clear properties absent from the source. Pin it
with one that the copy would overwrite.

Fold the replaceExtensions cases into the block that already covers that
function in parser.test.js rather than opening a second describe of the
same name, and cover the gaps around the .gjs fallback, .mjs orphan
invalidation, and the virtual flag landing on a .ts fallback twin.

Bench a source that does contain a .gts alongside the one that does not,
so the pair brackets the change instead of only measuring the case it
was built to win, and skip the comparison when the control predates the
replaceExtensions export.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wagenet
wagenet force-pushed the wagenet/eep-replaceextensions-perf branch from 7bf1672 to 3be7061 Compare August 14, 2026 20:45
@NullVoxPopuli
NullVoxPopuli marked this pull request as draft August 14, 2026 21:44
@NullVoxPopuli

Copy link
Copy Markdown
Member

converting to draft, because it appears things are still in flux

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants