perf-tool
v1.2.0
Published
Automated Android performance testing CLI for the GL App, powered by Flashlight
Maintainers
Readme
GL App — Performance Testing Tool
Automates Android performance testing for the GL App using Flashlight. Replaces the manual process of opening the app, navigating to screens, and taking screenshots of performance graphs — replacing it with a single CLI command that produces a complete, reproducible performance report.
Table of Contents
- Overview
- How it works
- Prerequisites
- Installation
- Running the tool
- The interactive session
- Output and report
- Understanding the metrics
- Project structure
- Module reference
- Flow reference
- Adding a new flow
- Adjusting tap coordinates
- Troubleshooting
1. Overview
The problem it solves
Performance testing was previously done by:
- Manually opening the app
- Navigating to certain screens
- Starting the Flashlight dashboard at
localhost:3000 - Clicking "Start Measuring", interacting, clicking "Stop"
- Taking screenshots of the performance graphs
This is slow, inconsistent, and can't be repeated identically across runs.
What this tool does
gl-perf
│
├─ checks ADB + Flashlight are installed
├─ detects connected Android device
├─ lets you pick which flows to run
│
├─ collects device info (model, Android version, RAM, storage, CPU, battery)
├─ measures network (download + upload speed)
│
└─ for each selected flow:
├─ clears app from recents (fresh state)
├─ writes a temp ADB shell script
├─ runs `flashlight test` (live output streamed to terminal)
├─ runs `flashlight report` (generates Flashlight HTML per flow)
└─ after all flows: generates one consolidated HTML + PDF report
containing device info + network info + Flashlight's
interactive report for every flow, then opens it in the browserConstraints
- Physical Android device only (connected via USB)
- One device at a time
- Release build (
com.lms.greatlakes) installed on the device - macOS host machine
2. How it works
The stabilization problem
React Native apps are slow to settle after launch. JIT compilation of JS, image cache population, and layout warm-up take 30-60 seconds. Measuring during this period gives misleading results — frame drops caused by startup noise, not by your actual UI.
Every flow in this tool:
- Launches the app
- Waits 60 seconds (stabilization — not measured)
- Then starts the interaction and measurement
Flashlight's --skip 60000 flag trims those 60 seconds out of the report automatically.
Measurement windows
Each flow has a precise measurement window:
Timeline (ms):
│── 0 ──────────── 60,000 ──── 65,000 ────────────── 85,000+ ──│
│ [Launch + Stabilize] [Pre-buffer] [Scrolling] [Post-buffer] │
│ NOT MEASURED │←────── MEASURED ──────────────→│The skip and measureDuration properties on each flow control this. flashlight report --skip 60000 --duration 25000 produces a report trimmed to exactly that window.
3. Prerequisites
On the host machine (macOS)
| Tool | Version | Install |
|---|---|---|
| Node.js | ≥ 18 | brew install node |
| ADB | any | brew install android-platform-tools |
| Flashlight | ≥ 0.18 | curl https://get.flashlight.dev \| bash |
Verify each is available:
node --version # v18.x.x or higher
adb version # Android Debug Bridge version X.Y
flashlight --version # flashlight/X.Y.ZIf flashlight is not found after installation, open a new terminal window (the installer adds it to ~/.zshrc).
On the Android device
- USB debugging enabled: Settings → Developer Options → USB Debugging
- Accept the RSA fingerprint prompt on the device screen when first connecting
- App
com.lms.greatlakesinstalled (Play Store release build) - Device connected via USB cable (not wireless ADB)
Confirm the device is visible:
adb devices
# Expected output:
# List of devices attached
# ABC123XYZ deviceThe word device must appear — unauthorized means you haven't accepted the RSA key prompt on the phone.
4. Installation
Install the CLI globally so the gl-perf command is available anywhere on your machine:
# From the npm registry (once published)
npm install -g perf-tool
# Or directly from the GitHub repository
npm install -g github:your-org/perf-tool
# Or from a local clone (for development)
npm install -g .Verify the command is available after installation:
gl-perf --help
# or just run it — it starts the interactive session immediately
gl-perfNote: Google Chrome must be installed on your Mac for PDF generation. No additional download happens during install.
Dependencies installed automatically:
| Package | Purpose |
|---|---|
| inquirer@8 | Interactive checkbox/number/confirm prompts |
| chalk@4 | Coloured terminal output |
| ora@5 | Animated spinners for long-running steps |
| puppeteer-core@24 | Headless browser bridge — uses your system Chrome to render the PDF |
All three CLI packages are pinned to CommonJS-compatible versions (v8/v4/v5). Do not upgrade to v9+ of any of these — they switched to ESM-only.
5. Running the tool
Navigate to the directory where you want results to be saved, then run:
# Run the interactive CLI
gl-perf
# With debug output (Node.js stack traces on fatal errors)
DEBUG=1 gl-perfResults are saved in a results/ folder created in your current working directory, not in the package installation directory. Run gl-perf from whatever directory you want the reports to land in.
mkdir ~/perf-results && cd ~/perf-results
gl-perf
# Reports will appear in ~/perf-results/results/YYYY-MM-DD_HH-mm-ss/6. The interactive session
Step 1 — Pre-flight checks (automatic)
════════════════════════════════════════════════════════
GL App · Android Performance Testing Tool
Powered by Flashlight
════════════════════════════════════════════════════════
✔ Device connected: ABC123XYZThe tool:
- Checks that
flashlightis in PATH — exits with install instructions if not - Runs
adb devices— exits if no device or multiple devices are connected
Step 2 — Flow selection
? Select flows to run (space to select, enter to confirm):
◯ Run All Flows — Run every flow sequentially
──────────────────────────────────────────
◯ Home Screen Scroll — Scrolls the home screen feed up and down after app stabilization
◯ Course List Navigation — Taps the Courses tab and scrolls through the course catalogue
◯ Lesson Screen Scroll — Opens a course and a lesson, then scrolls the lesson content screen
◯ Landscape Mode Test — Forces landscape rotation and measures render performance during orientation changeUse space to toggle, enter to confirm. You must select at least one.
- Run All Flows — select this single option to run every registered flow sequentially without ticking them individually. If this option is selected, any other individual selections are ignored.
- Individual selection — tick one or more specific flows to run only those.
Step 3 — Confirmation
Ready to run:
• Home Screen Scroll
• Course List Navigation
Est. total : ~7 min
Proceed? (Y/n)The estimate is calculated from sum(flow.totalDuration).
Step 4 — Device and network collection (automatic, ~30 s)
✔ Device: Xiaomi Redmi Note 12 · Android 15 (SDK 35)
✔ Network: DL 45.2 Mbps · UL 8.3 MbpsNetwork measurement runs sequentially:
- Download via
curlon the device (2 MB from Cloudflare CDN) - Upload via
curlPOST on the device (512 KB to httpbin)
If curl is not available on the device (older Android), download/upload show N/A.
Step 5 — Flow execution
Each flow runs sequentially (Flashlight can only profile one app at a time):
────────────────────────────────────────────────────────────
Flow : Home Screen Scroll
Est : ~3 min
────────────────────────────────────────────────────────────
[Flashlight live output streams here...]
[1/4] Launching com.lms.greatlakes ...
[1/4] Stabilizing for 60 s ...
...
✔ Done — Home Screen ScrollFlashlight's own output streams directly to the terminal so you can watch the profiling in real time.
Step 6 — Report generation (automatic, ~2 s)
✔ Report saved: /your/cwd/results/2025-03-10_14-30-00/report.html
✔ PDF saved: /your/cwd/results/2025-03-10_14-30-00/report.pdf
════════════════════════════════════════════════════════
All done!
Flows tested : 2
Results dir : results/2025-03-10_14-30-00
Report : results/2025-03-10_14-30-00/report.html
PDF : results/2025-03-10_14-30-00/report.pdf
════════════════════════════════════════════════════════The report opens automatically in the default browser.
7. Output and report
Directory structure
Every run creates a timestamped directory:
results/
└── 2025-03-10_14-30-00/
├── report.html ← consolidated HTML report (open this)
├── report.pdf ← PDF version of the same report
├── home-scroll/
│ ├── results.json ← raw Flashlight JSON data
│ └── flashlight-report/
│ └── report.html ← Flashlight's own HTML (per-flow detail)
└── course-list/
├── results.json
└── flashlight-report/
└── report.htmlThe consolidated report (report.html)
A single self-contained HTML file. No server required — open it directly in any browser.
Report sections:
Sticky nav bar — jump links to Device, Network, and each flow section.
Device Information card — model, Android version, CPU/ABI, RAM, storage, screen, battery, build ID.
Network Information card — download speed and upload speed only.
Flow result sections (one per tested flow):
- Flow name and description header
- Flashlight's original interactive React app inlined directly — the JS bundle is embedded in a
<script>block with block-scoped data and a unique mount ID per flow, so all flow reports coexist in one document without conflicts
8. Understanding the metrics
All performance metrics (FPS, CPU, RAM, jank rate, frame timeline, thread breakdown) are shown directly in Flashlight's own interactive HTML report, embedded inside each flow section of the consolidated report. Refer to the Flashlight documentation for a full explanation of each metric.
9. Project structure
perf-tool/
├── index.js # Entry point and orchestrator
├── package.json
├── .gitignore
├── src/
│ ├── adb.js # All ADB command helpers + device info
│ ├── network.js # Network speed and WiFi measurement
│ ├── prompts.js # Interactive CLI prompts (inquirer)
│ ├── runner.js # Flashlight CLI wrapper + JSON parser
│ ├── report.js # HTML + PDF report generator
│ └── flows/
│ ├── index.js # Flow registry
│ ├── homeScroll.js # Flow: Home Screen Scroll
│ ├── courseList.js # Flow: Course List Navigation
│ ├── lessonScroll.js # Flow: Lesson Screen Scroll
│ └── landscapeMode.js # Flow: Landscape Mode Test
# Results are written to process.cwd() — wherever you run `gl-perf` from:
results/ # Created at runtime in your working directory
└── YYYY-MM-DD_HH-mm-ss/
├── report.html
├── report.pdf
└── {flow-id}/
├── results.json
└── flashlight-report/report.html10. Module reference
index.js — Orchestrator
The entry point. Wires all modules together in order:
- Prints banner
- Calls
checkFlashlightInstalled()— runswhich flashlight - Calls
checkDevice()fromadb.js - Calls
promptUser()fromprompts.js - Creates the output directory
results/YYYY-MM-DD_HH-mm-ss/underprocess.cwd() - Calls
getDeviceInfo()fromadb.js - Calls
measureNetwork()fromnetwork.js - Loops over selected flows, calling
runFlow()fromrunner.jsfor each - Calls
generateReport()fromreport.js(writesreport.html) - Calls
generatePdf()fromreport.js(writesreport.pdf— non-fatal if it fails) - Runs
open report.html
Fatal errors exit with code 1. Individual flow failures are caught and reported without stopping remaining flows.
src/adb.js — ADB utilities
Exports: checkDevice, getDeviceInfo, adb, adbHost
adb(command)
Runs adb shell {command} and returns trimmed stdout. Returns empty string on any error — never throws.
adbHost(command)
Runs adb {command} (host-side, not shell). Used for adb devices.
checkDevice()
Parses adb devices output. Returns the device serial string if exactly one device with status device is connected. Returns null if zero or multiple devices are found.
getDeviceInfo()
Collects device metadata via multiple adb shell getprop and other commands. Returns:
{
model: 'Xiaomi Redmi Note 12', // brand + model
android: 'Android 15 (SDK 35)',
cpu: 'armeabi-v7a',
ram: '3.8 GB',
storage: '12.4 GB used / 116.9 GB total',
screen: '720x1650',
battery: '100% (33.0°C)',
buildId: 'AP3A.240905.015',
}src/network.js — Network measurement
Exports: measureNetwork(spinner?)
Measures download and upload speed on the connected Android device via ADB. Uses spawnSync (no host shell) so format strings like %{speed_download} reach the device shell unmodified.
Download speed: adb shell curl -s -o /dev/null -w %{speed_download} --max-time 15 https://speed.cloudflare.com/__down?bytes=2000000
Downloads 2 MB from Cloudflare and reads curl's speed_download variable (bytes/s), converted to KB/s or Mbps.
Upload speed: Uses dd if=/dev/urandom piped into curl -X POST to httpbin. Reads speed_upload.
Returns:
{
download: '45.20 Mbps',
upload: '8.31 Mbps',
}src/prompts.js — Interactive prompts
Exports: promptUser()
Runs two sequential inquirer prompts:
- Checkbox — multi-select from all registered flows (sourced from
flows/index.js). A "Run All Flows" shortcut appears at the top of the list; selecting it runs every flow sequentially without individually ticking each one. - Confirm — shows a summary with est. duration and waits for Y/n
Returns { selectedFlows: Flow[] }.
Calls process.exit(0) if the user declines the confirmation.
src/runner.js — Flashlight wrapper
Exports: runFlow(flow, outputDir)
The core execution module. For each flow:
Step 1 — Write temp script
Calls flow.getScript(BUNDLE_ID) and writes the output to /tmp/gl-perf-{flow.id}.sh with mode 0o755 (executable).
Step 2 — Clear app from recents
Before every flow test, the tool:
- Force-stops the app (
am force-stop) - Removes its task from the recents list (
dumpsys activity recents→am task remove) - Returns to the home screen (
KEYCODE_HOME)
All three are best-effort — a failure in any step does not abort the flow.
Step 3 — Run Flashlight test
flashlight test \
--bundleId com.lms.greatlakes \
--testCommand "bash /tmp/gl-perf-{flow.id}.sh" \
--duration {flow.totalDuration} \
--iterationCount 1 \
--resultsFilePath {outputDir}/{flow.id}/results.json \
--resultsTitle "{flow.name}"Uses spawn with stdio: 'inherit' so Flashlight's live output streams directly to the terminal.
Step 4 — Run Flashlight report
flashlight report {outputDir}/{flow.id}/results.json \
--output-dir {outputDir}/{flow.id}/flashlight-report \
--skip {flow.skip} \
--duration {flow.measureDuration}Returns:
{
flow,
resultsPath,
flashlightReportPath, // null if flashlight report failed
}Bundle ID constant:
// src/runner.js, line 7
const BUNDLE_ID = 'com.lms.greatlakes';Change this if the bundle ID changes.
src/report.js — HTML report generator
Exports: generateReport({ outputDir, deviceInfo, networkInfo, flowResults, timestamp })
Builds a single self-contained HTML string and writes it to {outputDir}/report.html.
HTML architecture:
- All CSS is inline in a
<style>block - No external dependencies (no CDN required)
- Each flow's Flashlight
report.jsbundle is read from disk, the React mount target is patched from#appto a unique#app-flow-NID, and the bundle is inlined inside a<script>{ ... }</script>block — the{ }scopes each flow'sconst rGdata variable so bundles don't conflict in the shared page - Device and network info rendered as simple tables
src/flows/index.js — Flow registry
Exports: getAllFlows()
A simple array of all flow module objects. Add new flows here after creating their definition file.
11. Flow reference
Common properties on every flow
{
id: String, // kebab-case, used as folder name and chart ID
name: String, // human-readable name shown in report
description: String, // one-line description
totalDuration: Number, // total ms passed to flashlight --duration
skip: Number, // ms passed to flashlight report --skip
measureDuration: Number, // ms passed to flashlight report --duration
getScript: Function // (bundleId: string) => string — returns bash script
}Flow 1 — Home Screen Scroll (home-scroll)
| Property | Value |
|---|---|
| totalDuration | 90,000 ms |
| skip | 60,000 ms |
| measureDuration | 25,000 ms |
Timeline:
0s ──────── 60s ── 65s ───────────────── 75s ── 80s ── 90s
[Stabilize] [Pre] [4× DOWN + 1× UP] [Post] [done]
└──── measured ───────────┘ADB actions in measurement window:
- 5 s quiet recording before first swipe
- 4×
swipe 360 1200 360 400 500(scroll down) with 2 s gaps - 1×
swipe 360 400 360 1200 500(scroll up) - 5 s quiet recording after last swipe
Flow 2 — Course List Navigation (course-list)
| Property | Value |
|---|---|
| totalDuration | 100,000 ms |
| skip | 60,000 ms |
| measureDuration | 30,000 ms |
Timeline:
0s ── 60s ─ 63s ─ 68s ──────────────────── 83s ─ 88s ─ 100s
[Stab] [nav] [pre] [5× DOWN + 1× UP] [post] [done]
└──────── measured ────────────────┘ADB actions:
- Tap bottom-nav Courses tab:
tap 360 1590 - 3 s wait for navigation
- 5 s pre-scroll buffer
- 5× scroll down + 1× scroll up
- 5 s post-scroll buffer
Coordinate note: tap 360 1590 targets the middle bottom-nav tab. On a 720×1650 device the three tabs are at approximately x=144 (Home), x=360 (Courses/Learn), x=576 (Profile).
Flow 3 — Lesson Screen Scroll (lesson-scroll)
| Property | Value |
|---|---|
| totalDuration | 105,000 ms |
| skip | 60,000 ms |
| measureDuration | 35,000 ms |
Timeline:
0s ─ 60s ─ 62s ─ 65s ─ 68s ─ 73s ─────────────────── 88s ─ 93s
[St] [tab] [course] [lesson] [pre] [4× DOWN + 2× UP] [post]
└──────── measured ────────────┘ADB actions:
- Tap Courses tab:
tap 360 1590, wait 2 s - Tap first course card:
tap 360 600, wait 3 s - Tap first lesson item:
tap 360 500, wait 3 s - 5 s pre-scroll buffer
- 4× scroll down + 2× scroll up
- 5 s post-scroll buffer
Coordinate note: tap 360 600 and tap 360 500 assume the first course card and first lesson item are positioned in the upper-centre of the screen. These are the most likely coordinates to need adjustment — see Adjusting tap coordinates.
Flow 4 — Landscape Mode Test (landscape-mode)
| Property | Value |
|---|---|
| totalDuration | 95,000 ms |
| skip | 60,000 ms |
| measureDuration | 30,000 ms |
Timeline:
0s ─ 60s ─ 63s ─ 68s ──── 71s ──────────── 82s ─ 85s ─ 90s ─ 95s
[St] [tab] [pre] [rotate] [3× scroll land] [rot] [rest] [post]
└──────────────── measured ──────────────────────┘ADB actions:
- Tap Courses tab, wait 3 s
- 5 s quiet pre-rotation buffer
- Disable auto-rotate:
settings put system accelerometer_rotation 0 - Force landscape:
settings put system user_rotation 1 - Wait 3 s for UI to re-layout
- 3× scroll down in landscape (axes swap:
swipe 1300 360 400 360 500) - Restore portrait:
settings put system user_rotation 0 - Re-enable auto-rotate:
settings put system accelerometer_rotation 1 - 5 s post buffer
A trap cleanup EXIT ensures portrait orientation is always restored even if the script is interrupted.
12. Adding a new flow
Step 1 — Create the flow file
# Example: a profile screen scroll flow
touch src/flows/profileScroll.jsStep 2 — Write the flow definition
// src/flows/profileScroll.js
'use strict';
module.exports = {
id: 'profile-scroll',
name: 'Profile Screen Scroll',
description: 'Opens the profile screen and scrolls through account details',
totalDuration: 90000, // total script duration — must be >= skip + measureDuration + buffer
skip: 60000, // always 60000 unless you change the stabilization period
measureDuration: 25000, // must cover your interaction + pre/post buffers
getScript(bundleId) {
return `#!/bin/bash
# GL Perf Tool • Profile Screen Scroll
BUNDLE="${bundleId}"
# Phase 1: Launch + stabilize
echo "[1/4] Launching..."
adb shell monkey -p "$BUNDLE" -c android.intent.category.LAUNCHER 1 > /dev/null 2>&1
sleep 60
echo "[1/4] Stabilization done."
# Phase 2: Navigate to profile
echo "[2/4] Tapping Profile tab..."
adb shell input tap 576 1590
sleep 3
# Phase 3: Pre-scroll buffer
echo "[3/4] Pre-scroll buffer..."
sleep 5
# Phase 4: Scroll
echo "[4/4] Scrolling..."
adb shell input swipe 360 1200 360 400 500
sleep 2
adb shell input swipe 360 1200 360 400 500
sleep 2
adb shell input swipe 360 400 360 1200 500
sleep 2
sleep 5
echo "[Done]"
`;
},
};Timing checklist:
totalDuration ≥ skip + measureDuration + a few seconds buffer
90000 ≥ 60000 + 25000 + 5000 ✓If totalDuration is too short, Flashlight will stop recording before your script finishes. Total script runtime is 60 (stabilize) + 3 (nav) + 5 (pre) + 3×2 (scrolls) + 5 (post) = 79 s → use totalDuration: 85000.
Step 3 — Register the flow
// src/flows/index.js
const homeScroll = require('./homeScroll');
const courseList = require('./courseList');
const lessonScroll = require('./lessonScroll');
const landscapeMode = require('./landscapeMode');
const profileScroll = require('./profileScroll'); // ← add this
const ALL_FLOWS = [
homeScroll,
courseList,
lessonScroll,
landscapeMode,
profileScroll, // ← add this
];Step 4 — Verify
node -e "const {getAllFlows} = require('./src/flows'); console.log(getAllFlows().map(f=>f.id))"
# Should print: [ 'home-scroll', 'course-list', 'lesson-scroll', 'landscape-mode', 'profile-scroll' ]13. Adjusting tap coordinates
The flows use hardcoded pixel coordinates for the 720×1650 test device (Xiaomi Redmi Note 12). These need to be validated and possibly adjusted whenever:
- Testing on a different device (different screen resolution)
- The app UI layout changes significantly
- A bottom navigation restructure is shipped
Finding the correct coordinates
Method 1 — ADB tap logger (recommended)
Enable pointer location on the device:
Settings → Developer Options → Pointer location → ONA crosshair follows your finger and the coordinate is shown in the status bar. Tap the element you want and note the X, Y values. Disable after use.
Method 2 — Screenshot + pixel inspection
# Take screenshot and pull to Mac
adb shell screencap /sdcard/screen.png
adb pull /sdcard/screen.png /tmp/screen.png
open /tmp/screen.pngOpen in Preview, move cursor to the element, read pixel coordinates from the bottom-left corner of the Preview window.
Method 3 — UIAutomator dump
adb shell uiautomator dump /sdcard/ui.xml
adb pull /sdcard/ui.xml /tmp/ui.xmlThen search the XML for text="Courses" or the relevant button and read its bounds attribute ([left,top][right,bottom]).
Coordinate reference for 720×1650 (current test device)
Screen dimensions: 720 × 1650 px
Centre X: 360
─────────────────────────────────────
Bottom navigation bar (~y = 1590):
Left tab (Home): x = 144
Middle tab (Courses): x = 360
Right tab (Profile): x = 576
Scroll gestures (portrait):
Scroll DOWN: swipe 360 1200 360 400 {duration_ms}
Scroll UP: swipe 360 400 360 1200 {duration_ms}
Scroll gestures (landscape, axes swapped, 1650×720):
Scroll DOWN: swipe 1300 360 400 360 {duration_ms}
Scroll UP: swipe 400 360 1300 360 {duration_ms}14. Troubleshooting
"Flashlight is not installed"
curl https://get.flashlight.dev | bashThen open a new terminal — the installer adds ~/.flashlight/bin to ~/.zshrc, which only takes effect in a new shell.
Verify:
which flashlight
flashlight --version"No Android device detected"
- Make sure the device is plugged in via USB
- Run
adb devices— if it showsunauthorized, unlock your phone and look for an RSA fingerprint dialog, then tap "Allow" - If nothing appears, try a different USB cable or port
Flashlight exits immediately with code 1
This usually means the app was not found on the device:
adb shell pm list packages | grep greatlakes
# Should return: package:com.lms.greatlakesIf it doesn't, install the production app from the Play Store or deploy a release build. The debug build (com.lms.greatlakes.debug) will not match the hardcoded bundle ID.
To use the debug build, change the constant in src/runner.js:
const BUNDLE_ID = 'com.lms.greatlakes.debug';Network shows "N/A" for download/upload
curl is not available on the Android device (older Android versions or locked-down builds). There is no fallback — the tool reports N/A. This is a display issue only — the performance data is unaffected.
Lesson Screen Scroll flow doesn't reach the lesson
The lesson-scroll flow taps (360, 600) to open the first course and (360, 500) to open the first lesson. If other content (banner, header) is at those coordinates:
- Find the correct coordinates using pointer location (see Adjusting tap coordinates)
- Update
src/flows/lessonScroll.js— search fortap 360 600andtap 360 500 - Increase the
sleepdurations if the screen is slow to respond
Landscape mode does not rotate
Auto-rotation must be disabled before user_rotation is set. The script does this, but some devices require auto-rotation to already be off at the OS level. Check:
adb shell settings get system accelerometer_rotation # should be 1 (on)If it was already 0 (off), the flow still works but the cleanup trap restores it to 1 which may not match the device's original setting. Adjust the cleanup function in landscapeMode.js if needed.
Multiple devices warning
The tool requires exactly one device. If you have multiple:
adb devices
# List of devices attached
# ABC123 device
# XYZ789 device ← unplug this oneUnplug all but the test device, then re-run.
Viewing Flashlight's raw report for a specific flow
Each flow's Flashlight HTML report is saved alongside the raw JSON:
results/2025-03-10_14-30-00/home-scroll/flashlight-report/report.htmlOpen it directly in a browser for Flashlight's own interactive view (thread-level CPU breakdown, detailed frame timeline).
Debugging with DEBUG=1
DEBUG=1 node index.jsOn any unhandled error, prints the full Node.js stack trace in addition to the error message.
