From d7f8265bede089f31e2f6b49881448e2bb83b168 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Thu, 9 Jul 2026 14:08:58 -0700 Subject: [PATCH 1/4] Prevent GC of value during async eval --- packages/devtools_app_shared/CHANGELOG.md | 1 + .../lib/src/service/eval_on_dart_library.dart | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index b2529769d62..7dadf83d6ea 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md @@ -6,6 +6,7 @@ found in the LICENSE file or at https://developers.google.com/open-source/licens ## 0.5.2-wip * Fix a `RangeError` thrown by `SplitPane` when the number of children changes between rebuilds. +* Fix garbage collection issues with the result list in `asyncEval` on both native VM and web. * The minimum Dart SDK version is bumped to 3.11.0. * The minimum Flutter SDK version is bumped to 3.41.0. diff --git a/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart b/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart index 3de4f8608ec..a772c303dec 100644 --- a/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart +++ b/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart @@ -429,6 +429,20 @@ class EvalOnDartLibrary extends DisposableController await safeEval( '() async {' ' final reader = widgetInspectorService.toObject("$readerId", "$readerGroup") as List;' + ' // Keep a strong reference to `reader` in the target app to prevent it' + ' // from being garbage collected by Chrome/VM before the future resolves.' + ' // Without this, the reader is only weakly referenced by the inspector' + ' // service and is aggressively GCed, causing a TypeError/TimeoutException' + ' // or failing the assertion that the retrieved result length is 1 or 2.' + ' bool isDone = false;' + ' int ticks = 0;' + ' Timer.periodic(const Duration(milliseconds: 50), (timer) {' + ' final _ = reader;' + ' // Stop pinning after a 5-second buffer when the future has completed.' + ' if (isDone && ++ticks > 100) {' + ' timer.cancel();' + ' }' + ' });' ' try {' // Cast as dynamic so that it is possible to await Future ' dynamic result = ($expression) as dynamic;' @@ -437,6 +451,7 @@ class EvalOnDartLibrary extends DisposableController ' reader.add(err);' ' reader.add(stack);' ' } finally {' + ' isDone = true;' ' postEvent("future_completed", {"future_id": $futureId, "client_id": $_clientId});' ' }' '}()', From ba981d3d5a05a763c61fc3393257aa47c8ecebf1 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Sat, 11 Jul 2026 12:01:23 -0700 Subject: [PATCH 2/4] fix --- .../lib/src/service/eval_on_dart_library.dart | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart b/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart index a772c303dec..bf6e72beecc 100644 --- a/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart +++ b/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart @@ -5,15 +5,15 @@ // This code is directly based on src/io/flutter/inspector/EvalOnDartLibrary.java // If you add a method to this class you should also add it to EvalOnDartLibrary.java import 'dart:async'; -import 'dart:core' hide Error; import 'dart:core' as core; +import 'dart:core' hide Error; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; -import 'package:vm_service/vm_service.dart' hide Error; import 'package:vm_service/vm_service.dart' as vm_service; +import 'package:vm_service/vm_service.dart' hide Error; import '../utils/auto_dispose.dart'; import 'service_manager.dart'; @@ -429,20 +429,23 @@ class EvalOnDartLibrary extends DisposableController await safeEval( '() async {' ' final reader = widgetInspectorService.toObject("$readerId", "$readerGroup") as List;' - ' // Keep a strong reference to `reader` in the target app to prevent it' - ' // from being garbage collected by Chrome/VM before the future resolves.' - ' // Without this, the reader is only weakly referenced by the inspector' - ' // service and is aggressively GCed, causing a TypeError/TimeoutException' - ' // or failing the assertion that the retrieved result length is 1 or 2.' + ' /* Keep a strong reference to `reader` in the target app to prevent it' + ' from being garbage collected by Chrome/VM before the future resolves.' + ' Without this, the reader is only weakly referenced by the inspector' + ' service and is aggressively GCed, causing a TypeError/TimeoutException' + ' or failing the assertion that the retrieved result length is 1 or 2.' + ' We use Future.delayed in a loop instead of Timer because Future is in' + ' dart:core and guaranteed to be resolved without requiring dart:async. */' ' bool isDone = false;' - ' int ticks = 0;' - ' Timer.periodic(const Duration(milliseconds: 50), (timer) {' - ' final _ = reader;' - ' // Stop pinning after a 5-second buffer when the future has completed.' - ' if (isDone && ++ticks > 100) {' - ' timer.cancel();' + ' () async {' + ' int bufferTicks = 0;' + ' for (int i = 0; i < 100; i++) {' + ' final _ = reader;' + ' await Future.delayed(const Duration(milliseconds: 50));' + ' /* Stop pinning after a 1-second buffer when the future has completed. */' + ' if (isDone || ++bufferTicks > 20) break;' ' }' - ' });' + ' }();' ' try {' // Cast as dynamic so that it is possible to await Future ' dynamic result = ($expression) as dynamic;' From c0b3c8f3d008bd67ad7818a222a238214d72f1a8 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Mon, 13 Jul 2026 10:58:49 -0700 Subject: [PATCH 3/4] note --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index ebb8dad9043..f018f749165 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -35,7 +35,7 @@ TODO: Remove this section if there are not any updates. ## Debugger updates -TODO: Remove this section if there are not any updates. +* Prevent values from being garbage-collected, while being evaluated. ## Network profiler updates From 24bd17c60ecf44e701fa74aa823a545650eae68a Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Mon, 13 Jul 2026 16:07:28 -0700 Subject: [PATCH 4/4] while-loop --- .../lib/src/service/eval_on_dart_library.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart b/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart index bf6e72beecc..8df2f921353 100644 --- a/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart +++ b/packages/devtools_app_shared/lib/src/service/eval_on_dart_library.dart @@ -439,11 +439,10 @@ class EvalOnDartLibrary extends DisposableController ' bool isDone = false;' ' () async {' ' int bufferTicks = 0;' - ' for (int i = 0; i < 100; i++) {' + ' /* Stop pinning after a 1-second buffer when the future has completed. */' + ' while (!isDone && ++bufferTicks <=20) {' ' final _ = reader;' ' await Future.delayed(const Duration(milliseconds: 50));' - ' /* Stop pinning after a 1-second buffer when the future has completed. */' - ' if (isDone || ++bufferTicks > 20) break;' ' }' ' }();' ' try {'