Skip to content

Support hot reload over websocket #2616

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

Merged
merged 12 commits into from
May 12, 2025
Merged

Support hot reload over websocket #2616

merged 12 commits into from
May 12, 2025

Conversation

jyameo
Copy link
Contributor

@jyameo jyameo commented May 6, 2025

  • Added WebSocket-based hot reload support: reloadSources in ChromeProxyService and DevHandler can now handle hot reload requests and responses over WebSockets (protected by bool flag useWebSocket).
  • Refactored the injected client to use a reusable function for handling hot reload requests and responses over WebSockets.

Related to #2605

@jyameo jyameo requested review from srujzs and biggs0125 May 6, 2025 15:45
Copy link
Contributor

@srujzs srujzs left a comment

Choose a reason for hiding this comment

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

Thanks Jessy! This generally looks good % my comment about requests coming in too quick.


/// Pending hot reload requests waiting for a response from the client.
/// Keyed by the request ID.
final _pendingHotReloads = <String, Completer<HotReloadResponse>>{};
Copy link
Contributor

Choose a reason for hiding this comment

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

The way the hot reload works in the client is it looks at the hotReloadSourcesUri which contains all the changed files and their libraries and then does an XHR. If whatever app that calls DWDS sends requests too quickly, it may be a possible that a hot reload request reads a version of the file that is no longer the correct version.

For example, the app sends a request and then sets the data in that URI. The app sends a second request with new data in that URI before the first request finishes. Then the first request is reading the new data 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.

I see. This makes sense. If I add a request/response mechanism for fetchLibrariesForHotReload, similar to how I handle HotReloadRequest with a Completer, would that help resolve the possible race condition you described? Essentially by sending a FetchLibrariesForHotReloadRequest and waiting for its response (using a Completer), we can ensure that the client fetches the correct set of changed libraries before proceeding to the hot reload step. What do you think about this approach?

Copy link
Contributor

@srujzs srujzs May 7, 2025

Choose a reason for hiding this comment

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

I think we want to have the completer wait for both fetchLibrariesForHotReload and hotReload. The former may return the right paths now, but those paths may be replaced with new compiled files before hotReload gets to execute if we don't wait for hotReload as well.

But I believe that should work. This more or less "blocks" the request queue until the previous request is fully finished.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry Jessy, I may have led you astray here and we should discuss this a bit more before we settle on an implementation.

I don't think the current code makes much of a difference as we just split the requests into two.

Going back to the original code you had, I'm realizing we just await the completer we created to finish before we return from reloadSources. This is a little confusing to me, as that means we're waiting for the hot reload to finish for every request. In that case, we don't really need a map of completers, we could just have one pending completer. There's also no race condition here because there's only one request ever being processed.

I'm assuming this isn't what we want, though, and we do want DWDS to handle multiple requests coming in without blocking (but maybe not, in which case the original code works!). In the Flutter tools case, we always await reloadSources to finish when a reload request comes in. How does the workflow look like for Firebase Studio?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries. Using a single completer instead of a map does make sense here.

In DWDS, the VmServerConnection is the component that receives VM service protocol requests and delegates them to the vmServiceInterface implementation, which in our case is the ChromeProxyService. There, we also await the reloadSources call to complete before moving on. Additionally, the ext.flutter.reassemble service extension is invoked after reloadSources has finished. All the requests are coming from Flutter Tools.

Regarding whether we want to support multiple requests without blocking. I’m not entirely sure. So far, I haven’t run into any race conditions, so it’s unclear if we actually need to handle concurrent requests. That said, it’s something we can keep an eye on as we flesh things out further.

I'm assuming Firebase Studio will behave similarly, but we’re currently unable to test that flow until we complete the implementation of the WebSocketProxyService, which will act as the alternative vmServiceInterface in that environment.

Let me know if you’d like me to revert to the previous code and apply the change to use a single completer instead or we could setup a call next week to discuss this further. Happy to update accordingly 🙂

Copy link
Contributor

@srujzs srujzs May 9, 2025

Choose a reason for hiding this comment

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

So far, any hot reload requests have been blocking with Flutter tools, so we don't really need to worry about multiple requests overlapping (at least within DWDS).

In the future, for Firebase Studio, if we do see that we'd like to be able to handle multiple calls to hot reload within DWDS, then adding back the map makes sense, and we'll have to make sure that the previous request completed before processing a new one by awaiting the previous request's completer before sending a new request over the socket. It looks like that isn't the current case though, so my preference would be to just revert to the code you had earlier as it's simpler and we don't need to separate the two requests. The only change we should have is a single completer instead of a map.

Sorry again for the churn, I saw the map and thought we wanted to have multiple concurrent requests within DWDS.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries at all, this makes sense. I’ll go ahead and make those changes and simplify things as suggested. Thanks for the clarification!

