npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

perf-tool

v1.2.0

Published

Automated Android performance testing CLI for the GL App, powered by Flashlight

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

  1. Overview
  2. How it works
  3. Prerequisites
  4. Installation
  5. Running the tool
  6. The interactive session
  7. Output and report
  8. Understanding the metrics
  9. Project structure
  10. Module reference
  11. Flow reference
  12. Adding a new flow
  13. Adjusting tap coordinates
  14. 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 browser

Constraints

  • 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:

  1. Launches the app
  2. Waits 60 seconds (stabilization — not measured)
  3. 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.Z

If 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.greatlakes installed (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      device

The 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-perf

Note: 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-perf

Results 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: ABC123XYZ

The tool:

  • Checks that flashlight is 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 change

Use 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 Mbps

Network measurement runs sequentially:

  1. Download via curl on the device (2 MB from Cloudflare CDN)
  2. Upload via curl POST 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 Scroll

Flashlight'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.html

The 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.html

10. Module reference

index.js — Orchestrator

The entry point. Wires all modules together in order:

  1. Prints banner
  2. Calls checkFlashlightInstalled() — runs which flashlight
  3. Calls checkDevice() from adb.js
  4. Calls promptUser() from prompts.js
  5. Creates the output directory results/YYYY-MM-DD_HH-mm-ss/ under process.cwd()
  6. Calls getDeviceInfo() from adb.js
  7. Calls measureNetwork() from network.js
  8. Loops over selected flows, calling runFlow() from runner.js for each
  9. Calls generateReport() from report.js (writes report.html)
  10. Calls generatePdf() from report.js (writes report.pdf — non-fatal if it fails)
  11. 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:

  1. 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.
  2. 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:

  1. Force-stops the app (am force-stop)
  2. Removes its task from the recents list (dumpsys activity recentsam task remove)
  3. 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.js bundle is read from disk, the React mount target is patched from #app to a unique #app-flow-N ID, and the bundle is inlined inside a <script>{ ... }</script> block — the { } scopes each flow's const rG data 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
  • swipe 360 1200 360 400 500 (scroll down) with 2 s gaps
  • 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.js

Step 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 → ON

A 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.png

Open 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.xml

Then 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 | bash

Then 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"

  1. Make sure the device is plugged in via USB
  2. Run adb devices — if it shows unauthorized, unlock your phone and look for an RSA fingerprint dialog, then tap "Allow"
  3. 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.greatlakes

If 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:

  1. Find the correct coordinates using pointer location (see Adjusting tap coordinates)
  2. Update src/flows/lessonScroll.js — search for tap 360 600 and tap 360 500
  3. Increase the sleep durations 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 one

Unplug 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.html

Open 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.js

On any unhandled error, prints the full Node.js stack trace in addition to the error message.