perf(mapper): optimize Rust NIF hot paths#3733
Open
djwhitt wants to merge 4 commits into
Open
Conversation
djwhitt
force-pushed
the
perf/mapper-nif-hot-paths
branch
from
July 25, 2026 20:38
9549232 to
0cdc329
Compare
djwhitt
marked this pull request as ready for review
July 26, 2026 00:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Optimize the Rust-backed mapper's existing single-document execution path without adding batching or changing the ClickHouse pipeline, mapping configuration, or public API.
The main strategy is to move reusable decisions into mapping compilation, share path work within each mapped document, and reduce intermediate BEAM terms and Rust heap allocations. The compiled mapping resource remains immutable; every
map/3invocation owns its own temporary cache, so the same compiled resource can be reused concurrently.Scope and compatibility
This PR does not add batch mapping, change pipeline concurrency, or change the Elixir/NIF interface. It preserves the existing behavior for:
flat_keys: truelookup modesfilter_nilbehaviorOptimization details
1. Compile path metadata once
Each path is still parsed into key, index, and wildcard segments, but compilation now also records:
flat_keys: true, avoiding rebuilding it per documentThe compiler counts shared prefixes across all path consumers and assigns compact integer cache indices. Runtime lookup therefore uses a pre-sized
Vec<Option<Term>>instead of a per-document hash map. Missing andnilvalues are cached as well, preventing repeated failed traversals.2. Adaptive root-map preloading
Production mappings often read many keys from a relatively small root map. When a compiled mapping contains at least eight root-key references, it records the referenced keys and can populate their cache slots in one map scan.
At runtime, preloading is only used when the document's root map is no larger than the compiled scan limit. Large or sparse roots keep direct
map_getlookup, avoiding a full scan when only a small fraction of keys are needed. Flat-key mode remains isolated from nested root preloading.The query cache is created inside
map_single/4; no cached BEAM terms are stored in the reusable compiled resource. This is what keeps sequential documents and concurrent callers isolated.3. Iterative path traversal and fused wildcard arrays
The common key/index path is evaluated iteratively. Wildcard fan-out is kept in a separate path so ordinary lookups do not pay for wildcard handling.
For array fields sourced from a single wildcard path, lookup and element coercion are combined in one pass. The mapper now walks the source list, resolves the suffix, applies the target array coercion, handles
filter_nil, and builds the final BEAM list directly. This removes the previous intermediate wildcard-result list followed by a second coercion pass.The specialization covers string, UInt64, Float64, DateTime64, JSON, map, and FlatMap arrays while preserving empty-list behavior for missing, empty,
nil, or non-list wildcard prefixes.4. Bulk map construction and fused FlatMap work
Output fields, JSON picks, exclusions, and flattened entries are collected into key/value arrays and emitted with
enif_make_map_from_arraysthrough Rustler. This replaces repeated persistentmap_putrebuilding where duplicate-key semantics do not require it.FlatMap processing also avoids unnecessary intermediate maps:
The optimized path explicitly preserves non-map elevation parents, removes empty map parents, honors excluded elevation subtrees, and keeps literal dotted top-level keys ahead of nested elevated values.
5. Lower-allocation coercion and transforms
Several scalar hot paths now operate directly on borrowed BEAM binary slices:
StringvaluesNewBinary; Unicode retains the existing Unicode-aware fallbackitoastack bufferInvalid UTF-8, invalid numeric text, booleans, Unicode case mapping, integer limits, and DateTime64 fallback behavior remain unchanged.
6. Stream compound JSON directly from BEAM terms
Compound list/map values previously required an intermediate
serde_json::Valuetree before conversion to a JSON string.JsonTermnow implements Serde'sSerializetrait over the original BEAM term and streams scalars, lists, and maps directly intoserde_json.Map entries with valid UTF-8 keys are collected and sorted by key bytes before serialization. That small entry list preserves the legacy deterministic JSON representation—including byte-level key order—without allocating a second tree containing every compound value. Invalid UTF-8 values and non-finite floats retain the prior
nullrepresentation.Performance
Single-threaded local benchmarks on Linux ARM64, Elixir 1.19.5 / OTP 27:
Additional production-shaped workloads:
Correctness and testing strategy
Focused hot-path regressions
optimized_mapper_test.exsexercises the optimization-specific invariants:nilprefixes; coalescing; small preloaded roots versus 200-key roots; cached JSON picks and Enum8 inference; nested and flat-key lookup isolation.filter_nil; missing, empty,nil, and non-list prefixes; root, indexed, nested, post-index, and repeated wildcard shapes.flat_keys: trueinputs.Independent equivalence checks
Correctness is checked at several levels rather than only asserting selected examples:
Native and CI coverage
cargo test -p mapper_exCI job so native tests cannot silently fall outside the Elixir suitecargo fmt --all -- --checkcargo clippy -p mapper_ex --all-targets -- -D warningsMIX_ENV=test ../bin/x mix compile --warnings-as-errorsMIX_ENV=test ../bin/x mix format --check-formattedMIX_ENV=test ../bin/x mix lint.all