Skip to main content
echolot-hero

Find where Android startup time actually goes — from a Perfetto trace down to a line of code.

PyPI Python versions License Status


Contents · What it is · Requirements · Quick start · What you get · Commands · Detectors · How it works · Project layout · Documentation · Status


What it is

A Perfetto trace of one cold start holds around half a million slices in eighty megabytes. Nobody reads that, and an AI agent pointed at the raw file produces confident guesses instead of answers.

echolot sits in between. It runs six SQL detectors over the trace and returns about twenty rows: where the time went, how much of it, and the evidence behind each claim. Same trace in, same report out — the trace_processor version is pinned and verified on every run.

[!TIP] The intended way to use it is through Claude Code: you describe the regression in plain words, the agent collects traces, reads the report and walks down to the code. The command line works on its own too — see without an agent.

Requirements

Python 3.10 or newer
adb on PATH — ships in the Android SDK platform-tools
Device a phone or emulator with USB debugging on
Agent (optional) Claude Code, for the guided workflow

Validated on Android 14 (emulator) and Android 13 (Galaxy A51).

Quick start

1. Install

pipx install echolot

2. Set up your project

Run this once inside your Android project:

cd ~/my-app && echolot init

This installs the .claude/ layer — a skill, the perf-hunter agent and three commands — then checks that this machine computes traces correctly.

3. Open the agent and type one word

/echolot

That is the only entry point you need to remember. It asks the tool where the project stands and takes the next step by itself:

  • first run — builds echolot.yml from your repository and a probe trace, asking you four questions along the way;
  • every run after — hunts down the regression you describe.
/echolot                          reads the state, does whatever is next
/echolot why is cold start slow   hunt, with that as the question
/echolot init | setup | hunt | reflect | doctor

Coming back later

echolot

Prints where the project stands — layer, config, traces, last report, last doctor — and one line saying what to do next.

[!IMPORTANT] After upgrading the package, run echolot init again. It brings the .claude/ layer up to date and leaves files you edited alone.

Without an agent

The same work, by hand or in CI:

