Skip to content

Add #[inline] to the hot Kernel/Ctx/NanoTime accessors - #872

Open
0-jake-0 wants to merge 1 commit into
mainfrom
inline-hot-accessors
Open

Add #[inline] to the hot Kernel/Ctx/NanoTime accessors#872
0-jake-0 wants to merge 1 commit into
mainfrom
inline-hot-accessors

Conversation

@0-jake-0

Copy link
Copy Markdown
Contributor

What this changes

#[inline] on the small, monomorphic per-node-per-cycle accessors in op.rs,
runtime/kernel.rs and runtime/time.rs, so a downstream crate compiling a
nitro! compiled() graph can inline them across the crate boundary instead
of emitting a real call.

Attributes only — not one function body is touched. The diff is 31 added
lines, all of them #[inline].

Why

Closes #823.

nitro!'s compiled() tier expands into the downstream crate: the derive
emits ::wingfoil::op::Ctx::new, __ctx.time(), __ctx.wall_time(),
__ctx.start_time(), __ctx.schedule(..), __k.begin_cycle(..) and
__k.end_cycle(..) at the call site. That includes this repo's own benches,
examples and integration tests, which link the lib as an extern crate. Those
calls land on concrete, non-generic pub fns that carried no #[inline], so
nothing guaranteed they were inlinable across the boundary. latency.rs and
ops.rs already annotate their hot leaves; this applies the same pattern to
the kernel/ctx/time layer.

The two clocks are untouched

Every invariant CLAUDE.md names under "Two clocks: engine time and the wall
snap" still holds, because no body changed:

  • Ctx::wall_time() stays on &self — the Cell is what allows that.
  • Ctx::new still leaves wall_time: None rather than copying the kernel's
    snap, so a cycle in which no op stamps still reads the clock zero times.
  • begin_cycle still only invalidates the snap; it never takes one eagerly.

What was annotated, and what was not

Annotated (op.rs): Ctx::new, nested, time, wall_time,
wall_time_precise, start_time, is_last_cycle, run_mode, schedule.

Annotated (kernel.rs): Kernel::start_time, time, wall_time,
run_mode, is_last_cycle, schedule, end_cycle, mark.

Annotated (time.rs): NanoTime::now, from_nanos_u128_saturating, the four
Mul impls, the three Add impls, Sub, and the trivial From conversions
u64 <-> NanoTime and Duration <-> NanoTime.

Deviations from the issue's list, in both directions. The line ranges were
treated as a guide, per the issue's own framing.

Added beyond the list, each on the same per-node-per-cycle path:

  • Ctx::nested — the island tier's constructor, built once per inner node per
    activation. Exactly Ctx::new's role for the other tier, and emitted
    downstream by the same derive.
  • Ctx::start_time / Ctx::is_last_cycle / Ctx::run_mode — sibling scalar
    getters to Ctx::time. start_time is emitted directly by the derive;
    is_last_cycle and run_mode are read from cycle bodies that monomorphize
    downstream (window/buffer flush, the run-mode-gated IO sinks).
  • Ctx::wall_time_precise — a one-line forwarder to the now-inlined
    NanoTime::now, on the latency-stamping path.
  • Kernel::start_time — called by Ctx::new itself, so once per node per
    cycle.
  • NanoTime From conversions for u64 and Duration — each is a single
    field access or a from_nanos, and they sit inside the annotated arithmetic
    (Add<Duration> goes through both) and the kernel's bound checks. The
    display-oriented conversions (f64, NaiveDateTime, pretty, the kdb
    timestamp pair) are cold and were left alone.

Excluded although in or near the cited range:

  • Kernel::begin_cycle — left alone, as the issue asks. Large, and once
    per cycle rather than per node.
  • No workspace [profile] / LTO change. Profiles apply only to builds
    rooted at this workspace and never to downstream consumers, so LTO cannot
    address the reported problem.
  • Kernel::run_for, cycles, new, with_ready, build, set_spin,
    set_timer_policy, due — construction, configuration or reporting, not the
    hot path. due in particular is only read by the in-crate interpreted
    runner, never emitted downstream.
  • Kernel::drain_ready — per cycle, does a channel drain, and is reached only
    from begin_cycle, which is staying out of line anyway.
  • Generic items (TimeQueue, Bucket) — they monomorphize downstream already
    and need nothing.

One annotated item deserves a note: Kernel::mark is private, so the
cross-crate argument does not apply to it. It is included because #[inline]
still enables cross-codegen-unit inlining within the crate (release defaults
to 16 CGUs), and it is small and runs once per due node per cycle. Happy to
drop it if a reviewer would rather keep the diff strictly to the pub surface.

No #[inline(always)] anywhere. Plain #[inline] throughout; nothing here
needed the stronger hint.

How it was verified

  • cargo fmt --all
  • cargo lint and cargo lint-all
  • cargo test -p wingfoil --all-features — 81 suites pass. The nine
    *_integration suites (aeron, etcd, fluvio, kafka, otlp, postgres,
    redis, zmq_cross_lang, zmq_etcd) fail with
    failed to initialize a docker client: Socket not found: /var/run/docker.sock — no Docker in this sandbox, pre-existing and
    unrelated.
  • cargo test -p wingfoil-derive
  • New behaviour is covered by a test asserting values and tick times — n/a,
    this changes no behaviour.

Measurement

No timing improvement is claimed, because none could be measured here. The
honest reading follows.

Timing: the sandbox is too noisy to say anything. Building --bench tiers
(a bench target is a downstream crate running compiled() graphs) and
re-running the identical binary against its own criterion baseline gave:

group same-binary re-run vs itself
dense_chain/compiled −7.19% ("Performance has improved")
accumulate/compiled +10.47% ("Performance has regressed")
dense_chain/nested −0.82%
accumulate/nested +1.63%

A ±10% swing with no code change at all is the noise floor of a shared cloud
VM. Any before/after number from this machine would be meaningless, so none is
reported.

Object code: a deterministic check that is not noise-sensitive. Comparing
nm -C on the tiers bench binary built before and after the change
(rustc 1.94.1, bench profile, no LTO):

before-only symbols, gone after:
  T wingfoil::op::Ctx::schedule
  T wingfoil::op::Ctx::wall_time

Those two were emitted as real out-of-line functions in the downstream binary
before this change and are fully inlined away after it.

The rest of the issue's list never appears as an out-of-line symbol in either
build: on this toolchain rustc's cross_crate_inlinable heuristic already
auto-exports MIR for sufficiently trivial non-generic functions, which covers
the plain field getters and the arithmetic impls. Ctx::wall_time and
Ctx::schedule are precisely the two with match bodies that exceed that
threshold — which is why they were the two still paying a call.

That is worth being clear about in both directions. It narrows the present
impact relative to the issue's framing, and it is the main argument for
landing the change anyway: the heuristic is an unstable implementation detail
with no guarantee attached. It varies by toolchain version, and a getter drops
out of it the moment its body grows a branch — silently, with no diagnostic.
#[inline] turns "currently inlined by luck" into "inlined by contract", and
pins the two that were already losing that lottery.

Kernel::begin_cycle remains an out-of-line symbol in both builds, as
intended.

Notes for the reviewer

The interesting judgement calls are Kernel::mark (private — see above) and
the NanoTime From impls (in the arithmetic hot path but not named in the
issue). Both are easy to drop if you disagree.

Worth knowing that the object-code comparison above is the reason this PR does
not lead with a percentage: the change is defensible as making an existing
guarantee explicit, not as a measured speedup.


Generated by Claude Code

`nitro!`'s `compiled()` tier expands into the *downstream* crate — the
derive emits `::wingfoil::op::Ctx::new`, `__ctx.time()`, `__ctx.wall_time()`,
`__ctx.start_time()`, `__ctx.schedule(..)` and `__k.end_cycle(..)` — and that
includes this repo's own benches, examples and integration tests, which link
the lib as an extern crate. The per-node-per-cycle accessors those calls land
on were concrete, non-generic `pub fn`s with no `#[inline]`, so nothing
guaranteed they could be inlined across the crate boundary. `latency.rs` and
`ops.rs` already annotate their hot leaves; this applies the same pattern to
the kernel/ctx/time layer.

Attributes only — no function body is touched, so the documented invariants
around the two clocks are unchanged: `Ctx::wall_time` stays on `&self` (the
`Cell` is what allows it), `Ctx::new` still leaves `wall_time: None` rather
than copying the kernel's snap, and `begin_cycle` still only invalidates.

`begin_cycle` is deliberately not annotated: it is large, it runs once per
cycle rather than per node, and inlining it is not the ask. No workspace
`[profile]` change either — profiles apply only to builds rooted at this
workspace and never to downstream consumers, so LTO cannot address the
reported problem.

Closes #823
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.

Add #[inline] to hot non-generic Kernel/Ctx/NanoTime accessors — downstream nitro! crates pay real calls

2 participants