Repository navigation
v0.9.0
PerfLens 0.9.0
Highlights
The hands-on validation pass 0.8.0 shipped without. Everything below was
found by running the profiler against a real 25-thread workload and looking
at the output — not by an assertion. The second half of the pass moved off
the dev box entirely and onto two reference devices: an ARM64 phone running
Kali and an x86_64 LXC container on a hybrid P/E-core host.
Upgrading: no API or wire-protocol changes. The agent changed twice, and
both are backward compatible — a 0.8.0 agent works against a 0.9.0 server,
and the server recovers line numbers from the raw ip for agents that predate
the symoff request. Users on 32-bit ARM should note that the release asset
is now perflens-agent-linux-armv7; the armv7l name published for 0.8.0
was never the one any installer asked for.
Fixed
-
On a hybrid P/E-core CPU, asking for
cyclesreturned nothing. Such a
machine never reports a barecycles; it reportscpu_core/cycles/and
cpu_atom/cycles/— twelve event streams for the six events the agent
requested. The agent'sstartresponse still advertises the plain names
it asked for, so every caller that trusted that list asked for an event
that does not exist./api/snapshot?event=cyclesreturning 404 was the visible half. The quiet
half was worse:filter_samples_by_eventcompared names with==, so
/api/threads,/api/window,/api/sourceand the live exports all
default toevent=cyclesand returned an empty, entirely plausible
result. The UI escaped only because it selects the first name the SSE
stamp offers.Event names now resolve against the names actually present — exact match
first, then base name, socyclesfinds both PMUs' cycles. An ambiguous
request names its candidates instead of 404ing, and a miss lists what is
available. MCP'spick_eventneeded the same treatment: on hybrid
hardware its fallback sorted alphabetically and quietly analysed
branch-instructions. -
The same event arrived spelled two ways. A live agent round yields
cpu_atom/cycles/; the identical event imported from aperf.datayields
cpu_atom/cycles/P, because the modifier rides after the last/where
_normalize_eventwas only looking behind a:. Both spellings now
normalize to one. -
agent --updatecould replace a 32-bit install with the 64-bit
binary.detect_asset_arch()readuname(), which reports the
kernel's architecture: on a 64-bit kernel the armv7 build and the
aarch64 build both answeraarch64. Asking the kernel was the wrong
question — a binary knows what it was compiled as. The asset now comes
from__x86_64__/__aarch64__/__arm__and the__AARCH64EB__/
__ARMEB__byte-order macros, which also retires a runtime endianness
probe that was inferring something the compiler already knew.This is the second deliberate exception to the agent freeze in this
release. The wire protocol is unchanged. -
32-bit ARM could not install, push or self-update. The release matrix
publishedperflens-agent-linux-armv7l, the stringuname -mreports,
but nothing asks for it under that name:install-agent.shmapsarm*to
armv7,push-agent's_ARCH_MAPmapsarmv7ltoarmv7, and the
agent's--updateresolvesarmv7. Confirmed against the live v0.8.0
release, where that URL is the only 404 of the five. Every other target
already agreed with its consumers, which is what makesarmv7lthe
outlier rather than the convention. -
POST /api/agent/commandaccepted stray top-level fields. Command
parameters belong insideargs; spelling them at the top level returned
ok:trueand changed nothing, so a caller typo looked like success.
Unknown keys are now rejected. -
Line-level source annotation reported each function's declaration line
instead of its hot line. The agent askedperf scriptforsymbut not
symoff, so every frame arrived as a bare symbol name; the server then
fell back to the symbol address, collapsing every sample in a function
onto the line its signature is on. A matrix-multiply hotspot pointed at
void matrix_multiply_naive(...)rather than the inner loop two lines
down. Both committed fixtures were captured through the same path and
carry no offsets, so the whole suite agreed with the broken behaviour.Fixed on both sides.
SCRIPT_FIELDSnow requestssymoff, which makes
new captures exact. Independently, the server recovers each module's
page-aligned load base from the raw instruction pointer — which was
present in the data all along — so agents that predate this release, and
every session already saved to disk, resolve correctly too. On a real
capture that took one file from 2 annotated lines to 10, and the profile
as a whole from 76 distinct source lines to 356.A perf too old to know
symoffrejects the field list and falls back to
the default output format, which prints the offset anyway. -
/api/index/statusreported 0 symbols on a server started with
--binary.pre_index()ran only on the runtimePATCH /api/config
path, so a binary passed at startup leftsymbols_loadedand
source_files_foundat zero while resolution quietly worked — which is
what madeperflens_statustell agents source annotation was unavailable
on servers where it was fine./api/index/filesreturned an empty list
for the same reason. Both now pre-index at startup, in a background
thread soservestill comes up immediately. -
Every agent reconnect saved an empty session. An agent started with
--serverreconnects with backoff, so disconnecting one produced a
connect/disconnect cycle every few seconds and a saved session for each —
nineteen empty rows in one short run. Sessions with no samples and no
chunks are no longer persisted. -
Session metadata recorded a hardcoded version,
"0.5.0"for live
captures and"0.4.0"forperflens import. Both now record the running
version. -
perflens_source_hotlinesdead-ended on a bare filename./api/source
keys on the compile-time path from DWARF, but an agent naturally passes
the basename it saw in a function table. It now resolves an unambiguous
basename, and otherwise names the candidates instead of returning a 404.
The saved-session branch already did this; only live data did not. -
Caching a malformed address could abort a request.
store_addr2line
caughtsqlite3.Errorbut not theOverflowErrorraised when an address
wider than a signed 64-bit integer reaches SQLite.
Changed
-
The agent is no longer frozen outright; it changes by explicit
decision. The wire protocol is unchanged by this release. -
Tailwind 4 and four Radix packages removed. They had been installed for a
visual overhaul that never started: notailwind.config, no
@import "tailwindcss", and not one Radix import. The Tailwind Vite
plugin ran on every build and emitted nothing — the bundle is byte-for-byte
identical without it. -
Documentation corrected against the code.
docs/architecture.svgstill
advertised/api/per-event(renamed to/api/snapshotin API v2) and
server.py(deleted in 0.8.0), and it is embedded in three pages.
CLAUDE.md still described a feature freeze and a 0.7.0 version hold. The
claim that no IPs exist "in history" is now stated accurately: the tracked
tree is clean, the history is not, and that was a deliberate decision.
The capability-probe cost is one figure across all seven places that
quoted it rather than three. -
tests/test_aggregator_diff.pydeleted. It defined notest_function,
so pytest collected nothing from it, and the invariant it claimed to pin
is covered bytest_aggregator.py::test_incremental_matches_batch. -
The parser retains roughly half the memory it used to. Every frame
field was a fresh string: a real ARM session held 426,284 function-name
objects for 408 distinct names. Interning the four frame fields plus
commandevent_typetakes a sample from ~3.3 KB to ~1.7 KB retained.
Addresses and offsets are interned too — measurement, not assumption:
profiling is repetitive, and that session showed 4,360 distinct addresses
and 516 distinct offsets across 426,284 frames.Measured on the device rather than in a microbenchmark, the default
--max-samples 500000now plateaus near 1.1 GB of RSS instead of 1.9 GB.
--max-sampleshelp text states the per-sample cost so the trade against
history depth can be made deliberately. -
DELETE /api/agentdocuments what it actually does. It ends and saves the
session, but an agent started with--serverdials back in within
seconds by design, so for that deployment it is a session reset rather
than a disconnect.
Added
tests/test_sessions.py— session persistence had no dedicated tests.- Regression tests for ip-based line recovery (including equivalence with
the exactsymoffpath), startup pre-indexing, the/api/index/status
response shape, and MCP source-path resolution. - Regression tests for hybrid-CPU event names: modifier normalization, base
name resolution across PMUs, the live-vs-import spelling agreement, the
ambiguous/missing HTTP responses, MCP event selection, and a test pinning
that repeated symbols share one string object.
Install
# Server (any machine with Python 3.10+; everything user-space):
uvx perflens # or: pipx install perflens
uvx --from ./perflens-0.9.0-py3-none-any.whl perflens # from this release
# Agent (on the target device — zero dependencies, no sudo):
curl -fsSL https://raw.githubusercontent.com/harshithsunku/perflens/master/install-agent.sh | shAssets
| Asset | What it is |
|---|---|
| perflens-0.9.0-py3-none-any.whl | Server — Python wheel |
| perflens-0.9.0.tar.gz | Server — Python sdist |
| perflens-agent-c-0.9.0-linux-aarch64.tar.gz | Agent — versioned tarball |
| perflens-agent-c-0.9.0-linux-aarch64_be.tar.gz | Agent — versioned tarball |
| perflens-agent-c-0.9.0-linux-armeb.tar.gz | Agent — versioned tarball |
| perflens-agent-c-0.9.0-linux-armv7.tar.gz | Agent — versioned tarball |
| perflens-agent-c-0.9.0-linux-x86_64.tar.gz | Agent — versioned tarball |
| perflens-agent-linux-aarch64 | Agent — static binary (stable name; used by install-agent.sh and --update) |
| perflens-agent-linux-aarch64_be | Agent — static binary (stable name; used by install-agent.sh and --update) |
| perflens-agent-linux-armeb | Agent — static binary (stable name; used by install-agent.sh and --update) |
| perflens-agent-linux-armv7 | Agent — static binary (stable name; used by install-agent.sh and --update) |
| perflens-agent-linux-x86_64 | Agent — static binary (stable name; used by install-agent.sh and --update) |
| perflens-tools-linux-aarch64.tar.gz | Static addr2line+readelf for perflens provision |
| perflens-tools-linux-aarch64.tar.gz.sha256 | Tools bundle checksum |
| perflens-tools-linux-x86_64.tar.gz | Static addr2line+readelf for perflens provision |
| perflens-tools-linux-x86_64.tar.gz.sha256 | Tools bundle checksum |