perf(runner): replace focus-tap settle with a brief wait

The full WaitForIdle after a field-focus tap cost ~0.5-1s per InputText
step on a physical device while the keyboard animated in. The tap registers
focus immediately and text is injected into the focused view, so a short
fixed wait suffices. Drops p95 step latency ~5.1s to ~4.0s; G1-G4 stay
green.
This commit is contained in:
pj committed 2026-06-10 16:53:12 +05:30
1 parent 41955d5635
commit 419665100b
2 files changed
+49 -39

No files matched your search

+20 -8
View File
@@ -226,7 +226,7 @@ func Run(ctx context.Context, options Options) (Summary, error) {
applySkipped := false
if nextErr == nil {
if err := applyAction(ctx, options.Driver, nextAction, tree, options.IdleTimeout); err != nil {
if err := applyAction(ctx, options.Driver, nextAction, tree); err != nil {
if isWDADrop(err) {
return summary, fmt.Errorf("step %d: the iOS XCTest runner could not be restarted - re-run the test: %w", stepIndex, err)
}
@@ -416,6 +416,12 @@ func ensureForeground(ctx context.Context, options Options, logger *slog.Logger,
// never hang the run.
const foregroundReadyAttempts = 8
// focusTapSettle is the pause after tapping a field to focus it, before typing.
// Long enough for focus to land, short enough to avoid the ~500ms-1s full
// settle the keyboard's open animation would otherwise cost every InputText
// step on a physical device.
const focusTapSettle = 250 * time.Millisecond
// waitForForeground blocks until the app under test is actually on screen, so
// the first observe never captures a leftover screen or a freshly-booted
// device's system dialog (e.g. Android's "set a screen lock" prompt). Drivers
@@ -507,7 +513,7 @@ func settleForForeground(ctx context.Context, options Options) {
cancel()
}
func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.Action, tree *hierarchy.Tree, idleTimeout time.Duration) error {
func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.Action, tree *hierarchy.Tree) error {
switch action.Kind {
case verifier.ActionKindTap:
x, y, ok := resolveCoordinates(action, tree)
@@ -556,13 +562,19 @@ func applyAction(ctx context.Context, drv driver.DeviceDriver, action verifier.A
}
tapped = true
}
// The focus tap raises the keyboard. Settle before sending key
// events so the keyboard animation cannot race them into the wrong
// field (or drop them entirely).
// The focus tap raises the keyboard. The tap registers focus
// immediately and the text is injected into the focused view (not typed
// on the visible keyboard), so a brief pause is enough for focus to land
// rather than a full settle, which costs ~500ms-1s per InputText step on
// a physical device while the keyboard animates in.
if tapped {
idleCtx, idleCancel := context.WithTimeout(ctx, idleTimeout)
_ = drv.WaitForIdle(idleCtx, idleTimeout)
idleCancel()
timer := time.NewTimer(focusTapSettle)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
// InputText replaces the field's content: erase what the target
// holds before typing. Appending instead lets repeated draws grow
+29 -31
View File
@@ -662,41 +662,39 @@ func TestApplyAction_InputTextErasesExistingTextBeforeTyping(t *testing.T) {
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindInputText, On: "id:username", Text: "alice"}
if err := applyAction(context.Background(), driverMock, action, tree, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, tree); err != nil {
t.Fatalf("applyAction: %v", err)
}
// The post-tap settle is now a brief internal sleep, not a WaitForIdle RPC,
// so the recorded driver actions are tap, erase, input.
actions := driverMock.Actions()
if len(actions) != 4 {
t.Fatalf("want tap, wait_for_idle, erase, input; got %v", actions)
if len(actions) != 3 {
t.Fatalf("want tap, erase, input; got %v", actions)
}
if actions[0].Kind != mockdriver.ActionTap {
t.Errorf("first action = %v, want tap", actions[0].Kind)
}
if actions[1].Kind != mockdriver.ActionWaitForIdle {
t.Errorf("second action = %v, want wait_for_idle (settle after focus tap)", actions[1].Kind)
if actions[1].Kind != mockdriver.ActionEraseText || actions[1].CharacterCount != len("stale-value") {
t.Errorf("second action = %+v, want erase_text of %d characters", actions[1], len("stale-value"))
}
if actions[2].Kind != mockdriver.ActionEraseText || actions[2].CharacterCount != len("stale-value") {
t.Errorf("third action = %+v, want erase_text of %d characters", actions[2], len("stale-value"))
}
if actions[3].Kind != mockdriver.ActionInputText || actions[3].Text != "alice" {
t.Errorf("fourth action = %+v, want input_text alice", actions[3])
if actions[2].Kind != mockdriver.ActionInputText || actions[2].Text != "alice" {
t.Errorf("third action = %+v, want input_text alice", actions[2])
}
}
// TestApplyAction_InputTextWithoutTargetSkipsSettle pins that the post-tap
// settle only runs when a focus tap actually happened: with no resolvable
// target there is no keyboard animation to absorb.
func TestApplyAction_InputTextWithoutTargetSkipsSettle(t *testing.T) {
// TestApplyAction_InputTextWithoutTargetSkipsFocusTap pins that with no
// resolvable target there is no focus tap (and so no settle), and InputText
// still runs at the cursor.
func TestApplyAction_InputTextWithoutTargetSkipsFocusTap(t *testing.T) {
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindInputText, X: -1, Y: -1, Text: "alice"}
if err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
t.Fatalf("applyAction: %v", err)
}
for _, recorded := range driverMock.Actions() {
if recorded.Kind == mockdriver.ActionWaitForIdle {
t.Errorf("no focus tap happened; settle must be skipped: %v", driverMock.Actions())
}
actions := driverMock.Actions()
if len(actions) != 1 || actions[0].Kind != mockdriver.ActionInputText {
t.Errorf("no target: want input_text only (no focus tap), got %v", actions)
}
}
@@ -714,7 +712,7 @@ func TestApplyAction_InputTextSkipsEraseForReplacingDriver(t *testing.T) {
driverMock.ReplacesText = true
action := verifier.Action{Kind: verifier.ActionKindInputText, On: "id:username", Text: "alice"}
if err := applyAction(context.Background(), driverMock, action, tree, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, tree); err != nil {
t.Fatalf("applyAction: %v", err)
}
if containsAction(driverMock.Actions(), mockdriver.ActionEraseText, "") {
@@ -735,7 +733,7 @@ func TestApplyAction_InputTextSkipsEraseWhenTargetEmpty(t *testing.T) {
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindInputText, On: "id:username", Text: "alice"}
if err := applyAction(context.Background(), driverMock, action, tree, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, tree); err != nil {
t.Fatalf("applyAction: %v", err)
}
if containsAction(driverMock.Actions(), mockdriver.ActionEraseText, "") {
@@ -749,7 +747,7 @@ func TestApplyAction_InputTextSurfacesFocusTapError(t *testing.T) {
driverMock.Failures[mockdriver.ActionTapSelector] = errors.New("adb unreachable")
action := verifier.Action{Kind: verifier.ActionKindInputText, On: "id:username", Text: "alice"}
err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond)
err := applyAction(context.Background(), driverMock, action, nil)
if err == nil {
t.Fatalf("expected focus tap failure to surface, got nil")
}
@@ -762,7 +760,7 @@ func TestApplyAction_InputTextSurfacesFocusTapError(t *testing.T) {
driverMock.Failures[mockdriver.ActionTap] = errors.New("tap driver error")
action := verifier.Action{Kind: verifier.ActionKindInputText, X: 10, Y: 20, Text: "alice"}
err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond)
err := applyAction(context.Background(), driverMock, action, nil)
if err == nil {
t.Fatalf("expected focus tap failure to surface, got nil")
}
@@ -776,7 +774,7 @@ func TestApplyAction_V8InputTextTapsAtCoordinates(t *testing.T) {
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindInputText, X: 50, Y: 100, Text: "alice"}
if err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
t.Fatalf("apply action: %v", err)
}
actions := driverMock.Actions()
@@ -795,7 +793,7 @@ func TestApplyAction_V8InputTextAtOriginStillTaps(t *testing.T) {
// InputText with (0,0) is a deliberate edge tap, not a sentinel).
action := verifier.Action{Kind: verifier.ActionKindInputText, X: 0, Y: 0, Text: "alice"}
if err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
t.Fatalf("apply action: %v", err)
}
if !containsAction(driverMock.Actions(), mockdriver.ActionTap, "") {
@@ -807,7 +805,7 @@ func TestApplyAction_DoubleTapDispatchesDoubleTapAtCoordinates(t *testing.T) {
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindDoubleTap, X: 100, Y: 200}
if err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
t.Fatalf("apply action: %v", err)
}
taps := 0
@@ -825,7 +823,7 @@ func TestApplyAction_DoubleTapDispatchesDoubleTapSelector(t *testing.T) {
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindDoubleTap, On: "id:save"}
if err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
t.Fatalf("apply action: %v", err)
}
taps := 0
@@ -843,7 +841,7 @@ func TestApplyAction_LongPressDispatchesAtResolvedCoordinates(t *testing.T) {
driverMock := mockdriver.New()
action := verifier.Action{Kind: verifier.ActionKindLongPress, X: 120, Y: 240}
if err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
t.Fatalf("apply action: %v", err)
}
found := false
@@ -869,7 +867,7 @@ func TestApplyAction_ScrollWithPrecomputedEndpointsSwipes(t *testing.T) {
DurationMillis: 300,
}
if err := applyAction(context.Background(), driverMock, action, nil, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
t.Fatalf("apply action: %v", err)
}
found := false
@@ -892,7 +890,7 @@ func TestApplyAction_ScrollDirectionUsesInversion(t *testing.T) {
}
action := verifier.Action{Kind: verifier.ActionKindScroll, Direction: "down", On: "id:list"}
if err := applyAction(context.Background(), driverMock, action, tree, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, tree); err != nil {
t.Fatalf("apply action: %v", err)
}
var swipe *mockdriver.Action
@@ -921,7 +919,7 @@ func TestApplyAction_ScrollScreenFallback(t *testing.T) {
// On unset: container falls back to whole-screen (root) bounds.
action := verifier.Action{Kind: verifier.ActionKindScroll, Direction: "up"}
if err := applyAction(context.Background(), driverMock, action, tree, time.Millisecond); err != nil {
if err := applyAction(context.Background(), driverMock, action, tree); err != nil {
t.Fatalf("apply action: %v", err)
}
var swipe *mockdriver.Action