final requestId = event.id;
try {
// Execute the hot reload.
await manager.fetchLibrariesForHotReload(hotReloadSourcesPath);
Copy link
Contributor

Choose a reason for hiding this comment

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

Based on the comments above it seems like these both need to happen before we 'ack' the hot reload request. I'm not clear on a couple things:

  1. Why do these need to be 2 separate functions if we need to make sure they're called together anyway?
  2. Once the future from fetchLibrariesForHotReload completes the files are loaded into the page and we have an ID associated with the request so we can avoid mixing up files. Why do we need to wait for the client to finish applying the hot reload patch (i.e. calling hotReload) to 'ack' the server and unblock it to process further requests?

cc @srujzs

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. In the non-websocket case, we use the list of libraries to disable breakpoints before we continue the hot reload. Doing this after the full hot reload means we may come across a previous breakpoint (maybe in a captured closure?) before we have the chance to disable all the breakpoints.

Once breakpoints work for hot reload, we'll definitely need this to be two methods so that the debugger can place breakpoints in the newly loaded libraries before they're added to the runtime (between hotReloadStart and hotReloadEnd).

  1. the files are loaded into the page

Not yet. This just fetches the list, but doesn't load the files into the page until a later call to hotReload. Once breakpoints work, that will be true though.

and we have an ID associated with the request so we can avoid mixing up files. Why do we need to wait for the client to finish applying the hot reload patch (i.e. calling hotReload) to 'ack' the server and unblock it to process further requests?

We don't pipe that ID to the DDC embedder though. I'm imagining a scenario where the first request fetches the list, maybe even loads the files onto the page, but before we call hotReloadEnd, the second request also fetches its list and loads the files onto the page, essentially clobbering the information in the embedder that we need for the first hotReloadEnd. I believe that's possible due to the async boundaries. We could have an ID associated with the request within the embedder and it could do some of that state management, but that doesn't exist today. We rate-limited hot restart requests in the past (_hotRestartRunIdCache), so we may be able to do something similar.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, from experience so far it seems like hot reloads tend to occur fast enough. So sequentializing the hot reloads like this is probably okay. I'm curious if that'll still be the case with more network heavy workflows like those involving Firebase Studio. We should keep an eye on this to make sure the experience remains smooth for developers.

@jyameo jyameo requested review from srujzs and biggs0125 May 9, 2025 18:06
@jyameo jyameo closed this May 12, 2025
@jyameo jyameo reopened this May 12, 2025
Copy link
Contributor

@srujzs srujzs left a comment

Choose a reason for hiding this comment

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

LGTM, thanks Jessy!

Copy link
Contributor

@biggs0125 biggs0125 left a comment

Choose a reason for hiding this comment

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

LGTM!

@jyameo jyameo merged commit 5dbb30e into dart-lang:main May 12, 2025
43 of 47 checks passed
copybara-service bot pushed a commit to dart-lang/sdk that referenced this pull request May 17, 2025
Revisions updated by `dart tools/rev_sdk_deps.dart`.

dartdoc (https://github.com/dart-lang/dartdoc/compare/95f4208..e38f392):
  e38f3921  2025-05-16  Sigurd Meldgaard  Remove analyzer_use_new_elements from analysis_options.yaml (dart-lang/dartdoc#4050)

protobuf (https://github.com/dart-lang/protobuf/compare/9bd149b..b7753f6):
  b7753f6  2025-05-16  Devon Carew  rev package:protobuf version (google/protobuf.dart#994)
  b5d20ff  2025-05-16  Ömer Sinan Ağacan  Update protobuf changelog with `#981` (google/protobuf.dart#995)

test (https://github.com/dart-lang/test/compare/55d1f9e..b9c59ea):
  b9c59ea0  2025-05-13  Liam Appelbe  Set a debug name for test isolates (dart-lang/test#2494)
  a1e295b4  2025-05-13  Liam Appelbe  Fix CI
  3c3878af  2025-05-13  Liam Appelbe  Include the test URI in the debug name
  90e64ec2  2025-05-13  Nate Bosch  Bump version
  d67c897b  2025-05-13  Liam Appelbe  Merge branch 'master' into isolate_debug_name
  e6d4877e  2025-05-12  Jacob MacDonald  release test packages (dart-lang/test#2495)
  4097e1be  2025-05-07  Liam Appelbe  revert workflow debugging
  7800c010  2025-05-07  Liam Appelbe  fmt
  455483b5  2025-05-07  Liam Appelbe  changelog
  c9b5b6fa  2025-05-07  Liam Appelbe  fmt
  39c4b31d  2025-05-07  Liam Appelbe  Set a debug name for test isolates

vector_math (https://github.com/google/vector_math.dart/compare/0279cb8..13f185f):
  13f185f  2025-05-16  Kevin Moore  Remove prefer-inline for non-trivial Matrix functions (google/vector_math.dart#347)

webdev (https://github.com/dart-lang/webdev/compare/1ea8462..5dbb30e):
  5dbb30eb  2025-05-12  Jessy Yameogo  Support hot reload over websocket (dart-lang/webdev#2616)

Change-Id: I85b001857d0864bd50390d82aa142938a4c530d4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/428927
Commit-Queue: Devon Carew <[email protected]>
Reviewed-by: Kevin Moore <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants