mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
perf: reduce per-step iteration time (~3.9s to ~2.3s) (#29)
* perf(sidecar): use exec-out + tmpfs for hierarchy dump Avoids FUSE overhead on /sdcard and shell startup cost by using exec-out with /data/local/tmp. Saves ~100ms per hierarchy fetch. * perf(sidecar): replace Thread.sleep waitForIdle with real idle detection Poll `dumpsys window -a` for mAnimating=true every 50ms instead of blindly sleeping. Breaks early when device is idle, saving 500-800ms per step since most settle in <200ms after an action. * test(sidecar): add idle detection parsing tests * perf(runner): parallelize hierarchy, metrics, and logs fetch Run fetchHierarchy, captureMetrics, and collectLogs concurrently via errgroup so metrics+logs (~150ms) hide behind the hierarchy fetch (~2s) instead of running serially. * perf(runner): pipeline post-action screenshot with next step Defer the post-action screenshot from step N and run it concurrently with step N+1's hierarchy/metrics/logs fetch. Saves ~335ms per step by hiding screenshot latency behind the hierarchy fetch. * test(runner): add tests for parallel fetch and pipelined screenshots Verify that hierarchy, metrics, and logs are all called per step. Verify post-action screenshots are written with correct step indices when pipelined, including the final flush after the loop. * perf(sidecar): grep mAnimating on-device instead of pulling full dump The full `dumpsys window -a` output is ~88KB per poll. Running grep on-device transfers only a count byte, cutting per-poll overhead from ~63ms to ~56ms and eliminating 88KB of ADB transfer.
This commit is contained in:
6 files changed
+205
-17
No files matched your search
+44
-14
@@ -8,6 +8,8 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/priyanshujain/sanderling/internal/agent"
|
||||
"github.com/priyanshujain/sanderling/internal/driver"
|
||||
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
||||
@@ -59,6 +61,8 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
||||
stepIndex := 0
|
||||
var lastAction *verifier.Action
|
||||
var lastLogTime time.Time
|
||||
var pendingPostScreenshotStep int
|
||||
pendingPostScreenshot := false
|
||||
for time.Now().Before(deadline) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
break
|
||||
@@ -66,12 +70,40 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
||||
stepIndex++
|
||||
stepStart := time.Now()
|
||||
|
||||
// Fetch hierarchy BEFORE pausing the SDK: uiautomator dump calls
|
||||
// waitForIdle internally, and the SDK's Choreographer-held pause
|
||||
// stalls the main thread, so dumping during the pause yields the
|
||||
// pre-pause (stale) tree. Doing this first also means the spec sees
|
||||
// a hierarchy that matches the snapshots captured a moment later.
|
||||
tree, hierarchyErr := fetchHierarchy(ctx, options.Driver)
|
||||
// Hierarchy, metrics, and logs are independent device reads. Run
|
||||
// them concurrently so metrics+logs hide behind the hierarchy
|
||||
// fetch (~2s). All three must finish before snapshotStep pauses
|
||||
// the SDK.
|
||||
var tree *hierarchy.Tree
|
||||
var hierarchyErr error
|
||||
var metrics *trace.Metrics
|
||||
var logs []verifier.LogEntry
|
||||
|
||||
g, _ := errgroup.WithContext(ctx)
|
||||
g.Go(func() error {
|
||||
tree, hierarchyErr = fetchHierarchy(ctx, options.Driver)
|
||||
return nil
|
||||
})
|
||||
si := stepIndex
|
||||
g.Go(func() error {
|
||||
metrics = captureMetrics(ctx, options, logger, si)
|
||||
return nil
|
||||
})
|
||||
logSince := lastLogTime
|
||||
g.Go(func() error {
|
||||
logs = collectLogs(ctx, options.Driver, logSince)
|
||||
return nil
|
||||
})
|
||||
if pendingPostScreenshot {
|
||||
postStep := pendingPostScreenshotStep
|
||||
g.Go(func() error {
|
||||
captureScreenshot(ctx, options, logger, postStep, true)
|
||||
return nil
|
||||
})
|
||||
pendingPostScreenshot = false
|
||||
}
|
||||
g.Wait()
|
||||
|
||||
if hierarchyErr != nil {
|
||||
logger.Warn("hierarchy fetch failed", "step", stepIndex, "err", hierarchyErr)
|
||||
}
|
||||
@@ -80,17 +112,10 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
||||
treeSize = len(tree.Elements)
|
||||
}
|
||||
|
||||
// Sample metrics now, while the app is still running freely.
|
||||
// Measuring after snapshotStep would see the SDK-paused app and
|
||||
// report CPU=0 for every step.
|
||||
metrics := captureMetrics(ctx, options, logger, stepIndex)
|
||||
|
||||
snapshot, err := snapshotStep(ctx, options)
|
||||
if err != nil {
|
||||
return summary, fmt.Errorf("step %d snapshot: %w", stepIndex, err)
|
||||
}
|
||||
|
||||
logs := collectLogs(ctx, options.Driver, lastLogTime)
|
||||
lastLogTime = stepStart
|
||||
|
||||
exceptions := decodeExceptions(snapshot)
|
||||
@@ -173,7 +198,8 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
||||
idleCtx, idleCancel := context.WithTimeout(ctx, options.IdleTimeout)
|
||||
idleErr := options.Driver.WaitForIdle(idleCtx, options.IdleTimeout)
|
||||
if nextErr == nil {
|
||||
captureScreenshot(ctx, options, logger, stepIndex, true)
|
||||
pendingPostScreenshot = true
|
||||
pendingPostScreenshotStep = stepIndex
|
||||
}
|
||||
if idleErr != nil && idleCtx.Err() == nil {
|
||||
logger.Warn("wait_for_idle failed", "step", stepIndex, "err", idleErr)
|
||||
@@ -181,6 +207,10 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
||||
idleCancel()
|
||||
}
|
||||
|
||||
if pendingPostScreenshot {
|
||||
captureScreenshot(ctx, options, logger, pendingPostScreenshotStep, true)
|
||||
}
|
||||
|
||||
summary.EndTime = time.Now()
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user