Real-time remote Linux performance profiler with a live web UI (perf + flame graphs + line-level source annotation)
Project description
๐ Read the documentation site โ
Hosted on GitHub Pages โ features, architecture deep-dive, CLI & HTTP API reference, live UI tour.
Sample counts climb live as perf record rounds stream in. Flip to flame graph, click a function, drop into source with line-level heat. Zero polling โ Server-Sent Events.
PerfLens
PerfLens is a remote Linux performance profiler with a real-time web UI. Drop the agent on any Linux device (ARM or x86), point it at a PID, and watch flame graphs, function tables, perf stat metrics, and line-level annotated source update live in your browser.
No frontend frameworks. No Docker. Plain HTML/CSS/JS for the UI, and a single static C agent binary (~2 MB) with zero runtime dependencies โ it runs on anything from bare-metal embedded boards to servers, installs with one curl command, and updates itself with --update.
Highlights
- Real-time streaming โ
perf recordruns in ~8s rounds; each round is compressed with zstd and streamed over a 5-byte framed TCP protocol - Live web UI โ Server-Sent Events push parsed function tables, flame graphs, and
perf statpanels to the browser as new data arrives - Source-level annotation โ
addr2linemaps samples back to source lines; the UI heat-colors hot lines red/amber/green - Differential profiling โ snapshot a baseline (or pick a saved session) and the flame graph recolors by change (red grew, blue shrank) while the function table shows per-function ฮ; did-my-fix-help in one glance
- Timeline scrubbing โ drag across a Device Health sparkline to rebuild the flame graph and function table from only the samples collected in that window (e.g. select a CPU spike)
- Per-thread profiling โ filter flame graphs, function tables, and source annotations by thread; dedicated thread analysis view with per-thread CPU breakdown, plus an optional real-time Live CPU column fed by the agent
- Device health strip โ live CPU, memory, temperature, load, and network sparklines; opt-in disk I/O and per-thread CPU collectors (off by default to stay light on embedded targets) toggled from the UI at runtime
- Interactive SVG flame graphs โ vanilla JS, no d3, no bundling; zoomable, hoverable
- Shareable URLs โ tab, event, thread filter, flame-graph zoom, and replayed session live in the URL hash; refresh or paste a link and land on the same view
- Cross-compilation toolchain support โ
--toolchain-prefixderives addr2line and readelf from a single prefix;--sysrootresolves shared libraries and source files under a sysroot tree - ARM + x86 โ same agent code runs on aarch64, aarch64_be, armv7l, x86_64
- Session save / replay โ raw chunks saved to disk, replayed lazily on demand via the UI's session list
- Static C agent โ single binary with vendored zstd, no runtime dependencies; cross-compiles to aarch64, aarch64_be, armv7l, armeb, x86_64; one-line curl install and built-in self-update
- Zero-friction server install โ
uvx perflens(orpipx/pip install --user); everything resolves user-space, no sudo, corporate-machine friendly. Missing binutils?perflens provisiondownloads static addr2line/readelf into~/.perflens/bin - Capability probing โ the agent discovers which perf events and call-graph modes (
fp/dwarf/lbr) actually work on the target before collecting - Zstd compression โ typical perf script payloads compress 20โ40ร before hitting the wire
Architecture
The pipeline in one sentence: perf record โ agent โ TCP+zstd โ server โ parser โ source mapper โ SSE โ browser.
Target device
- The agent probes the kernel's
perf_event_paranoid, enumerates candidate events (cycles,instructions,cache-*,branch-*,page-faults,context-switches,cpu-migrations), tries call-graph modes in order (fp,dwarf,lbr), and picks the first that produces non-empty stacks - Each collection round runs
perf recordandperf statin parallel for N seconds, thenperf scriptto flatten the output - The combined text is compressed with in-process zstd (level 1) and framed with a 5-byte header
- Reconnects with exponential backoff if the server drops
- Single static binary โ no Python, no libc, no zstd needed on the target. Suitable for old or minimal ARM/x86 Linux devices.
Local machine
perflens serveruns a FastAPI/uvicorn HTTP layer (web.py) in front of a plain-threads agent side (server.py: TCP listener, recv loops, aggregation worker); one agent at a time, any number of SSE browser clientsparser.pyparsesperf scriptandperf stattext into per-event sample lists;aggregator.pyfolds each new chunk incrementally into function summaries and flame graph trees (O(new samples) per chunk, not O(total))source_mapper.pypipelines addresses throughaddr2linein batches of 500, applies compile-time path prefix rewrites, and builds annotated source views;symcache.pypersists resolutions and source-file indexes under~/.perflens/cacheso warm restarts skip the work- A single
SourceMapperis created at startup and shared across requests โ no per-request forking - Sessions are spooled to disk as compressed chunks while streaming and replayed lazily on demand (with a config-keyed replay cache) when the user opens them from the UI
Wire protocol
Every message is a 5-byte header followed by a payload of exactly LEN bytes:
header = struct.pack('!IB', len(payload), flag)
sock.sendall(header + payload)
| Field | Size | Meaning |
|---|---|---|
LEN |
4 bytes (uint32, big-endian) | Payload length in bytes |
FLAG |
1 byte (uint8) | Frame type (see below) |
PAYLOAD |
LEN bytes |
Perf data, JSON command/response, or JSON metrics |
The protocol is bidirectional โ data and health metrics flow agent โ server, commands flow server โ agent over the same socket:
| Flag | Direction | Payload |
|---|---|---|
0 |
agent โ server | Raw perf script text, optionally followed by a ### PERF_STAT ### section |
1 |
agent โ server | Same, zstd-compressed |
2 |
server โ agent | Command request (JSON: start, stop, pause, resume, configure, ...) |
3 |
agent โ server | Command response / hello handshake (JSON) |
4 |
agent โ server | Device health metrics (JSON, every 2s: CPU, memory, temperature, network, per-process stats; opt-in disk I/O and per-thread CPU via configure_metrics) |
The server reads the 5 header bytes first, then exactly LEN more. Compression is in-process zstd on both ends (vendored in the agent, the zstandard package on the server, external zstd binary as a fallback). Typical ratio on real perf script output is 20โ40ร.
Quick Start
Option A โ install with uv/pip (recommended)
# On the machine where you want to view profiles (Python 3.10+, no sudo):
uvx perflens serve \
--source-dir /path/to/sources \
--binary /path/to/unstripped-binary
# โ http://localhost:8080
# Equivalent alternatives:
# pipx install perflens then: perflens serve ...
# pip install --user perflens then: perflens serve ...
# On the target Linux device โ one-line install (no sudo, ~/.perflens/bin):
curl -fsSL https://raw.githubusercontent.com/harshithsunku/perflens/master/install-agent.sh | sh
# Option 1: agent connects to server
~/.perflens/bin/perflens-agent --server <server-ip>
# Option 2: agent listens, server connects to agent
~/.perflens/bin/perflens-agent --listen
# Then use the Live Debug wizard in the UI to connect to <device-ip>:9999
# Update later (downloads, verifies, atomically replaces itself):
~/.perflens/bin/perflens-agent --update
# Or push the agent to the device from your machine (ssh arch-detect):
perflens push-agent user@device
Release assets published on every tagged release:
| Asset | What it is |
|---|---|
perflens-<ver>-py3-none-any.whl |
Server โ Python wheel (uvx --from ...) |
perflens-agent-linux-x86_64 |
Agent โ static binary, Linux x86_64 |
perflens-agent-linux-aarch64 |
Agent โ static binary, Linux aarch64 |
perflens-agent-linux-aarch64_be |
Agent โ static binary, Linux aarch64 BE |
perflens-agent-linux-armv7l |
Agent โ static binary, Linux armv7l |
perflens-agent-linux-armeb |
Agent โ static binary, Linux armv7 BE |
perflens-tools-linux-{x86_64,aarch64}.tar.gz |
Static addr2line+readelf for perflens provision |
Option B โ build the agent yourself
# Build (on your build machine)
cd agent-c
make # native x86_64
make CROSS=aarch64-linux-gnu- # ARM64 little-endian
make CROSS=aarch64_be-linux-musl- # ARM64 big-endian
make CROSS=arm-linux-gnueabihf- # ARMv7 little-endian
make CROSS=armeb-linux-musleabihf- # ARMv7 big-endian
# Deploy (single file, no dependencies)
scp perflens-agent user@device:/tmp/
ssh user@device
/tmp/perflens-agent --server <server-ip> # connects to server
/tmp/perflens-agent --listen # or: wait for server to connect in
The agent is a single static binary (~2 MB) with zstd built in.
Option C โ from source (dev / contributors)
# Server (editable install pulls fastapi/uvicorn/orjson/zstandard)
uv venv && uv pip install -e .
.venv/bin/perflens serve \
--source-dir /path/to/source \
--binary /path/to/myprogram \
--port 9999 \
--http-port 8080
# Agent (on the target device โ build once, copy the binary)
cd agent-c && make && scp perflens-agent user@device:/tmp/
ssh user@device
/tmp/perflens-agent --server <server-ip> # connects to server
/tmp/perflens-agent --listen # or: wait for server
Then browse to http://<server-ip>:8080.
Prerequisites
| Component | Needs |
|---|---|
| Target device | Linux and perf โ nothing else (the static agent has zstd built in) |
| Local machine | Python 3.10+ and uv/pip. addr2line/readelf from binutils for source mapping โ if missing, perflens provision downloads static builds into ~/.perflens/bin (no sudo). For cross-compiled targets: a matching toolchain with <prefix>addr2line and <prefix>readelf |
| Binary | Compiled with -g (debug symbols), not stripped |
| Source | A checkout of the source tree readable from the server machine |
Configuration
Server CLI
| Option | Default | Description |
|---|---|---|
--port PORT |
9999 |
TCP port the agent connects to |
--http-port PORT |
8080 |
HTTP port for the web UI |
--source-dir DIR |
. |
Root of the source tree for line annotation |
--binary PATH |
โ | Unstripped binary (enables addr2line) |
--map PATH |
โ | GNU ld linker map file (optional symbol fallback) |
--path-map FROM=TO |
โ | Rewrite compile-time paths to local paths (e.g. /build/src=/home/user/src) |
--addr2line PATH |
โ | Custom addr2line binary (overrides bin/ and PATH) |
--readelf PATH |
โ | Custom readelf binary |
--toolchain-prefix PREFIX |
โ | Cross-compilation prefix (e.g. arm-linux-gnueabihf-); derives addr2line and readelf |
--sysroot DIR |
โ | Sysroot for resolving shared library modules and source files |
--max-samples N |
500000 |
Raw-sample ring buffer cap (aggregates always cover the full session) |
--sessions-dir DIR |
~/.perflens/sessions |
Where saved sessions are stored (PERFLENS_HOME moves the whole ~/.perflens root) |
--http-bind ADDR |
127.0.0.1 |
Web UI bind address (0.0.0.0 to expose โ the UI has no auth) |
--browse-root DIR |
~ |
Directory the wizard's file picker is confined to |
--token SECRET |
โ | Shared secret agents must present (or PERFLENS_TOKEN) |
--inline / --no-inline |
on | Enable/disable inline function resolution via addr2line -i |
--import FILE |
โ | Import a perf.data file at startup and make it available as a session |
Agent CLI
Three run modes (must pick one):
| Mode | Description |
|---|---|
--listen |
Daemon: bind --port, wait for server to connect in |
--server HOST |
Daemon: connect out to server (reconnects with exponential backoff) |
--output FILE |
Headless: collect once, write to file (- for stdout). Requires --pid. |
Options:
| Option | Default | Description |
|---|---|---|
--pid PID |
โ | PID of process to profile (required for --output; set via UI wizard in daemon modes) |
--port PORT |
9999 |
TCP port (listen or connect) |
--frequency HZ |
99 |
perf record -F sampling frequency |
--duration SECS |
8 |
Length of each collection round |
--rounds N |
1 |
Number of collection rounds (--output mode only) |
--token SECRET |
โ | Shared secret sent to the server in the hello (or PERFLENS_TOKEN) |
--update |
โ | Self-update from the latest GitHub release, then exit |
--version |
โ | Print version and exit |
HTTP API
| Endpoint | Method | Description |
|---|---|---|
/api/status |
GET | Server + agent connection state, sample totals |
/api/stream |
GET | Server-Sent Events: status, agent_connected, event_types, data_version (per chunk), perf_stat, metrics_<type> |
/api/per-event?event=<evt> |
GET | Cached per-event snapshot (gzip); clients fetch it when SSE data_version bumps |
/api/sessions |
GET | List saved sessions (metadata only) |
/api/sessions/<id> |
GET | Lazy-replay a session (parses raw chunks on demand, cached) |
/api/export/session/<id>?format= |
GET | Export a session: collapsed (FlameGraph stacks) or json |
/api/export/flamegraph?event=&session= |
GET | Standalone SVG flame graph |
/api/source?file=<path>&event=<evt>&tid=<tid> |
GET | Annotated source for a single file (optionally filtered by thread) |
/api/thread-view?event=<evt>&tid=<tid> |
GET | Per-thread flamegraph and function summary |
/api/thread-summary?event=<evt> |
GET | Thread overview: all threads with sample counts and top functions |
/api/time-window?event=&start=&end= |
GET | Flame graph + function summary for samples received in a time range (timeline scrubbing) |
/api/index/status |
GET | Source-index / DWARF file-list state (truncated preview) |
/api/index/files?offset=&limit=&q= |
GET | Paginated DWARF source-file list |
/api/metrics/current |
GET | Latest device health metrics per type |
/api/metrics/history?type=&start= |
GET | Health metrics time series |
/api/connect |
POST | Connect out to a --listen agent ({"host": ..., "port": ...}) |
/api/agent/command |
POST | Send a command to the connected agent (start, stop, pause, ...) |
/api/wizard/state |
GET/POST | Persisted Live Debug wizard state |
/api/browse?path= |
GET | File picker listing (confined to --browse-root) |
/api/config/binary |
POST | Set the unstripped binary at runtime |
/api/config/source |
POST | Set the source directory at runtime |
/api/config/pathmap |
POST | Set compile-time path rewrites at runtime |
/api/config/toolchain |
POST | Set toolchain prefix and sysroot at runtime |
/api/import |
POST | Import an uploaded perf.data file as a session (needs perf on the server) |
/api/stop |
GET | Disconnect the active agent (triggers normal session save) |
/* |
GET | Static files from ui/ |
Supported perf events
| Event | Typical use | Mode |
|---|---|---|
cycles |
CPU time / hot paths | record + stat |
instructions |
IPC, retired instruction count | record + stat |
cache-misses |
Last-level cache misses | record + stat |
cache-references |
LLC accesses | record + stat |
branch-misses |
Branch prediction misses | record + stat |
branch-instructions |
Total branches | record + stat |
page-faults |
Minor/major page faults | stat only |
context-switches |
Scheduling pressure | stat only |
cpu-migrations |
Inter-CPU movement | stat only |
The agent probes each event before use and only emits the ones the kernel actually supports.
Building release packages
./build_package.sh # server wheel/sdist + native C agent
./build_package.sh --server # Python wheel + sdist only
./build_package.sh --agent-c # C agent only (native static binary)
Output lands in dist/:
dist/
โโโ perflens-<ver>-py3-none-any.whl # server (uvx / pipx / pip)
โโโ perflens-<ver>.tar.gz # server sdist
โโโ perflens-agent-c-<ver>.tar.gz # agent tarball
โโโ perflens-agent-linux-<arch> # agent raw binary (stable name)
CI
.github/workflows/test.yml runs the pytest suite on Python 3.10โ3.13 (parser, aggregator differentials against device-captured fixtures, source mapper, HTTP API, provisioning against a fake release server, and the C-agent wire protocol driven through a fake framing server with a perf shim), plus a puppeteer browser E2E that replays a fixture session through the real UI (node tests/e2e_ui.mjs, self-contained).
.github/workflows/build.yml lints (ruff), runs the pytest suite, builds and smoke-runs the Python wheel (with a wheel-contents check), builds the static C agent for five architectures (x86_64, aarch64, aarch64_be, armv7l, armeb), and builds static addr2line/readelf tools bundles (x86_64, aarch64) for perflens provision. Big-endian agent targets use musl toolchains from musl.cc since Ubuntu only ships little-endian sysroots. Tagged pushes (v*) create a GitHub Release and attach all artifacts โ including raw perflens-agent-linux-<arch> binaries with stable names that install-agent.sh and the agent's --update fetch from releases/latest/download/. Tagged pushes also publish the package to PyPI via Trusted Publishing (OIDC โ no stored tokens).
Project layout
perflens/
โโโ install-agent.sh # curl-able agent installer (arch detect, no sudo)
โโโ agent-c/
โ โโโ src/ # C agent modules (agent.h + 10 .c files, static binary, zero deps)
โ โโโ Makefile # native + cross-compile targets
โ โโโ vendor/zstd/ # vendored zstd amalgamation
โโโ pyproject.toml # pip/uv package (console script: perflens)
โโโ src/perflens/ # the server package
โ โโโ server.py # agent TCP protocol + state + sessions
โ โโโ web.py # FastAPI/uvicorn HTTP layer + SSE hub
โ โโโ cli.py # perflens serve/import/push-agent/provision
โ โโโ parser.py # perf script / perf stat parser
โ โโโ aggregator.py # incremental per-event aggregation
โ โโโ source_mapper.py # addr2line pipeline + path remap
โ โโโ symcache.py # persistent caches (~/.perflens/cache)
โ โโโ provision.py # user-space static-tools download
โ โโโ ui/ # single-page app (ships in the wheel)
โ โโโ index.html
โ โโโ app.js # all UI logic (vanilla JS)
โ โโโ style.css # dark + light themes
โโโ docs/
โ โโโ hero.svg
โ โโโ architecture.svg
โ โโโ wire-protocol.svg
โโโ tests/
โ โโโ conftest.py # shared fixtures (device-captured sessions)
โ โโโ test_*.py # pytest suite (parser, aggregator, HTTP, agent, ...)
โ โโโ e2e_ui.mjs # puppeteer browser E2E (self-contained)
โ โโโ fixtures/ # gzipped perf sessions from real devices
โ โโโ sample_workload.c # multi-function test program
โ โโโ Makefile # gcc -g -O0 -lm
โโโ build_package.sh # local wheel + agent builds
โโโ .github/workflows/test.yml # pytest matrix + browser e2e
โโโ .github/workflows/build.yml # lint + test + wheel + agents + release
โโโ VERSION
โโโ LICENSE (MIT)
โโโ README.md (this file)
Troubleshooting
perf_event_paranoid too high. The agent warns at startup if /proc/sys/kernel/perf_event_paranoid > 1 and the UI may show limited events.
sudo sysctl -w kernel.perf_event_paranoid=1
No function names. Compile with -g and do not strip. file ./myprogram should say not stripped and with debug_info.
No source line mapping. Double-check --binary points at the exact unstripped binary running on the target and --source-dir contains the source files. Use --path-map /build/src=/home/me/src when your build was done under a different root.
Agent can't connect. The server must be reachable on --port. Check with nc -zv <server-ip> 9999.
LXC / container: perf record -p <pid> is empty. Some container environments strip the perf capability set. A system-wide perf record -a usually works; the agent's -p <pid> mode does not.
Call-graph probing hangs / slow startup. Call-graph probing tests fp, dwarf, then lbr in sequence โ this adds ~6โ12 seconds on first connection. Normal.
Design rules
These are the rules the project is built to:
- Simplicity first โ a small, deliberate server stack (fastapi/uvicorn/orjson/zstandard, all user-space via uv); plain HTML/JS/CSS, no bundler, no npm; the agent stays zero-dependency static C
- Defensive parsing โ
perfoutput format varies across kernel versions; parser is forgiving - No secrets in code โ generic and open-source-friendly
- No over-engineering โ if it doesn't earn its complexity, it gets cut
See CLAUDE.md for the full internal reference, or the documentation site for the polished version.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file perflens-0.7.0.tar.gz.
File metadata
- Download URL: perflens-0.7.0.tar.gz
- Upload date:
- Size: 119.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0638f3239d9a7da6c283143b972e2df0949b5d90dfa464ef92b280f36dc7b92
|
|
| MD5 |
d32c1273d602a123492b132f0bddf898
|
|
| BLAKE2b-256 |
9d74173797d93d49e88ab86b51322c08a1e301f3689da277ea4b0cdcaacd4a8d
|
Provenance
The following attestation bundles were made for perflens-0.7.0.tar.gz:
Publisher:
build.yml on harshithsunku/perflens
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
perflens-0.7.0.tar.gz -
Subject digest:
a0638f3239d9a7da6c283143b972e2df0949b5d90dfa464ef92b280f36dc7b92 - Sigstore transparency entry: 2193226821
- Sigstore integration time:
-
Permalink:
harshithsunku/perflens@c7f1b180020a10ec70dacb258bd2866c3e088df7 -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/harshithsunku
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@c7f1b180020a10ec70dacb258bd2866c3e088df7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file perflens-0.7.0-py3-none-any.whl.
File metadata
- Download URL: perflens-0.7.0-py3-none-any.whl
- Upload date:
- Size: 119.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
70f8d84a080be0897b2f9e3ddfa60046475c1dd3424a49a9ac1f3ce2792cdd93
|
|
| MD5 |
0cebe06343be749a2839cafc6c4af492
|
|
| BLAKE2b-256 |
78896cba4f544080343525cfd35af8cc9d39f6181577a4628ae867bc2cf1781a
|
Provenance
The following attestation bundles were made for perflens-0.7.0-py3-none-any.whl:
Publisher:
build.yml on harshithsunku/perflens
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
perflens-0.7.0-py3-none-any.whl -
Subject digest:
70f8d84a080be0897b2f9e3ddfa60046475c1dd3424a49a9ac1f3ce2792cdd93 - Sigstore transparency entry: 2193226824
- Sigstore integration time:
-
Permalink:
harshithsunku/perflens@c7f1b180020a10ec70dacb258bd2866c3e088df7 -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/harshithsunku
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@c7f1b180020a10ec70dacb258bd2866c3e088df7 -
Trigger Event:
push
-
Statement type: