From eff4692413430d528db457857d38048763fd2d38 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Mon, 27 Jul 2026 10:49:00 +1000 Subject: [PATCH 1/5] repro flake --- .github/workflows/jnigen.yaml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/jnigen.yaml b/.github/workflows/jnigen.yaml index 22045c9659..6748a1f078 100644 --- a/.github/workflows/jnigen.yaml +++ b/.github/workflows/jnigen.yaml @@ -143,11 +143,14 @@ jobs: run: dart run jnigen:setup - name: Run VM tests run: | - if [ "${{ matrix.java-version }}" != "17" ]; then - dart test -x bindings --test-randomize-ordering-seed random - else - dart test --test-randomize-ordering-seed random - fi + for i in $(seq 1 10); do + echo "=== Iteration $i ===" + if [ "${{ matrix.java-version }}" != "17" ]; then + dart test -x bindings --test-randomize-ordering-seed random || exit 1 + else + dart test --test-randomize-ordering-seed random || exit 1 + fi + done - name: build in_app_java APK run: flutter build apk --target-platform=android-arm64 working-directory: ./pkgs/jnigen/example/in_app_java From a91d91268d4aba6a8041593b21f603a9615e26a3 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Mon, 27 Jul 2026 13:26:45 +1000 Subject: [PATCH 2/5] maybe fix the flake --- .../simple_package_test/runtime_test_registrant.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart index f5f518de6f..0330471b77 100644 --- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart @@ -740,12 +740,13 @@ void registerTests(String groupName, TestRunnerCallback test) { final result = await callbackPort.first; expect(result, 'callback_executed'); - expect(runner.isFinished, isTrue); + expect(runner.waitForFinished(1000), isTrue); isolate.kill(priority: Isolate.immediate); await exitPort.first; runJavaGC(); + await _waitUntil(() => runner.isArgCollected); expect(runner.isArgCollected, isTrue); }); @@ -773,7 +774,7 @@ void registerTests(String groupName, TestRunnerCallback test) { arg.release(); // Runner finished even though message isn't delivered. - expect(runner.isFinished, isTrue); + expect(runner.waitForFinished(1000), isTrue); expect( callbackPort.first.timeout(const Duration(milliseconds: 200)), throwsA(isA()), @@ -781,6 +782,7 @@ void registerTests(String groupName, TestRunnerCallback test) { // Arg is cleaned up even though message isn't delivered. runJavaGC(); + await _waitUntil(() => runner.isArgCollected); expect(runner.isArgCollected, isTrue); }); @@ -818,6 +820,7 @@ void registerTests(String groupName, TestRunnerCallback test) { // Arg is cleaned up even though message isn't delivered. runJavaGC(); + await _waitUntil(() => runner.isArgCollected); expect(runner.isArgCollected, isTrue); }); @@ -848,6 +851,7 @@ void registerTests(String groupName, TestRunnerCallback test) { await exitPort.first; runJavaGC(); + await _waitUntil(() => runner.isArgCollected); expect(runner.isArgCollected, isTrue); }); @@ -883,6 +887,7 @@ void registerTests(String groupName, TestRunnerCallback test) { // Arg is cleaned up even though message isn't delivered. runJavaGC(); + await _waitUntil(() => runner.isArgCollected); expect(runner.isArgCollected, isTrue); }); @@ -920,6 +925,7 @@ void registerTests(String groupName, TestRunnerCallback test) { // Arg is cleaned up even though message isn't delivered. runJavaGC(); + await _waitUntil(() => runner.isArgCollected); expect(runner.isArgCollected, isTrue); }); }, skip: !canRunJavaGC); From 6e540a13774256e6e72eca47e2c4bc90c75f438a Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Mon, 27 Jul 2026 14:20:33 +1000 Subject: [PATCH 3/5] wait longer --- .../test/simple_package_test/runtime_test_registrant.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart index 0330471b77..4fb8b56f98 100644 --- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart @@ -61,7 +61,7 @@ const trillion = 1024 * 1024 * 1024 * 1024; Future _waitUntil(bool Function() predicate) async { for (var i = 0; i < 8; ++i) { - await Future.delayed(Duration(milliseconds: (1 << i) * 100)); + await Future.delayed(Duration(milliseconds: (1 << i) * 1000)); if (predicate()) { return; } From 5255585f02120d19963593b86981d45f1d419042 Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Mon, 27 Jul 2026 15:26:02 +1000 Subject: [PATCH 4/5] More GC --- pkgs/jnigen/pubspec.yaml | 1 + .../runtime_test_registrant.dart | 35 +++++++++++-------- pkgs/jnigen/test/test_util/java_gc.dart | 30 +++++++++++++++- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/pkgs/jnigen/pubspec.yaml b/pkgs/jnigen/pubspec.yaml index 68312cc87e..8508aacf19 100644 --- a/pkgs/jnigen/pubspec.yaml +++ b/pkgs/jnigen/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: dev_dependencies: build_runner: ^2.4.12 dart_flutter_team_lints: ^3.5.2 + ffi: ^2.1.3 jni: path: ../jni json_serializable: ^6.8.0 diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart index 4fb8b56f98..7cac264b3e 100644 --- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart @@ -61,7 +61,7 @@ const trillion = 1024 * 1024 * 1024 * 1024; Future _waitUntil(bool Function() predicate) async { for (var i = 0; i < 8; ++i) { - await Future.delayed(Duration(milliseconds: (1 << i) * 1000)); + await Future.delayed(Duration(milliseconds: (1 << i) * 100)); if (predicate()) { return; } @@ -719,6 +719,19 @@ void registerTests(String groupName, TestRunnerCallback test) { }); group('Interface implementation on destroyed isolate', () { + Future _waitUntilCollected(MyConsumerRunner runner) async { + for (var i = 0; i < 8; ++i) { + if (i > 0) { + await Future.delayed( + Duration(milliseconds: (1 << i) * 100)); + } + await runBothGC(); + if (runner.isArgCollected) { + return; + } + } + } + test('non-blocking callback - successful callback flow', () async { final runnerPort = ReceivePort(); final callbackPort = ReceivePort(); @@ -745,8 +758,7 @@ void registerTests(String groupName, TestRunnerCallback test) { isolate.kill(priority: Isolate.immediate); await exitPort.first; - runJavaGC(); - await _waitUntil(() => runner.isArgCollected); + await _waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -781,8 +793,7 @@ void registerTests(String groupName, TestRunnerCallback test) { ); // Arg is cleaned up even though message isn't delivered. - runJavaGC(); - await _waitUntil(() => runner.isArgCollected); + await _waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -819,8 +830,7 @@ void registerTests(String groupName, TestRunnerCallback test) { ); // Arg is cleaned up even though message isn't delivered. - runJavaGC(); - await _waitUntil(() => runner.isArgCollected); + await _waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -850,8 +860,7 @@ void registerTests(String groupName, TestRunnerCallback test) { isolate.kill(priority: Isolate.immediate); await exitPort.first; - runJavaGC(); - await _waitUntil(() => runner.isArgCollected); + await _waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -886,8 +895,7 @@ void registerTests(String groupName, TestRunnerCallback test) { ); // Arg is cleaned up even though message isn't delivered. - runJavaGC(); - await _waitUntil(() => runner.isArgCollected); + await _waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -924,11 +932,10 @@ void registerTests(String groupName, TestRunnerCallback test) { ); // Arg is cleaned up even though message isn't delivered. - runJavaGC(); - await _waitUntil(() => runner.isArgCollected); + await _waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); - }, skip: !canRunJavaGC); + }, skip: !canRunJavaGC || !canDoGC); } group('Dart exceptions are handled', () { diff --git a/pkgs/jnigen/test/test_util/java_gc.dart b/pkgs/jnigen/test/test_util/java_gc.dart index cdd6113af8..4d5fb47712 100644 --- a/pkgs/jnigen/test/test_util/java_gc.dart +++ b/pkgs/jnigen/test/test_util/java_gc.dart @@ -5,7 +5,7 @@ import 'dart:ffi'; import 'dart:io'; -import 'package:jni/_internal.dart'; +import 'package:ffi/ffi.dart'; import 'package:jni/jni.dart'; // Can't reliably force GC on Android. @@ -30,6 +30,34 @@ void runJavaGC() { } while (result.exitCode != 0); } +final _executeInternalCommand = () { + final dylib = DynamicLibrary.process(); + if (dylib.providesSymbol('Dart_ExecuteInternalCommand')) { + return dylib + .lookup, Pointer)>>( + 'Dart_ExecuteInternalCommand', + ) + .asFunction, Pointer)>(); + } + return null; +}(); + +final canDoGC = _executeInternalCommand != null; + +void runDartGC() { + if (_executeInternalCommand == null) return; + final gcNow = 'gc-now'.toNativeUtf8(); + _executeInternalCommand!(gcNow.cast(), nullptr); + calloc.free(gcNow); +} + +Future runBothGC() async { + runDartGC(); + await Future.delayed(Duration.zero); + runDartGC(); + runJavaGC(); +} + /// A wrapper around Java's `java.lang.ref.WeakReference` for testing GC /// collection. class JWeakReference { From 70f41f4804a4656ef6131c45544d8ae0ec5b45cd Mon Sep 17 00:00:00 2001 From: Liam Appelbe Date: Mon, 27 Jul 2026 15:32:31 +1000 Subject: [PATCH 5/5] fix --- .../runtime_test_registrant.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart index 7cac264b3e..cc036ded02 100644 --- a/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart +++ b/pkgs/jnigen/test/simple_package_test/runtime_test_registrant.dart @@ -719,7 +719,7 @@ void registerTests(String groupName, TestRunnerCallback test) { }); group('Interface implementation on destroyed isolate', () { - Future _waitUntilCollected(MyConsumerRunner runner) async { + Future waitUntilCollected(MyConsumerRunner runner) async { for (var i = 0; i < 8; ++i) { if (i > 0) { await Future.delayed( @@ -758,7 +758,7 @@ void registerTests(String groupName, TestRunnerCallback test) { isolate.kill(priority: Isolate.immediate); await exitPort.first; - await _waitUntilCollected(runner); + await waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -793,7 +793,7 @@ void registerTests(String groupName, TestRunnerCallback test) { ); // Arg is cleaned up even though message isn't delivered. - await _waitUntilCollected(runner); + await waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -830,7 +830,7 @@ void registerTests(String groupName, TestRunnerCallback test) { ); // Arg is cleaned up even though message isn't delivered. - await _waitUntilCollected(runner); + await waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -860,7 +860,7 @@ void registerTests(String groupName, TestRunnerCallback test) { isolate.kill(priority: Isolate.immediate); await exitPort.first; - await _waitUntilCollected(runner); + await waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -895,7 +895,7 @@ void registerTests(String groupName, TestRunnerCallback test) { ); // Arg is cleaned up even though message isn't delivered. - await _waitUntilCollected(runner); + await waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); @@ -932,7 +932,7 @@ void registerTests(String groupName, TestRunnerCallback test) { ); // Arg is cleaned up even though message isn't delivered. - await _waitUntilCollected(runner); + await waitUntilCollected(runner); expect(runner.isArgCollected, isTrue); }); }, skip: !canRunJavaGC || !canDoGC);