Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ea18e1f
ci(docs): pin Nim 2.2.8, install nim package for compiler API, add da…
elijahr May 5, 2026
341e0b6
ci(bench): add [skip ci] to snapshot commit + defensive fallback late…
elijahr May 5, 2026
8326fc7
chore(docs): remove legacy nim doc artifacts (json/, nimdoc.cfg)
elijahr May 5, 2026
c7cb1d3
docs: align mkdocs config with typestates pattern (additive-only)
elijahr May 5, 2026
1c5ec4a
ci(bench): fix Bencher threshold model boundary configuration
elijahr May 5, 2026
977953a
docs: move existing guide pages into docs/guide/
elijahr May 5, 2026
10243ea
docs: chart presentation core — hero panel + multi-panel layout + lib…
elijahr May 5, 2026
84225f7
docs: add example.json BMF fixture for chart fallback rendering
elijahr May 5, 2026
3ed149b
docs: expand sipsic/mupsic/mupmuc API pages to match sipmuc structure
elijahr May 5, 2026
b7e82cd
docs: scaffold 6 guide page skeletons (typestates-pattern)
elijahr May 5, 2026
32dcd2f
docs: restructure mkdocs nav with top-level Guide grouping
elijahr May 6, 2026
1f8a827
docs: route mpsc_unbounded (and audit spsc_unbounded) to consistent p…
elijahr May 6, 2026
5e1fdc1
docs: prose pass on guide pages (C2 skeletons -> full content)
elijahr May 6, 2026
eac2150
docs: render latency panel (#bench-latency) — log-y stepped ladder
elijahr May 6, 2026
3a7ae75
docs: polish hero panel — labeling, tooltip, alternatives sort, legend
elijahr May 6, 2026
c27ca64
test(bench): pin chart contract surfaces (LIBRARY_COLORS, BLOCKING_LI…
elijahr May 6, 2026
76d1cf4
docs: README hybrid benchmarks block + narrative + nim_channels color
elijahr May 6, 2026
f133b21
ci(docs): JS syntax check + bench-fallback freshness alert
elijahr May 6, 2026
d12ae35
test(bench): make panel routing test catch logical regressions
elijahr May 6, 2026
656974c
docs: complete CHANGELOG [Unreleased] for v4.2.0 omnibus
elijahr May 6, 2026
1b54958
fix(bench): keep hero heading in renderAllEmpty for UX consistency
elijahr May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 114 additions & 16 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ on:
paths-ignore:
- '*.md'
- '.github/workflows/docs.yml'
# Loop-prevention layer 1 of 2 (design §5.X): the snapshot push
# Loop-prevention layer 1 of 3 (design §5.X): the snapshot push
# writes BMF JSON under docs/assets/bench-results/. paths-ignore
# makes the bench job skip any push consisting solely of these
# snapshot files, so the snapshot commit cannot retrigger bench.
# The companion guard is the bot-actor `if:` on the matrix and
# bench-upload jobs below. The snapshot commit has NO `[skip ci]`
# marker because docs.yml has no paths-ignore and must run on
# snapshot pushes to deploy the fresh chart data to Pages.
# Companion guards are (2) the bot-actor `if:` on the matrix and
# bench-upload jobs below, and (3) `[skip ci]` in the snapshot
# and defensive-fallback commit messages (added v4.2.0 thread
# D1) to suppress recursive triggers under workflow_dispatch
# re-runs and any future paths-ignore loosening. docs.yml's
# chart deploy is acceptable to defer because the next non-bot
# push to devel rebuilds and redeploys the docs site.
- 'docs/assets/bench-results/**'
push:
branches: [main, devel]
Expand Down Expand Up @@ -658,6 +661,7 @@ jobs:
echo "Found $count valid BMF JSON fragment(s) to merge."

- name: Merge BMF JSON
id: merge
# `merge_bmf.py` unions per-slug measure dicts across N inputs
# and exits 1 on (slug, measure) collisions. Pure stdlib (no
# third-party deps), so no Python install step is needed beyond
Expand Down Expand Up @@ -758,8 +762,32 @@ jobs:
# `--threshold-measure throughput`; the actual measure key emitted by
# bench_{spsc,mpsc,mpmc,unbounded}.nim is `throughput_ops_ms`, so the
# earlier threshold never matched any measure. Track 6 corrects this.
#
# NOTE on continue-on-error (v4.2.0): the Bencher CLI is currently
# rejecting the multi-measure threshold configuration with
# "Failed to validate the model for the throughput_ops_ms Measure
# Threshold: Invalid threshold model: Invalid model, no boundary
# provided" even though the invocation matches the documented
# bencher.dev pattern (see https://bencher.dev/docs/explanation/thresholds/
# "Two-Measure Example"). The failing step short-circuits the job
# before `Snapshot to docs assets` runs, leaving the chart on
# docs/benchmarks.md without fresh data. As a release-day band-aid
# we mark this step continue-on-error so the snapshot path keeps
# flowing; the threshold gating itself remains dormant until
# Task 6.4's 10-run soak completes anyway, so functionally nothing
# is lost in the short term.
#
# TODO(post-v4.2.0): diagnose the boundary-flag binding issue.
# Hypothesis: clap absorbs the trailing `--threshold-lower-boundary`
# into the first measure block (latency) instead of the second
# (throughput), so throughput's model has no boundary. Try giving
# each measure both boundaries explicitly, splitting into two
# `bencher run` invocations, or pinning the bencherdev/bencher
# action to a specific tag once Bencher confirms the bug/expected
# syntax.
- name: Track base branch benchmarks with Bencher
if: github.event_name == 'push' && env.BENCHER_API_TOKEN != ''
continue-on-error: true
run: |
bencher run \
--project lockfreequeues \
Expand Down Expand Up @@ -795,22 +823,24 @@ jobs:
# Track 5 PR 5 Task 5.5.b: snapshot the merged BMF into the docs
# assets tree so the chart on `docs/benchmarks.md` always points
# at the most recent devel run. Loop-prevention against bench.yml
# itself relies on two layers (no `[skip ci]` marker — see below):
# itself relies on three defensive layers:
# 1. `paths-ignore: docs/assets/bench-results/**` on bench.yml
# — the snapshot push only changes files under that prefix,
# so bench.yml does not re-evaluate at all.
# 2. `if: github.actor != 'github-actions[bot]'` on the parent
# bench / bench-upload jobs — defense-in-depth in case the
# paths-ignore is ever loosened.
#
# We deliberately do NOT include `[skip ci]` in the commit
# message: GitHub's native skip applies to ALL workflows on the
# commit, and docs.yml has no paths-ignore. Suppressing docs.yml
# would leave the freshly snapshotted JSON un-deployed on Pages,
# so the chart on the live docs site would lag the bench run
# that produced the snapshot. The two layers above already
# prevent bench.yml self-trigger; docs.yml does not trigger any
# bench work, so allowing it to run is the desired behavior.
# 3. `[skip ci]` in the snapshot commit message (added v4.2.0,
# thread D1) — third defensive guard preventing recursive
# bench → snapshot → bench loops in edge cases where the
# paths-ignore evaluation order or workflow_dispatch
# re-runs could otherwise re-trigger. GitHub's native
# `[skip ci]` suppresses ALL workflows on the commit;
# docs.yml's chart deploy is acceptable to defer because
# the next non-bot push to devel will redeploy the JSON
# via the normal docs build path. Symmetric with the
# defensive fallback step's commit (which also carries
# `[skip ci]`).
# Only fires on `push` to `refs/heads/devel`; tag pushes,
# workflow_dispatch, PR runs, and main pushes do nothing here.
- name: Snapshot to docs assets (devel push only)
Expand All @@ -832,7 +862,7 @@ jobs:
echo "::notice::Snapshot identical to current checked-in latest.json; skipping commit."
exit 0
fi
git commit -m "chore(bench): refresh snapshot"
git commit -m "chore(bench): refresh snapshot [skip ci]"
# Long bench runs leave a wide window for `devel` to advance
# before we push (a docs-only follow-up commit, an unrelated
# merge, etc.); a plain `git push` would then be rejected
Expand All @@ -856,3 +886,71 @@ jobs:
done
echo "::error::Snapshot push failed after 3 retries; aborting." >&2
exit 1

# v4.2.0 thread D1: defensive fallback snapshot.
#
# When `Merge BMF JSON` (id: merge) fails or is cancelled on a
# `devel` push or `workflow_dispatch`, the regular Snapshot step
# short-circuits the job and the chart on docs/benchmarks.md
# would silently 404 against the missing `latest.json`. This
# step writes a placeholder `latest.json` with `_status:
# "fallback"` so the chart page can render an explicit "no
# data — last attempt failed" state. Operators can pivot from
# the embedded `_workflow_run` URL to triage.
#
# Loop-safety: same three layers as the regular snapshot
# (paths-ignore + bot-actor guard + `[skip ci]` in the commit
# message). The fallback is positioned AFTER the cost-gate-
# guarded Bencher upload steps so a missing `bench` PR label
# still permits this gh-pages-side snapshot path; the fallback
# does not consume Bencher quota.
#
# `if:` scoping (IMP-5): only fires when `merge` outcome is
# `failure` or `cancelled`. We deliberately do NOT fire on
# `success` (the regular snapshot handles that) and do NOT
# fire on `skipped` (means the job never reached merge — no
# signal to publish).
#
# The placeholder JSON omits `_commit` (IMP-6); `_workflow_run`
# is sufficient for traceability and avoids the misleading
# impression that the placeholder corresponds to a measured
# bench at that SHA.
- name: Defensive snapshot fallback
if: |
always() && (
(github.event_name == 'push' && github.ref == 'refs/heads/devel') ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/devel')
) && (steps.merge.outcome == 'failure' || steps.merge.outcome == 'cancelled')
run: |
set -euo pipefail
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
mkdir -p docs/assets/bench-results
cat > docs/assets/bench-results/latest.json <<'EOF'
{
"_status": "fallback",
"_reason": "merge_bmf step ${{ steps.merge.outcome }}",
"_merge_outcome": "${{ steps.merge.outcome }}",
"_timestamp": "${{ github.event.head_commit.timestamp || github.event.repository.updated_at }}",
"_workflow_run": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
EOF
git add docs/assets/bench-results/latest.json
if git diff --cached --quiet; then
echo "::notice::Fallback latest.json identical to current checked-in copy; skipping commit."
exit 0
fi
git commit -m "bench: defensive fallback snapshot [skip ci]"
# Same retry pattern as the regular snapshot step: fetch +
# rebase + push up to 3 times to survive concurrent advances
# of devel during long bench runs.
for attempt in 1 2 3; do
if git push origin HEAD:devel; then
exit 0
fi
echo "::notice::fallback push attempt $attempt rejected; rebasing onto origin/devel and retrying."
git fetch origin devel
git rebase origin/devel
done
echo "::error::Fallback snapshot push failed after 3 retries; aborting." >&2
exit 1
85 changes: 62 additions & 23 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ on:
- '.github/workflows/docs.yml'
- 'src/**'
workflow_dispatch:
schedule:
- cron: '17 5 * * *' # daily at 05:17 UTC; redeploys docs to pick up
# snapshot pushes that carried [skip ci] (per
# bench.yml Patch A) so the chart is at most
# ~24h stale even if devel is quiet.

permissions:
contents: write
Expand All @@ -39,36 +44,41 @@ jobs:
- name: Setup Nim
uses: jiro4989/setup-nim-action@v2
with:
nim-version: 'stable'
nim-version: '2.2.8'

- name: Expose Nim compiler API for mkdocstrings-nim
- name: Cache Nimble packages
uses: actions/cache@v4
with:
path: ~/.nimble
key: ${{ runner.os }}-nimble-docs-${{ hashFiles('*.nimble') }}
restore-keys: |
${{ runner.os }}-nimble-docs-

- name: Install Nim compiler API package (for mkdocstrings-nim)
# mkdocstrings-nim's extractor.nim does `import compiler/[ast, parser, ...]`,
# which expects Nim's compiler/ source tree to be on the search path.
# The bundled Nim distribution includes compiler/ on disk but does not
# add it to the path; jiro4989/setup-nim-action does not install the
# compiler-as-Nimble-package either. Append `path="$lib/.."` to the
# bundled config/nim.cfg so `import compiler/<X>` resolves to
# `<nim-install-root>/compiler/<X>.nim`. Done after Setup Nim and
# before pip install so the mkdocstrings-nim extractor can compile
# on first invocation.
run: |
set -euo pipefail
nim_root="$(dirname "$(dirname "$(readlink -f "$(which nim)")")")"
if [ ! -d "$nim_root/compiler" ] || [ ! -f "$nim_root/config/nim.cfg" ]; then
echo "::error::expected $nim_root to contain compiler/ and config/nim.cfg"
exit 1
fi
# `$lib` here is a Nim config-file variable resolved by Nim, not a
# bash variable — heredoc with quoted delimiter writes it literal.
cat >> "$nim_root/config/nim.cfg" <<'NIMCFG'
path="$lib/.."
NIMCFG
echo "Appended compiler-API path to $nim_root/config/nim.cfg"
# which expects Nim's compiler/ source tree to be importable. Installing
# the `nim` Nimble package places the compiler source on the Nimble path
# so `import compiler/<X>` resolves cleanly. This matches the typestates
# repo's docs.yml pattern and replaces the previous in-place nim.cfg
# patching (preserved as a ROLLBACK comment at bottom of file).
run: nimble install nim -y

- name: Create watch directories
run: mkdir -p docs/overrides

- name: Install dependencies
run: |
pip install -r docs-requirements.txt

- name: JS syntax check (bench-charts.js)
# Catches typos that mkdocs --strict treats as opaque static
# assets. See Phase 4.6.3 green mirage audit; cheap defense
# against ships-to-prod browser-console errors. Node is
# preinstalled on GitHub-hosted ubuntu runners so no
# actions/setup-node step is required.
run: |
node --check docs/assets/bench-charts.js

- name: Resolve docs source ref
# mkdocs.yml reads source_ref from $DOCS_SOURCE_REF (with default
# `devel` — the repo's default branch). Set it explicitly per
Expand Down Expand Up @@ -171,3 +181,32 @@ jobs:
fi
python3 -c "import json; json.load(open('/tmp/latest.json'))"
echo "::notice::mike asset endpoint OK: $URL"
# Phase 4.6.3 green mirage audit: HTTP 200 + valid JSON is not
# enough — bench.yml's defensive fallback step (thread D1)
# writes a placeholder `{"_status": "fallback", ...}` when the
# bench pipeline breaks, and that placeholder satisfies both
# checks above. Without this freshness alert a silently-broken
# bench would render an explicit "no data" chart but the docs
# build would stay green indefinitely. Strategy:
# - Always emit `::warning::` when _status == "fallback" so
# the Actions tab shows yellow on every run.
# - Fail (`::error::`) only on the daily `schedule` cron, so
# unrelated docs PRs are not blocked while the bench is
# broken — the maintainer still gets a daily nudge.
STATUS=$(jq -r '._status // "ok"' /tmp/latest.json)
if [ "$STATUS" = "fallback" ]; then
REASON=$(jq -r '._reason // "unknown"' /tmp/latest.json)
echo "::warning::Bench latest.json is in fallback mode (reason: ${REASON})"
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "::error::Fallback persists across daily cron — bench pipeline is broken"
exit 1
fi
fi

### ROLLBACK ###
# If `nimble install nim -y` ever breaks (registry change, package rename,
# layout regression), revert the "Install Nim compiler API package" step
# to the previous in-place nim.cfg patch:
# - locate nim_root via `dirname (dirname (readlink -f $(which nim)))`
# - append `path="$lib/.."` to `$nim_root/config/nim.cfg`
# Remove this rollback block after one release cycle (v4.3.0).
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ nimblecache/
.tmp/

# Generated documentation
/htmldocs/
/site/

# Legacy nim doc artifacts (removed in v4.2.0)
/json/
/nimdoc.cfg
/htmldocs/

# Personal IDE / chatmode tooling — keep out of the repo entirely.
.github/instructions/
.github/chatmodes/
Expand Down
Loading
Loading