Repository navigation
Conversation
A registry typically holds many more data loaders than a single dispatchAll() has work for, and graphql-java calls dispatchAll() many times per request. In ticker mode, dispatch only the data loaders whose dispatchDepth() is non-zero; a key queued after the check is picked up by the next tick.
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.
Fixes #292.
In ticker mode,
dispatchAll()ran a fulldispatch()on every registered data loader, including loaders with nothing queued. A request's registry usually holds many more loaders than any onedispatchAll()has work for, and graphql-java callsdispatchAll()many times per request. Most of the work, and roughly 48 bytes of allocation per idle loader per call, went to loaders with empty queues. #292 has the profile data.Change
dispatchAllWithCount()now:dispatchDepth() > 0whose predicate passes.dispatchDepth()is a single atomic read of the queue head.Behaviour
dispatchAll(), every loader whose predicate passes is dispatched at least everyscheduleuntilclose(). A key queued after the depth check is dispatched by the next tick, the same window as a key queued just after a dispatch today.close()semantics from Keep one pending ticker task per ScheduledDataLoaderRegistry #291, and the ticker surviving a throwing predicate.beginDispatchinstrumentation no longer fires for them.lastDispatchTimeis no longer refreshed. The only built-in reader isDispatchPredicate.dispatchIfLongerThan. With it, the first key queued after an idle period now dispatches on the nextdispatchAll(), rather than waitingdurationafter the last empty dispatch. "Time since dispatch" then means time since the last dispatch that did something.Benchmark
This adds
ScheduledDataLoaderRegistryDispatchPerformance(JMH). It builds a ticker-mode registry the way DGS does:DISPATCH_NEVERfor the registry,DISPATCH_ALWAYSper loader, and an hour-long schedule so the ticker never fires during a measurement. Caching is off so every load is queued. Each operation queues one key on N loaders, then callsdispatchAllWithCount().The runs were on JDK 21, Apple M-series, with
-prof gc. Each variant ran twice (one fork, 3×2 s warmup, 5×2 s measurement), interleavingmasterand this branch. Another JMH workload was running on the machine at the same time, so treat the absolute numbers as indicative. The ranges below span both runs.master(da59d1d)With this change, the cost of a
dispatchAll()that has work no longer depends on how many loaders are registered: 2,840 B per call with 20 or 200 loaders. Run the benchmark with./gradlew jmhJar, thenjava -jar build/libs/*-jmh.jar ScheduledDataLoaderRegistryDispatchPerformance -prof gc.End-to-end. We ran a graphql-java 25 workload with DGS-style ticker registries: 200 loaders, a query fanning out to 12 of them over three nested levels, 3 ms simulated I/O, and 48 concurrent requests. With this change applied:
dispatchAll()went from 32.3 µs to 2.5 µs;We've applied the same change as a local patch to our service. Based on the profile in #292, we expect it to remove most of the ~3% of CPU that went to empty dispatches.
Tests
New:
ticker_mode_dispatch_all_only_visits_data_loaders_with_queued_keys. It uses 20 loaders with one queued key, and asserts:Without the
dispatchDepth()check it fails, because the predicate runs for all 20 loaders.The existing ticker tests from Keep one pending ticker task per ScheduledDataLoaderRegistry #291 still pass: one pending task,
rescheduleNow, ticking after a throwing predicate, andclose../gradlew buildon JDK 21 passes.