Skip to content

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 04:13
· 43 commits to master since this release

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 cycles returned nothing. Such a
    machine never reports a bare cycles; it reports cpu_core/cycles/ and
    cpu_atom/cycles/ — twelve event streams for the six events the agent
    requested. The agent's start response 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=cycles returning 404 was the visible half. The quiet
    half was worse: filter_samples_by_event compared names with ==, so
    /api/threads, /api/window, /api/source and the live exports all
    default to event=cycles and 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, so cycles finds both PMUs' cycles. An ambiguous
    request names its candidates instead of 404ing, and a miss lists what is
    available. MCP's pick_event needed 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 a perf.data yields
    cpu_atom/cycles/P, because the modifier rides after the last / where
    _normalize_event was only looking behind a :. Both spellings now
    normalize to one.

  • agent --update could replace a 32-bit install with the 64-bit
    binary.
    detect_asset_arch() read uname(), which reports the
    kernel's architecture: on a 64-bit kernel the armv7 build and the
    aarch64 build both answer aarch64. 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
    published perflens-agent-linux-armv7l, the string uname -m reports,
    but nothing asks for it under that name: install-agent.sh maps arm* to
    armv7, push-agent's _ARCH_MAP maps armv7l to armv7, and the
    agent's --update resolves armv7. 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 makes armv7l the
    outlier rather than the convention.

  • POST /api/agent/command accepted stray top-level fields. Command
    parameters belong inside args; spelling them at the top level returned
    ok:true and 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 asked perf script for sym but 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_FIELDS now requests symoff, 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 symoff rejects the field list and falls back to
    the default output format, which prints the offset anyway.

  • /api/index/status reported 0 symbols on a server started with
    --binary.
    pre_index() ran only on the runtime PATCH /api/config
    path, so a binary passed at startup left symbols_loaded and
    source_files_found at zero while resolution quietly worked — which is
    what made perflens_status tell agents source annotation was unavailable
    on servers where it was fine. /api/index/files returned an empty list
    for the same reason. Both now pre-index at startup, in a background
    thread so serve still comes up immediately.

  • Every agent reconnect saved an empty session. An agent started with
    --server reconnects 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" for perflens import. Both now record the running
    version.

  • perflens_source_hotlines dead-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
    caught sqlite3.Error but not the OverflowError raised 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: no tailwind.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.svg still
    advertised /api/per-event (renamed to /api/snapshot in 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.py deleted. It defined no test_ function,
    so pytest collected nothing from it, and the invariant it claimed to pin
    is covered by test_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
    comm and event_type takes 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 500000 now plateaus near 1.1 GB of RSS instead of 1.9 GB.
    --max-samples help text states the per-sample cost so the trade against
    history depth can be made deliberately.

  • DELETE /api/agent documents what it actually does. It ends and saves the
    session, but an agent started with --server dials 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 exact symoff path), 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 | sh

Assets

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