Draft: perf: build the executable schema once per process - #550
Draft
smartive-nicolai[bot] wants to merge 1 commit into
Draft
Draft: perf: build the executable schema once per process#550smartive-nicolai[bot] wants to merge 1 commit into
smartive-nicolai[bot] wants to merge 1 commit into
Conversation
`execute()` regenerated the SDL, re-merged every resolver and called `makeExecutableSchema` on every operation. All three are pure functions of the models, the additional resolvers and the resolver wrapper — resolvers receive request state as their context argument, so nothing request-scoped is captured — which means the work was repeated per request for no reason. Measured in a consumer with ~80 entity models: ~48 ms of fixed cost per operation (generate 6.7 ms, makeExecutableSchema 37.5 ms, validate 3.9 ms). Six operations for one page render went from 367 ms to 18.6 ms, with byte-identical results. - `createExecutor()` builds the schema explicitly, for consumers that want to pay the cost at boot rather than on the first request, and can be passed to `execute` via `executor`. - `execute()` otherwise caches per process, keyed on the identity of all three inputs. WeakMaps throughout, so a caller passing a freshly created `resolverWrapper` per call falls back to the previous behaviour — a rebuild — and the entry is collected along with the wrapper rather than accumulating. `validate` stays per call, since `introspection` selects rules rather than affecting the schema, so the introspection guard is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
🎉 This PR is included in version 29.3.1-next.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
execute()regenerated the SDL, re-merged every resolver and calledmakeExecutableSchemaon every operation. All three are pure functions of(models, additionalResolvers, resolverWrapper).createExecutor()builds the schema explicitly, for consumers that want to pay the cost at boot rather than on the first request. Pass it toexecuteviaexecutor.execute()otherwise caches per process, keyed on the identity of all three inputs.Why
Measured in
zwei-wealth-platform(~80 entity models), per operation before any SQL runs:generate(models)makeExecutableSchemaparsevalidateBecause the cost is per operation and independent of payload, it dominates: a query doing 1 SQL and returning 30 bytes cost 54 ms. The six operations one page render issues:
executeGraphQLMeasured end-to-end against the consumer with this branch built and linked in, not just in isolation. A control arm still rebuilding the schema per call measured 498 ms in the same run, so the drop is the change and not a warmer host. All six operations returned byte-identical data before and after.
Why the cache is safe
contextValue, never into the schema;getResolvers(models)closes over models only.introspectionselects validation rules, not the schema, sovalidatestays per call and the introspection guard is unchanged.resolverWrapperper call (an inline arrow) falls back to the previous behaviour — a rebuild — and the entry is collected along with the wrapper, so it cannot accumulate. This was the main hazard of a naive identity cache and is covered by a test.Tests
tests/api/execute.spec.ts, 5 cases. The load-bearing one is cross-request isolation: two users, one cached schema, each must get its own data — the failure mode a naive singleton would introduce, and it would be silent. Also covered: schema built once for repeated calls, rebuilt when the wrapper identity changes, a pre-built executor is used as-is, and introspection is still rejected on a cached schema.Verified the cache assertion actually fails without the change (bypassing the cache turns it red), so it is a real regression guard.
Full suite green locally: lint + 164 tests + build.
Note for reviewers
Opened as a draft — I have not marked it ready or merged. Landing this in
zwei-wealth-platformadditionally needs a release plus a bump there, since it pins29.3.0exactly.🤖 Generated with Claude Code