Standalone collection and metadata comparison, 2026-10-22¶
Map-projection medians decrease 36–51% versus preceding StreamFusion across Flink 2.2.1 and 1.18.1. On 2.2, width-64 BIGINT reverse and prepend beat both prior StreamFusion and paired stock with nonoverlapping measured ranges. Keys/entries remain 25–34% slower than stock; ROW construction remains slower too.
The final string exit borrows bytes during an ownership copy into generic rows. JOIN beats paired stock on both lines; TYPEOF sampled exit allocations decrease 38–42%. Timing ranges and stock-control drift limit claims about a distinct metadata throughput gain. These improve the existing default-enabled extension, whose standalone regressions were accepted on 2026-10-17.
Collection techniques, the string exit boundary and query admission describe the current implementation and its representation guards.
Method and provenance¶
Actual preceding Java production code is frozen at d951a838. Collection/BIGINT
cohorts use implementation 3c47a89d; the final string cohort uses f7867f55, which
replaces binary string packing with synchronous borrowing for generic-row copies.
The temporary diagnostic commits change only their runner, benchmark assertions
and the recorded implementation. The same instrumented benchmark and metrics
helper are compiled against each revision on each released Flink line. Native
source is unchanged within these comparisons; each cohort pair uses one identical
release core DSO built with --no-default-features --features mimalloc.
Each expression/cohort starts a fresh JVM. Stock/native order alternates within it: two warmup pairs and five measured pairs, two million source rows, parallelism one, a 2 GiB heap, 1,024-row batches and Temurin 17.0.20.1 on an isolated Ubuntu 24.04 runner. Cluster construction and the initial explain are outside timing. Execution-time planning, deployment, runtime source construction, both row/Arrow transposes, production JNI/IPC and the rowwise blackhole sink remain inside.
MAP input has three non-NULL BIGINT values keyed by a, b, c, with a NULL map
every eighth row. NULL keys/values and other admitted value types are correctness
checks, without a type-wide performance claim. Numeric source arrays have width
8/64 and a value domain equal to that width. STRING arrays cycle over eight values
with 64-byte suffixes. Array parents are NULL every eighth row and element indices
divisible by seven are NULL. Constructor/TYPEOF controls keep the same source work
even when they do not read the array. These are low element-cardinality fixtures.
Candidate jobs require consumed NativeCalc input, both transpose plan nodes and the expected Rust function without a JVM evaluator. TYPEOF is a folded constant, not a per-row TYPEOF kernel. The intentionally generated MAP-constructor control instead requires its generated MAP function. Separate profiled processes are excluded from elapsed-time comparisons.
The initial run completed seven matched cases on 1.18 and eight on 2.2, then stopped
at an incorrect Rust-only assertion for the generated MAP constructor. The follow-up
completed those controls and profiles. Unchanged 1.18 Kafka fixtures subsequently
exposed binary string output's legacy RowData.toString() incompatibility; that
prototype was removed. Final JOIN/TYPEOF pairs were rerun on both engines and both
lines with generic-row output. Every completed non-string comparison stays primary;
old string prototypes and initial unpaired controls remain retained separately.
Selection is based on implementation and completion, never favorable timing.
Runner CPUs differ: initial 2.2 uses EPYC 7763, initial 1.18 Xeon Platinum 8573C, follow-up MAP controls use EPYC 9V74/9V45, and final strings use EPYC 7763/9V74. Every case compares both cohorts on the same runner with its own paired stock. Do not aggregate elapsed times across these legs or attribute cross-line differences to Flink alone. Short jobs still include startup and separate-JVM drift.
The measurements use frozen source snapshots around integration onto main 717b8d94, whose separate
changes cover synchronization/date evaluation and an Iceberg checkpoint fixture.
Collection/BIGINT and final generic-string routines remain the same on integration;
these recorded binaries do not measure the combined main revision.
Provenance and selection, 420 primary measured jobs and 168 warmups and all medians/ranges accompany initial 2.2, initial 1.18, follow-up 2.2, follow-up 1.18, final strings 2.2 and final strings 1.18.
Complete-job results¶
All rows use two million source rows. Negative percentages mean less elapsed time;
prior is actual preceding StreamFusion, and stock is the current paired Flink
control. Widths describe source arrays; MAP inputs always contain three entries.
Flink 2.2¶
| Case | Prior stock / SF, s | Current stock median (range), s | Current SF median (range), s | SF time vs prior / stock |
|---|---|---|---|---|
| array (initial) | 0.915 / 0.888 | 0.950 (0.918–0.980) | 0.878 (0.862–1.133) | -1.2% / -7.6% |
| mapentries (initial) | 2.162 / 5.260 | 2.150 (2.138–2.194) | 2.677 (2.671–2.849) | -49.1% / +24.5% |
| mapkeys (initial) | 1.554 / 3.276 | 1.534 (1.517–1.580) | 2.051 (2.026–2.282) | -37.4% / +33.7% |
| mapvalues (initial) | 1.524 / 2.654 | 1.587 (1.518–1.635) | 1.518 (1.492–1.778) | -42.8% / -4.3% |
| prepend64 (initial) | 2.421 / 2.429 | 2.360 (2.301–2.544) | 1.962 (1.952–2.025) | -19.2% / -16.8% |
| reverse64 (initial) | 2.267 / 2.573 | 2.283 (2.266–2.351) | 2.173 (2.153–2.235) | -15.6% / -4.8% |
| reverse8 (initial) | 0.928 / 0.922 | 0.924 (0.841–0.990) | 0.803 (0.792–1.036) | -12.9% / -13.2% |
| row (initial) | 0.886 / 1.061 | 0.989 (0.885–1.034) | 1.083 (1.052–1.225) | +2.2% / +9.6% |
| map (followup) | 0.937 / 1.326 | 0.901 (0.871–0.924) | 1.375 (1.255–1.435) | +3.7% / +52.7% |
| join (string-final) | 3.098 / 2.631 | 2.812 (2.797–2.926) | 2.577 (2.557–2.732) | -2.1% / -8.4% |
| typeof (string-final) | 0.765 / 0.623 | 0.753 (0.719–0.812) | 0.617 (0.575–0.880) | -0.9% / -18.0% |
Flink 1.18¶
| Case | Prior stock / SF, s | Current stock median (range), s | Current SF median (range), s | SF time vs prior / stock |
|---|---|---|---|---|
| array (initial) | 0.705 / 0.693 | 0.712 (0.696–0.779) | 0.734 (0.674–0.781) | +5.9% / +3.1% |
| mapentries (initial) | 1.845 / 4.856 | 1.886 (1.873–2.041) | 2.382 (2.345–2.506) | -51.0% / +26.3% |
| mapkeys (initial) | 1.375 / 2.855 | 1.384 (1.366–1.420) | 1.837 (1.814–2.011) | -35.7% / +32.8% |
| mapvalues (initial) | 1.367 / 2.362 | 1.430 (1.413–1.453) | 1.381 (1.365–1.446) | -41.5% / -3.4% |
| reverse64 (initial) | 1.815 / 2.364 | 1.797 (1.780–1.846) | 1.962 (1.911–2.094) | -17.0% / +9.2% |
| reverse8 (initial) | 0.666 / 0.706 | 0.631 (0.617–0.663) | 0.658 (0.632–0.764) | -6.9% / +4.3% |
| row (initial) | 0.717 / 0.805 | 0.721 (0.710–0.753) | 0.827 (0.803–0.977) | +2.7% / +14.8% |
| map (followup) | 0.493 / 0.707 | 0.473 (0.460–0.508) | 0.683 (0.655–0.743) | -3.4% / +44.3% |
| join (string-final) | 2.658 / 2.464 | 2.719 (2.696–2.753) | 2.422 (2.346–2.494) | -1.7% / -10.9% |
| typeof (string-final) | 0.655 / 0.630 | 0.651 (0.617–0.853) | 0.587 (0.555–0.673) | -6.8% / -9.8% |
CPU and allocation profiles¶
Matching previous/candidate JVMs use async-profiler 4.6, CPU sampling at 1 ms and Java allocation sampling at 1 MiB, two million rows, one warmup pair and one separate diagnostic pair. Profiled durations never enter elapsed-time comparisons. The process includes setup, planning and stock jobs; scopes below select native operator stacks. Counts are inclusive and overlap. Allocation weights estimate Java allocated bytes, not copied bytes, retained memory or native heap totals; GB is 10^9 bytes.
| Flink / case / inclusive scope | Prior CPU samples | Current CPU samples | Prior allocated GB | Current allocated GB |
|---|---|---|---|---|
| 2.2 / mapvalues / NativeCalc | 3,154 | 880 | 5.575 | 0.924 |
| 2.2 / mapentries / NativeCalc | 8,673 | 3,124 | 16.465 | 6.109 |
| 2.2 / typeof / exit | 561 | 543 | 1.674 | 1.034 |
| 1.18 / mapvalues / NativeCalc | 1,587 | 406 | 5.560 | 0.870 |
| 1.18 / mapentries / NativeCalc | 4,732 | 1,622 | 16.454 | 6.110 |
| 1.18 / typeof / exit | 632 | 522 | 1.672 | 0.971 |
MAP_VALUES JVM expression upcalls fall from 1,994 samples to zero on 2.2 and 1,029 to zero on 1.18; MAP_ENTRIES falls from 4,265 and 2,370 to zero. The outer production JNI/C Data boundary remains. Its preceding inclusive stack also contains JVM callback work, so these counts do not isolate JNI transition costs. Value-only MAP projections remove the nested string writer because the keys' caches are unobserved.
Reverse64 BIGINT writer samples fall from 952 to 549 on 2.2 and 620 to 295 on 1.18. Whole-NativeCalc estimated allocations do not improve: 1.578 to 1.582 GB and 1.467 to 1.588 GB, respectively. Reserving capacity targets writer CPU; rowwise array copying remains. MAP_ENTRIES still spends 2,594/1,346 inclusive samples in its exit.
The final generic-row TYPEOF exit reduces sampled Java allocations by 38.2% on 2.2 and 41.9% on 1.18. It removes an intermediate owned UTF-8 buffer while retaining the required generic-row copy. This is a measured allocation reduction, not proof of sustained metadata throughput.
Collection/array counter scopes and compressed stacks
and final generic-string profiles
retain twenty total captures. Sixteen support the final techniques; four older TYPEOF
captures belong to rejected binary packing and are marked non-primary. All compressed
CPU/allocation stacks and their hashes are checked in; original JFR hashes and workflow
provenance remain in the counters. Recreate them with profiler 4.6's
jfrconv --cpu -o collapsed input.jfr output.cpu and
jfrconv --alloc --total -o collapsed input.jfr output.allocation.
Independent local string confirmations¶
The final generic-string implementation was also compared on an idle Core i7-12650H Linux host with OpenJDK 17, the same rows/queries/resources, two warmups and five alternating pairs. Full production class archives select actual preceding Java and the candidate. Both use the same integrated release/mimalloc DSO; the relevant string and literal kernels are unchanged. These are separate confirmations, not primary cloud timing cells. A continuous monitor recorded no competing build/test work, less than 0.5 outside logical cores, at least 2 GiB available and I/O pressure below 5%.
| Flink / case | Prior stock / native, s | Current stock / native, s |
|---|---|---|
| 2.2 / join | 1.964 / 1.789 | 1.956 / 1.672 |
| 2.2 / typeof | 0.407 / 0.375 | 0.421 / 0.366 |
| 1.18 / join | 1.922 / 1.726 | 1.999 / 1.692 |
| 1.18 / typeof | 0.413 / 0.374 | 0.392 / 0.365 |
All local 2.2 observations and all local 1.18 observations retain 112 jobs, warmups, ranges, class/library hashes, route assertions and every host sample. JOIN beats current stock with nonoverlapping ranges on both lines; only the 2.2 prior/current JOIN ranges are disjoint. TYPEOF prior/current ranges overlap. Its 1.18 stock control includes an outlier; those trials remain included.
Limits and rejected work¶
The 2.2 width-64 reverse and prepend native ranges are below both paired stock and preceding native ranges. Short reverse, ARRAY and MAP_VALUES ranges overlap their stock controls, so their favorable medians are weaker evidence. On 1.18, both reverse widths still lose to stock despite improving preceding StreamFusion. ROW medians increase by 2–3% versus preceding code; the 2.2 ROW stock control shifts by about 12%, and both native ranges overlap. The 1.18 ARRAY control regresses by 5.9% with overlapping ranges. No constructor-wide speedup is established.
A generated binary-exit prototype for collections increased width-64 reverse and prepend medians and did not materially improve MAP construction. It was removed; the rejection and all exploratory trials retain that attempt. The intermediate map-view pass overlapped an integration workload and is diagnostic only. None of those local runs enters the primary comparison.
The MAP constructor remains 44–53% slower than stock, with overlapping prior/current ranges. Final JOIN beats paired stock with nonoverlapping ranges on both lines, but its 2.1%/1.7% reductions versus prior code have overlapping ranges. TYPEOF medians change by -0.9%/-6.8% versus prior code; ranges overlap, including a high 2.2 native outlier. Its stock controls shift by -1.6%/-0.7%. Favorable medians and lower sampled allocations do not establish an isolated metadata throughput gain.
Binary string packing was also removed after unchanged Kafka fixtures exposed
legacy RowData.toString() incompatibility. Its earlier JOIN/TYPEOF timings remain
in the follow-up JSON, excluded from primary summaries. The
scoped rejection
records why typed parity and ownership alone were insufficient.
The surviving key/entry losses include required UTF-16 transport and rowwise output materialization. No broad JNI redesign is inferred from these standalone jobs; the separate bounded native-boundary investigation retains an output-schema cache and defers broader ownership redesign. It does not remeasure or resolve these workload-specific losses.
Validation¶
The final focused correctness suite passes 345 checks on Flink 2.2.1 and 308 on 1.18.1 with 37 release-specific skips. It covers the six admitted map value types, NULL/empty maps, NULL keys/values, hash collisions, malformed UTF-16, generic and binary inputs, composed-consumer and unverified-boundary guards, ownership after source mutation and batch closure, IPC, buffer growth, object reuse and row kinds. Wide decimals and full-range timestamps remain owned in represented scalar ROWs. The string ownership checks also assert generic row rendering, preserving the released legacy RowData consumer behavior that exposed the binary-packing prototype. The diagnostic route assertions are also exercised with small generated-MAP and Rust-MAP_VALUES jobs on both lines; their times are not performance observations.
Reproduction¶
Build the release core before timing, then run from the repository root:
cargo build --manifest-path native/Cargo.toml -p streamfusion --release \
--no-default-features --features mimalloc
SF_BENCHMARK=true mvn -pl streamfusion-runtime test -Pbench \
-Dnative.build.skip=true -Dnative.test.resource.skip=true \
-Dtest=DynamicCollectionBenchmark '-Dsf.extraJvmArgs=-Xmx2g' \
'-Dcollection.expression=MAP_VALUES(m)' -Dcollection.map=true \
-Dcollection.rows=2000000 -Dcollection.warmup=2 -Dcollection.runs=5 \
-Dcollection.rustFunction=MAP_VALUES
Use -Pbench,flink-1.18 for that release line. The raw records retain each expression,
source type and width; array STRING cases also set -Dcollection.bytes=64.
The MAP-constructor control sets -Dcollection.generatedFunction=MAP in place of the
Rust-function assertion. TYPEOF has no runtime function assertion, but still requires
consumed NativeCalc work. Preceding controls compile the same benchmark and metrics
helper against frozen d951a838; only the helper sources are overlaid, leaving its
production code intact. Use the same release library in both cohorts because native
source is unchanged. Keep other builds and tests off the timing runner.