Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve (and shorten) query polling implementation. #4243

Merged
merged 1 commit into from
Dec 18, 2018

Conversation

benjamn
Copy link
Member

@benjamn benjamn commented Dec 17, 2018

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.
@@ -19,6 +19,5 @@
"src/core/__tests__/fetchPolicies.ts",
"src/data/__tests__/queries.ts",
"src/errors/__tests__/ApolloError.ts",
"src/scheduler/__tests__/scheduler.ts"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This small change is deceptively significant, because it means the scheduler.ts tests are now required to pass tsc type checking.

Copy link
Member

@hwillson hwillson Dec 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why TypeScript has trouble inferring the type of queryManager.pollingInfoByQueryId here (it assumes any).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -537,13 +526,8 @@ export class ObservableQuery<
);
}

if (this.isCurrentlyPolling) {
this.scheduler.stopPollingQuery(this.queryId);
this.isCurrentlyPolling = false;
Copy link
Member Author

@benjamn benjamn Dec 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -662,7 +661,7 @@ export class QueryManager<TStore> {
let transformedOptions = { ...options } as WatchQueryOptions;

return new ObservableQuery<T>({
scheduler: this.scheduler,
queryManager: this,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>(
Copy link
Member Author

@benjamn benjamn Dec 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

@hwillson hwillson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome @benjamn - looks great! My comments here are just comments on your comments, so nothing new to add. LGTM!

@@ -662,7 +661,7 @@ export class QueryManager<TStore> {
let transformedOptions = { ...options } as WatchQueryOptions;

return new ObservableQuery<T>({
scheduler: this.scheduler,
queryManager: this,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -19,6 +19,5 @@
"src/core/__tests__/fetchPolicies.ts",
"src/data/__tests__/queries.ts",
"src/errors/__tests__/ApolloError.ts",
"src/scheduler/__tests__/scheduler.ts"
Copy link
Member

@hwillson hwillson Dec 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right - at some point we should probably get all exclude files tsc ready, and remove them from the tsconfig.test.json.

@benjamn benjamn merged commit b483ee8 into wip-reduce-bundle-size Dec 18, 2018
benjamn added a commit that referenced this pull request Jan 2, 2019
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.
benjamn added a commit that referenced this pull request Jan 17, 2019
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.
@danilobuerger
Copy link
Contributor

Breaks MockedProvider in react-apollo as a public api was removed without major semver bump. See apollographql/react-apollo#2738

benjamn added a commit that referenced this pull request Jan 18, 2019
This reverts commit b4f0c8e.

Should fix apollographql/react-apollo#2738,
until we can find a better solution.
benjamn added a commit that referenced this pull request Jan 19, 2019
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.
benjamn added a commit that referenced this pull request Jan 22, 2019
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.
@benjamn benjamn deleted the simplify-query-polling branch January 22, 2019 18:11
benjamn added a commit that referenced this pull request Jan 22, 2019
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.
benjamn added a commit that referenced this pull request Jan 22, 2019
…removal

Un-revert "Improve (and shorten) query polling implementation. (#4243)"
benjamn added a commit that referenced this pull request May 10, 2019
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.
benjamn added a commit that referenced this pull request May 10, 2019
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.
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants