Skip to content

Commit 3a982e0

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/issue-315-release-gates
Conflicts were all additive-vs-additive, kept both sides: - .github/workflows/ci.yml — main's `dashboard-coverage` job (#301) and this branch's `release-gates` job landed in the same slot after `build-and-test`. - CHANGELOG.md — main's two dashboard entries and this branch's two release-gate entries both appended to [Unreleased] > Fixed. - ROADMAP.md — the pipeline-integrity list: three bullets flipped to `done` here, a fourth (the dashboard guard) added and flipped on main. Package validation still passes against the 2.4.0 baseline: #311's new surface (ISpanHashProvider, the span-keyed overloads, StringInternTable) is purely additive, so the gate this branch introduces stays green on the merged tree.
2 parents e372b2e + 9610e9a commit 3a982e0

53 files changed

Lines changed: 3236 additions & 107 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/benchmarks.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ jobs:
155155
exit 0
156156
fi
157157
echo "have_head=true" >> "$GITHUB_OUTPUT"
158+
echo "head_shards=${#head_files[@]}" >> "$GITHUB_OUTPUT"
158159
159160
# Stitch every shard's joined report back into one: keep shard 0's metadata,
160161
# concatenate all shards' Benchmarks arrays.
@@ -418,3 +419,11 @@ jobs:
418419
git -c user.name="github-actions" -c user.email="github-actions@github.com" \
419420
commit -m "Sync custom dashboard from ${GITHUB_SHA:0:7}"
420421
git push origin gh-pages
422+
423+
# Deliberately the LAST step: a dashboard-wiring mistake must not cost us the
424+
# measurement, so the comment and the gh-pages publish above happen first and this
425+
# only reddens the job. Skipped unless every shard reported, since a partial merge
426+
# is legitimately missing whole benchmark classes (the job runs with if: always()).
427+
- name: Check dashboard coverage of the merged report
428+
if: steps.merge.outputs.have_head == 'true' && steps.merge.outputs.head_shards == env.SHARD_TOTAL
429+
run: node scripts/check_dashboard_coverage.js /tmp/pr-report-full.json

.github/workflows/ci.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ jobs:
107107
working-directory: ${{ github.workspace }}
108108
run: ./.github/scripts/validate-packages.ps1 -NuGetDirectory "${{ github.workspace }}/nuget"
109109

110+
# The benchmark workflow only fires on `src/**`, so a dashboard-only change never
111+
# reaches the report-backed run of this same script in benchmarks.yml. These are the
112+
# structural checks, which need no measurements and cost seconds.
113+
dashboard-coverage:
114+
name: dashboard-coverage
115+
runs-on: ubuntu-latest
116+
117+
steps:
118+
- uses: actions/checkout@v4
119+
with:
120+
filter: tree:0
121+
122+
- name: Check dashboard wiring
123+
run: node scripts/check_dashboard_coverage.js
124+
110125
aot-publish:
111126
name: aot-publish (linux-x64, ${{ matrix.tfm }})
112127
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ All notable changes to Celerity are documented here. This project follows [Keep
66

77
### Added
88

9+
- **`ISpanHashProvider`** in `Celerity.Hashing` — a `Hash(ReadOnlySpan<char>)` contract returning exactly what `Hash(string)` returns for the same characters. All 23 built-in `String*Hasher` types implement it. Closes [#311](https://github.com/marius-bughiu/Celerity/issues/311).
10+
- **Span-keyed lookups**`TryGetValue` / `ContainsKey` / `Contains` now take a `ReadOnlySpan<char>` on `FrozenCelerityDictionary`, `FrozenCeleritySet`, `CelerityDictionary<string, …>`, `CeleritySet<string, …>` and `Trie<TValue>`, so a caller holding a slice of a buffer no longer allocates a `string` per probe. Results match the `string` overloads; an empty span means `""`, never the `null` key. Additive on all three target frameworks. Closes [#311](https://github.com/marius-bughiu/Celerity/issues/311).
11+
- **`StringInternTable` / `StringInternTable<THasher>`** in `Celerity.Collections` — canonicalizes tokens probed as spans: `GetOrAdd` returns the shared `string` and allocates only on a miss, so a parse over few distinct tokens allocates per token rather than per occurrence. Implements `IReadOnlyCollection<string>`; not thread-safe. Closes [#311](https://github.com/marius-bughiu/Celerity/issues/311).
12+
- Test coverage for the above: `SpanHashParityTests` (the `Hash(s) == Hash(s.AsSpan())` contract, per hasher), `SpanLookupTests`, `StringInternTableTests`, `StringInternTableDifferentialTests`, a `Celerity.Fuzz` target, and Native AOT smoke-test coverage. Closes [#311](https://github.com/marius-bughiu/Celerity/issues/311).
13+
- `StringInternTableBenchmark` in the CI-tracked suite, plus `SpanLookup` rows on the frozen-dictionary and trie benchmarks, with the matching dashboard cards. Closes [#311](https://github.com/marius-bughiu/Celerity/issues/311).
14+
- Docs for the new surface: `ISpanHashProvider`, span-keyed lookups and `StringInternTable` in the API reference, and matching README entries. Closes [#311](https://github.com/marius-bughiu/Celerity/issues/311).
915
- **`StringKeyProbeBenchmark`** — the tracked suite's first `string`-keyed dictionary and set rows (lookup hit, lookup miss, set `Contains`), shown as the **String-keyed probe** card on the benchmark dashboard. Closes [#308](https://github.com/marius-bughiu/Celerity/issues/308).
1016
- **`ReferenceKeyProbeTests`** — cross-collection coverage for vacant-slot detection on the open-addressed collections with reference-type keys. Closes [#308](https://github.com/marius-bughiu/Celerity/issues/308).
1117
- **`BTreeDictionary<TKey, TValue, TComparer>` and `BTreeSet<T, TComparer>`** (with `BTreeDictionary<TKey, TValue>` / `BTreeSet<T>` aliases and the `DefaultComparer<T>` struct comparer) in `Celerity.Collections` — the library's first sorted map and set, and the B-tree the BCL lacks. Up to 31 keys per node keep a lookup `log₃₂(n)` node visits deep instead of chasing the `log₂(n)` pointers a red-black tree costs, and both add the ordered surface a hash table cannot answer: `Min` / `Max`, lower / upper bound, `EnumerateRange` in `O(log n + k)`, and in-order enumeration. They win on the interleaved insert + lookup + range-scan workload and on memory, and lose slightly on a delete-dominated one. Not thread-safe. Closes [#305](https://github.com/marius-bughiu/Celerity/issues/305).
@@ -19,6 +25,8 @@ All notable changes to Celerity are documented here. This project follows [Keep
1925

2026
- **A breaking API change could ship silently.** `dotnet pack` now validates every package against its last published version across all three TFMs and fails on any break, so a removed or narrowed public member can no longer reach NuGet.org with CI green. It runs on every PR, not just at release, and intentional breaks are recorded in a reviewed suppression file. CI-only; no consumer-visible behaviour change. Closes [#315](https://github.com/marius-bughiu/Celerity/issues/315).
2127
- **A bad `CHANGELOG.md` could half-publish a release.** The release-notes check ran after the irreversible NuGet push, so a missing section — or one over GitHub's ~125k release-body cap, which this repo has overrun before — left six packages published and no release. It now runs before anything is pushed, with a body-size assertion added, and the validated notes are handed to the release step verbatim. Closes [#315](https://github.com/marius-bughiu/Celerity/issues/315).
28+
- **The `EnumMap` and `EnumSet` cards on the benchmark dashboard rendered empty.** The page required an `(ItemCount: N)` suffix on every result name, and both benchmarks deliberately declare no item-count sweep, so their measurements were published and then discarded at render time. Both cards now chart their real numbers, and unparameterized benchmarks are excluded from the headline speedup stats. Closes [#301](https://github.com/marius-bughiu/Celerity/issues/301).
29+
- A blank dashboard card is now a red CI check rather than a silent gap: `scripts/check_dashboard_coverage.js` fails when a published result name is unparseable, when a card has no measurements behind it, or when a charted collection is missing from either `COLLECTIONS` array or from the CI benchmark suite. Closes [#301](https://github.com/marius-bughiu/Celerity/issues/301).
2230
- **The coverage gate measured only one of the six shipped packages.** Coverlet's assembly filter is exact-match, so `Celerity.Hashing`, `Celerity.Primitives`, and the three showcase packages had been outside the gate since the 2.0.0 package split — any of them could have dropped to 0% with CI green. All six are now measured, the gaps that exposed are backfilled to **100% line and branch** coverage, and the floor is raised from 95%/90% to match. Closes [#314](https://github.com/marius-bughiu/Celerity/issues/314).
2331

2432
## [2.4.0] - 2026-07-26

CONTRIBUTING.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,35 @@ dotnet run -c Release -- --filter '*' # run everything with the default (slow, h
7676

7777
### CI
7878

79-
The `benchmarks` job in [`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs the full suite on `ubuntu-latest` after `build-and-test` succeeds. It uses a faster `CiConfig` (3 warmup × 5 measurement iterations) so the whole suite completes in ~5 min.
79+
[`.github/workflows/benchmarks.yml`](.github/workflows/benchmarks.yml) runs the CI-tracked core suite (the `CoreBenchmarks` array in `Program.cs`) at full BenchmarkDotNet accuracy, sharded across a parallel matrix. On a PR each shard measures its slice of both the PR head and the `main` tip back-to-back on the same runner, so hardware variance cancels; an aggregate job stitches the shard reports back together.
8080

8181
Results are parsed by [`benchmark-action/github-action-benchmark`](https://github.com/benchmark-action/github-action-benchmark) and:
8282

83-
- **On a PR**: a comment is posted with the comparison vs the last `main` baseline. If any benchmark regresses by more than **200%** (i.e. is 2× slower or worse), the job fails red. The threshold is deliberately loose because GitHub-hosted runners are noisy — we'll tighten it once we have history to calibrate against.
84-
- **On a push to `main`**: the new measurement is appended to the `gh-pages`-stored history powering the dashboard at `https://marius-bughiu.github.io/Celerity/dev/bench/` (enable Pages on the `gh-pages` branch once the first run creates it).
83+
- **On a PR**: a comment is posted with the same-runner A/B comparison vs `main`. Rows that move by more than ±10% *and* beyond the combined standard deviation of both measurements are flagged; the flags are advisory, so a noisy row does not fail the job.
84+
- **On a push to `main`**: the new measurement is appended to the `gh-pages`-stored history powering the dashboard at <https://marius-bughiu.github.io/Celerity/dev/bench/>.
8585

8686
If a change is motivated by performance, include before/after numbers from a local Release run in the PR description — the CI job is a guardrail, not a precision instrument. Numbers without `-c Release` are not useful — BenchmarkDotNet refuses to run in Debug.
8787

88+
### The dashboard
89+
90+
The dashboard reads BenchmarkDotNet result *names*, so a benchmark's naming is part of its contract with the site. A name that the page's parser does not recognise is dropped silently — the data publishes to `gh-pages` correctly and the card just renders blank, with nothing red anywhere.
91+
92+
Two rules keep a benchmark chartable:
93+
94+
- Methods are named `{TypeName}_{Op}``EnumSet_Contains`, `Dictionary_Lookup`. The type name decides whether the row is the Celerity arm or the BCL baseline (`BCL_TYPES` in the dashboard source), and `{Op}` is what the card is titled.
95+
- A `[Params]` sweep property must be called **`ItemCount`**. A class may declare no sweep at all — `EnumMap` / `EnumSet` are bounded by the enum universe, so a synthetic item count would chart a dimension that does not exist — in which case the dashboard renders a single bucket. Any *other* property name is rejected rather than charted under an "items" label it does not mean.
96+
97+
Adding a collection to the site means updating three lists by hand, since the published data alone does not tell the page what to draw: the ship card in `web/index.html`, and the `COLLECTIONS` array in **both** `web/dev/bench/index.html` and `web/dev/bench/detail.html` (`items: [NO_SWEEP]` for an unparameterized class).
98+
99+
[`scripts/check_dashboard_coverage.js`](scripts/check_dashboard_coverage.js) enforces all of this so the failure mode is a red check rather than a blank card. It lifts the `COLLECTIONS` tables and the name parsers out of the dashboard HTML rather than reimplementing them, so it validates the code that actually ships. Run it any time you touch a benchmark or the dashboard:
100+
101+
```bash
102+
node scripts/check_dashboard_coverage.js # structural checks
103+
node scripts/check_dashboard_coverage.js path/to/joined-report-full.json # + verify the data
104+
```
105+
106+
The structural half — the two `COLLECTIONS` arrays agree, and every charted collection has a `{Key}Benchmark` registered in `CoreBenchmarks` — runs on every PR in `ci.yml`. The full check runs in the aggregate job of `benchmarks.yml`, against the merged report, and additionally asserts that every published name parses and that every card resolves to both a BCL and a Celerity measurement.
107+
88108
## Versioning
89109

90110
Celerity uses [MinVer](https://github.com/adamralph/minver) to derive NuGet package versions exclusively from **git tags**. There is no `<Version>` or `<PackageVersion>` property in any `.csproj` file — the single source of truth is the `v`-prefixed annotated tag on the commit that represents a release.

0 commit comments

Comments
 (0)