Status: implemented (Part A + Part B)
Area: bases/agent (native agent loader / runtime)
Related files:
bases/agent/src/clj/criterium/agent/loader.cljbases/agent/src/clj/criterium/agent/platform.cljbases/agent/src/clj/criterium/agent/runtime.cljbases/agent/src/clj/criterium/agent.cljbases/agent/test/criterium/agent/loader_test.cljbases/agent/test/criterium/agent/platform_test.cljbases/agent/test/criterium/agent/degradation_test.cljbases/agent/test/criterium/agent/integration_test.cljTwo defects in the bundled-agent extraction path were found while debugging a
user report (published jar org.hugoduncan/criterium {:mvn/version "0.5.245-ALPHA"}):
The path reported by
(first (criterium.agent/jvm-opts))did not exist.
criterium.agent/jvm-opts → runtime/agent-path → loader/extract-agent.
extract-agent extracts the platform binary to a content-addressed temp file
and registers a JVM shutdown hook that deletes that file on exit
(register-cleanup-hook! / extracted-agents in loader.clj).
The documented workflow for jvm-opts (see criterium/agent.clj) is:
"use (jvm-opts) to get the correct path, then restart your JVM with that option."
These are incompatible:
(jvm-opts) → extracts /tmp/criterium-agent-<hash>.dylib,
returns ["-agentpath:/tmp/criterium-agent-<hash>.dylib"].-agentpath.-agentpath at launch (before any Clojure runs,
so it cannot self-extract) → file missing → failure.The path is stable (SHA256-named) so it looks valid, but the file is ephemeral and tied to the lifetime of the producing JVM.
Reproduced against the published jar on macos-arm64: the file exists during the producing JVM's life (full 112432 bytes) and is gone the instant it exits.
extract-with-lock wraps the entire extraction in (catch Exception _e false).
Any failure (locking unsupported, noexec tmpdir failing verify-permissions!,
disk full, atomic-move across filesystems, permission errors, etc.) is swallowed
and surfaces only as extract-agent returning nil. Several other stages also
fail silently or ambiguously:
platform/detect → nil for unsupported platform (legitimate) is
indistinguishable from a misdetection bug; no diagnostic about os.name/os.arch.(io/resource resource-path) → nil returns nil with
no message (packaging problem looks identical to "unsupported").read-hash throws for a missing .sha256 but the binary-missing case is silent.runtime/agent-path catches Exception, prints a one-line WARNING to stdout,
and returns nil, discarding the cause/stack.A. Make extraction persistent so the jvm-opts / -agentpath workflow works:
the extracted binary must outlive the producing JVM. Cleanup-on-exit becomes
opt-in and is not used by the default agent-path path.
B. Eliminate silent failures at every extraction stage. Every failure must be diagnosable: it either throws a structured error or is logged with full context (stage + cause + relevant paths). "Unsupported platform" remains a legitimate, clearly-signalled non-error.
extract-agent (default, used by runtime/agent-path) extracts to the
content-addressed temp file and does not register a deletion hook.java.io.tmpdir).(extract-agent {:cleanup-on-exit? true}),
that registers the existing shutdown-hook behavior.:cleanup-on-exit? false.load-agent!, jvm-opts) do not
need deletion, so both use the persistent default.Consider extracting to a stable per-user cache dir (e.g.
<user.home>/.cache/criterium/agent/<hash>/…) instead of java.io.tmpdir, with
versioned GC, so the binary survives temp reapers and reboots. Out of scope for
the first cut but note it in the docstring/decision log.
(criterium.agent/jvm-opts) returns in JVM A and JVM A exits, the file
referenced by the returned -agentpath: string still exists and is loadable by
a freshly launched JVM B.load-agent! still works (loads into the current JVM).nil, with a single
clear debug/info message including os.name and os.arch. This is the only
legitimate nil.ex-info, or log an error with full context and return a
value the caller can distinguish from "unsupported".(catch Exception _ false) / (catch Exception _ nil) that
discards the cause anywhere in the extraction path.{:stage <kw> :platform <s> :resource-path <s> :target-path <s> :tmpdir <s>} plus the original throwable as :cause.platform/detect
os.name/os.arch was unrecognized.platform/describe) returning the raw + canonical values
for logging, so a misdetected platform is diagnosable, not silent.read-hash (:stage :read-hash)
.sha256, but include resource-path, the hash
resource path, and a packaging-fix pointer in ex-info data (not just a
string).binary resource lookup (:stage :resolve-binary)
(io/resource resource-path) → nil must raise a clear failure: "agent
binary not found in JAR resources at <resource-path>" with ex-info data
and the packaging-doc pointer. Must NOT be confused with "unsupported".extract-with-lock — replace the blanket catch with per-step context.
Each sub-step records its stage so the surfaced error names the failing
operation. Sub-stages and their :stage keys:
:lock-dir-create — mkdirs of lock parent:open-lock — FileChannel/open of lock file (e.g. locking unsupported on FS):acquire-lock — .lock:create-temp — Files/createTempFile:copy — resource → temp copy (e.g. ENOSPC):atomic-move — Files/move (e.g. AtomicMoveNotSupportedException across FS):set-executable — POSIX perms (only UnsupportedOperationException is a
benign no-op; anything else is a failure, not swallowed):verify-permissions — readable/executable check (e.g. noexec tmpdir);
error message must explicitly mention a possible noexec mount:unlock-cleanup — lock file deletion failure should warn, not abortex-info includes :stage and :cause.
Do not collapse to false.integrity verification (:stage :verify-hash) — NEW
read-hash. On mismatch, fail with both hashes in the
ex-info data and delete the bad file. (Promotes the hash from "naming only"
to actual integrity checking.)runtime/agent-path
(catch Exception e (println "WARNING:" ...) nil):
println to stdout) with full stage context and stack.nil for unsupported platform" contract, but
only for the genuine unsupported case — failures must be visible.jvm-opts returning [] only for the unsupported case; failures should
surface, not masquerade as "no agent".Use the project's existing logging facility if one exists; otherwise emit to
*err* (never *out*) and include stage + context. Confirm the convention
before implementing and keep it consistent across the loader/runtime.
nil/false without a corresponding diagnostic, except genuine "unsupported
platform", which logs a clear single message.noexec tmpdir produces a message that explicitly suggests the noexec cause.Add/extend under bases/agent/test/criterium/agent/:
loader_test.clj
:cleanup-on-exit? true.:verify-hash failure,
bad file removed.with-redefs the relevant fs ops or
point at a read-only / noexec temp dir): assert each :stage surfaces.:resolve-binary
failure (currently silent nil).degradation_test.clj
nil with a clear, asserted log message,
distinct from failure.integration_test.clj
jvm-opts path survives producing-JVM exit (spawn a child JVM
that loads -agentpath from a path produced by a separate process), gated on
the binary being present like the existing integration guard.Note: the resource-path inconsistency below has since been resolved — see "Follow-ups (now done)" near the end of this document.
docs/contributor/building-agent.md and build/src/build/agent.clj
(resources-base-dir → bases/agent/resources/native/{platform}) document/use
a native/{platform}/… layout.platform/resource-path) and its tests use
criterium/agent/{platform}/…, which is what the published jar actually
contains.criterium/agent/…), so this
is latent, but the native/ references should be reconciled to avoid future
breakage. Track as a separate cleanup task.extract-agent does not delete on JVM exit; cleanup is opt-in
({:cleanup-on-exit? true}).jvm-opts / -agentpath workflow verified across separate JVMs
(file persists after the producing JVM exits).catch/nil/false anywhere in the extraction/runtime path;
every failure is a structured ex-info (carrying :stage +
:criterium.agent/extraction-failure + context) or logged-with-context to
stderr. The extract-with-lock blanket (catch Exception _ false) is gone.verify-hash!, stage
:verify-hash), bad bytes deleted before publishing.nil, now with a clear stderr message
including os.name/os.arch via platform/describe.:copy, :verify-hash, :resolve-binary), and the existing
cross-JVM integration case.loader.clj, runtime.clj, agent.clj updated to reflect
persistent extraction and the new error contract.runtime/agent-path logs failures richly to stderr (stage + context) and
still returns nil, deliberately preserving the graceful degradation of
higher-level allocation tracking (with-allocation-tracing etc.) rather than
throwing. The failure is now visible (non-silent) instead of a bare one-line
stdout WARNING. If callers later want hard failures to surface, extract-agent
itself now throws structured errors that agent-path could re-raise.extract-with-lock: :lock-dir-create, :open-lock,
:acquire-lock, :create-temp, :copy, :verify-hash, :atomic-move,
:set-executable, :verify-permissions. The fragile filename-regex used to
derive the file extension was replaced with the platform argument.*warn-on-reflection*.Explicit, fail-loud extract API added: criterium.agent/extract-agent!
(delegating to criterium.agent.runtime/extract-agent!). It performs the same
persistent extraction as agent-path but throws the structured per-stage
error instead of degrading to nil, for tooling that must fail clearly.
Accepts the same {:cleanup-on-exit? bool} option. Documented with the full
error/stage contract; covered by degradation-test/explicit-extract-test.
Resource-path convention inconsistency resolved. The canonical location (used
by .github/workflows/release.yml) is
bases/criterium/resources/criterium/agent/{platform}/libcriterium.{ext},
which resolves on the classpath to criterium/agent/{platform}/... — exactly
what platform/resource-path reads. The stale native/ references were
corrected in build/src/build/agent.clj (resources-base-dir,
validate-agent-binaries!), .gitignore, docs/contributor/building-agent.md,
projects/agent/README.md, and the integration_test namespace comment.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |