Theta sketch compact() before storage vs. keeping the updatable form for low-cardinality tenant streams
0 reputation · 05 Jan 2022, 11:22 UTC
0 reputation · 05 Jan 2022, 11:22 UTC
We run Apache DataSketches (Java 6.x) Theta sketches for per-tenant hourly unique counts. Most tenants see well under 500 distinct values per window, but a default sketch is constructed with 4096 nominal entries, so each instance holds a mostly empty internal buffer. Across thousands of tenants kept in heap, the unused capacity is becoming a measurable memory cost.
The documented options appear to be: call compact() before serializing or caching each window's result, or leave sketches in the default updatable form and accept the larger footprint. Compaction returns a new sketch instance and copies internal arrays, so doing it after every short batch window adds CPU work for updates that are already cheap. There also seems to be no built-in way to pre-size a sketch to a small expected cardinality without giving up the library's accuracy guarantees if a tenant suddenly spikes.
Given this workload, which trade-off is the right default?
compact() on every hourly window worth the copy cost when update volume is low, or should compaction only happen at serialization boundaries?nominalEntries (e.g., 512) a defensible alternative, and how badly do error bounds degrade if actual cardinality exceeds it?35775 reputation · 05 Jan 2022, 23:12 UTC
For the described workload – thousands of tenants, most with < 500 distinct values per hour – the sensible default is to keep each tenant’s sketch in its updatable form while it is being updated, and to call compact() once at the end of the hourly window before the sketch is serialized or cached. The compaction cost is negligible compared to the cost of hashing each incoming item, and the memory savings are substantial: a compacted sketch for a tenant with 200 distinct values consumes roughly 200 × 4 bytes ≈ 0.8 kB versus the 4096 × 4 bytes ≈ 16 kB of the default buffer.
compact() is copying the internal hash table. For a nominal size of 4096, this is a few hundred microseconds – far less than the time spent hashing a small batch of items.Lowering nominalEntries to 512 reduces per‑sketch memory to 512 × 4 bytes ≈ 2 kB. However, the 95 % relative error bound rises from about 1.6 % (for 4096) to roughly 4.4 % (≈ √(1/512)). If your application can tolerate that higher error, and you have confidence that no tenant will exceed the nominal size in a single hour, the smaller sketch may be acceptable. The risk is that a sudden cardinality spike will cause the sketch to underestimate and the error bound to be violated silently.
Encapsulate the lifecycle in a small helper that hides the compaction logic:
class TenantSketch {
private final Sketch<Double> mutable;
private Sketch<Double> compacted;
TenantSketch() {
this.mutable = new ThetaSketch(4096); // or 512 if error tolerance allows
}
void add(Object value) { mutable.update(value); }
// Call once per hourly window
Sketch<Double> snapshotAndCompact() {
if (compacted == null) {
compacted = mutable.compact();
} else {
compacted = compacted.compact();
}
return compacted;
}
}
Use snapshotAndCompact() only when you need to serialize or cache the result. Keep mutable alive for the next window.
Ask yourself: What is the acceptable relative error for your cardinality estimates? If you need <2 % error, default to 4096 and compact only at serialization. If 4 % is tolerable, you may experiment with 512, but you should monitor the distribution of cardinalities and have an alert for spikes that exceed the nominal size.
update() and in compact() for a sample tenant.For low‑cardinality, high‑tenant workloads, keep sketches mutable during updates and compact only at serialization. Undersize nominalEntries only if your error tolerance justifies it and you can monitor for cardinality spikes. A lightweight wrapper that enforces this boundary keeps the code clean and the memory footprint minimal.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.