Skip to content

Per-shard coverage artifacts ship 8 copies of the source tree — 85.7% of the payload is text collect already has checked out #74

Open

Description

@sotashimozono

Sharded coverage upload is 64× larger than it needs to be, and it exhausted the Actions artifact storage quota on lab-sotashimozono today — every PR in that org went red at Shard labels with

Failed to CreateArtifact: Artifact storage quota has been hit.

The shard jobs had already done their work; only the upload failed. No test ran on any PR after that point.

Where the bytes are

Julia writes *.cov as a 9-character counter column, one space, then a verbatim copy of the source line. Measured on one live testshards-coverage-s1 artifact from lab-sotashimozono/ParaLinearAlgebra.jl (48 files, 965,565 B uncompressed):

bytes share
counter columns 136,629 14.3 %
embedded source text 815,592 85.7 %

and only 2,134 of 15,181 lines (14.1 %) carry a counter at all — the rest are -.

Each of the 8 shards uploads the whole tree, not the files it happened to touch, so every run ships 8 near-identical copies of the package's source through artifact storage. Live totals in that one repo:

family objects live
coverage 625 170.84 MB
out 2410 28.46 MB
records 503 21.80 MB
lcov 298 4.63 MB
timings 81 0.12 MB

Per run: 8 coverage artifacts totalling 2.30 MB are merged into one lcov of 0.02 MB. The merged product is 1 % of its inputs.

The source is already at the destination

collect runs actions/checkout before download-artifact — deliberately, and the comment says why (Codecov needs the file network). So the 85.7 % that is source text is already present in that job's workspace, at the same paths, at the same revision. Verified: for all 48 files in that artifact, the text embedded in the .cov is byte-identical to the repo file at the run's head_sha.

Proposed change

Upload a sidecar of counted lines only — <lineno>,<count> — and have restore_counters rebuild the .cov files from the checkout.

Prototyped against the real artifact:

original .cov payload : 965,565 bytes
sidecar payload       :  15,049 bytes   -> 64.2x smaller
round-trip byte-identical: 48/48 files

Round trip is counter + checked-out source line -> "%9s %s", and it reproduces the original .cov exactly, so everything downstream of restore_counters — CoverageTools, the lcov merge, the Codecov upload — is untouched.

This does not re-open the reason the conversion lives in collect

sharded-tests.yml records it:

and collect merges the results anyway, so doing it here does it N times for one answer. Measured on this repository it cost 3-25s per shard, the variance being a 20s JuliaSyntax precompile

That cost is Julia startup, not the conversion. The sidecar is a line filter — awk, no Julia, no depot, no precompile — so the objection does not apply to it. The merge still happens once, in collect, exactly as now.

Where it lands

  • sharded-tests.yml, "Collect this shard's raw coverage counters": write <path>.cov.idx instead of cping the .cov.
  • src/coverage.jl, restore_counters: accept .cov.idx and reconstruct against dest. Keeping it in the package rather than in the workflow is that file's own stated principle ("a merge written as a step script is unreachable from Pkg.test()").
  • Backward compatible: a parts/ tree containing real .cov files still restores by copy, so an in-flight run mid-upgrade is fine.

One new failure mode, and it should be loud

Reconstruction needs the source file present at dest with the same line count. If a counter file has no matching source, or the line counts disagree, that must refuse by name rather than silently emit a short .cov — a truncated counter file would under-report coverage with nothing to announce it. Worth carrying the line count in the sidecar header so the check is exact.

Happy to send the PR.

