Skip to content

Conversation

@DanTup
Copy link
Contributor

@DanTup DanTup commented Apr 22, 2020

Description

@devoncarew @jonahwilliams a while back, we discussed the idea of pushing debouncing reload requests into flutter_tool rather than editors doing it. Right now VS code is adding a 200ms debounce which can easily double the hot reload time (or worse).

By moving it into the tool we might be able to speed things up significantly (for example if always applied the first reload immediately, then queue/debounce subsequent ones.. this might result in 2 hot reloads for some Save All operations, but could also slash the time of the common single-file save case).

This PR doesn't immediately apply the first one, but does add debounce support (and reduce it to 50ms - though we may want to play with that) and support queueing (instead of just rejecting) and merging (if a request is queued, just return the same result).

It was a bit more complicated than I expected, because we can't merge requests of different types (reload, restart, reloadMethod).

This isn't fully tested yet, but I wanted to start a discussion about it before going too far. WDYT?

Tests

I added the following tests:

  • Multiple quick reload requests are merged together
  • TODO: Needs more

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Did any tests fail when you ran them? Please read Handling breaking changes.

  • No, no existing tests failed, so this is not a breaking change.
  • Yes, this is a breaking change. If not, delete the remainder of this section.
    • I wrote a design doc: https://flutter.dev/go/template Replace this with a link to your design doc's short link
    • I got input from the developer relations team, specifically from: Replace with the names of who gave advice
    • I wrote a migration guide: Replace with a link to your migration guide

@fluttergithubbot fluttergithubbot added tool Affects the "flutter" command-line tool. See also t: labels. work in progress; do not review labels Apr 22, 2020
@jonahwilliams
Copy link
Contributor

Sorry, it took me a while to get to this.

Overall I love the idea of removing delays around hot reload. In terms of the implementation, doing in the daemon seems like the most reasonable approach, since ultimately this is only required for IDE consumption.

In terms of the debounce mechanics - why exactly do we need to debounce instead of say, queue? Is it due to multiple events created from saveAll actions?

@DanTup
Copy link
Contributor Author

DanTup commented May 5, 2020

In terms of the debounce mechanics - why exactly do we need to debounce instead of say, queue? Is it due to multiple events created from saveAll actions?

Yep! Unfortunately in VS Code we don't have any "Save All" event (request is at microsoft/vscode#86087) so our only options are to either debounce on the client (which we do now) or sent multiple events to Flutter (which we could do if we move this here).

My thinking is that by moving it here:

  • it simplifies things on the client (including if multiple reload events come from other sources than save-all, like the user triggering the command, double-clicking buttons, etc.)
  • allows Flutter to control the timeout
  • if Flutter wanted to do something different (eg. don't debounce (or have a smaller timeout for) the initial request in a batch to reduce latency in the common case where there's only a single event - at the expense of a Save All maybe doing two reloads), that's simpler here

@DanTup
Copy link
Contributor Author

DanTup commented Jun 1, 2020

@jonahwilliams discussing reload times reminded me of this - having the debounce here would allow removing it from the editors so you have more control (though this does make Flutter responsible for merging multiple saves from a Save All from an editor like VS code that doesn't have an event for this).

@jonahwilliams
Copy link
Contributor

Lets do it!

@DanTup
Copy link
Contributor Author

DanTup commented Jun 1, 2020

@jonahwilliams Ok cool! Do you want to scan over the approach here and see if it seems reasonable? It needs some more tests, though I'm not sure how to handle them without adding these delays in the test (since what we're testing is timebased, and an integration tests so we can't easily interfere with the passing of time).

@jonahwilliams
Copy link
Contributor

Yeah, I'll set aside some time tomorrow to do a deep dive on this. For testing with timeouts, probably a combination of real integration tests as well as fake async.

@jonahwilliams jonahwilliams self-requested a review June 1, 2020 18:51
@devoncarew
Copy link
Contributor

Hey, from the IntelliJ perspective:

With that change, IntelliJ has two usage patterns:

  • listen for save all actions from the user and issue a hit reload request
  • react to a 'hot reload' action performed by the user; here, we programmatically save all modified files and issue a save all request

For both usage patterns, IntelliJ will be modifying source files on disk and issuing an immediate hot reload request, w/ no artificial delay. If flutter tools relies solely on file watching to know about changed files, we'll likely have a race condition between when it gets a reload request and when it knows about changed files.

We may want to either 1) have flutter tools poll for file changes before performing a hot reload 2) have flutter tools be in charge of an (100ms?) artificial delay before it performs a hot reload, in order to collect any file watching events.

@devoncarew
Copy link
Contributor

If flutter tools relies solely on file watching to know about changed files, we'll likely have a race condition between when it gets a reload request and when it knows about changed files.

From some brief trial and error locally, we may not actually have a race condition, or if we do, we're always wining it. I always see my changes active on reload even without an artificial delay.

