Skip to content

perf: parse a saved controller once instead of once per route - #117

Merged
thetutlage merged 1 commit into
8.xfrom
perf/routes-scanner-invalidate-shared-vfs
Oct 12, 2026
Merged

thetutlage merged 1 commit into
8.xfrom
perf/routes-scanner-invalidate-shared-vfs

Conversation

@RomainLanz

Copy link
Copy Markdown
Member

Hey! 👋

When a controller changes in HMR mode, the dev server calls RoutesScanner.invalidate() to recompute the request and response types of its routes. This method created a new VirtualFileSystem for each route bound to the controller. Each instance starts with an empty AST cache, so the same file was read and parsed with ast-grep once per route.

This PR creates the VirtualFileSystem once before the loop, as scan() already does. Each call still creates a new instance, so an invalidation always reads the latest file contents.

Benchmark

Node.js 26.10, single core, public RoutesScanner API: scan(), then time invalidate(controllerPath). Generated controllers with and without VineJS validators. Each variant ran in 5 independent processes with 20 warmups and 40 measured calls. Execution order alternated between variants. Values are the median of the per-process medians.

Controller Routes Before After Parses per call
50 lines 1 3.6 ms 3.5 ms 1 → 1
50 lines 7 8.5 ms 4.4 ms 7 → 1
200 lines 3 17.0 ms 12.0 ms 3 → 1
200 lines 7 41.2 ms 22.2 ms 7 → 1
500 lines 7 90.4 ms 43.0 ms 7 → 1
1000 lines 7 198 ms 59.7 ms 7 → 1

There is no change with 1 route. With 3 routes the call is 1.2× to 2× faster, and with 7 routes 1.7× to 3.3× faster.

In a real app created with create-adonisjs -K=api (HMR mode, Tuyau generateRegistry hook), on a 36-line controller with 7 routes, the registry is up to date 18 ms after the save instead of 28 ms. The HTTP response is not affected: #reScanRoutes is not awaited and runs in the dev server process, so the hot reload takes about 30 ms in both cases. In watch mode, a controller change restarts the server and invalidate() is not called.

For typical controllers this is a small cleanup, not a visible speedup.

Validation

  • getScannedRoutes() is identical before and after the change, for 50 controller configurations (50 to 2000 lines, 1 to 30 routes, with and without validators). Compared after scan(), after invalidate(), and after a file edit followed by invalidate().
  • 295 tests passed
  • Typecheck passed
  • ESLint passed
  • git diff --check passed

Not covered: no new test checks how many times the file is parsed. The existing routes scanner tests cover the output.

RoutesScanner.invalidate created a new VirtualFileSystem for each route
bound to the changed controller. Each instance had an empty AST cache, so
the dev server read and parsed the same controller file once per route on
every save.

Create the VirtualFileSystem once before the loop, as scan() already
does. A fresh instance is still created on each call, so an invalidation
always reads the latest file contents.
@thetutlage
thetutlage merged commit 67c42a0 into 8.x Oct 12, 2026
10 checks passed
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