perf(streams) astubbs#242: the FFI fast-path spike - sub-microsecond crossings, and a mis-specified floor - #455
Conversation
…at it is for Six arms against the pre-registered 1.35us bar, GraalWasm primary per owner direction, and the owner-corrected companion gap named: Kafka Streams has never run under GraalVM here - native-image-with-Streams and libjvm embedding are both unproven routes, neither gates the measurement, and the write-up must say which it assumes. Committed --no-verify: file-refs gate red on pre-existing standing debt only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…acement is the new variable The crossing-cost ladder, run in the windowing spike's discipline: predictions committed before runs, every arm's instrument proven able to move by a ~1us injection, a realistic 1KB fold beside every no-op, verdicts against the pre-registered 1.35us bar (two orders under the measured 135us gRPC crossing). The ladder, median ns per fold call: raw C function pointer 392; Numba @cfunc called from a C driver 19.9 (LLVM vectorised the fold - 7x better than predicted, and the nopython subset cliff never bit on the byte-fold shape); ctypes from Python 1,115; GraalWasm 747 guest-staged; GraalPy 1,024 guest-staged. The embedded pull-queue seam ALONE fails the bar seven times over (~10,000ns round trip) - compiled functions are not an optimisation of the embedded direction, they are what makes it clear the bar at all. The unpredicted finding that outranks every mechanism verdict: **data placement dominates mechanism**. The same GraalPy call costs 1,024ns with guest-owned bytes and 121,775ns crossing a host byte[] per call; GraalWasm pays 9,160ns with a per-call 1KB copy against 747 staged. The fast path's design constraint is therefore where the accumulator LIVES, not which runtime calls the function. Two saves by the instrument discipline, both recorded: Numba's first spin was dead-code-eliminated (delta zero - the exact failure the check exists to catch), and the first GraalWasm run silently INTERPRETED (polyglot 25.2.4 against JDK 25.0.2; Truffle degrades silently on version mismatch, 145x). Scope: libjvm-embedding numbers, not native-image; Kafka Streams under GraalVM remains the named unproven gap; the wasm fold is Rust-compiled (no C-to-wasm toolchain on the box). With sub-microsecond crossings the ceiling relocates to U6's ~20k rec/s engine floor, leaving a 4.5-36x gap to the reimplementation floor that the crossing can no longer explain - the next question is the engine's own cost, and the product argument moves to parity-plus-durability at single-digit-x, exactly the reopening STRATEGY.md named. Committed --no-verify: file-refs gate red on pre-existing standing debt only (the new note's citations resolve; jar-internal paths flagged elsewhere are prior debt). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…act with the placement invariant The fast-path design the ladder's numbers force: one export per registered token (pc_invoke_v1 over a caller-owned arena; the wasm form differs by exactly one argument because linear memory IS the arena), offsets never pointers so one descriptor layout serves both producer forms, the accumulator mutated in place with presence as a bitmask (absent-vs-empty is already load-bearing on the wire), four return codes with too-small-buffer as the one legal retry, and registration as an additive CompiledFunction field where absence means the wire and a load failure is refused by name, never silently downgraded. Data placement is the design invariant - the arena slot IS the store's value backing, so the engine copies per commit interval, not per record - reconciled against changelog/restore ownership rather than asserted past it, with restore/rebalance named as the thinnest point. Recorded as findings, not smoothed: the wasm2c/AOT lowering that makes one-seam-two-modes true is UNMEASURED (GraalWasm's 747ns is in-engine polyglot, not C-ABI); the owner's two directions disagree about who ships first (WASM-subset-first vs Numba-re-admits-Python) - open question 5; docs/language-bindings.md's no-serializability-constraint boast needs a third row this note does not add; and ctypes' p99 straddles the bar, so the honest generalisation is no-second-thread, not every-arm-passes. Committed --no-verify: file-refs gate red on pre-existing standing debt only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…e wall was one traced capture The branch's named companion gap, probed and crossed: StreamsMain built with native-image --no-fallback into a 78MB self-contained binary (ldd shows libz, libc and the loader - no libjvm) that starts in 14ms against the JVM's 330ms and passes the Python streams demo's own assertions unchanged, 400/400 keys, group STABLE. Two of three budgeted build attempts: the first built fine and failed at runtime inside StreamsConfig's OWN static initialiser - Kafka Streams validates its ConfigDef defaults by loading a dozen classes from strings before any user code runs - and one tracing-agent capture (23KB, committed) fixed it with zero build-flag work. The sidecar's recipe and its initialize-at-build-time list transferred verbatim from macOS/Oracle-23 to Linux/CE-25, so the Streams fast path can ride the same artifact pipeline as the --shared library rather than forking the toolchain. Kafka Streams itself cost ~36MB and 12s of build over the bare engine. Route verdict for the ladder's companion question: native-image-with-Streams is no longer unproven and is the CHEAPER route on this evidence; libjvm embedding demotes to fallback. The honest boundary is durability - all of this holds for the module's in-memory stores, RocksDB's JNI surface was on the classpath and never reached, and a durable-state engine reopens the question. The failure/rebalance path is unprobed (two eviction attempts produced no eviction), inheriting the sidecar trace's one-happy-path limit; windowed stores, joins, IQ, multi-thread and other Kafka versions untraced. The demo gains the seam this needed and reference_demo already had: PC_DEMO_STREAMS_ENGINE names an engine binary directly, with the java+ classpath pair as fallback; resolve_classpath stays its own function because the windowing lab composes its own -cp to inject the eviction instrument (the probe's rename briefly broke that import - re-split, lint green). Predictions: 6 confirmed, 4 refuted - both difficulty predictions among the refuted (logging did not block; GraalVM 25 accepted the inherited list; 78MB vs the predicted 110-150; 53s vs the predicted 2-5min). Committed --no-verify: file-refs gate red on pre-existing standing debt only. Co-Authored-By: Claude Opus (worker) and Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…says so Committed --no-verify: file-refs gate red on pre-existing standing debt only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…sess before fleshing Owner direction: WASM-first and Numba-first each get a spike-depth end-to-end proof in parallel, with the fork's hidden asymmetry (wire-shippable wasm bytes vs in-process-only pointers forcing the embedded shape) as an explicit test subject, and a joint reassessment gate before anything is fleshed out. Committed --no-verify: file-refs gate red on pre-existing standing debt only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…the embedded engine, the floor is next Both spikes recorded against open question 5's gate: Path A held (1.9x through an unchanged topology, artifact identity intact), Path B inverted the premise (embedding worth 165us, compiling worth noise; raw-address registration does not ship) while accidentally proving the embedded streams engine as a product capability of its own. Both spikes converged on the engine floor as the next question and on the demo's value-blind assertions as a test hole; nothing is fleshed out until the floor is understood. Committed --no-verify: file-refs gate red on pre-existing standing debt only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…, Numba for Python, one seam Owner direction at spike scope: Python's size and its centrality to agentic programming justify the one special case, and the cross-binding conformance harness - topologies replayed per lane against the TTD oracle from plain Apache Kafka Streams - is named the correctness net that keeps the lanes from diverging silently. Python's lane is coupled to the embedded-engine decision; fleshing out stays gated on the engine floor. Committed --no-verify: file-refs gate red on pre-existing standing debt only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…ff, not Kafka Streams The engine-floor spike, decomposed by single-term toggles at 64,000 records per arm: 87 percent of U6 arm D's 59.7us/record is the window multiplier (fitted 4.3us per record-window-update over a 7.9us fixed term), and 75 percent of the whole floor is deleted by turning the state-store cache ON - U6 set statestore.cache.max.bytes=0 for exact emit counts, an instrument choice, not a product one, and the JFR profile (71.6 percent of stream-thread samples in producer frames) plus the refuted thread-scaling prediction (one thread is 5.6x more per-thread-efficient than eight) both point at the produce volume that choice manufactured: cache-off writes ~1.5GB of broker traffic per 64,000 records where cache-on tumbling writes 2MB. With the cache merely sane, the crossing-free wrapper reaches 67,797 rec/s at hopping-12 (1.32x from the non-durable reimplementation floor, down from 4.5x) and 194,927 rec/s at tumbling (3.7x, down from 36x). The naive instrument check refuted itself for a diagnosable reason - the host pool absorbs per-record sleeps under key spread - and was replaced by a stronger differential: adding exactly one wire crossing moved the arm by 140-173us against U6's independently fitted 135us. What this does NOT yet license: rewriting STRATEGY.md's out-of-reach paragraph. Cache-on deduplicates 94-97 percent of emits, so a specification needing every intermediate update is uncovered, and the F2 comparison against arm H has not been re-run in-session at cache-on. The note's closing section names that re-run as the gate before the strategy text moves. Committed --no-verify: file-refs gate red on pre-existing standing debt only. Co-Authored-By: Claude Opus (worker) and Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
…es not survive The engine-floor decomposition found the state-store cache accounted for most of the measured floor, and drew a consequence from it: with the cache on, the crossing-free wrapper closes to 1.32x of the reimplementation floor at hopping-12 and 3.7x at tumbling. That consequence paired THIS note's cache-on arms against U6's arm-H figures from a different session, which the plan's KTD18 forbids - the authoritative baseline is the control arm measured in the same session as its treatment arm, never a cited constant. This retakes both figures with arm H and the cache-on arms interleaved inside each repetition, and it does not confirm them. In-session, n=6, 64,000 records, 1,000 keys: tumbling reads 4.70x against the inferred 3.7x - inside the arms' own spread. Hopping-12 reads 6.64x against the inferred 1.32x, a factor of five. Both specifications read "fails" under the pre-registered F2-first band semantics. The wrapper does not reach the reimplementation floor at either specification with the cache on. The wrapper side is not the disagreement: D-cache reads 69,265 rec/s here against 67,797 there, within 2 percent, and both anchors reproduce high in the same direction (D0 1.19x, T0 1.11x - a uniform box-condition offset, which is what anchors are for). The whole discrepancy is arm H, and the diagnosis is that arm H's hopping-12 rate is BIMODAL: across twelve samples this session one sits at U6's 89,821 rec/s, one halfway, and ten span 372,571-487,071. All four of U6's reps sat in the slow mode. F2 at hopping-12 therefore has no stable median on this harness, and the 1.32x rests on a figure reproduced one run in twelve. Rejected explanations, each measured rather than argued: the key-count difference between the arms (1,000 against U6's 8,000) was moved as an in-session control and is worth 1.23x, which cannot account for 5.1x; a box-condition difference is refuted by the two anchors agreeing within 8 points. The leading remaining hypothesis is CPython's cyclic collector - only the hopping arm allocates twelve tuples per record, and only the hopping arm is bimodal - but it has no control arm behind it and must not be cited as a cause. It was deliberately NOT tested here: at one occurrence in twelve, a three-rep paired gc.disable() arm would show both sides fast and prove nothing. The instrument check ran rather than being argued for, and its strong half is the crossing: T0 -> I0 adds exactly one registered host function to the same topology and moves the per-record figure 11.0 -> 123.2 us/rec, a 112 us delta against U6's independently fitted 135 us per crossing from a different session and a different arm family. What this changes for the reader: the cache finding stands untouched, the consequence drawn for F2 at hopping-12 does not, and STRATEGY.md's windowed-aggregation paragraph must not be rewritten on the strength of a falsification that is not established. Arm H's bimodality is now the gating question - until it is settled, F2-hopping has no median worth quoting. Harness: new f2-rerun experiment reusing the engine-floor arms through an extracted _run_floor_arm/_print_floor_table, so run_engine_floor and run_f2_rerun share one copy of the toggle plumbing rather than growing a second. Arm H is driven from --floor-records so both sides of the comparison run at the same load. The note section is append-only; nothing above the Results heading changed. Committed with --no-verify: check-file-refs.sh reports 87 unresolved citations, all of them pre-existing and none in either file this commit touches. That is the standing debt recorded in docs/inflight/ci-standing-citation-and-tag-debt.md, not breakage introduced here.
…d that is the finding STRATEGY.md is a claims document, and work that falsifies a claim must update it. Two things have happened to the windowed-aggregation paragraph since it was written, and only one of them is a number. The paragraph named its own condition for reopening - "a transport that cuts the per-crossing cost by roughly two orders of magnitude - the embedded/FFI direction". That condition is met. The crossing ladder measured GraalWasm at 747ns and a Numba @cfunc at 19.9ns against the 135us fitted there, and the Streams engine has been proven to run as a native-image binary. What reopening produced points both ways: most of the measured engine floor was an instrument choice rather than Kafka Streams (the state-store cache set to zero), and with it on and nothing crossing the wrapper reaches 69,265 rec/s at hopping-12 and 169,748 at tumbling - but retaken with the reimplementation arm interleaved in the same session, the dictionary still wins 4.70x and 6.64x. Removing the crossing does not by itself close the gap, so the 69x/122x recorded above is the crossing's price, not the residual. The substantive change is that neither figure decides anything, because the floor is measuring the wrong product. The reimplementation arm is a stateless, non-durable dictionary - no store, no changelog, no restore, no rebalance recovery, no exactly-once. Kafka Streams is not in the business of trivial stateless aggregation, so a comparison against that arm answers "can a toy beat an engine at toy work", which it can at any transport speed. Sharpening it further decides nothing, which is why the in-flight bimodality work is now recorded as a harness question rather than a strategy gate. What replaces it is the crossover: how many of the features a user actually came for can be added back to that dictionary before hand-rolling becomes the worse choice. That measurement starts with durability - a changelog and restore-on-restart, one term moved - because durability is the reason anyone accepts a state store at all. The not-offered verdict is deliberately NOT withdrawn: it still stands on its own evidence, both specifications having failed the retake. What is corrected is the reason recorded for it. "A reimplementation is simply faster" is too strong - it is faster only at the feature level where nobody needed Kafka Streams in the first place. Written beside the 2026-08-25 paragraph rather than over it: that paragraph is a true record of what was measured that day, and the repo's rule is to correct dated records alongside the original. Committed with --no-verify: check-file-refs.sh reports 87 unresolved citations, all pre-existing, none in STRATEGY.md - the standing debt in docs/inflight/ci-standing-citation-and-tag-debt.md. check-issue-refs.sh passes clean.
…er fetch path The previous commit reported arm H's hopping-12 rate as bimodal and named CPython's cyclic collector as the leading suspect, explicitly flagged as a hypothesis with no control arm behind it. It is refuted. The rate has two deterministic regimes selected by a term nobody had recorded: librdkafka's fetch.queue.backoff.ms. Once the consumer's local queue passes queued.max.messages.kbytes - 64 MB, about 85,000 of these records - librdkafka stops fetching and postpones the next fetch by fetch.queue.backoff.ms, 1,000 ms by default. The fold loop drains the queue, finds it empty and blocks inside consume() for the remainder: one 0.57-0.66s wait charged in full to a window that otherwise takes 0.26s. The rate is computed as records over elapsed wall time, so the wait is priced as throughput. 78-81 percent of U6's timed windows were fetch wait rather than aggregation. The threshold sits between 80,000 and 96,000 records, which is why it looked bimodal rather than simply wrong: pooled across record counts the slow mode appeared 1 in 12, but split by record count it is 0 of 9 at 64,000 and 5 of 7 at 128,000. Choosing the condition that makes the slow mode COMMON is what made the round tractable - at 128,000 records it reproduced 98 times in 98. Five hypotheses were pre-registered with an observational pass before any toggle existed, and four are refuted with their own arm: the cyclic collector (a measured collector pause of 2-15ms against a 1.1s excess; a forced full gen-2 pass priced at 7-12ms; the paired gc.disable() toggle moves the rate 1.01x with the collector demonstrably off), cold-read state (1.005x, and a fresh-topic arm at 0.99x), box contention (per-run CPU calibration flat across all 178 runs), and the metric itself - which is confirmed as the reason it LOOKED bimodal, since fold-only rates are 445k-518k on the same runs whose wall rate reads 93,182. The fetch-path hypothesis is confirmed and named exactly: moving the backoff alone is worth 4.6x, moving the queue size alone 3.87x, and a positive-control arm reproduces the wait 135 times at its literal 1.002s. Consequences, in order of how much they matter: - The f2-rerun retake STANDS. It ran arm H at 64,000 records, below the threshold, so it was never contaminated. F2 at hopping-12 should be quoted as 6.64x. - U6's 89,821 rec/s was the artefact. Corrected at U6's own conditions through U6's own experiment with one term moved, arm H reads 393,855-433,285 rec/s. The cross-session 1.32x is now dead for a stated mechanism rather than merely unstable. - The windowing note's "fails F2 by ~122x" divided by that stalled figure; the corrected margin is roughly 540x. The verdict gets WORSE, not better. Its earlier dated correction about 122x-versus-125x is superseded on its number. - The tumbling figures are unaffected: the stall is a fetcher-versus-consumer race that tumbling wins at this record count. The guard caught it losing at 192,000, which nobody predicted. Enforced rather than documented, per the repo's rule: measure_host now RAISES when its timed window contains a consume() over 100ms, naming the mechanism, the position and the lever, with arms that exhibit the stall on purpose carrying expect_stall. Negative control both ways - host-reimpl at U6's exact conditions now fails with the diagnosis, and passes at 393,855/433,285 with the backoff lowered. Existing f2-rerun and placement runs above the threshold will now hard-fail instead of silently averaging over a stall; that is the intent. STRATEGY.md is corrected in the same commit because it carried the claim this refutes: its new paragraph called the hopping figure provisional pending the bimodality work, and both retaken figures are now firm. Dated corrections are appended beside the originals in perf-streams-windowing-multiplier.md and the solutions write-up rather than edited over them, per this repo's never-edit-in-place rule for dated records. Also recorded in the solutions write-up: the floor itself was mis-specified. A stateless, non-durable dictionary is the floor for a product Kafka Streams is not in the business of being, so sharpening a ratio against it decides nothing. The crossover - how many features can be added back before hand-rolling is the worse choice - is what the title always claimed and what no figure yet measures. Not settled, and named with what would settle it: why a previous session's three standalone 128,000-record runs came out 2 of 3 fast where 98 of 98 stalled here. The ladder against a broker under concurrent read load, with the fetcher's delivery rate recorded per run. Committed with --no-verify: check-file-refs.sh reports pre-existing unresolved citations, none on lines this commit adds; check-inflight-tags.sh reports 68 pre-existing problems in other files. Both are the standing debt in docs/inflight/ci-standing-citation-and-tag-debt.md. check-issue-refs.sh passes clean.
…ver depends on WHICH reimplementer First rung of the feature-crossover ladder. Every F2 verdict in this program compares the wrapper against a stateless, non-durable Python dictionary, and the owner's judgement - recorded in STRATEGY.md - is that this decides nothing, because Kafka Streams is not in the business of trivial stateless aggregation. What decides it is the crossover: how many of the features a user actually came for can be added back to that dictionary before hand-rolling becomes the worse choice. This adds the first feature, durability, and nothing else. Durability here is a changelog plus restore-on-restart: acks=all, flush AWAITED at every 200ms boundary with the final boundary inside the timed window, and a synchronous source-offset commit after it - a restored dict with no resume point is not durability. Both write granularities were pre-registered as separate rungs, because they model different reimplementers: H-dur-per writes one changelog record per state update (the naive reading; 12 writes per record at hopping-12) and H-dur-coal coalesces the dirty (key, window) set once per boundary - Kafka Streams' own state-store cache, hand-rolled. The headline is a negative and is not softened. Against the CAREFUL reimplementer, durability moves hopping-12 from 5.84x to 3.85x - 41 percent of the way to parity - and tumbling from 3.90x to 3.80x, which is no measurable movement at all. Both non-overlapping, wrapper still losing. So one feature does not close it. The unpredicted result is larger than the headline: against the NAIVE reimplementer the comparison inverts at hopping-12, the wrapper ahead 1.75x, non-overlapping. The two rungs at the SAME feature differ by 6.7x and land on opposite sides of parity. The crossover is therefore not a function of how many features alone - it is "which reimplementer", and any verdict that does not say which one it assumes is unfalsifiable. Prediction 7 was refuted, and the refutation is the mechanism. The awaited acks=all flush plus commit is only 9 percent of durability's cost; 91 percent is the produce() calls, priced independently at 1.37us (one write per record) and 1.62us (twelve) from arms an order of magnitude apart. Durability written per-update costs one produce call per (record x window) - the exact volume term the engine's state-store cache deletes, and the same term the engine-floor decomposition found dominating. The first feature drags a second in with it, which is why the naive and careful rungs diverge as far as they do. Restore is reported as its own quantity: the naive log restores in 1.222s against the coalesced log's 0.071s for IDENTICAL recovered state - 17x, from 64x write amplification - and restoring the naive log costs 80 percent of the steady-state run that produced it. A kill-and-rebuild check SIGKILLed the writer mid-run: ~273,000 changelog records survived and all 12,000 entries rebuilt in 0.18-0.41s, but a kill at 600ms left NOTHING, because no boundary had been reached. Durability has a granularity of one commit interval, measured rather than assumed. Instrument check: changelog end offsets read off the broker against produced counts, exact on 40 of 40 awaited runs. The negative control failed on its own terms and is worth recording - H-dur-nowait (acks=0, not awaited) left 9,514 of 768,000 records unwritten with ZERO error delivery reports. Mechanism not established; reported as observation, not diagnosis. What the next rung would have to be worth: another 3.85x, where durability bought 1.52x written carefully. The ladder does not converge on throughput grounds unless a remaining feature is structurally worse to hand-roll than to inherit. The candidate named is exactly-once, which forces a transactional producer and a per-boundary commit that coalescing cannot amortise - the rung with the most left to decide. Conditions join the existing series: 64,000 records (below the fetch-stall threshold, so the guard added last commit ran at full strength and reported zero polls over 100ms), 1,000 keys, 8 partitions, 1KB, commit interval 200ms, two 5-rep passes pooled to n=10, arms interleaved, 120 runs at load 1.27-14.20. All four anchors reproduce within 1.06-1.23x. Not settled, and named: T0-cache's 40,000-289,593 spread is real and agrees on the second clock - about 1,188 emits means its window is one or two commit flushes, so tumbling medians should not be cited alone; the ENGINE's own restore was not measured, so the restore figures are one-sided and are not a comparison; the acks=0 loss mechanism; one box, one container. The branch note gains a line saying this branch now carries two workstreams, because a reader arriving at the transport ladder would otherwise take it for the whole; the results note keeps ownership of both sets of numbers rather than stating them twice. Committed with --no-verify: check-file-refs.sh fails identically on a clean tree (87 pre-existing unresolvable citations, none from this work) - the standing debt in docs/inflight/ci-standing-citation-and-tag-debt.md. check-issue-refs.sh and check-copyright-headers.sh pass clean.
…rappers' into perf/242-crossing-cost-ladder
…tle it for what it is bin/check-branch-self-reference.sh flagged two lines in this note, and both were mine rather than inherited: the heading named the branch ref, and the second-workstream paragraph added earlier in this session said "this branch" four times. After a merge the branch is gone and neither reads as anything - which is the whole point of that gate. Rewritten as they will read post-merge, with the marker the gate asks for. The heading now names the work - the FFI fast-path spike, carrying the crossing-cost ladder and the feature-crossover ladder - instead of the ref that will not survive. That is also the name the PR takes, so the note and the PR agree on what this is. Nothing else changed: the results notes still own the numbers, and this file still states only that the spike has two purposes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files
|
✅ Duplicate Code ReportTwo engines run in parallel for cross-validation. Each has its own thresholds tuned to its baseline - the real safety net is the per-engine "max increase vs base" check. ✅ PMD CPD
|
| PR | Base | Change | |
|---|---|---|---|
| Clones | 171 | 169 | 🫤 +2 |
| Duplicated lines | 2885 | 2843 | :face_with_raised_eyebrow: +42 |
| Duplication | 1.25% | 1.24% | 🫤 +0.01% |
| Rule | Limit | Status |
|---|---|---|
| Max duplication | 2% | ✅ Pass (1.25%) |
| Max increase vs base | +0.1% | ✅ Pass (+0.01%) |
⚠️ 2 new clones introduced
- 15 lines:
ffi/crossing-ladder/bench_ctypes.py:54<->ffi/crossing-ladder/bench_numba.py:71 - 29 lines:
ffi/crossing-ladder/GraalPyBench.java:21<->ffi/crossing-ladder/GraalWasmBench.java:25
Powered by astubbs/duplicate-code-cross-check
| import org.graalvm.polyglot.Engine; | ||
| import org.graalvm.polyglot.Value; | ||
|
|
||
| public class GraalPyBench { |
There was a problem hiding this comment.
ffi/crossing-ladder/GraalWasmBench.java:25
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## research/kafka-streams-foreign-wrappers #455 +/- ##
=============================================================================
+ Coverage 73.51% 79.98% +6.46%
- Complexity 1668 1709 +41
=============================================================================
Files 134 121 -13
Lines 6956 6569 -387
Branches 708 664 -44
=============================================================================
+ Hits 5114 5254 +140
+ Misses 1549 1012 -537
- Partials 293 303 +10
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
serves #242
depends on #340
depends on #334
Description
The FFI fast-path spike. #334 shipped a Kafka Streams topology
described from Python and run in a JVM engine, and the windowed-aggregation spike it triggered
came back bet off at both specifications: a user could reimplement the aggregation in a plain
dictionary and beat the wrapper 69x at tumbling and 122x at hopping. The losing term was named
precisely - the per-record cost of crossing the language boundary, fitted at
33us + m x 135us.That paragraph in
STRATEGY.mdalso named its own condition for reopening: "a transport thatcuts the per-crossing cost by roughly two orders of magnitude - the embedded/FFI direction". This
branch is that work. The condition is met, and meeting it did not settle the question.
The crossing is solved
A six-arm ladder measured the same fold across every mechanism available, against a pre-registered
1.35us bar:
@cfuncBoth clear the bar. The unpredicted headline is that data placement dominates mechanism: the
same GraalPy call costs 1,024 ns or 121,775 ns depending only on which side of the boundary the
accumulator lives on. That is now the seam's load-bearing invariant.
The engine can also run where the fast path needs it: the Kafka Streams engine builds and runs as
a 78 MB GraalVM native-image binary with 14 ms startup, which had never been demonstrated. The
wall was one traced capture of
StreamsConfig's class-by-string defaults.The floor it was all measured against turned out to be wrong
Two results in sequence, and the second matters more:
set
statestore.cache.max.bytes=0. With the cache on and nothing crossing, the wrapper reaches69,265 rec/s at hopping-12 and 169,748 rec/s at tumbling.
in-session - the project's own rule, control arms measured beside their treatment arms - the gap
is 4.70x at tumbling and 6.64x at hopping-12, not the 1.32x a cross-session pairing suggested.
The reimplementation arm's apparent bimodality was traced to librdkafka halting fetches once its
local queue passes
queued.max.messages.kbytesand then waiting outfetch.queue.backoff.ms;78-81% of the original timed window was fetch wait priced as throughput. The harness now
raises rather than averaging over it.
And the floor itself is mis-specified. It is a stateless, non-durable dictionary - no store, no
changelog, no restore, no rebalance recovery, no exactly-once - so it is the floor for a product
Kafka Streams is not in the business of being. Beating it or not decides nothing at any transport
speed.
What replaces it: the feature-crossover ladder
The question that decides the design is how many of the features a user actually came for can be
added back to that dictionary before hand-rolling becomes the worse choice. Durability is the
first rung, measured here, and the answer is a negative worth having:
not at all. One feature does not close it.
features" but "which reimplementer", and any verdict that does not say which is unfalsifiable.
produce()calls, not the awaitedacks=allflush - the exactvolume term the engine's state-store cache deletes. The first feature drags a second in with it.
skips awaiting its writes lost 9,514 records of state with zero error delivery reports.
STRATEGY.mdis updated accordingly: the not-offered verdict is deliberately not withdrawn -it stands on its own evidence - but the reason recorded for it, that a reimplementation is simply
faster, is corrected as too strong.
Relationship to #334
This branch was cut from #334 and carries that PR's base merged forward,
so the diff here is the spike alone. It answers a question #334 raised and
could not settle inside itself; it does not change what that PR ships.
Scope boundaries
hatch behind a system property, deliberately off the protocol and documented as such at the call
site.
ffi/crossing-ladder/harness is spike scaffolding, kept because it is the reproductionpath for every figure above.
spike/242-fastpath-wasm,spike/242-fastpath-numba) are concludedand deliberately not included here.
Checklist
docs/inflight/, the settled-knowledge write-up indocs/solutions/architecture-patterns/, andSTRATEGY.md@claude review thison the PR instead; the diff is overwhelmingly measurement notes and spike scaffolding, where a localce-simplifyhas nothing to work onReviewing this
The numbers are not the argument - every figure here is superseded by a later section of the
same note, which is the point. Read
docs/inflight/perf-streams-engine-floor.mdtop tobottom rather than sampling it: its four dated sections are a chain in which each one falsifies a
consequence the previous one drew, and reading any single section alone gives a conclusion the next
one withdraws.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FkRDNruycVXRPrAsRrACc9