Improve (and shorten) query polling implementation.#4243
Improve (and shorten) query polling implementation.#4243benjamn merged 1 commit intowip-reduce-bundle-sizefrom
Conversation
This implementation has the following benefits: - It collapses the QueryScheduler abstraction into the QueryManager (which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between the QueryManager and its ObservableQuery objects. - It's about 100 bytes smaller than the previous implementation, after minification and gzip. - It uses setTimeout rather than setInterval, so event loop starvation never leads to a rapid succession of setInterval catch-up calls. - It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval. - Fewer independent timers means better batching behavior, usually. - Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped.
| "src/core/__tests__/fetchPolicies.ts", | ||
| "src/data/__tests__/queries.ts", | ||
| "src/errors/__tests__/ApolloError.ts", | ||
| "src/scheduler/__tests__/scheduler.ts" |
There was a problem hiding this comment.
This small change is deceptively significant, because it means the scheduler.ts tests are now required to pass tsc type checking.
There was a problem hiding this comment.
Right - at some point we should probably get all exclude files tsc ready, and remove them from the tsconfig.test.json.
| queryManager.startPollingQuery(queryOptions, queryId); | ||
|
|
||
| let count = 0; | ||
| queryManager.pollingInfoByQueryId.forEach((info: { interval: number }, qid: string) => { |
There was a problem hiding this comment.
I don't understand why TypeScript has trouble inferring the type of queryManager.pollingInfoByQueryId here (it assumes any).
There was a problem hiding this comment.
TS seems to be able to infer the type when I check on my side, after a full npm run clean; npm i (I'm then able to drop the types in the forEach function signature). Something might just be out synch behind the scenes.
|
|
||
| if (this.isCurrentlyPolling) { | ||
| this.scheduler.stopPollingQuery(this.queryId); | ||
| this.isCurrentlyPolling = false; |
There was a problem hiding this comment.
No need to keep track of isCurrentlyPolling here in ObservableQuery, because the QueryManager tracks that information in pollingInfoByQueryId, and stopping is no longer required before restarting.
|
|
||
| return new ObservableQuery<T>({ | ||
| scheduler: this.scheduler, | ||
| queryManager: this, |
There was a problem hiding this comment.
I hope it's safe to change ObservableQuery constructor parameters like this. From what I can tell, ObservableQuery is used as a type elsewhere, but this is the only place where new instances are created?
There was a problem hiding this comment.
I think it should be safe. We are exporting ObservableQuery, but I doubt anyone is using it externally (for anything other than types) since they'd have to provide their own QueryScheduler instance.
| import { ObservableQuery } from '../../core/ObservableQuery'; | ||
|
|
||
| // Used only for unit testing. | ||
| function registerPollingQuery<T>( |
There was a problem hiding this comment.
This used to be a method of QueryScheduler, even though it was only used for testing. Turning test-only methods into test functions is an easy way to save bundle size.
|
|
||
| return new ObservableQuery<T>({ | ||
| scheduler: this.scheduler, | ||
| queryManager: this, |
There was a problem hiding this comment.
I think it should be safe. We are exporting ObservableQuery, but I doubt anyone is using it externally (for anything other than types) since they'd have to provide their own QueryScheduler instance.
| queryManager.startPollingQuery(queryOptions, queryId); | ||
|
|
||
| let count = 0; | ||
| queryManager.pollingInfoByQueryId.forEach((info: { interval: number }, qid: string) => { |
There was a problem hiding this comment.
TS seems to be able to infer the type when I check on my side, after a full npm run clean; npm i (I'm then able to drop the types in the forEach function signature). Something might just be out synch behind the scenes.
| "src/core/__tests__/fetchPolicies.ts", | ||
| "src/data/__tests__/queries.ts", | ||
| "src/errors/__tests__/ApolloError.ts", | ||
| "src/scheduler/__tests__/scheduler.ts" |
There was a problem hiding this comment.
Right - at some point we should probably get all exclude files tsc ready, and remove them from the tsconfig.test.json.
This implementation has the following benefits: - It collapses the QueryScheduler abstraction into the QueryManager (which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between the QueryManager and its ObservableQuery objects. - It's about 100 bytes smaller than the previous implementation, after minification and gzip. - It uses setTimeout rather than setInterval, so event loop starvation never leads to a rapid succession of setInterval catch-up calls. - It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval. - Fewer independent timers means better batching behavior, usually. - Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped.
This implementation has the following benefits: - It collapses the QueryScheduler abstraction into the QueryManager (which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between the QueryManager and its ObservableQuery objects. - It's about 100 bytes smaller than the previous implementation, after minification and gzip. - It uses setTimeout rather than setInterval, so event loop starvation never leads to a rapid succession of setInterval catch-up calls. - It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval. - Fewer independent timers means better batching behavior, usually. - Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped.
|
Breaks |
This reverts commit b4f0c8e. Should fix apollographql/react-apollo#2738, until we can find a better solution.
This reverts commit 9739ff6. Now that we have a uniform interface for terminating ApolloClient instances (#4336), there should be no need for any external code to access the QueryScheduler abstraction, which this commit removes. We should wait to merge and release this change until after apollographql/react-apollo#2741 has been merged and released, so that we don't break older versions of MockedProvider.
This reverts commit 9739ff6. Now that we have a uniform interface for terminating ApolloClient instances (#4336), there should be no need for any external code to access the QueryScheduler abstraction, which this commit removes. We should wait to merge and release this change until after apollographql/react-apollo#2741 has been merged and released, so that we don't break older versions of MockedProvider.
This reverts commit 9739ff6. Now that we have a uniform interface for terminating ApolloClient instances (#4336), there should be no need for any external code to access the QueryScheduler abstraction, which this commit removes. We should wait to merge and release this change until after apollographql/react-apollo#2741 has been merged and released, so that we don't break older versions of MockedProvider.
…removal Un-revert "Improve (and shorten) query polling implementation. (#4243)"
The last time I touched this code in #4243, I thought it was worthwhile to preserve the behavior of sometimes batching polling query fetches in the same tick of the event loop. However, as @voxtex points out in #4786, almost any batching strategy will lead to unpredictable fetch timing, skipped fetches, poor accounting for fetch duration (which could exceed the polling interval), and so on. On top of that, even the premise that "batching is good" comes into question if it doesn't happen consistently. An implementation which uses independent timers seems much simpler and more predictable.
The last time I touched this code in #4243, I thought it was worthwhile to preserve the behavior of sometimes batching polling query fetches in the same tick of the event loop. However, as @voxtex points out in #4786, almost any batching strategy will lead to unpredictable fetch timing, skipped fetches, poor accounting for fetch duration (which could exceed the polling interval), and so on. On top of that, even the premise that "batching is good" comes into question if it doesn't happen consistently. An implementation which uses independent timers seems much simpler and more predictable.
This implementation has the following benefits:
It collapses the
QuerySchedulerabstraction into theQueryManager(which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between theQueryManagerand itsObservableQueryobjects.It's about 100 bytes smaller than the previous implementation, after minification and gzip.
It uses
setTimeoutrather thansetInterval, so event loop starvation never leads to a rapid succession ofsetIntervalcatch-up calls.It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval.
Fewer independent timers means better batching behavior, usually.
Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped.