Skip to content

feat: read tak measurements from the packfile and chart them - #5

Merged
jdx merged 11 commits into
mainfrom
packfile
Jul 25, 2026
Merged

feat: read tak measurements from the packfile and chart them#5
jdx merged 11 commits into
mainfrom
packfile

Conversation

@jdx

@jdx jdx commented Jul 25, 2026

Copy link
Copy Markdown
Owner

The Performance section said "detected" and stopped. It now shows the data.

19 measurements charted

What it does

src/pack.ts implements enough of git's packfile format to pull every note out in one request: sideband demux, object headers, delta resolution, tree walk.

Deltas are not optional — a real fetch of jdx/communique's notes ref returns 21 objects, 3 of them ref_delta. I checked before writing the resolver rather than discovering it in production.

Why not the GitHub API

GET /git/trees + GET /git/blobs is far less code, but costs one request per note and only works on GitHub. One request speaking git's own protocol keeps the forge-agnostic property that made notes worth choosing in the first place — the same reasoning as git.ts.

Why pako and not DecompressionStream

Pack objects are concatenated zlib streams with no length prefix, and the Compression Streams API never reports how much input it consumed — so there is no way to find where the next object begins. pako exposes strm.next_in, which is exactly that number. This is the one place the code reaches past a library's public typings, and it says so.

Two judgement calls worth reviewing

Runner is part of the series key, not a label. Absolute counts shift between machine classes, so charting two runners as one line would invent a step change that never happened.

Points sort by timestamp, never by version string. Tool versions are frequently not orderable — nightly, 2024.01.15, lts-iron — so they stay opaque.

The y-axis does not start at zero. These series are flat plateaus separated by steps; a zero-based axis compresses every interesting movement into a hairline. Axis labels carry the absolute numbers so the shape cannot be misread as bigger than it is.

Verified against real data

/gh/jdx/communique, 19 measurements recovered from a live fetch:

range     1,260,616 – 1,638,766 instructions
overall   -18.3%

with the −23.1% step at v1.1.2 visible in the line. 19 data points, each with a hover tooltip carrying its version. Repos without tak data (sharkdp/hyperfine, jdx/tak) still render normally, and a failed pack read degrades to "detected" rather than losing the page.

Charts are inline SVG — the page still ships zero client JavaScript.


🤖 Generated with Claude Code


Note

Medium Risk
New untrusted-binary parsing on the request path (pack/delta/tree) with mitigation via caps and graceful degradation; behavior change for every repo page that has tak notes (extra git fetch + CPU).

Overview
The Performance section moves from “detected only” to full history: after ls-refs finds refs/notes/tak, the app fetches note bodies via git smart HTTP (src/pack.ts), parses line-delimited JSON, and charts series on the repo page.

src/pack.ts adds forge-agnostic pack reading: sideband demux, zlib inflate (via pako), ref_delta / ofs-delta resolution, notes tree walk, plus strict size/time limits so hostile repos degrade to “detected, not read” instead of killing the Worker. src/lib/data.ts wires fetchNotes into performance(), with parseNote / toSeries grouping by bench·tool·runner and sorting by timestamp.

Sparkline.astro renders zero-JS inline SVG charts (instructions preferred, wall_min_ms fallback). [repo].astro shows charts when a series has ≥2 points, with clearer empty/error states. Docs, lockfile, .charts CSS, and wrangler migrations_dir on the D1 binding are updated accordingly.

Reviewed by Cursor Bugbot for commit ab6595d. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Added performance history charts with sparklines, labels, tooltips, and change indicators.
    • Performance data is now retrieved and displayed as benchmark series over time.
    • Charts can show instruction counts or wall-clock timing when available.
  • Bug Fixes
    • Improved handling of flat, incomplete, or unavailable performance data with clear status messaging.
  • Documentation
    • Updated project status to reflect available performance history charts and completed note-content reading.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Performance history

Layer / File(s) Summary
Git notes pack retrieval and parsing
package.json, src/pack.ts
Adds Git upload-pack fetching, pkt-line and sideband decoding, zlib inflation, pack parsing, delta resolution, tree traversal, and note extraction.
Performance note transformation
src/lib/data.ts
Fetches note contents, parses records, groups them into benchmark series, and reports parsed record counts.
Performance chart rendering
src/pages/gh/[owner]/[repo].astro, src/components/Sparkline.astro, src/styles/global.css, README.md
Selects chart values, renders SVG sparklines, handles sparse data, adds chart layout styling, and updates status documentation.

Wrangler migration configuration

Layer / File(s) Summary
D1 migration directory scope
wrangler.jsonc
Scopes the migrations directory setting to the D1 database binding.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RepositoryPage
  participant PerformanceData
  participant fetchNotes
  participant GitHub
  participant Sparkline
  RepositoryPage->>PerformanceData: load repository performance
  PerformanceData->>fetchNotes: fetch notes ref
  fetchNotes->>GitHub: request upload-pack
  GitHub-->>fetchNotes: return packfile
  fetchNotes-->>PerformanceData: parsed note records
  PerformanceData-->>RepositoryPage: grouped performance series
  RepositoryPage->>Sparkline: render selected series
  Sparkline-->>RepositoryPage: inline SVG chart
Loading

Poem

I’m a rabbit with charts in my burrow tonight,
Packfiles bring numbers and sparklines shine bright.
Deltas unwind with a hop and a cheer,
History now stretches from version to year.
~ goes the graph as the benchmarks appear!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: reading tak measurements from packfiles and rendering charts.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​pako@​2.0.41001007181100
Addedpako@​3.0.11001009887100

View full report

Comment thread src/lib/data.ts
Comment thread src/pack.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

Adds server-rendered performance history charts sourced from tak git notes.

  • Fetches and parses git packfiles with bounded delta resolution and notes-tree traversal.
  • Parses tak records into timestamp-ordered benchmark series grouped by tool and runner.
  • Renders inline SVG sparklines and explicit unreadable, unsupported, and sparse-data states.
  • Adds pako and corrects the D1 migration-directory configuration.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failures remain within the follow-up review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
src/pack.ts Adds bounded pack fetching, inflation, delta resolution, and notes-tree traversal; the displayed follow-up concerns are addressed or already covered by existing threads.
src/lib/data.ts Parses validated tak JSONL records, groups them into timestamp-ordered series, and preserves detected-only fallback behavior.
src/components/Sparkline.astro Adds an inline SVG performance chart and correctly centers constant-valued series.
src/pages/gh/[owner]/[repo].astro Selects chartable metrics and renders charts or appropriate empty and failure states.
wrangler.jsonc Moves the migration directory setting into the D1 binding configuration.

Reviews (12): Last reviewed commit: "fix: fail loudly when a notes-tree limit..." | Re-trigger Greptile

