@perf-labs/appium-driver
v0.1.0-alpha.11
Published
PerfLab Appium driver: write a journey module, run it on BrowserStack with `perflab farm run --driver appium --journey-module`.
Readme
@perf-labs/appium-driver
PerfLab's built-in Appium driver for perflab farm run --driver appium --journey-module <file> (BrowserStack App Automate, iOS and Android; --platform ios|android, default ios). A thin step vocabulary over W3C WebDriver: you write the journey, the driver owns the session, run tokens, step markers, the readiness wait (expectNext) after a touch and the result files. Design: docs/plans/54b-appium-driver.md.
A journey module default-exports defineJourney({ app, home, options?, setup?, journeys }); see the plan for the full shape. A journey needs no import from this package: selectors are plain objects ({ id }, { text }, { ...sel, childOf }, or a bare string) and everything else is on the t it is given (t.acceptAlert, t.openLink, ...). defineJourney is only an identity helper for editor types. The CLI runs its own bundled copy, so do not instanceof driver errors (WdError) from your own copy; test e.wdError.
Run directly (what farm run does): node dist/src/cli.js --journey-module j.mjs --app bs://… --tokens-js tokens.js --journeys a,b --repeat 3 --out DIR [--local-identifier ID], with BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY. Exit 0, or 1 when the session or an iteration failed (the result files are written either way).
Step checks and Android
t.waitAll([{ shown: sel }, { gone: sel }, { keyboard: 'up' }, ...]) waits until all conditions hold in one poll (the poll asks them one lookup after another). On Android the ids are asked in one UiAutomator2 lookup (a walk of the whole tree costs the same for one id and for ten); elsewhere each condition is waited for in turn. The keyboard condition is answered by the platform (Android mobile: isKeyboardShown, iOS the Return key), so a journey needs no platform branch. t.swipeFromUntilVisible(from, direction, target, times) swipes until a target shows.
Android specifics (UiAutomator2, measured on a Galaxy S23): a lookup that finds nothing walks the whole tree (about 1.2 s), one that finds a shown element takes 0.1 to 0.2 s, so findShown asks for one element first. ignoreUnimportantViews is on (about 30 % faster lookups).
Inverted bounds: when the keyboard opens, TapIn translates the chat up while Android still cuts the chat nodes by the container's untranslated rect. The chat's scroll container and its nodes come back with top below bottom and displayed=false, and are left out of lookups. While the keyboard is up (and at most every 3 s) waitAll reads the page source with allowInvisibleElements and counts such a node as shown when it sits in an inverted scroll container, its real bottom edge is on screen and below the edge the clipped-away nodes pile up on (at least two clipped messages are needed to find the edge). See src/snapshot.ts.
How to write journeys that run fast on Android, with measured costs: docs/writing-fast-journeys.md.
GC before the action (--action-gc on|off, default off)
With --action-gc on the driver asks the app for a full garbage collection before every declared step, after the step's readiness wait and before its start marker, and once more before the run's flush. Design: docs/plans/gc-before-action.md; device checks: docs/plans/gc-before-action-measurement.md.
- Each command has a number
n, unique in the session. Android sends it as a broadcast (mobile: broadcast, action<bundleId>.perflab.COMMAND, extrascmd=gc,n) and reads the answergc:<n> ...from the broadcast's result. iOS opens<scheme>devtools/perf-lab?cmd=gc&n=<n>and waits up to 10 s for the elementperflab.ackwhose label starts withgc:<n>. - The step's start marker carries
gc_n, so the backend (derivation 18) pairs the step with its GC record without mapping clocks. - No answer turns the command off for the rest of the session (
actionGc: unsupportedindriver-log.json); the test still passes. Safari link mode sends none (safari). - The GC command ends the app's open windows: runs with and without it are a different instrument (
actionGcaspect), never compared as an app change.
GC at the run end (--end-gc on|off, default off)
One GC command at the end of every run, after the journey's last step and its readiness wait, before the markers and the flush, also when --action-gc is off. Its record gives the end reading after a GC (derivation 19: mem.end_after_gc_mb, mem.retained_growth_mb, mem.js_heap_end_mb; setup fact sdk.end_gc). End the journey on its home screen (docs/writing-fast-journeys.md), so the reading holds what the journey kept, not an open heavy screen; sdk.journey_ends_home = 0 flags a run that did not.
- Same command and fallbacks as above; an app that does not answer turns it off for the rest of the session.
- The command ends the last open window (end reason
gc-command) instead of the flush: that one window per run is shorter than on a driver without it. Driver 0.2.0 carries both commands, so runs before and after it are a different instrument already (driverVersionaspect). - Off by default: with an SDK that answers it, the run sends a
PERF_LAB gcline, which the production API beforefeat/gc-before-actionrejects (400 bad_item, the batch is lost). Turn it on (--end-gc on,endGc: true) only after the migration and the API are deployed. Older SDKs do not answer and send no line.