@jonahwilliams
Copy link
Contributor

We don't use file watching in the tool, instead preferring to manually check timestamps after a request. Timestamp granuality isn't great on windows, but as long as the is a ms or two of delay between saving and the request it should be fine

@DanTup
Copy link
Contributor Author

DanTup commented Jun 2, 2020

Timestamp granuality isn't great on windows, but as long as the is a ms or two of delay between saving and the request it should be fine

Ooooooh.... slightly off-topic but I've seen a few issues in the past where people said they were seeing stale content after a reload (like the n-1 content was reloaded) and if just occurred to me that this might be related to FAT file times..

The granularity for FAT modified time is a whole two seconds (!). Presumably Flutter is looking at files "modified since the last reload" so it seems like this should work fine as long as the change was flushed to disk before Flutter read it.

However, on a second reload - depending on how the FAT write time is rounded, maybe we might think a save that happened just after the last reload actually happened before it (and therefore was already included)?

So would it makes sense for Flutter to reload any changes made "since two seconds before the last reload"? There's a chance that will include changes that were already included in the previous reload, but it seems better to overlap and include slightly too much than too little? (it's possible this is already done and it's unrelated to the issue, but it felt plausible!).

@DanTup
Copy link
Contributor Author

DanTup commented Jun 2, 2020

