Skip to main content

Real-time remote Linux performance profiler with a live web UI (perf + flame graphs + line-level source annotation)

Project description

PerfLens โ€” real-time Linux perf profiling in your browser

docs site build PyPI release stars quick start license agent arch wire install

๐Ÿ“– Read the documentation site โ†’
Hosted on GitHub Pages โ€” features, architecture deep-dive, CLI & HTTP API reference, live UI tour.

Live demo: function table updating in real time as perf samples stream in, then flame graph, then source view
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 record runs 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 stat panels to the browser as new data arrives
  • Source-level annotation โ€” addr2line maps 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-prefix derives addr2line and readelf from a single prefix; --sysroot resolves 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 (or pipx / pip install --user); everything resolves user-space, no sudo, corporate-machine friendly. Missing binutils? perflens provision downloads 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

PerfLens 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 record and perf stat in parallel for N seconds, then perf script to 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 serve runs 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 clients
  • parser.py parses perf script and perf stat text into per-event sample lists; aggregator.py folds each new chunk incrementally into function summaries and flame graph trees (O(new samples) per chunk, not O(total))
  • source_mapper.py pipelines addresses through addr2line in batches of 500, applies compile-time path prefix rewrites, and builds annotated source views; symcache.py persists resolutions and source-file indexes under ~/.perflens/cache so warm restarts skip the work
  • A single SourceMapper is 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

PerfLens wire protocol: 5-byte header + payload

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 โ€” perf output 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


Download files

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

Source Distribution

perflens-0.7.0.tar.gz (119.9 kB view details)

Uploaded Source

Built Distribution

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

perflens-0.7.0-py3-none-any.whl (119.1 kB view details)

Uploaded Python 3

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

Hashes for perflens-0.7.0.tar.gz
Algorithm Hash digest
SHA256 a0638f3239d9a7da6c283143b972e2df0949b5d90dfa464ef92b280f36dc7b92
MD5 d32c1273d602a123492b132f0bddf898
BLAKE2b-256 9d74173797d93d49e88ab86b51322c08a1e301f3689da277ea4b0cdcaacd4a8d

See more details on using hashes here.

Provenance

The following attestation bundles were made for perflens-0.7.0.tar.gz:

Publisher: build.yml on harshithsunku/perflens

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

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

Hashes for perflens-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 70f8d84a080be0897b2f9e3ddfa60046475c1dd3424a49a9ac1f3ce2792cdd93
MD5 0cebe06343be749a2839cafc6c4af492
BLAKE2b-256 78896cba4f544080343525cfd35af8cc9d39f6181577a4628ae867bc2cf1781a

See more details on using hashes here.

Provenance

The following attestation bundles were made for perflens-0.7.0-py3-none-any.whl:

Publisher: build.yml on harshithsunku/perflens

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

Supported by

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