Skip to content

Conversation

biggs0125
Copy link
Contributor

@biggs0125 biggs0125 commented Sep 8, 2025

#3366 seems to be caused by two different blobs getting loaded into the same iframe.

This change uses a Completer to make the 'execute' operation to make it atomic.

The execute command which triggers the blobs to load should be resetting the iframe. However, there is an async gap after the iframe as been reset. During which another 'execute' command can be triggered and the iframe would again synchronously be reset. But both blobs would then be injected into the same iframe breaking some invariants later on.

Also add a timeout to the blocking call just in case an earlier 'execute' never completes. This will still allow the newer call to progress eventually.

@biggs0125 biggs0125 requested a review from devoncarew September 8, 2025 17:37
Copy link

github-actions bot commented Sep 8, 2025

PR Health

Breaking changes ✔️
Package Change Current Version New Version Needed Version Looking good?

This check can be disabled by tagging the PR with skip-breaking-check.

Changelog Entry ✔️
Package Changed Files

Changes to files need to be accounted for in their respective changelogs.

This check can be disabled by tagging the PR with skip-changelog-check.

Coverage ✔️
File Coverage

This check for test coverage is informational (issues shown here will not fail the PR).

This check can be disabled by tagging the PR with skip-coverage-check.

API leaks ✔️

The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.

Package Leaked API symbol Leaking sources

This check can be disabled by tagging the PR with skip-leaking-check.

License Headers ✔️
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
Files
no missing headers

All source files should start with a license header.

Unrelated files missing license headers
Files
pkgs/samples/lib/brick_breaker.dart
pkgs/samples/lib/fibonacci.dart
pkgs/samples/lib/hello_world.dart
pkgs/samples/lib/main.dart
pkgs/samples/lib/sunflower.dart

This check can be disabled by tagging the PR with skip-license-check.

@biggs0125
Copy link
Contributor Author

It's worth noting this only really works for 2 concurrent calls to 'execute'. But this is the only case we're seeing at the moment so this simpler solution should be okay than something more general but more complicated.

@@ -37,11 +38,20 @@ class ExecutionServiceImpl implements ExecutionService {
required bool isNewDDC,
required bool isFlutter,
}) async {
if (_activeExecuteCompleter != null) {
Copy link
Member

@devoncarew devoncarew Sep 8, 2025

Choose a reason for hiding this comment

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

I think we must have a general design issue to allow us request an execution while already performing one. The UI does disable the 'run' button when executing; perhaps this was introduced as a side effect of the genai preview feature? We may want to file an issue to track down how nested executions are possible.

In any case, guarding against it in the execution impl (here) seems reasonable. You're waiting (a bit) for the first execution to finish. I think that's reasonable; you could also just do an early exit if you see that there's a non-null completer (not start a 2nd execution). I don't have an opinion which to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case I think the double execute is from the embedding context. I tried to track down what's triggering the two events but wasn't very successful.

But given there are probably other ways to introduce the same effect, making this atomic seems like a safety measure to avoid these double calls everywhere.

I was worried about the second execute actually containing different code than the first. For example, if the embedding environment sends an "empty" request followed by a real request in quick succession. We likely just want the second one.

Maybe we could throw out the first request, that seems like a safe alternative here. I don't have any strong opinions on that solution vs the one I have here. Do you have a preference?

Copy link
Member

Choose a reason for hiding this comment

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

It would be simplest to do an early exit if _activeExecuteCompleter is non-null and assume that the existing completer will complete correctly in the future, but no, no real preference.

if (!reload) {
await _reset();
}

return _send(reload ? 'executeReload' : 'execute', {
await _send(reload ? 'executeReload' : 'execute', {
Copy link
Member

Choose a reason for hiding this comment

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

Looking at the implementation of _send, this will complete very quickly - it's not doing a lot of computation, loading resources, ... anything expensive. I'm not sure that putting a completer here will actually serialize execution requests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The completer is more for the Future returned by _reset. We have to make sure _send and _reset (specifically the iframe part of _reset) happen in an atomic action.

I agree _send should be very fast so I'm not particularly worried about that one completing in time. But it can introduce an async gap so we still have to include it in the atomic block.

_reset has a timeout so that part should be fine. But if _reset throws then the first call will never complete its completer and the timeout would get triggered. So the timeout is really just a safety measure.

Copy link
Member

Choose a reason for hiding this comment

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

Ah! I missed that this also included the _reset(). You might wrap both the call to _reset, and to _send, in a try/finally, so that the completer gets finished even if reset throws an exception.

@biggs0125 biggs0125 requested a review from devoncarew September 8, 2025 23:17
Copy link
Member

@devoncarew devoncarew left a comment

Choose a reason for hiding this comment

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

lgtm, w/ a suggestion to wrap some code in a try { } finally { } block.

@@ -37,11 +38,20 @@ class ExecutionServiceImpl implements ExecutionService {
required bool isNewDDC,
required bool isFlutter,
}) async {
if (_activeExecuteCompleter != null) {
Copy link
Member

Choose a reason for hiding this comment

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

It would be simplest to do an early exit if _activeExecuteCompleter is non-null and assume that the existing completer will complete correctly in the future, but no, no real preference.

if (!reload) {
await _reset();
}

return _send(reload ? 'executeReload' : 'execute', {
await _send(reload ? 'executeReload' : 'execute', {
Copy link
Member

Choose a reason for hiding this comment

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

Ah! I missed that this also included the _reset(). You might wrap both the call to _reset, and to _send, in a try/finally, so that the completer gets finished even if reset throws an exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants