refactor(runner): observe each step via the atomic Snapshot RPC

fetchSyncedState now issues one Snapshot per attempt so hierarchy and
screenshot describe the same on-device frame. The transitional retry
stays: that case handles a fully-captured but mid cross-fade frame,
which atomic capture cannot fix.
This commit is contained in:
pj committed 2026-05-31 15:27:39 +05:30
1 parent c57be03b69
commit 1931c0b57f
1 file changed
+14 -31
+14 -31
View File
@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"strings" "strings"
"sync"
"time" "time"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -97,9 +96,9 @@ func Run(ctx context.Context, options Options) (Summary, error) {
// budget on a hung tab. // budget on a hung tab.
g, gctx := errgroup.WithContext(ctx) g, gctx := errgroup.WithContext(ctx)
si := stepIndex si := stepIndex
// fetchSyncedState pairs hierarchy and screenshot in the same goroutine // fetchSyncedState issues a single Snapshot RPC so hierarchy and
// so when a retry happens on a transitional tree, the screenshot stays // screenshot describe the same frame, then re-fetches the pair
// aligned with the final hierarchy snapshot. // while the tree still looks transitional.
g.Go(func() error { g.Go(func() error {
tree, hierarchyErr = fetchSyncedState(gctx, options, logger, si) tree, hierarchyErr = fetchSyncedState(gctx, options, logger, si)
return nil return nil
@@ -486,14 +485,6 @@ func resolveCoordinates(action verifier.Action, tree *hierarchy.Tree) (int, int,
return 0, 0, false return 0, 0, false
} }
func fetchHierarchy(ctx context.Context, drv driver.DeviceDriver) (*hierarchy.Tree, error) {
xmlText, err := drv.Hierarchy(ctx)
if err != nil {
return nil, err
}
return hierarchy.Parse(xmlText)
}
// transitionalRetryAttempts caps how many times we re-fetch hierarchy when a // transitionalRetryAttempts caps how many times we re-fetch hierarchy when a
// tree carries more than one route-level Screen tag (NavHost cross-fade in // tree carries more than one route-level Screen tag (NavHost cross-fade in
// flight). Each retry pauses transitionalRetrySleep before the next fetch. // flight). Each retry pauses transitionalRetrySleep before the next fetch.
@@ -508,31 +499,23 @@ const (
// and re-fetches the pair, up to transitionalRetryAttempts times. This // and re-fetches the pair, up to transitionalRetryAttempts times. This
// handles transitions whose async work begins after the sidecar's settle // handles transitions whose async work begins after the sidecar's settle
// poll has already exited. // poll has already exited.
//
// The driver's Snapshot RPC captures both reads under a backend-side mutex
// so they describe the same on-device frame; the retry exists for the
// orthogonal case where the frame itself is transitional.
func fetchSyncedState(ctx context.Context, options Options, logger *slog.Logger, stepIndex int) (*hierarchy.Tree, error) { func fetchSyncedState(ctx context.Context, options Options, logger *slog.Logger, stepIndex int) (*hierarchy.Tree, error) {
var tree *hierarchy.Tree var tree *hierarchy.Tree
var hierarchyErr error var hierarchyErr error
var pngBytes []byte var pngBytes []byte
retryLoop: retryLoop:
for attempt := range transitionalRetryAttempts { for attempt := range transitionalRetryAttempts {
var wg sync.WaitGroup hierarchyJSON, image, snapshotErr := options.Driver.Snapshot(ctx)
var localTree *hierarchy.Tree if snapshotErr != nil {
var localHierErr error hierarchyErr = snapshotErr
var localImg driver.Image tree = nil
var localImgErr error } else {
wg.Add(2) tree, hierarchyErr = hierarchy.Parse(hierarchyJSON)
go func() { pngBytes = image.PNG
defer wg.Done()
localTree, localHierErr = fetchHierarchy(ctx, options.Driver)
}()
go func() {
defer wg.Done()
localImg, localImgErr = options.Driver.Screenshot(ctx)
}()
wg.Wait()
tree = localTree
hierarchyErr = localHierErr
if localImgErr == nil {
pngBytes = localImg.PNG
} }
if hierarchyErr != nil || !isTransitionalHierarchy(tree) { if hierarchyErr != nil || !isTransitionalHierarchy(tree) {
break break