Comment thread src/pack.ts Outdated
Comment thread src/components/Sparkline.astro Outdated
Comment thread src/pages/gh/[owner]/[repo].astro Outdated
Comment thread src/pack.ts Outdated
Comment thread src/pages/gh/[owner]/[repo].astro Outdated
Comment thread src/pack.ts Outdated
Comment thread src/pack.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/data.ts`:
- Around line 241-268: Update parseNote’s record validation to verify each
supported metric value in r.metrics, including instructions and wall_min_ms, is
either absent or a number before pushing the record to out. Preserve acceptance
of records without those optional metrics and continue skipping records with
invalid metric types.

In `@src/pack.ts`:
- Around line 264-288: Replace the repeated full-queue scan in the
delta-resolution loop with an index from each pending delta’s baseRef to its
dependents, then process only dependents when a base resolves. Update the logic
around remaining, stuck, and byOffset/byId so each pending delta is revisited
only after its referenced base becomes available, while retaining the
missing-base error when unresolved deltas remain.
- Around line 135-179: Update applyDelta to validate every delta read before
consuming opcode parameters, ensuring pos remains within delta.length and
truncated instructions throw an error. Before each copy operation, validate that
copyOffset and copySize stay within base.length, and reject any instruction
whose output range exceeds the allocated target size; preserve the existing
target-size and final outPos checks while preventing subarray clamping or
undefined-byte coercion.
- Around line 409-442: Update fetchNotes to enforce a bounded request timeout
using the project’s existing timeout or abort mechanism, allowing callers to use
the “detected, not read” fallback when git-upload-pack is slow or unresponsive.
Remove the ineffective cf cacheTtl/cacheEverything options from this POST
request; do not rely on POST caching keyed by notesSha.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: f1ad9fa7-54a1-45be-bf0e-fa87bcf65eeb

📥 Commits

Reviewing files that changed from the base of the PR and between 07aa3aa and 33c1bcd.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • README.md
  • package.json
  • src/components/Sparkline.astro
  • src/lib/data.ts
  • src/pack.ts
  • src/pages/gh/[owner]/[repo].astro
  • src/styles/global.css
  • wrangler.jsonc

Comment thread src/lib/data.ts
Comment thread src/pack.ts
Comment thread src/pack.ts Outdated
Comment thread src/pack.ts
Comment thread src/pack.ts Outdated
Comment thread src/pack.ts Outdated
Comment thread src/pack.ts
Comment thread src/pack.ts
jdx and others added 7 commits July 25, 2026 20:11
The Performance section said "detected" and stopped. It now shows the data.

`src/pack.ts` implements enough of git's packfile format to pull every note out
in a single request: sideband demux, object headers, delta resolution, tree
walk. Deltas are not optional — a real fetch of jdx/communique's notes ref
returns 21 objects of which 3 are ref_deltas.

The alternative was GitHub's tree and blob endpoints, which is far less code but
costs one request per note and only works on GitHub. One request that speaks
git's own protocol keeps the forge-agnostic property that made notes worth
choosing in the first place.

Uses pako rather than DecompressionStream. Pack objects are concatenated zlib
streams with no length prefix, and the Compression Streams API never reports how
much input it consumed, so there is no way to find where the next object starts.
pako exposes `strm.next_in`, which is exactly that.

Series are keyed on (bench, tool, runner) with runner in the key rather than as
a label: absolute counts shift between machine classes, so charting two runners
as one line would invent a step change that never happened. Points sort by
timestamp, never by version string, since tool versions are frequently not
orderable.

Charts are inline SVG with no client JavaScript. The y-axis does not start at
zero on purpose — these series are flat plateaus separated by steps, and a
zero-based axis flattens every interesting movement into a hairline; the labels
carry the absolute numbers so the shape cannot be misread.

Verified against jdx/communique: 19 measurements recovered, 1,260,616 –
1,638,766 instructions, -18.3% overall, with the -23.1% step at v1.1.2 visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- `parseTree` scanned for the mode and name terminators with unbounded inner
  loops. Tree bytes come off the network, so an unterminated entry would spin
  past the end of the buffer forever and hang the request instead of failing it.
  Every scan is bounded now.

- Any public repo can point the reader at an arbitrarily large notes ref, and a
  Worker that exhausts its CPU or memory budget is killed rather than degrading.
  Caps the declared response size, the pack size and the object count, which
  turns that into the "detected, not read" state — a page that still renders.
  The content-length check happens before arrayBuffer(), since buffering is the
  step no later limit can save.

- `parseNote` accepted any line carrying `v` and `metrics`, so a record without
  a string `ts` reached `toSeries`, where localeCompare throws. One malformed
  line would have taken down every chart for the repository. All fields the
  reader depends on are type-checked.

- A flat series drew along the bottom edge and read as clipped. Widening the
  denominator was not enough: every point still sat at `v - lo === 0`. The band
  is widened around the value instead, so the line runs through the middle.
  Labels report the real range rather than the padded one. An unchanged
  benchmark is a good result and should not look like a rendering failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wrangler warned "Unexpected fields found in top-level field: migrations_dir" on
every command. It is a per-binding key, not a top-level one; it worked only
because the value happened to match wrangler's ./migrations default.

Unrelated to the packfile work in this PR, but it was one line and the warning
appeared on every build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The size cap trusted Content-Length, which a chunked reply simply omits — and
arrayBuffer() on an unbounded body is the one step no later limit can rescue.
The body is now read through and counted as it arrives, so the allocation is
bounded whatever the sender declares.

Two more places sized by remote data rather than by us: a delta declares its
own target size and it is allocated up front, and a small pack can inflate to
something far larger. Both are checked before allocating.

The fallback copy said data could not be read whenever no series was chartable,
which also fired after a perfectly successful fetch of a repo with a single
measurement. It now distinguishes "nothing readable" from "not enough points
yet", and says what the threshold is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`applyDelta` trusted its instructions. `Uint8Array.subarray` clamps
out-of-range indices instead of throwing, and an out-of-bounds `delta[pos++]`
yields undefined which the bitwise ops coerce to 0 — so a corrupt delta
produced silently zero-filled output that still satisfied the
`outPos === targetSize` check. Wrong data that looks valid is the one outcome
this reader exists to avoid. Every read and write is now checked against the
base, the delta and the declared target.

Delta resolution swept the pending list and retried whatever was stuck, which
is quadratic. A reverse-ordered chain within the 20k object cap is ~200M checks
— enough to exhaust the CPU budget before the detected-only fallback runs.
Deltas are now indexed under the base they wait on, so settling an object wakes
exactly those that were blocked on it.

The per-object cap did not bound the total: many individually-legal objects
still add up, and all of them are retained until parsing completes. Added a
running total.

`parseNote` checked that `metrics` was an object but not that its values were
numbers, so a non-numeric one reached Math.min/Math.max as NaN and broke the
SVG silently rather than being skipped like every other malformed input.

Chart labels omitted `tool` even though `toSeries` keys on it, so a repo
publishing two tools under one benchmark and runner got two identically
labelled charts.

Also removes a stale doc comment that had been orphaned onto MAX_RECORD_V,
still claiming reading note contents was unimplemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The per-object size check ran on `inflator.result`, which is too late: by then
pako has already materialised the entire decompressed object, so a small pack
holding one enormously compressible blob would exhaust the Worker before the
guard executed. Output is now accumulated through `onData` and abandoned the
moment it passes the cap, so the allocation stays bounded whatever the object
claims to be.

The fetch had no timeout, so a slow or hanging remote held the request open
until the runtime killed it — losing the whole page rather than degrading to
the detected-only fallback that every other failure here produces.

Verified the chunked accumulation decodes identically: 19 measurements,
1,260,616 - 1,638,766, unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pack request carries the wanted sha in its *body* while the URL is
identical for every request to a repo, so caching it by URL served one
commit's pack for another sha — silently charting stale measurements. Dropped
the cache hints entirely; the rendered page is cached on its own URL, which is
where caching belongs.

The overflow guard dropped chunks but pako kept inflating: `push` processes
everything it is handed before returning, so a zip-bomb still cost the full
decompression even though the output was discarded. Bounded memory, unbounded
CPU. Input is now fed in 64 KiB slices so the check runs between them and the
rest of the stream is abandoned.

That means tracking `next_in` across pushes rather than reading it once, which
is the part that would silently corrupt every subsequent object offset if it
were wrong. Verified against the real pack: 19 notes, identical versions, and
v1.2.3 still reports 1,267,056 instructions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread src/pack.ts Outdated
Git trees are a DAG, not a tree: identical subtrees are stored once and
referenced from many parents. Walking without a visited set revisits each
shared child once per path that reaches it, so the work grows exponentially in
the depth of the layering while the number of distinct objects stays small —
inside every size cap already in place.

Notes fan out at most two levels in practice, but the pack comes from an
arbitrary public repository and is not obliged to be well behaved.

Verified the walk still recovers all 19 notes with the guard in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread src/pack.ts Outdated
Comment thread src/pack.ts Outdated
The visited guard I added last commit was wrong in a way that loses data. Output
keys carry the parent prefix, so the same subtree reached by two different paths
yields two different sets of commit ids — deduplicating on the tree id alone
skipped every note under the second path, silently undercounting measurements.

Keyed on (prefix, id) now, which still collapses the only genuinely redundant
case: the same subtree at the same prefix.

That alone does not bound the walk, since a DAG can have exponentially many
distinct paths, so depth and total notes are capped too. Notes fan out at most
two levels in practice, but the pack comes from an arbitrary public repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5aa9292. Configure here.

Comment thread src/pack.ts Outdated
Comment thread src/lib/data.ts
Comment thread src/pack.ts
…part

- `readCapped` returned `arrayBuffer()` unmetered when `res.body` was null,
  which is the one path the streaming cap could not cover. Buffering then
  checking is still weaker than metering, but it is bounded.

- `MAX_NOTES` bounds what is emitted, not what is walked. A DAG with many
  distinct prefixes over shared, entry-heavy subtrees reparses those subtrees
  once per prefix while emitting almost nothing, so the traversal slipped past
  every existing cap. Added its own visit ceiling.

- A pack that was fetched and parsed but held nothing this reader understands
  produced the same shape as a fetch that failed, so the page blamed a read
  error for a successful read. `Performance.read` distinguishes them and the
  three empty states now say which one happened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread src/pack.ts Outdated
Every traversal ceiling returned quietly, so `fetchNotes` handed back whatever
had been collected before the limit and the caller had no way to tell a
truncated walk from a complete one. The page then charted a partial history and
reported it as the whole thing — an incomplete chart presented as complete,
which is the exact failure this reader exists to prevent.

All three limits throw now, degrading to "detected, not read", which is honest
about what happened.

Also raises the depth ceiling from 4 to 8. Git nests note fanout deeper as the
note count grows, and a limit tuned to the two levels usually seen would have
rejected legitimately large repositories rather than pathological ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jdx
jdx merged commit ed51367 into main Jul 25, 2026
6 checks passed
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