Repository navigation
Fix android_hardware_smoke_test retry mechanisms - #195708
Conversation
99e6e2f to
f9d6dc0
Compare
…rrors Route retryable adb and flutter drive commands through _runUncheckedSubprocess so a failed first attempt does not call foundError before attempt 2 runs. Decode subprocess streams with malformed UTF-8 tolerance so native crash tombstones in logcat do not throw FormatException.
…r EGL failures Reset sys.boot_completed and restart Android userspace via adb shell stop and start before attempt 2 so SurfaceFlinger rebinds cleanly after QuickBoot display wedges. Report the specific retryable failure reason in retry and error logs while keeping non-retryable test assertions failing fast on attempt 1.
…ing VM connect Check adb shell pidof when Dart VM service connection stalls during flutter drive and send SIGKILL once the target process has exited. Treat terminated dead-target runs as retryable with an explicit crash reason so userspace recovery starts immediately instead of hitting shard timeout.
…markers Pass $TAG:I *:W when dumping logcat in FlutterActivityTest so Log.i attempt markers are not stripped. Match explicit fatal EGL, Vulkan, and host-pipe errors across Kotlin and Dart while ignoring benign HWUI capability-probe warnings.
f9d6dc0 to
78e400c
Compare
There was a problem hiding this comment.
Code Review
This pull request enhances the Android hardware smoke tests by introducing a robust recovery mechanism for emulator graphics and display compositor failures. It adds helper functions to run unchecked ADB commands, monitor the target process during VM connection, and reactively restart Android userspace when fatal graphics errors or blank captures are detected. The review feedback suggests adding a timeout to the _pollAdbUntil function to prevent infinite hangs in CI if the device or ADB gets permanently wedged.
| /// Runs [executable] without recording a shard error via [foundError] on | ||
| /// non-zero exit codes, decoding output with malformed UTF-8 tolerance so | ||
| /// native crash tombstones in `adb logcat` do not throw [FormatException]. | ||
| Future<_SubprocessResult> _runUncheckedSubprocess( |
There was a problem hiding this comment.
This looks like it is almost the same as runCommand. Are the only differences: 1. don't check exit code and 2. allowMalformed in Utf8Decoder?
Should we just modify runCommand instead of making this new function? We could add a bool checkExitCode = true parameter. For allowMalformed, should we just always use true, or do some existing runCommand callers expect it to be false for some reason?
There was a problem hiding this comment.
Done. I actually tried putting the changes in runCommand first, but I didn't like the list of parameters this resulted in. But you're right, I can just migrate it to something better in a followup. Filed #196126. As far as I can tell, allowMalformed should be fine for all existing callers.
| 'sys.boot_completed', | ||
| '0', | ||
| ], description: 'sys.boot_completed reset'); | ||
| await _pollAdbUntil( |
There was a problem hiding this comment.
Do adb shell stop and adb shell start need to be polled?
There was a problem hiding this comment.
Good catch, they do not. Polling setprop sys.boot_completed 0 already waits for adbd to finish cycling after adb root, and stop and start do not restart adbd. Updated both to call _runAdb directly.
| // Intentionally scope retries strictly to blank-display compositor wedges or EGL | ||
| // initialization errors. General test assertion or golden pixel mismatch failures | ||
| // must fail fast on attempt 1 so real visual regressions are not masked. |
There was a problem hiding this comment.
nit: I think this comment should go above or inside the _detectRetryableFailureReason function instead of here at the call site. But I'll leave it up to whatever you prefer.
There was a problem hiding this comment.
Done, moved into the doc comment on _detectRetryableFailureReason.
| // Match blank-capture exceptions reported by driver_test.dart (errorBlankScreenshot | ||
| // in constants.dart). Keep this substring in sync with both files. | ||
| if (driverOutput.contains('Captured screenshot is blank/empty')) 'blank display capture', |
There was a problem hiding this comment.
Seems like this should just check for errorBlankScreenshot directly without having to manually keep strings in sync.
If we think the current string of 'blank/empty' is too general and risks getting a false positive from an unrelated output line, then we could expand it to 'Captured screenshot is blank/empty'. And we can just update its throwing source to account for that at
There was a problem hiding this comment.
Expanded errorBlankScreenshot to 'Captured screenshot is blank/empty' in constants.dart so constants.dart and run_android_hardware_smoke_tests.dart hold the exact same string, and updated driver_test.dart so it only logs errorBlankScreenshot once screenshot retries are exhausted. dev/bots (tests_on_bots) cannot import constants.dart directly because avoid_relative_lib_imports and depend_on_referenced_packages are enabled and dart test rejects standalone bot packages that depend on flutter: sdk: flutter app packages, so I noted that package boundary in the sync comment.
| result.output, | ||
| terminatedDeadDrive: result.terminatedDeadDrive, |
There was a problem hiding this comment.
Should we just pass result instead of multiple arguments?
There was a problem hiding this comment.
Done, updated _detectRetryableFailureReason to take _DriveAttemptResult result directly.
| } | ||
|
|
||
| io.stderr.writeln( | ||
| 'flutter drive failed with exit code ${result.exitCode} on attempt $attempt/$maxAttempts.', |
There was a problem hiding this comment.
This should be updated for the terminatedDeadDrive case. Otherwise it'll be a confusing message about failing with exit code 0.
There was a problem hiding this comment.
Done, branched on result.terminatedDeadDrive to log 'flutter drive was terminated after target app crash on attempt ... (exit code ...).' instead.
| final _SubprocessResult pidResult = await _runAdb(<String>[ | ||
| 'shell', | ||
| 'pidof', | ||
| 'com.example.android_hardware_smoke_test', | ||
| ]); |
There was a problem hiding this comment.
Should we cache the pid instead of running pidof on every output line? Or does it change during the run?
There was a problem hiding this comment.
Oh wait, I see. This is only run once because every line exits early until a line is encountered that matches slowVmConnectPatterns. So I think this is fine. Following the code flow requires a bit too much thinking for my taste, but I'm okay with it.
There was a problem hiding this comment.
Extracted killDriveIfTargetExited out of the line callback to make this read a bit easier
Extend runCommand with checkExitCode, malformed UTF-8 decoding, and stderrListener instead of duplicating subprocess plumbing in run_android_hardware_smoke_tests.dart, and cover the new behavior in run_command_test.dart. Call _runAdb directly for adb shell stop and start, expand errorBlankScreenshot in constants.dart, and simplify the driver crash liveness callback and retry diagnostics.
b-luk
left a comment
There was a problem hiding this comment.
Thanks for the changes. LGTM
|
Thanks for the review! |
flutter/flutter@09175cd...e36b370 2026-11-07 [email protected] Roll Fuchsia Linux SDK from KIc0XZbhLmvDpTluZ... to Sf2yvdTiFXAKkfLkD... (flutter/flutter#196160) 2026-11-07 [email protected] Roll pub packages (flutter/flutter#196155) 2026-11-07 [email protected] [iOS a11y] Use a real `UIScrollView` as a11y scrollable container (flutter/flutter#195731) 2026-11-07 [email protected] Add ParagraphStyle.hyphenCharacter to dart:ui (flutter/flutter#195345) 2026-11-07 [email protected] Roll pub packages (flutter/flutter#196143) 2026-11-06 [email protected] [Linux] Fix layer position when compositing without glBlitFramebuffer (flutter/flutter#196083) 2026-11-06 [email protected] Add support for riscv64 desktop linux engine (flutter/flutter#178712) 2026-11-06 [email protected] Add ResizeImagePolicy.cover (flutter/flutter#194665) 2026-11-06 [email protected] Roll Skia from f533d4034900 to b11315d0a096 (6 revisions) (flutter/flutter#196138) 2026-11-06 [email protected] iOS: Remove dead code/unused imports from FlutterPlatformViewsController (flutter/flutter#196084) 2026-11-06 [email protected] Replace pull_request_target workflows with pull_request + workflow_run (flutter/flutter#195536) 2026-11-06 [email protected] [master] Update Flutter DEPS to Dart 3b174b7217fe09b21da836ea623932b5f041b091 (flutter/flutter#196004) 2026-11-06 [email protected] Fix android_hardware_smoke_test retry mechanisms (flutter/flutter#195708) 2026-11-06 [email protected] Roll Skia from 452b9c1e5db6 to f533d4034900 (1 revision) (flutter/flutter#196129) 2026-11-06 [email protected] [flutter_tools] Use `ErrorHandlingFileSystem.deleteIfExists` for `IOSCoreDeviceControl` temp directory cleanup (flutter/flutter#195913) 2026-11-06 [email protected] Adds guideline tests to flutter create samples (flutter/flutter#195827) 2026-11-06 [email protected] refactor(flutter_tools): unify OptionDescriptor identity and addTo (flutter/flutter#196076) 2026-11-06 [email protected] Roll pub packages (flutter/flutter#196114) 2026-11-06 [email protected] Roll Skia from 5ce64e63a0e7 to 452b9c1e5db6 (5 revisions) (flutter/flutter#196111) 2026-11-06 [email protected] Add PlatformDispatcher.onAlwaysUse24HourFormatChanged callback (flutter/flutter#194670) 2026-11-06 [email protected] Roll Packages from 0478162 to 63881f9 (10 revisions) (flutter/flutter#196108) 2026-11-06 [email protected] Roll Skia from 793b8ac73d4b to 5ce64e63a0e7 (4 revisions) (flutter/flutter#196107) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Fixes #194928
Fixes #191348
Fixes #193557
Related to #193205
This makes
android_hardware_smoke_testrecover cleanly from transient emulatorSurfaceFlingerwedges and host-GPU-pipe startup crashes instead of timing out or failing the shard after a passing retry. See #193557 (comment) for my root-cause investigation and local emulator reproduction.What changed
run_android_hardware_smoke_tests.dartalready retried failedflutter driveruns once, while invokingadb logcat -candflutter drivethroughrunCommand, which calledfoundErroron any non-zero exit and marked the shard failed even when attempt 2 passed.runCommandnow supportscheckExitCode: falseso retryable commands do not record a shard error on attempt 1, andstartCommanddecodes output streams withUtf8Decoder(allowMalformed: true)so raw bytes in native crash tombstones do not throwFormatExceptionwhen inspectingadb logcat -d.Retrying with
flutter drive --no-buildonly restarted the app process and leftSurfaceFlingerwedged when an emulator QuickBoot snapshot broke display composition. When attempt 1 fails with a blank screenshot or fatal graphics error inadb logcat -d, the runner now resetssys.boot_completedto0, restartssurfaceflingerandzygoteviaadb shell stopandadb shell start, waits forsys.boot_completed=1, and unlocks the screen before attempt 2. Normal test assertion failures and golden mismatches still fail immediately on attempt 1.When the app crashed during native EGL or Vulkan startup right after printing its Dart VM service URI,
flutter drivelooped against the closedadb forwardport until LUCI killed the shard on timeout.startCommandandrunCommandnow accept astderrListeneralongsideoutputListenerso the runner can watchstdoutandstderrfor slow-VM-connect warnings, checkadb shell pidof, sendSIGKILLtoflutter driveonce the app process has exited, and trigger the userspace-restart retry immediately.In
FlutterActivityTest.kt, queryinglogcat -d --pid=$pid *:WstrippedLog.iattempt markers, while bothFlutterActivityTest.ktandrun_android_hardware_smoke_tests.darttreated routineHWUIwarnings as fatal errors.FlutterActivityTest.ktnow passes$TAG:I *:Wto keep attempt markers visible in the dump. Both files now match explicit fatal EGL, Vulkan, and host-pipe errors while ignoring benignHWUIcapability-probe logs.Pre-launch Checklist
///).