Skip to content

Commit fd9a155

Browse files
committed
perf: batch repeated iOS swipes in runner
1 parent b38dc99 commit fd9a155

4 files changed

Lines changed: 74 additions & 1 deletion

File tree

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,29 @@ final class RunnerTests: XCTestCase {
288288
let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
289289
dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
290290
return Response(ok: true, data: DataPayload(message: "dragged"))
291+
case .dragSeries:
292+
guard let x = command.x, let y = command.y, let x2 = command.x2, let y2 = command.y2 else {
293+
return Response(ok: false, error: ErrorPayload(message: "dragSeries requires x, y, x2, and y2"))
294+
}
295+
let count = max(Int(command.count ?? 1), 1)
296+
let pauseMs = max(command.pauseMs ?? 0, 0)
297+
let pattern = command.pattern ?? "one-way"
298+
if pattern != "one-way" && pattern != "ping-pong" {
299+
return Response(ok: false, error: ErrorPayload(message: "dragSeries pattern must be one-way or ping-pong"))
300+
}
301+
let holdDuration = min(max((command.durationMs ?? 60) / 1000.0, 0.016), 10.0)
302+
for idx in 0..<count {
303+
let reverse = pattern == "ping-pong" && (idx % 2 == 1)
304+
if reverse {
305+
dragAt(app: activeApp, x: x2, y: y2, x2: x, y2: y, holdDuration: holdDuration)
306+
} else {
307+
dragAt(app: activeApp, x: x, y: y, x2: x2, y2: y2, holdDuration: holdDuration)
308+
}
309+
if idx < count - 1 && pauseMs > 0 {
310+
Thread.sleep(forTimeInterval: pauseMs / 1000.0)
311+
}
312+
}
313+
return Response(ok: true, data: DataPayload(message: "drag series"))
291314
case .type:
292315
guard let text = command.text else {
293316
return Response(ok: false, error: ErrorPayload(message: "type requires text"))
@@ -1022,6 +1045,7 @@ enum CommandType: String, Codable {
10221045
case tapSeries
10231046
case longPress
10241047
case drag
1048+
case dragSeries
10251049
case type
10261050
case swipe
10271051
case findText
@@ -1053,6 +1077,8 @@ struct Command: Codable {
10531077
let count: Double?
10541078
let intervalMs: Double?
10551079
let tapBatch: Bool?
1080+
let pauseMs: Double?
1081+
let pattern: String?
10561082
let x2: Double?
10571083
let y2: Double?
10581084
let durationMs: Double?

src/core/__tests__/dispatch-press.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import test from 'node:test';
22
import assert from 'node:assert/strict';
3-
import { shouldUseIosTapSeries } from '../dispatch.ts';
3+
import { shouldUseIosDragSeries, shouldUseIosTapSeries } from '../dispatch.ts';
44
import type { DeviceInfo } from '../../utils/device.ts';
55

66
const iosDevice: DeviceInfo = {
@@ -32,3 +32,12 @@ test('shouldUseIosTapSeries disables fast path for single press or modified gest
3232
test('shouldUseIosTapSeries disables fast path for non-iOS devices', () => {
3333
assert.equal(shouldUseIosTapSeries(androidDevice, 5, 0, 0), false);
3434
});
35+
36+
test('shouldUseIosDragSeries enables fast path for repeated iOS swipes', () => {
37+
assert.equal(shouldUseIosDragSeries(iosDevice, 3), true);
38+
});
39+
40+
test('shouldUseIosDragSeries disables fast path for single swipe and non-iOS', () => {
41+
assert.equal(shouldUseIosDragSeries(iosDevice, 1), false);
42+
assert.equal(shouldUseIosDragSeries(androidDevice, 3), false);
43+
});

src/core/dispatch.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,37 @@ export async function dispatchCommand(
180180
throw new AppError('INVALID_ARGS', `Invalid pattern: ${pattern}`);
181181
}
182182

183+
if (shouldUseIosDragSeries(device, count)) {
184+
await runIosRunnerCommand(
185+
device,
186+
{
187+
command: 'dragSeries',
188+
x: x1,
189+
y: y1,
190+
x2,
191+
y2,
192+
durationMs: effectiveDurationMs,
193+
count,
194+
pauseMs,
195+
pattern,
196+
appBundleId: context?.appBundleId,
197+
},
198+
{ verbose: context?.verbose, logPath: context?.logPath, traceLogPath: context?.traceLogPath },
199+
);
200+
return {
201+
x1,
202+
y1,
203+
x2,
204+
y2,
205+
durationMs,
206+
effectiveDurationMs,
207+
timingMode: 'runner-series',
208+
count,
209+
pauseMs,
210+
pattern,
211+
};
212+
}
213+
183214
for (let index = 0; index < count; index += 1) {
184215
const reverse = pattern === 'ping-pong' && index % 2 === 1;
185216
if (reverse) await interactor.swipe(x2, y2, x1, y1, effectiveDurationMs);
@@ -379,6 +410,10 @@ export function shouldUseIosTapSeries(device: DeviceInfo, count: number, holdMs:
379410
return device.platform === 'ios' && count > 1 && holdMs === 0 && jitterPx === 0;
380411
}
381412

413+
export function shouldUseIosDragSeries(device: DeviceInfo, count: number): boolean {
414+
return device.platform === 'ios' && count > 1;
415+
}
416+
382417
function computeDeterministicJitter(index: number, jitterPx: number): [number, number] {
383418
if (jitterPx <= 0) return [0, 0];
384419
const [dx, dy] = DETERMINISTIC_JITTER_PATTERN[index % DETERMINISTIC_JITTER_PATTERN.length];

src/platforms/ios/runner-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type RunnerCommand = {
1818
| 'tapSeries'
1919
| 'longPress'
2020
| 'drag'
21+
| 'dragSeries'
2122
| 'type'
2223
| 'swipe'
2324
| 'findText'
@@ -37,6 +38,8 @@ export type RunnerCommand = {
3738
count?: number;
3839
intervalMs?: number;
3940
tapBatch?: boolean;
41+
pauseMs?: number;
42+
pattern?: 'one-way' | 'ping-pong';
4043
x2?: number;
4144
y2?: number;
4245
durationMs?: number;

0 commit comments

Comments
 (0)