Batch-native Kafka JSON serialization¶
Applies to: the Kafka JSON sink encode path
Whole-batch encode instead of per-record Jackson¶
A whole Arrow batch crosses JNI once and arrow-json encodes its rows in one writer pass, rather than
transposing the batch to RowData and invoking Flink/Jackson once per record. The JNI call
materializes the final heap byte[] values directly because KafkaProducer's Java API requires them
— there is no intermediate native pointer registry or second copy/drain call. Flink's stock Kafka
sink still consumes those bytes, so the optimization changes only serialization while retaining its
transaction and recovery path.
A 4096-row scalar JSON Criterion comparison measured 592 µs / 6.92 M rows/s for one batch writer versus 3.55 ms / 1.15 M rows/s when invoking the production writer once per row: 6.0x from batching alone. Broker tests additionally prove committed exactly-once output before and after a post-checkpoint failover.
Records produce straight from the encode buffer¶
The JSON encoder used to split its one line-delimited batch buffer into a fresh Vec<u8> per record
before producing — a per-record allocation and copy that was pure waste, since librdkafka copies
borrowed payloads into its own queue on produce anyway, and the encode-to-Java path materializes a
heap byte[] regardless. The encoder now returns the single buffer plus per-row line ranges, and
both the native producer and the JNI materialization read the row slices in place.
Together with the escape fast path below, the 4096-row Criterion encode dropped 592 µs → 497 µs per batch (6.9 → 8.2 M rows/s); the q9 differential profile had shown the copy as part of the sink's memmove + allocator-churn tail.
Escaping is a bulk scan, not a per-byte table walk¶
arrow-json hands every string value to serde_json's serializer, which scans one byte at a time against an escape table — and almost no real value needs escaping at all. The sink's encoder factory now supplies a string encoder that answers "anything to escape?" with a word-at-a-time scan (the standard SWAR zero-byte/less-than masks over eight bytes per step — the same idea simdjson's serializer applies with SIMD) and bulk-copies the clean value; values that do escape take a loop replicating serde_json's exact table, so output stays byte-identical (pinned by a parity test against the stock arrow-json writer across every escape class).
Strings are the wide columns of the changelog-heavy sinks — q9's upsert rows carry itemName,
description, and two Nexmark extra paddings per record.