Activity

  1. sotashimozono commented on Aug 19, 2026

    @sotashimozono
    MemberAuthor

    After #76 and #77: the suite runs, and the gate still cannot pass — for a reason worth separating out

    Verified on a consumer PR under a full quota. All eight shards now execute and pass (before #76 nothing ran at all). The run then fails at exactly one place:

    ### Completeness
    | units observed | 0 |
    | units run      | 0 |
    TestShards: no shard reported how many units it observed — cannot verify the run.
    

    That is correct and should stay correct: Every unit ran, exactly once is a claim, the records did not arrive, so the claim cannot be made. Faking it would be the one failure this package exists to prevent.

    But it exposes a design point that #76/#77 only worked around:

    The completeness records share a transport — and therefore a failure mode — with the coverage payload, and they are four orders of magnitude smaller. shard-*.tsv / ran-*.tsv are a few KB of TSV; the coverage artifacts in the same run were 170 MB in one repo. When artifact storage is the constrained resource, putting a 2 KB correctness record behind it means a reporting problem can永久ally block a correctness gate, and there is no honest way to unblock it from inside the workflow.

    Worth considering a different carrier for the completeness data specifically — job outputs, or the summary — so that the one thing that must not degrade does not depend on the resource that ran out. Not proposing a patch here; it is a design decision about where the guarantee lives.

    Smaller, and worth knowing while diagnosing

    continue-on-error: true reports the step's conclusion as success in the jobs API even when the upload failed, so a step list reads as though everything uploaded. The only reliable signal is that the run has zero artifacts. That cost me a wrong reading; a ::warning:: next to each tolerated upload would make the log say what the step list does not.

  2. sotashimozono commented on Aug 19, 2026

    @sotashimozono
    MemberAuthor

    Correction to the storage table; the payload measurement stands.

    The "live" column here was summed without filtering expired, and an expired artifact keeps its size_in_bytes metadata row after GitHub deletes the bytes. Re-measured with expired == false, across the four largest consumers:

    family live files live bytes retention
    -out-* 7457 58.73 MB 7 days
    -records 750 16.93 MB 30 days
    -lcov 908 10.33 MB 7 days
    -coverage-* 0 0 MB 1 day

    So -coverage-* is not 170 MB of stored bytes — it is zero in steady state, because its retention is one day. The 170 MB was cumulative-created. Account-wide live total is 90.7 MB against a documented 500 MB pool, so this family was never the storage problem, and the claim that it "exhausted the artifact storage quota" is not supported.

    What is unaffected: the content measurement. Julia writes *.cov as a 9-character counter column plus a verbatim copy of the source line; 815,592 of 965,565 bytes (85.7 %) on a real 48-file artifact are that text, and only 2,134 of 15,181 lines carry a counter. That is a fact about the payload, not about storage totals, and it is why #75 exists.

    Why this stays open: the reason to fix it changes from "storage" to "the bytes an in-flight run has to create" — which is the thing a quota gate actually blocks on, since the quota is enforced at CreateArtifact before any bytes are sent. #75 shipped counter_index/restore_counters (~62× measured, round-trip byte-exact) and the workflow still plain-cps the raw .cov at sharded-tests.yml:611-624. This issue is the home for that ~5-line wiring.

    #81 lists it as item 2; treat this issue as authoritative for it.

  3. sotashimozono commented on Aug 20, 2026

    @sotashimozono
    MemberAuthor

    One more data point on the transport question, so the obvious alternative is not re-derived later: job outputs do not work for this. The workflow already uses GITHUB_OUTPUT (the matrix in labels, have in collect), but those are single jobs. A matrix job's outputs collide — N shards writing the same output name leave one value — so per-shard records cannot be aggregated that way. GITHUB_STEP_SUMMARY is human-readable only and not reachable from collect.

    That leaves reading the shard jobs' logs from collect through the Actions API, which works but trades a file for a token and a parse. Recording it as the remaining option rather than proposing it.

  4. sotashimozono commented on Oct 14, 2026

    @sotashimozono
    MemberAuthor

    Hermes の整理(自動。何も変更していません)

    状態: 本文の中心である storage 枯渇の主張は、2026-08-19 のコメントで撤回済みです (-coverage-* は retention 1 日で定常 0 MB、account 全体 90.7 MB / 500 MB)。ただし本文はそのままなので、初見の人は撤回前の表を読みます。残る論点は payload 計測 (85.7% が source text) だけです。

    論点: 残作業が何かの食い違いです。

    次の一手: 本文冒頭に撤回の注記を足す (または題を「.cov の source 複製を送らない」に直す)。残作業を「counter_index の workflow 配線」1 件に絞ると、close 条件が明確になります。

    関連: #75, #76, #77, #85 (出荷済みの対応), #81 (CLOSED, 配線を本 issue に委ねた), #78 / #80 / #82 (CLOSED, artifact を使わない設計の議論)。

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions