Skip to content

Ship both PerfContext-enabled and PerfContext-disabled RocksDB libraries in every prebuild archive - #27

Merged
cb1kenobi merged 6 commits into
kris/disable-perf-contextfrom
chris/dual-perf-context-libs
Oct 8, 2026
Merged

cb1kenobi merged 6 commits into
kris/disable-perf-contextfrom
chris/dual-perf-context-libs

Conversation

@cb1kenobi

@cb1kenobi cb1kenobi commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

#26 Compile out RocksDB PerfContext to remove per-comparison TLS overhead sets -DWITH_PERF_CONTEXT=OFF in the overlay portfile, so the single library every release publishes has PerfContext compiled out. That is a one-way choice for every consumer of the archives: get_perf_context() counters become permanently zero, PerfContext::ToString() returns an empty string, and rocksdb.db.mutex.wait.micros is never recorded. Published releases are immutable, so anyone who later needs the counters — a support engineer diagnosing read amplification on a shipped build, say — has to build RocksDB and rocksdb-js from source. #26's own description raises this as an open question rather than a settled decision.

❓ Your call: Is shipping both libraries the right answer, rather than keeping #26 as it stands and accepting that prebuilds have no perf counters? Both cost one extra RocksDB compile per target; the alternative costs nothing but is unrecoverable per release. If you would rather take #26's simpler shape, this PR is droppable — #26 merges on its own.

💡 Solution

Each of the ten release archives now contains two RocksDB static libraries instead of one. The archive count, the release naming, the revision handling and the publication behaviour are all unchanged.

Path in the archive PerfContext
lib/librocksdb.a, lib/rocksdb.lib enabled — the historical path, with its historical meaning
lib/no-perf-context/librocksdb.a, lib/no-perf-context/rocksdb.lib compiled out

One copy of the public headers, one set of compression dependency libraries, and one CMake package and pkg-config file are shared between them. Both libraries are built from the same source, the same patches, the same architecture and the same configure options, differing only in WITH_PERF_CONTEXT.

The compatibility story is the thing to read carefully. Relative to the releases published today, lib/librocksdb.a keeps exactly the meaning it has always had. Relative to #26's branch, this PR moves the default path back to the instrumented library — so #26's measured gain is no longer obtained by linking the default path. It is obtained only once rocksdb-js selects lib/no-perf-context/. That is a deliberate trade: the default stays compatible, and the consumer opts in.

rocksdb-js is unmodified here, as scoped. It links the prebuild directly rather than through the CMake package, so selecting the variant is a change to the one RocksDB library path it hands the linker — lib/no-perf-context/librocksdb.a in place of lib/librocksdb.a, with its include and dependency-library flags untouched. Archives published before this change have no lib/no-perf-context/ directory, so a consumer asking for it must fail loudly rather than fall back to lib/librocksdb.a and silently link the library it was trying to avoid. The README states this.

⚠️ Look hardest: the claim that the two libraries are configuration-identical apart from WITH_PERF_CONTEXT. Everything else rests on it, and it is a property of running the same portfile twice rather than something the diff can show directly.

⚖️ Alternatives

❓ Your call: The planning round returned better-alternative-exists and I adopted it. The design had each variant installed into its own --x-install-root; the reviewer's smaller sequence stages the first install to dist/ as the workflow already does, then runs vcpkg install 'rocksdb[no-perf-context]' --recurse into the same root and copies one file out. It reaches the same end state with no experimental flag, no second dependency install and less disk, and nothing ruled it out. The rejected two-root version is disqualified by "installs every dependency a second time for no output that reaches the archive".

  • Twenty archives, one per variant, selected by rocksdb-js's installer — rejected: the requirement is ten archives, and doubling the asset count doubles the release job's asset-verification surface for a choice that belongs to packaging, not to the consumer's installer.
  • Patch RocksDB so perf_context is not a thread_local at all — this attacks the actual root cost rather than the counters. Rejected on a fact: the macros in monitoring/perf_context_imp.h are used at 293 sites in 37 files in v11.8.1, so it is a permanent downstream fork of a core header that every RocksDB upgrade must re-validate, against an upstream option that already exists.
  • Select the variant with an environment variable the portfile reads — rejected: an environment variable is not part of vcpkg's port ABI hash, so the second install could be served the first one's binary-cache entry and the release would ship one library twice. A vcpkg feature is part of that hash. Confirmed locally: the two installs hash to 84e36ef9… and cac1ee72….
  • Ship a second CMake package describing the disabled library — rejected: a second share/ tree and a second set of find_dependency calls, for one consumer that links the file path directly. Documented in the README instead.
  • A WITH_PERF_CONTEXT:BOOL cache-entry check to catch an upstream rename — replaced, not rejected as unworkable. The planning review held that CMake's -D creates a cache entry whether or not the project declares the variable, so the check could not detect a rename; that is half right, since an undeclared -D lands as UNINITIALIZED and a declared option() as BOOL. The replacement reads the generated build system for the -DNPERF_CONTEXT the option exists to produce, which checks the effect rather than the declaration.

❓ Your call: Only the release library has a second variant. debug/lib/librocksdbd.a is built for both variants but only the enabled one is staged, because it is large and no consumer links it. Adding it is one cp, if you would rather have the symmetry.

❓ Your call: No Windows COFF symbol check was added. Instead the counter-name check below runs on all ten targets, including the two cross-compiled windows-arm64 ones, which is strictly more coverage than a dumpbin check on the two native Windows targets would have been. If you want an MSVC-mangling symbol check as well, it is additive.

🔧 Changes

File What changed
vcpkg-overlays/rocksdb/vcpkg.json Adds the no-perf-context feature — the entire variant-selection mechanism.
vcpkg-overlays/rocksdb/portfile.cmake Maps that feature to WITH_PERF_CONTEXT through INVERTED_FEATURES so the option is derived once, not written twice. Adds a compression-feature assertion and the -DNPERF_CONTEXT read-back between configure and install. The hard-coded -DWITH_PERF_CONTEXT=OFF #26 added is gone.
.github/scripts/build-rocksdb-variants.sh (new, 116 lines) Installs the enabled variant and stages it exactly as the workflow did before, reinstalls with the feature, compares the two include/ trees, and fails before any archive exists if either library is missing, if the staged enabled library moved, or if the two hash alike. Both build jobs call it, so they cannot drift.
.github/scripts/verify-prebuild-archive.sh (new, 175 lines) Verifies a fresh extraction of the finished archive: the counter-name markers and their strict-inequality comparison, the nm symbol reading with tool failures fatal, the probe, skipped explicitly where the runner cannot execute the target, and the three places a probe binary can land across single- and multi-config generators.
tools/perf-context-probe/probe.cc (new, 100 lines) Writes 2000 keys, sets a counting PerfLevel and resets, then reads user_key_comparison_count after 2000 point lookups and a full iteration.
tools/perf-context-probe/CMakeLists.txt (new, 25 lines) Links the archive's own CMake package and overrides only the imported library location, so the two probe builds differ in exactly one input.
.github/workflows/build.yml Adds run_probe to the matrix, routes both build jobs through the shared script, raises the build timeout to 120 minutes for the second compile, and adds the archive-verification step with a musl variant that runs in the Alpine container, because musl binaries cannot run on the glibc runner and BusyBox supplies neither bash nor a grep that can count markers in a binary. #26's symbol-only step is removed, superseded. The release body now describes both variants.
README.md Replaces #26's "the builds compile out PerfContext" paragraph with the archive layout, how to select the disabled library by link line and by CMake, what it gives up, that IOStatsContext is unaffected, and how rocksdb-js would select it.
.github/DESIGN.md (new), DESIGN.md (new) The invariant, why it cannot be read off the artifact, and where each piece of code enforces it; indexed from the repository root.

There are no test files: this repository's only test surface is the workflow itself, which is why the verification is build steps rather than a suite.

Product and architecture tour

One portfile, two libraries

What stops the two libraries differing in anything but WITH_PERF_CONTEXT?

Nothing in this change writes a second set of build flags. The variant is a vcpkg feature, so the same portfile runs twice and the only thing that differs between the runs is the one option the feature controls. That makes "configuration-identical" a structural property rather than a second flag list somebody has to keep in sync.

A feature is also part of vcpkg's port ABI hash, which is what makes the second install safe. An environment variable would not be, and the second install could then be served the first one's binary-cache entry — producing an archive with the same library in both places, passing every structural check. Locally the two resolved to 84e36ef9… and cac1ee72…, and only RocksDB was rebuilt: every dependency kept its hash and was reused.

Producing one archive

  1. Install the enabled variant — ordinary vcpkg install rocksdb, exactly as before this PR.
  2. Stage it — the whole install tree is copied to dist/, exactly as before, so the historical layout is produced by the historical code path.
  3. Reinstall with the feature — vcpkg install 'rocksdb[no-perf-context]' --recurse replaces RocksDB in the same vcpkg root. Dependencies stay installed and are not rebuilt.
  4. Compare the headers — the two installs' include/ trees must be byte-identical, because the archive ships one copy.
  5. Copy one file — the disabled release library lands in dist/lib/no-perf-context/, and the job fails here if either library is missing, if the staged enabled one changed, or if the two hash alike.

Every release archive holds two release RocksDB static libraries differing only in WITH_PERF_CONTEXT, and the library at the historical lib/ path is always the PerfContext-enabled one.
The debug library under debug/lib/ is unchanged and exists only in the enabled variant.

What a consumer sees

Does an existing consumer have to do anything?

Before After
lib/librocksdb.a is the only RocksDB library, and on #26's branch it has PerfContext compiled out. lib/librocksdb.a is unchanged and PerfContext-enabled; lib/no-perf-context/librocksdb.a is the compiled-out build, selected by path.

No existing consumer changes. -L<prefix>/lib -lrocksdb keeps resolving to the enabled library, find_package(RocksDB CONFIG) and pkg-config rocksdb both describe it, and the shared headers compile against either. The alternate library is opt-in, by naming its path.

The consequence worth ruling on is that #26's performance gain is now opt-in too. On #26's branch the default path is the fast one; here it is the compatible one. Nothing in production gets faster until rocksdb-js links lib/no-perf-context/, which is deliberately out of scope for this PR and is the rollout question in the description.

Which check reaches which target

The archive is immutable once published and the two files look alike — so what proves which is which, on targets whose binaries the runner cannot even run?

Verification coverage by target

Four independent checks, deliberately overlapping. Only the counter-name comparison reaches all ten targets, which is why the two cross-compiled Windows targets rest on it.

flowchart LR
    cfg[Configure read-back] --> all[All 10 targets]
    names[Counter-name count] --> all
    nm[nm symbol check] --> six[6 non-Windows]
    probe[Behavioural probe] --> seven[7 native]
Loading

The configure read-back runs inside the portfile, between configure and install, and asserts -DNPERF_CONTEXT is present exactly when the feature is on. It exists because vcpkg only warns when a -D names a variable the project never declared, so an upstream rename of WITH_PERF_CONTEXT would otherwise flip the variant in silence. Verified by renaming the option upstream, which failed the build in 2.1 seconds, before any compilation.

The counter-name check is the one that needs no toolchain and no matching architecture. Perf-counter names reach .rodata only through PerfContext::ToString(), which NPERF_CONTEXT compiles away. It is a count comparison rather than a presence test because the same name is also a PerfContextBase field, and vcpkg compiles MSVC release objects with /Z7, so on Windows CodeView type information carries that name into the .lib whichever variant it is. Both variants declare the struct identically, so the literal is exactly the enabled library's margin. A presence test would have failed all four Windows targets on their first run.

Every check asserts in both directions, because a one-sided check still passes when the two libraries are swapped. A control string present in both libraries guards the search itself: a grep that could not read the files would otherwise report zero matches, and zero is the answer an absence test is looking for.

✅ Verification

Route: a real local build of both variants for darwin-arm64 (macOS 26, AppleClang 21, RocksDB 11.8.1, vcpkg pinned at 2026.06.24), packaged into an archive, then that archive verified by the same script CI runs — plus eight fail-closed tests.

Build. Both variants built from the one portfile. vcpkg resolved them to distinct ABI hashes (84e36ef9… enabled, cac1ee72… disabled) and rebuilt only RocksDB for the second install, reusing every dependency. rocksdb[no-perf-context] retained all five compression default features. The portfile reported WITH_PERF_CONTEXT=ON, NPERF_CONTEXT defined=FALSE and WITH_PERF_CONTEXT=OFF, NPERF_CONTEXT defined=TRUE. Installed public headers compared byte-identical.

Archive. lib/librocksdb.a 27,848,096 bytes; lib/no-perf-context/librocksdb.a 27,725,440 bytes. Verification against the extracted archive: counter-name occurrences 1 vs 0; rocksdb::perf_context undefined-symbol references 59 vs 0 (the 59 matches what #26 measured on the shipped macOS v11.8.1 prebuild); probe user_key_comparison_count 40,959 vs 0.

Archive size. 94,658,072 → 95,885,400 bytes, +1,227,328 bytes (+1.3%), 90.3 MiB → 91.4 MiB, measured by packing the same dist/ tree with and without the second library. The two libraries are nearly identical, and xz -9's dictionary spans both, so the cost is far below a second library's size.

Fail-closed tests. Each of these was built and run, and each failed the job: the same library staged twice; the disabled library missing; the two libraries swapped; a zero-length library (caught by the control-string guard, which is what stops a failed search reading as an absence); an upstream rename of WITH_PERF_CONTEXT (caught at configure, in 2.1s, before any compilation); a compression feature dropped from the port's defaults; and — simulating Windows /Z7 — the counter name appended to both libraries of a Windows-shaped archive, which passes, and to the same pair swapped, which still fails.

Lint. shellcheck clean on both new scripts. actionlint on build.yml reports 12 pre-existing shellcheck findings, all in code this PR does not touch, down from 18 on the base branch; none in the new steps.

What did not run, and why it cannot run here. build.yml triggers only on schedule and workflow_dispatch, so this PR gets no CI at all — gh pr checks 27 reports none, and a green PR is not evidence of anything. The nine targets other than darwin-arm64 are therefore unexercised: both musl container paths, all four Windows targets, and the probe on the six other native targets. The only way to exercise them is a workflow_dispatch run from this branch, and that publishes a prerelease GitHub release, which is outside what I was asked to do here. The rocksdb-js test suite was not run against these libraries, and no hot-path benchmark was repeated — #26 owns that measurement and this PR changes neither library's code.

🚧 Remaining limitations

  • The disabled library has no consumer yet. All three review legs raised this and it is the one finding kept open. Shipping the path does not make rocksdb-js use it, and rocksdb-js is out of scope here by instruction. Until it selects lib/no-perf-context/, Compile out RocksDB PerfContext to remove per-comparison TLS overhead #26's measured gain is not realised by anything in production — see the rollout call below.
  • windows-arm64 and windows-arm64-static-md cannot run a probe on an x64 runner, and darwin-x64 cannot run one on an arm64 runner. They are covered by the configure-time NPERF_CONTEXT guard and the counter-name check; darwin-x64 additionally by nm.
  • The counter-name check's first Windows run is its first Windows run. Its reasoning and the /Z7 behaviour were verified from vcpkg's toolchain file and simulated locally, but no real .lib has been through it. It fails loudly rather than silently if the margin does not hold.
  • CI time roughly doubles per target. Dependencies are reused, so the delta is one RocksDB compile (debug and release) per target. The non-musl build step's timeout moves 60 → 120 minutes to absorb it; the musl step has no timeout, as before.
  • No companion docs PR. The consumers of these archives are build systems, not Harper users, so the archive layout and the link-line change are documented in this repository's README.md rather than in HarperFast/documentation. If rocksdb-js later exposes a user-visible choice between the variants, that is where the docs change belongs.
  • The Ready gate cannot be "CI is green" on this PR, because nothing runs. Whatever validation you want before merge has to be a deliberate dispatch run.

❓ Your call: How much do you want validated before merge, given that validating costs a published prerelease? The repository's own documented route for exactly this ("to validate a build configuration change before the final release") is a workflow_dispatch with a prerelease_revision, which publishes vX.Y.Z-<rev> — ten real archives to inspect. I have not run one, because publishing a release was out of scope for this task. The alternative is to merge on the strength of the darwin-arm64 evidence and let the next nightly be the first full run, which is cheap to revert but publishes to the real tag.

❓ Your call: Rollout, and it is the reason this PR is a draft stacked on #26. Suggested order: merge #26, merge this into it, publish a prerelease_revision build, inspect all ten archives' layouts and sizes, then land the rocksdb-js change that selects the alternate path and benchmark it before any final release — because a final release dispatches to rocksdb-js automatically. The alternative is to let a final release ship first and treat the alternate path as dormant.

🤖 Generated by Claude (Anthropic); posted via @cb1kenobi.

Related PRs: #26 overlaps (this PR is stacked on it — it targets kris/disable-perf-context and must merge into it, not into main), #13 independent (merged to main; touches the patch-preparation step, not the build or verification steps changed here — git merge-tree origin/main HEAD is conflict-free), #24 independent (same file, same reason, also conflict-free)
Complexity: complicated

Review-Coverage: authored=claude; ran=gemini,codex,cursor-composer,cursor-muse; adjudicated=domain; blocked=cursor-grok(failed); declined=cursor-kimi; rounds=4; full=2 @ a4e0f72

Review-Attention: deep ~35m (critical: build-rocksdb-variants.sh, portfile.cmake +1; decisions: enabled-default-path, one-archive-two-libs, windows-arm64-evidence, disabled-debug-build, do-less-alternative) @ a4e0f72

cb1kenobi and others added 4 commits October 8, 2026 00:34
PR #26 compiled PerfContext out for every consumer of the published
archives, which is a one-way choice: releases are immutable, so a
consumer that later needs the counters has to build from source.

Each archive now carries both libraries instead. The PerfContext-enabled
build stays at lib/librocksdb.a (lib/rocksdb.lib on Windows), so the
historical path keeps its historical meaning, and the compiled-out build
lands at lib/no-perf-context/ alongside it. The public headers, the
dependency libraries and the CMake and pkg-config packages are shared.
The release still publishes ten archives, one per platform target.

The variant is a vcpkg feature rather than an environment variable
because a feature is part of the port's ABI hash, so the second install
cannot be served the first one's binary-cache entry. Both installs run
the same portfile, which is what keeps them identical apart from
WITH_PERF_CONTEXT.

Verification runs against a fresh extraction of the finished archive:
the two libraries must be present and distinct, must reference
rocksdb::perf_context in opposite directions, and - on the seven targets
whose runner can execute their own output - must record a non-zero and a
zero user_key_comparison_count respectively. The portfile reads
NPERF_CONTEXT back out of the generated build system, which covers the
three cross-compiled targets the other two checks cannot reach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The symbol check read an nm failure as zero references, so an unreadable
disabled library passed the one check that was supposed to catch it. nm
and grep exit statuses are now separated, with grep's status 1 the only
accepted zero.

Windows archives had no compiled-artifact check at all: nm covers eight
targets and the probe seven, leaving both windows-arm64 targets resting
on a build-system string search. Perf-counter names only reach .rodata
through PerfContext::ToString(), which NPERF_CONTEXT compiles away, so
searching the archived library for user_key_comparison_count separates
the variants with no toolchain and no matching architecture. That check
now runs on all ten targets, in both directions, with a control string
present in both libraries guarding the search itself.

The portfile's build-system check matched the macro name anywhere; it
now matches the definition form the option actually generates
(-DNPERF_CONTEXT, verified as the only form CMake emits here).

Also: the design note promised exactly two libraries while the archive
retains the enabled debug build, the README's CMake override did not say
that Debug configurations have no second variant to select, and several
new comments narrated code rather than stating a constraint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The counter-name check added in the previous commit would have failed
every Windows target on its first run. user_key_comparison_count is not
only the string literal PerfContext::ToString() emits, it is also a
PerfContextBase field name, and vcpkg's MSVC release flags include /Z7
(scripts/toolchains/windows.cmake), so CodeView type information carries
that name into the .lib whether or not PerfContext is compiled in.

Both variants declare the struct identically, so the literal is exactly
the enabled library's margin: the check now requires strictly more
occurrences in the enabled library than the disabled one, which is
unaffected by a debug-info contribution both of them share. Verified by
appending the name to both libraries of a Windows-shaped archive, which
passes, and to a swapped pair, which still fails.

The portfile's definition regex also matched NPERF_CONTEXT_EXTRA; it now
requires a non-identifier character or end of line after the name, and
accepts the MSBuild PreprocessorDefinitions form after '>' as well as
'=', ';' and '"'. Checked against the real generated build.ninja for
both variants and against seven hand-written forms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Alpine's grep is BusyBox, whose binary-input handling and -o occurrence
counting the marker comparison cannot rely on. The verify container now
installs the grep package, which puts GNU grep at /usr/bin/grep, ahead
of BusyBox's /bin/grep on Alpine's default PATH. The control-marker
guard already made a grep that could not read the libraries fail the
job rather than pass it, so this closes a false-failure path, not a
false-pass one.

Also corrects the design note: six of the ten targets are non-Windows
and get the nm reading, not eight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gemini-code-assist[bot]

This comment was marked as resolved.

cb1kenobi and others added 2 commits October 8, 2026 01:42
The two candidates covered a single-config generator on Unix and a
multi-config one on Windows, but not Ninja on Windows, where the
executable is perf-context-probe.exe at the top of the build directory.
That left the step depending on MSYS implicitly resolving the .exe
suffix for [[ -x ]]. All three locations are now tried explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both scripts were mode 644 and took five and four required environment
variables with no usage output, so running either directly failed with
either "permission denied" or bash's own "parameter null or not set"
naming one variable at a time. They are now executable, report every
missing variable at once with a usage block, and derive what can be
derived: WORKSPACE from the script's own location, VCPKG_ROOT and
VCPKG_CMD from WORKSPACE, PROBE_DIR from the repository root, SCRATCH
from mktemp and RUN_PROBE from its only sensible local default. Those
derivations produce exactly the values the workflow passes explicitly,
so CI behaviour is unchanged.

Running it by hand then exposed a real defect. vcpkg treats an installed
superset as satisfying a request, so `vcpkg install rocksdb` against a
tree already holding rocksdb[no-perf-context] reports "already
installed" and does nothing — staging the PerfContext-disabled library
at the enabled path. The staging hash check caught it, which is the
check working, but the job failed where it should have succeeded. The
enabled install is now preceded by a tolerant `vcpkg remove rocksdb`, so
it is authoritative about its own feature set rather than depending on
the tree being pristine. CI clones vcpkg per job and never saw this; a
re-run, a warm tree or a local invocation does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cb1kenobi
cb1kenobi marked this pull request as ready for review October 8, 2026 21:21
@cb1kenobi
cb1kenobi merged commit 1d9e833 into kris/disable-perf-context Oct 8, 2026
@cb1kenobi
cb1kenobi deleted the chris/dual-perf-context-libs branch October 8, 2026 21:22
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