Drop the per row context copy in navigation resolvers, and page connections with one query - #1406
Merged
Conversation
…ip the connection count when nothing reads it The navigation, navigation list and navigation connection resolvers run once per parent row, and each built a ResolveEfFieldContext by copying every property of the GraphQL.NET context. That forced the lazily computed ones, SubFields, Path, ResponsePath, Parent and Arguments, to be computed and allocated for every row, when the resolver only needs the DbContext and the filters. They now resolve those two directly. A field selected without arguments skips the argument reads and the push down lookup, since GraphQL.NET builds the argument dictionary lazily per field per row. The in memory ids and where predicate of a navigation field, on the path where the arguments are not pushed into the query, is compiled once per request per field rather than once per parent row. A root connection ran a COUNT query on every request. It now runs only when the selection reads it, through totalCount or pageInfo, or when last or before place the window from the end. A selection of only edges or items paged with first and after is a single query. Documented under paging, with tests for each side of the decision. Smaller per request work moved or cached: a projection expression is analyzed once when its field is registered rather than on every request that selects it, the walks over graph type hierarchies are cached per graph type, a selection set of plain fields skips the derived navigation scan, and the filter hierarchy lookup is cached per entity type. Measured with RequestSplitBenchmark on EF InMemory, 100 parents with 5 children each, paired runs. The nested query fell from 2,858us and 3,989KB to 2,761us and 3,924KB; the library's allocation beyond GraphQL.NET and EF fell from about 69KB to about 4KB per request. A where and orderBy on a navigation evaluated in memory, the new FullWithInMemoryArguments benchmark, fell from 4,052us and 4,554KB to 3,311us and 4,144KB.
…onger needs the count A root connection selecting pageInfo without totalCount still ran the COUNT query, since hasNextPage was computed from the count, and that is the selection a Relay client makes on every page. When the count is not needed for totalCount, or to place a window bounded by last or before, the page query now reads one row past first: that row answers hasNextPage and is dropped from the page before the filters run. hasPreviousPage without the count is true when the page starts after the first item and has rows; an empty page past the end reports false, which the Relay specification allows when paging forward. The count query scans every row matching the where, however small the page, where the page query stops after first rows, so the page fetch is one query instead of two, at the cost of one extra row. Documented under paging, with tests for a middle page, the last page and a page past the end.
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.
Per row overhead in the navigation resolvers
Measured with
RequestSplitBenchmark(EF InMemory, 100 parents with 5 children each), the trees the library builds per request are about 0.2% of a request, as recorded before. What remained was per parent row in the navigation, navigation list and navigation connection resolvers: each built aResolveEfFieldContextby copying every property of the GraphQL.NET context, which forced the lazily computedSubFields,Path,ResponsePath,ParentandArgumentsto be computed and allocated for every row, when the resolver only needs the DbContext and the filters.PredicateCache).ProjectionPaths), the walks over graph type hierarchies are cached per graph type, a selection set of plain fields skips the derived navigation scan, and the filter hierarchy lookup is cached per entity type.FullWithInMemoryArguments)The library's allocation beyond GraphQL.NET and EF fell from about 69 KB to about 4 KB per request. Timings carry 5-10% run to run noise on InMemory; the allocations are exact.
One query per connection page
A root connection ran a COUNT query on every request. It now runs only when
totalCountis selected, or whenlastorbeforeplace the window from the end. Otherwise the page query reads one row pastfirst: that row answershasNextPageand is dropped from the page before the filters run.hasPreviousPagewithout the count is true when the page starts after the first item and has rows; an empty page past the end reports false, which the Relay specification allows when paging forward.The count scans every row matching the where, however small the page, where the page query stops after
firstrows. A Relay page fetch, which selectspageInfoand nottotalCount, is one query instead of two, at the cost of one extra row per page.Documented under paging, with tests for items only pages, a page past the end,
last, andpageInfowithouttotalCounton a middle page, the last page and past the end.