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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

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?

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.

@jyameo jyameo requested review from srujzs and biggs0125 May 9, 2025 18:06
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