Skip to content

[v5] Add the profile reader and the profile read endpoints - #260

Draft
ldionne wants to merge 1 commit into
users/ldionne/v5-timeseriesfrom
users/ldionne/v5-profiles
Draft

ldionne wants to merge 1 commit into
users/ldionne/v5-timeseriesfrom
users/ldionne/v5-profiles

Conversation

@ldionne

@ldionne ldionne commented Sep 25, 2026

Copy link
Copy Markdown
Member

This PR implements the profile endpoints:

GET /runs/{uuid}/profiles                  -- {test, uuid} for a run
GET /profiles/{uuid}                       -- metadata + top-level counters
GET /profiles/{uuid}/functions             -- functions, hottest first
GET /profiles/{uuid}/functions/{fn_name}   -- disassembly + counters

Stack created with GitHub Stacks CLI • Give Feedback 💬

@ldionne
ldionne added this pull request to stack #255 September 25, 2026 19:16
Step 3 accepted profile blobs, checked the format version byte and stored
them, deliberately parsing nothing further. This adds the reader for D12's
version 2 binary format and the four endpoints over it:

    GET /runs/{uuid}/profiles                  -- {test, uuid} for a run
    GET /profiles/{uuid}                       -- metadata + top-level counters
    GET /profiles/{uuid}/functions             -- functions, hottest first
    GET /profiles/{uuid}/functions/{fn_name}   -- disassembly + counters

All `read` scope. Counters are served raw, never as percentages; the client
computes shares.

The reader is pure and database-free, so the endpoints are the only thing
that knows both the format and HTTP. The format's whole point is that its
index is uncompressed and its per-instruction data is not, so `read_profile`
parses the index eagerly and only a request for one function's disassembly
pays for bz2. The metadata and function-list endpoints therefore decompress
nothing, and a test pins that by clobbering every bz2 stream in an otherwise
valid blob and checking those two responses are unchanged.

Every way a stored blob can fail to be read is one `ProfileError`, which R4
turns into `internal_error` (500): the data is bad, not the request. That
guarantee holds by construction -- every byte goes through a bounds-checked
cursor -- with an enumerated net at the two entry points. Two exhaustive
sweeps over a golden blob (every prefix, and every single-byte flip) assert
the result is always either a successful read or a `ProfileError`, never a
stray `struct.error` or `UnicodeDecodeError`. A blob cannot be trusted about
how much work it describes, so counts are refused against the bytes that
could hold them, bz2 is expanded incrementally under a budget, and a
function's instruction count is capped.

The blob stays out of every default result set (D5): only the two queries
that serve a profile's contents name `data`, and the listing is pinned by
renaming the column out from under the server and checking it still answers.
The disassembly endpoint gives the connection back before expanding anything,
so the most expensive read in the API does not hold a pooled connection and
an open transaction across it.

Tests use golden blobs written by v4's own writer rather than a hand-rolled
encoder, so what is tested is the format as it exists in the wild.

Two deliberate deviations, both recorded in the design docs:

R1 claimed percent-encoding rescued a function name as a path segment. It
does not. v4's importer runs `objdump -C`, so stored names are demangled, and
`std::operator/(std::filesystem::path const&, ...)` contains a `/` that `%2F`
cannot save -- a server decodes before routing. Rather than move the name to
a query parameter, the segment now spans the remainder of the path, which
works here and did not work for test names for a reason worth stating: the
function name is the *last* segment, so nothing follows it to collide with.
The documented URL shape is unchanged. Two residual names remain unreachable
(one ending in `/`, one containing a `.` or `..` segment); unlike a machine
name these cannot be refused at creation, since they arrive inside a blob
stored verbatim, and no symbol a compiler emits hits either.

The read-side bounds are new policy. A blob accepted under D5's 50 MB cap can
still expand without limit, and submission is specified not to parse it, so
the bound can only live in the reader and can only be reported as corruption.
D12 now states it as floors rather than exact numbers. The expansion floor is
set to dominate the store cap at the ratios these sections really reach, so
that a well-formed profile the server accepted stays readable; D12 says
plainly that this cannot be guaranteed, because bz2's ratio has no bound.

D12 also gains a specification of the binary format itself, which existed
nowhere in the design docs -- only in v4's Python -- and was the one piece of
this system that could not be re-implemented from the documents. It records
two places where v4's own comments contradict what v4's code writes, so the
next reader does not "correct" the accurate description back.

The CFG view stays deferred; nothing here is for it.
@ldionne
ldionne force-pushed the users/ldionne/v5-profiles branch from a25df41 to cbca0cd Compare September 25, 2026 19:53

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant