Class KeyPartitionProfiler
Two-phase recording
The hot path is split into a warm-up phase and a capture phase to keep per-record cost lock-free even on the hot partition:
- Phase 1 — warm-up (first
WARMUP_FRACTIONof the session): everyrecord(byte[], int)call increments the per-partition counter (LongAdder) and the CMS (CountMinSketchbacked byAtomicLongArray). Nothing else is touched. Every operation is lock-free. - Phase 2 — capture (remaining session): counters + CMS as above, plus a per-partition
Setof hashed keys whose CMS estimate has crossed the natural top-K threshold (count × K ≥ partitionTotal). The Set isConcurrentHashMap.newKeySet()— also lock-free. Long-tail keys never enter the Set; only keys that are at-or-above the1/Kfraction of partition traffic do.
At snapshot time we re-evaluate each Set member against the final
partitionTotal / K threshold (filters out keys that briefly crossed early but fell
back) and push them through a bounded top-K min-heap. The heap is built once per snapshot —
not on the hot path — so the per-update lock-contention problem of an always-on heap is gone.
Why WARMUP_FRACTION = 1/3
A key emerging at fraction τ of the session with frequency p is captured
iff p × K × (1 − τ/T) ≥ 1. The worst-case captured key — one that emerges right at
the warm-up boundary — needs p ≥ 1 / (K × (1 − w)). Setting w = 1/3 means
we still capture keys that are at least 1.5× the natural top-K threshold, which
leaves enough fidelity to surface meaningfully-hot late-emerging keys while keeping enough
Phase 1 time for the CMS to settle. (w = 1/2 would only catch ≥ 2× keys;
w = 1/5 would catch ≥ 1.25× keys but with less CMS warm-up.)
PII safety
Raw keys are never stored. Each key is hashed with murmur3-128 before being passed to the CMS or the per-partition Set.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final doubleFraction of the session spent in Phase 1 (CMS + counters only). -
Constructor Summary
ConstructorsConstructorDescriptionKeyPartitionProfiler(String storeName, String storeVersion, long startTimeMs, long durationMs, int partitionCount, int maxTopK) -
Method Summary
Modifier and TypeMethodDescriptionvoidemitJsonTo(org.apache.logging.log4j.Logger logger, String logPrefix, String reason) Emit the profile across multiple log lines.booleanvoidrecord(byte[] keyBytes, int partitionId) Hot-path entry.toJson()Emit the full profile as a single-line JSON object.
-
Field Details
-
WARMUP_FRACTION
public static final double WARMUP_FRACTIONFraction of the session spent in Phase 1 (CMS + counters only). At1/3the worst- case late-emerging key captured by Phase 2 has frequency ≥1.5/K, derived fromp_min = 1/(K × (1 − w)). See class Javadoc for the full derivation.- See Also:
-
-
Constructor Details
-
KeyPartitionProfiler
-
-
Method Details
-
record
public void record(byte[] keyBytes, int partitionId) Hot-path entry. Records a single read for the given key/partition. Every operation is lock-free.Phase 1: increments counter and CMS only.
Phase 2: also adds the key to the per-partition Set iff its CMS estimate already crosses the natural top-K threshold (count × K ≥ partitionTotal). -
isExpired
public boolean isExpired() -
getStoreName
-
getStoreVersion
-
toJson
Emit the full profile as a single-line JSON object. Convenience for tests; production code should preferemitJsonTo(org.apache.logging.log4j.Logger, java.lang.String, java.lang.String)which splits the payload across multiple log lines.Shape:
{ "storeName": ..., "storeVersion": ..., "startTimeMs": ..., "durationMs": ..., "actualDurationMs": ..., "totalRequests": ..., "skewFactor": ..., "partitionDistribution": [ { "partitionId", "count", "percentage" }, ... ], "topKeysByPartition": { "<partitionId>": [ { "keyHash", "estimatedCount", "percentageOfPartition" }, ... ] } } -
emitJsonTo
Emit the profile across multiple log lines. One header line carries the session-level metrics + partition distribution; each non-empty partition's top-K is emitted on its own subsequent line, sharing a commonsessionIdfor correlation.Splitting avoids single log lines in the hundreds of MB at large
topK×partitionCount(some async appenders OOM or truncate at line-length limits).
-