echolot collect -c echolot.yml -n 5                              # 5 repeats of the scenario
echolot analyze .echolot/traces/*.perfetto-trace -c echolot.yml  # build the report
echolot doctor -q                                                # exit 0/1: is this environment sane?

Results land in .echolot/out/report.md for you, report.json for the agent.

What you get

A Marker Report: one section per detector that fired, nothing else.

Example report (click to expand)
# Marker Report

Runs: **5**, numbers are medians across them
Process: `com.example.app` (pid 12903)
Scenario window: **1184 ms** (from 1102 to 1291)
Detectors fired: **3 of 6**

## Where the main thread spent its time

_measured as SELF time, children subtracted_

| Where | N | Self, ms | Total, ms | Max, ms |
|---|---|---|---|---|
| draw | 4 | 125.4 | 130.1 | 61.2 |
| TextLayout:initLayout | 61 | 88.0 | 88.0 | 4.1 |
| inflate | 12 | 47.3 | 210.7 | 12.9 |

## Blind spots: threads burning CPU with no instrumentation

_the only detector that finds a problem inside uninstrumented code_

| Where | Total, ms | Instrumented, ms | Evidence |
|---|---|---|---|
| DefaultDispatcher-worker-2 | 340.2 | 0.0 | 0 slices |

## Monitor contention

| Where | N | Total, ms | Max, ms | Evidence |
|---|---|---|---|---|
| Lock contention on a monitor lock | 9 | 61.5 | 22.4 | owner tid 12931 |

**Silent:** gc_pressure, binder_txn, runnable_starvation

The report is written for two readers at once: report.md reads like a findings list, report.json carries the same numbers in a shape the agent can walk. An 81 MB trace with 475k slices comes out as a 14 KB report.json in about five seconds.

Commands

Three audiences share one CLI, and echolot --help says which is which — the grouping below is generated from the same registration, so the two cannot drift apart.

Every argument after /echolot is a verb of the same name, doing the same thing plus whatever loop needs an agent. One word, one meaning, both surfaces.

Yours

command what it does
echolot where this project stands, and the next step
echolot init install or update the .claude/ layer; checks the environment
echolot hunt "<what regressed>" open an investigation — see below
echolot doctor environment + self-check on a synthetic trace; exit 0/1, -q for three lines

The pipeline — for CI, and for traces by hand

command what it does
echolot collect capture N traces of one scenario — launch, command or gradle
echolot analyze run the detectors, build a Marker Report
The agent's, behind /echolot — you do not call these
command what it does
probe processes, threads by CPU, scenario anchor candidates
names slice name inventory and detector mask coverage
domains slice-to-code map and instrumentation coverage
mark the first temporary markers for a project with none, from the manifest and the SDK — --apply / --remove
calibrate thresholds derived from known-healthy runs
explain list the detectors and their parameters
For improving the tool
command what it does
reflect the same kind of report, over an agent session — how the tool was used, where it got in the way

The investigation

.echolot/traces/ and .echolot/out/report.json mean "the latest set". An investigation is the label that says which question that set was recorded for, so that coming back a week later does not answer a question about scrolling with cold-start traces.

echolot hunt "cold start was 3s, now 7s" --since "the tab redesign"

That opens one, moves the previous set of traces aside without deleting it, and says whether the last investigation left temporary markers in your sources. echolot hunt on its own says what is open.

echolot hunt --list          every investigation, newest first
echolot hunt --show 2        one of them in full — including where its traces went
echolot hunt --resume        carry on with the open one
echolot hunt --done "..."    record what it came to

Each one is numbered, and everything it produces is filed under it: every round of traces by path, every report as a copy in .echolot/hunts/<n>/reports/. So a question asked three weeks ago still knows what was measured to answer it, and what each round concluded on the way.

Nothing moves to make this work — collect still writes to .echolot/traces/ and the latest report is still .echolot/out/report.json, so every example above and any CI job keep working unchanged.

You rarely type any of it. /echolot reads the state and, when an investigation has been sitting untouched with traces behind it, asks whether to carry on or start something new — and never asks inside the hunting loop, which re-records and re-instruments on purpose.

Detectors

detector what it catches
main_thread_block where the main thread spent its time, by self time
gc_pressure frequent or expensive GC, and waits on allocation
monitor_contention lock contention, with the owner's tid as evidence
binder_txn long synchronous IPC, and death by a thousand cuts
runnable_starvation thread ready to run but preempted on CPU
uninstrumented_cpu threads burning CPU with no instrumentation

That last one is the only detector that finds a problem inside code nobody instrumented. It does not guess — it states a fact:

thread DefaultDispatcher-worker-2 was Running for 340 ms, zero slices

Which is exactly where to add trace{} and record again.

[!NOTE] Each detector is one self-contained .sql file with its metadata in the header. Drop a file into echolot/sql/detectors/ and it is picked up — there is no registration step in code. See docs/detectors.md.

How it works

flowchart LR
    A["Android device"]
    B["trace<br/>81 MB · 475k slices"]
    C["6 SQL detectors<br/>pinned trace_processor"]
    D["report.md<br/>~20 rows"]
    E["report.json<br/>14 KB"]
    F(["You"])
    G(["The agent"])

    A -->|"echolot collect"| B
    B -->|"echolot analyze"| C
    C --> D --> F
    C --> E --> G

Project layout

android-project/
├── echolot.yml       ← the project half, committed
├── local.yml         ← device serials, binary path; in .gitignore
└── .echolot/         ← traces, reports, run log, reflect reports; in .gitignore

Read it the way you read gradle.properties and local.properties: one tool per machine, and the binding to a project living inside that project's repository.

Documentation

Start at the documentation index, or jump straight in:

document about
🎬 Collecting collect, the three modes, merging repeats
🔎 Analysing probe, names, domains — from a trace to a place in the code
🏷️ Marking mark — first markers for a project with no instrumentation
⚙️ Detectors writing your own, the context views, self time versus total
📏 Calibrating thresholds from healthy runs, why rank beats percentile
🔒 Determinism the pinned trace_processor, doctor, the self-check
🤖 The agent layer the .claude/ layer, and why the loop lives in a subagent
🪞 Reflect the report over an agent session, for improving the tool

Agent-facing reference material ships inside the package under echolot/claude/skills/echolot/references/ — the report schema, the config schema, how ART names things, and how to capture a trace by hand.

Status

v0. Everything planned for it is in place except CI mode.

The detectors were validated against a synthetic trace — 46 checks inside doctor — and against live traces from Android 14 (emulator) and Android 13 (Galaxy A51). The naming masks for GC, locks and binder were narrowed against those real traces, and every narrowing is pinned by a check.

[!WARNING] CI mode is not done. scenario.budget_ms is declared in the config but never read. What is missing is analyze with an exit code — either against the budget or on "any detector fired", which after calibrate amounts to a comparison against the baseline.

A failed detector never fails the run: the error goes to stderr and into report.json.

License

Apache 2.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

echolot-0.3.0.tar.gz (154.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

echolot-0.3.0-py3-none-any.whl (167.4 kB view details)

Uploaded Python 3

File details

Details for the file echolot-0.3.0.tar.gz.

File metadata

  • Download URL: echolot-0.3.0.tar.gz
  • Upload date:
  • Size: 154.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for echolot-0.3.0.tar.gz
Algorithm Hash digest
SHA256 02b01911aec3013d18e3357d33bde177d7288bca22ec1ddeeb5375b29e03ab5e
MD5 0e4470be0cc33941f733f85f7daeeb76
BLAKE2b-256 bd95241c7bd8a54384bb6e2bd29b1de82930b6a944919254a1c6c5f34f9984ff

See more details on using hashes here.

Provenance

The following attestation bundles were made for echolot-0.3.0.tar.gz:

Publisher: publish.yml on grishan0v/echolot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file echolot-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: echolot-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 167.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for echolot-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 90fc124973d2f31eeeb609f888075010a56c8b3a9f7b7d62ca2de07c43723e41
MD5 bd9092504ae9ad73b48eef3f83005c0a
BLAKE2b-256 313e92618856cca4effc4d5f6d1f2bde635f3c8c20bb49a41736af86e03ec6b9

See more details on using hashes here.

Provenance

The following attestation bundles were made for echolot-0.3.0-py3-none-any.whl:

Publisher: publish.yml on grishan0v/echolot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.5.0

2 files

0.4.0

2 files

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page