Well, it seemed plausible (the original case was #32573 btw), but apparently FAT is cleverer(?) than I, as it always rounds up:

https://devblogs.microsoft.com/oldnewthing/20140903-00/?p=83

So probably not the source of that issue!

as long as the change was flushed to disk before Flutter read it

That's probably also not the current source, since both editors have had fairly large (> 100ms) built-in delays up until this point.

Edit: Though this commenter has it only at the terminal (no artificial delay) and not from the editor, so maybe related? 🤷‍♂️

Copy link
Contributor

@jonahwilliams jonahwilliams left a comment

Choose a reason for hiding this comment

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

Generally like the idea. It would be nice if debounce logic was structured so that we could get unit test coverage of it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think real delays are fine for an integration test

Copy link
Contributor

Choose a reason for hiding this comment

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

I would use Map.remove here and below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! (it required adding unawaited() around it as it returns the item being removed which was a future).

Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be more easily represented as an enum?

enum OperationType {
  reload,
  restart,
  ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, done!

Copy link
Contributor

Choose a reason for hiding this comment

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

When would thus be null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

debounce? It won't be null, but could be false. Right now the client has to opt-in to this debounce behaviour, since we don't want to result in two lots of delays for existing clients that don't know about this. If they don't send debounce=true we'll execute the restart/reload immediately.

@devoncarew
Copy link
Contributor

We don't use file watching in the tool, instead preferring to manually check timestamps after a request. Timestamp granuality isn't great on windows, but as long as the is a ms or two of delay between saving and the request it should be fine

OK, good to know, and comforting that there aren't any race conditions.

It looks like IntelliJ won't need debouncing for hot reload requests, so hopefully the debouncing can be an opt-in parameter for clients (or, the debounce period can be a parameter, and will default to 0ms).

@DanTup
Copy link
Contributor Author

DanTup commented Jun 30, 2020

@devoncarew

hopefully the debouncing can be an opt-in parameter for clients (or, the debounce period can be a parameter, and will default to 0ms).

It's currently opt-in here. You have to pass debounce: true to get this (otherwise it'll use 0ms). This is to avoid changing anything for clients already doing their own.

@jonahwilliams

It would be nice if debounce logic was structured so that we could get unit test coverage of it.

Yeah, I'll take a look and see what I can come up with. Sorry for the delay!

@DanTup
Copy link
Contributor Author

DanTup commented Jun 30, 2020

@jonahwilliams to make it testable, I pulled the implementation out into its own class:

DanTup@8a2e87f

I've written some tests and they pass when running real-time, but I'm stuck on making them work with FakeAsync (I think it's because of this behaviour -> https://stackoverflow.com/questions/62656200/why-does-this-test-using-fakeasync-just-hang-on-the-await-even-though-the-futu). If I can get that working properly, does it seem like a reasonable solution?

@DanTup DanTup marked this pull request as ready for review July 1, 2020 09:53
Comment on lines 358 to 371
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jonahwilliams this is a workaround for the issue described #60675. I don't know if it's the best way, or if we should pull this out somewhere and make it more reusable for the other tests that might have this issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like a handy feature, you could definitely put it into one of the common testing libraries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved it to test/src/common.dart since none of the others seemed appropriate, but lmk if somewhere else (or a new one) would be better. This might make it easy to fix #60675.

@DanTup
Copy link
Contributor Author

DanTup commented Jul 1, 2020

Pushed my changes into this PR, including a fix for the unit tests (unsure if there's a better one though). PTAL!

@DanTup
Copy link
Contributor Author

DanTup commented Jul 1, 2020

I'm not sure what's up with the analyze bot:

▌10:02:44▐ RUNNING: cd .; bin/flutter analyze --dartdocs --flutter-repo
Package "path_provider_linux" has conflicts:
  1 source wants "/root/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.0.1+2/lib":
    /tmp/flutter sdk/dev/benchmarks/macrobenchmarks/.packages
  1 source wants "/root/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.0.1+1/lib":
    /tmp/flutter sdk/dev/integration_tests/android_views/.packages
Package "url_launcher_web" has conflicts:
  1 source wants "/root/.pub-cache/hosted/pub.dartlang.org/url_launcher_web-0.1.2/lib":
    /tmp/flutter sdk/dev/benchmarks/macrobenchmarks/.packages
  1 source wants "/root/.pub-cache/hosted/pub.dartlang.org/url_launcher_web-0.1.1+6/lib":
    /tmp/flutter sdk/dev/integration_tests/flutter_gallery/.packages

@jonahwilliams
Copy link
Contributor

The path_provider_linux bug was fixed a few days ago, there was a bug in update_packages

Copy link
Contributor

Choose a reason for hiding this comment

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

you can remove reloadMethod - I'm going to be building this functionality into the regular hot reload instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That method is currently published in the docs so removing it may be a breaking change? (I don't know who would've used it though)

Copy link
Contributor

Choose a reason for hiding this comment

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

I would be surprised if anyone is using it, but it could be mapped to hot reload for backwards compat. Doesn't need to be solved in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sgtm - I'll do in a separate PR once this one's done!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was taking a look at removing reloadMethod, but it looks like there is a lot of code relating to this (for ex. arguments passed to device.connect and VM service calls in vmservice.dart. It's not clear how much of this should go (whether it's just the daemon call, or everything related to it everywhere).

If it's easier for you to do this when you're building that functionality into regular hot reload, I'm happy to leave it for you 😄


try {
await Future.wait<void>(<Future<void>>[
_flutter.hotReload(debounce: true),
Copy link
Contributor

Choose a reason for hiding this comment

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

there might be enough latency on CI that we don't hit the 50ms debounce. You could make it configurable (only for the purpose of testing)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code is interacting with a flutter run instance, so I don't think that would be easy.

Is your concern that we might see > 50ms between the two "simultaneous" reloads, or that we might see < 50ms between the first set and the second set? (I think we could address the second issue by chaining the 60ms delay onto the end of the initial reloads.. not sure about the first though).

Copy link
Contributor

Choose a reason for hiding this comment

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

The former. It could be configurable via some parameter passed to flutter daemon that is hidden?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've pushed changes that add an argument to override the debounce duration (it's not so much hidden as undocumented). I set the value to 250ms for the test, but let me know if you think it needs to be higher (I'm not sure what a good balance is between avoiding flakes and not making too many slow tests!).

Copy link
Contributor

Choose a reason for hiding this comment

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

250 ms should be good enough for now, but if there are problems with flakiness we can always crank it higher

@DanTup
Copy link
Contributor Author

DanTup commented Jul 14, 2020

I'm temporarily committing with logging turned on to try and debug the failing test on Windows... It's failing in the opposite way to I'd expect from flakes... < 2 hot reloads rather than > 2! May be that + 10ms isn't enough to separate the two sets of requests (in which case I might do * 2!)

@DanTup
Copy link
Contributor Author

DanTup commented Jul 15, 2020

I've removed the debug logging and increased the time between the reloads we want to be grouped together, and those that don't. Let me know if there's anything else, or if you're happy for me to land. Thanks!

Copy link
Contributor

@jonahwilliams jonahwilliams left a comment

Choose a reason for hiding this comment

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

LGTM

@fluttergithubbot fluttergithubbot merged commit 51fcf8f into flutter:master Jul 15, 2020
DanTup added a commit that referenced this pull request Jul 15, 2020
This was testing that concurrent reloads were rejected. This is now allowed (as of #55376 (which has non-devicelab integration + unit tests).
@DanTup DanTup deleted the hot-reload-debounces branch July 22, 2020 17:46
Pragya007 pushed a commit to Pragya007/flutter that referenced this pull request Aug 11, 2020
Pragya007 pushed a commit to Pragya007/flutter that referenced this pull request Aug 11, 2020
This was testing that concurrent reloads were rejected. This is now allowed (as of flutter#55376 (which has non-devicelab integration + unit tests).
mingwandroid pushed a commit to mingwandroid/flutter that referenced this pull request Sep 6, 2020
mingwandroid pushed a commit to mingwandroid/flutter that referenced this pull request Sep 6, 2020
This was testing that concurrent reloads were rejected. This is now allowed (as of flutter#55376 (which has non-devicelab integration + unit tests).
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jul 30, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants