perf(strawberry-shake): index type descriptor lookups to remove O(N²) mapping#20
Open
cliedeman wants to merge 1 commit into
Open
perf(strawberry-shake): index type descriptor lookups to remove O(N²) mapping#20cliedeman wants to merge 1 commit into
cliedeman wants to merge 1 commit into
Conversation
…) mapping TypeDescriptorMapper scaled super-linearly with operation count (profiled at ~1.9s for 200 ops and ~4.3s for 275 ops on a 672-op client). Three field resolutions did linear scans over collections that grow with every operation: - AddProperties -> GetFieldTypeDescriptor looped over every operation and then did typeDescriptors.Values.First(t => t.Model == fieldType), an O(types) reference scan per field. Profiling showed each call scanned ~110-160 operations on average (1.4M-3.4M operation scans total), the dominant cost. - AddInputTypeProperties -> GetInputTypeDescriptor did typeDescriptors.Values.First(t => t.Model.Type.Name == ...) per input field. Fix: build dictionaries once per Map call and resolve in constant time. - descriptorsByModel maps each output type model to its descriptor (reference keyed), replacing the First() scan in the operation-scanning fallback. - interfaceDescriptorsBySelectionSet maps an interface output type's selection set node to its descriptor. A field whose result is an interface is resolved directly by the selection set its syntax node points at, which uniquely identifies the registered interface descriptor and avoids the per-field operation loop entirely. The original loop is retained as a fallback for any field whose selection set is not in the index, so behavior is unchanged. - descriptorsByTypeName maps input descriptors by GraphQL type name (first insertion wins, mirroring the prior First() enumeration order). Selection set nodes and output type models are compared by reference (the analyzer interns selection sets; models have no custom equality), so the indexes return the identical descriptor instances the scans returned. Correctness: generated output is byte-identical. Verified the broad test slice (StarWars*, ResultType*, Operation*, Entity*, DataType*) produces exactly the same pass/fail set and byte-identical generated sources with and without the change (identical SHA over all generated files). Before/after TypeDescriptorMapper.Map on the 672-op ILOPS client: N=200: 1933ms -> 454ms (4.3x) N=275: 4307ms -> 435ms (9.9x), now flat with operation count Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Root cause
TypeDescriptorMapper(StrawberryShake C# generator) scaled super-linearly with operation count: profiled at ~1.9s for 200 ops and ~4.3s for 275 ops on a real 672-op client. Three field resolutions performed linear scans over collections that grow with every operation:AddProperties→GetFieldTypeDescriptorlooped over every operation and then didtypeDescriptors.Values.First(t => t.Model == fieldType), an O(types) reference scan per field. Profiling showed each call scanned ~110–160 operations on average (1.4M–3.4M operation scans in total) — the dominant cost.AddInputTypeProperties→GetInputTypeDescriptordidtypeDescriptors.Values.First(t => t.Model.Type.Name == …)per input field.Fix
Build dictionaries once per
Mapcall and resolve in constant time:descriptorsByModelmaps each output type model to its descriptor (reference keyed), replacing theFirst()scan in the operation-scanning fallback.interfaceDescriptorsBySelectionSetmaps an interface output type's selection-set node to its descriptor. A field whose result is an interface is resolved directly by the selection set its syntax node points at, which uniquely identifies the registered interface descriptor — eliminating the per-field operation loop. The original loop is retained as a fallback for any field whose selection set is not in the index, so behavior is unchanged.descriptorsByTypeNamemaps input descriptors by GraphQL type name (first insertion wins, mirroring the priorFirst()enumeration order).Selection-set nodes and output type models are compared by reference (the analyzer interns selection sets; the models define no custom equality), so the indexes return the identical descriptor instances the scans returned.
Correctness (generated output byte-identical)
Verified the broad test slice (
StarWars*,ResultType*,Operation*,Entity*,DataType*) on this machine produces the exact same pass/fail set and byte-identical generated sources with and without the change (identical SHA over all generated files). An empirical equivalence check over the full 672-op client confirmed the selection-set index returns the same descriptor as the operation-scanning loop for all 33,781 field resolutions (0 misses, 0 mismatches). The 51 pre-existing snapshot failures on this machine are an unrelated BOM/encoding environment issue present onmain; the change neither adds nor removes any.Before / after —
TypeDescriptorMapper.Map(672-op ILOPS client)The mapper is now flat with operation count instead of super-linear.
Relates to #17
— Claude
🤖 Generated with Claude Code