Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,6 @@ build:remote-cache --remote_cache_compression=true
# of the configs above without touching the committed file.
try-import %workspace%/.bazelrc.user

# Differential Fuzzing Configuration
build:fuzzer --config=clang
build:fuzzer --copt=-fsanitize=fuzzer-no-link,address
build:fuzzer --linkopt=-fsanitize=fuzzer-no-link,address
build:fuzzer --linkopt=-rtlib=compiler-rt
build:fuzzer --copt=-fno-omit-frame-pointer
build:fuzzer --copt=-O2

# BuildBuddy Remote Cache Configuration
build:buildbuddy --bes_results_url=https://app.buildbuddy.io/invocation/
build:buildbuddy --bes_backend=grpcs://remote.buildbuddy.io
Expand Down
2 changes: 1 addition & 1 deletion .github/Dockerfile.ci
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ RUN apt-get update && apt-get install -y \
ninja-build \
libc++-18-dev \
libc++abi-18-dev \
openjdk-21-jre-headless \
openjdk-21-jdk-headless \
&& rm -rf /var/lib/apt/lists/*

# clang-tidy-21 and clang-format-21 from apt.llvm.org. We keep the
Expand Down
13 changes: 11 additions & 2 deletions .github/scripts/run-bazel-in-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,17 @@ DOCKER_OPTS+=(--user "$(id -u):$(id -g)")
DOCKER_OPTS+=(-e HOME=/tmp)
DOCKER_OPTS+=(-e USER=zetasketch-cpp)
DOCKER_OPTS+=(-v "$WORKSPACE_DIR:/workspace")
mkdir -p "$WORKSPACE_DIR/.bazel-cache"
DOCKER_OPTS+=(-v "$WORKSPACE_DIR/.bazel-cache:/tmp/bazel-cache")
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
mkdir -p "$WORKSPACE_DIR/.bazel-cache"
DOCKER_OPTS+=(-v "$WORKSPACE_DIR/.bazel-cache:/tmp/bazel-cache")
else
mkdir -p "$HOME/.cache/bazel"
DOCKER_OPTS+=(-v "$HOME/.cache/bazel:/tmp/bazel-cache")
if [ -f "$WORKSPACE_DIR/.local_docker_opts.sh" ]; then
# shellcheck disable=SC1091
source "$WORKSPACE_DIR/.local_docker_opts.sh"
fi
fi
DOCKER_OPTS+=(-w /workspace)

BAZEL_OPTS=()
Expand Down
60 changes: 0 additions & 60 deletions .github/workflows/fuzzing.yml

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
bazel-*
scratch/
ai/

# Local developer overrides
.local_docker_opts.sh
11 changes: 11 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@ git_repository(
bazel_dep(name = "rules_cc", version = "0.0.9")
bazel_dep(name = "protobuf", version = "27.2")
bazel_dep(name = "googletest", version = "1.14.0")
bazel_dep(name = "rules_jvm_external", version = "6.1")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
artifacts = [
"com.google.zetasketch:zetasketch:0.1.0",
],
repositories = [
"https://repo1.maven.org/maven2",
],
)
use_repo(maven, "maven")
3,593 changes: 3,590 additions & 3 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

175 changes: 105 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,86 +3,123 @@
[![Build and Test][build-badge]][build-link]
[![Static Analysis][static-badge]][static-link]
[![Sanitizers][sanitizer-badge]][sanitizer-link]
[![Differential Fuzzing][fuzzing-badge]][fuzzing-link]
[![Code Coverage][coverage-badge]][coverage-link]
[![codecov][codecov-badge]][codecov-link]
[![arm64][arm64-badge]][arm64-link]

The purpose of this repository is to provide a standalone, modern, and rigorous
C++ implementation of Google's ZetaSketch format, which is the foundational
serialization schema used for HyperLogLog++ aggregate mutations within Cloud
Bigtable and BigQuery.
The purpose of this repository is to provide a standalone C++ implementation
of Google's ZetaSketch format. This format is the serialization schema used
for HyperLogLog++ aggregate mutations within Cloud Bigtable and BigQuery.

It is a well-established fact that Google relies extensively upon C++ internally
for these operations; however, the open-source community has hitherto been
provided only with a Java implementation. The absence of a native C++ library
has compelled developers into awkward compromises. This project remedies that
deficiency by providing an implementation that is entirely written in safe,
modern, and performant C++23.
Google utilizes C++ internally for these operations; however, the open-source
community is provided with a Java implementation. The absence of a native C++
library requires developers to utilize cross-language bindings. This project
provides an implementation written in C++23.

## Architectural Design and Precision
## Verification of Bit-Exactness of Output Compared to Google's Zetasketch

The primary objective of this project is to achieve precise byte-identical
serialization with the original Java implementation. Any discrepancy in the
serialized byte array, however minor, would render the output incompatible with
the existing Cloud Bigtable ecosystem. To ensure this strict conformity, our
architectural strategy avoids unnecessary reinvention.

Instead of attempting to reverse-engineer the serialization logic, we rely upon
direct integration. We have incorporated the exact Protocol Buffer definitions
(`hllplusplus.proto`) directly from Google's repository. We compile these
definitions using the `protoc` compiler, thereby ensuring that the structural
encoding remains accurate. Furthermore, the hashing algorithm is not merely an
imitation; we have copied the exact `farmhash.cc` and `farmhash.h` source files
from Google's FarmHash repository. This guarantees that the `Fingerprint64`
hashing operations produce results identical to the original implementation.

For the internal state machine, which governs the complex transitions between
sparse and dense representations, we have executed an idiomatic translation of
the `zetasketch-rs` Rust codebase. The Rust language, with its emphasis on
deterministic memory management and strict typological control, provided a
superior template for translation into modern C++ than the original,
inheritance-heavy Java architecture.
The implementation is verified against the upstream Java reference library
([google/zetasketch](https://github.com/google/zetasketch)) to ensure exact
byte-for-byte serialization compatibility. The testing apparatus utilizes the
following methodologies:

### 1. Corpus Size and Dimensionality
The validation regimen consists of two components: a static golden corpus
and a dynamic differential fuzzer.
* **Static Golden Corpus (`golden_corpus.tsv`)**: Contains exactly 61
pre-computed test vectors generated by the upstream Java ZetaSketch library.
These vectors scale from 0 elements up to 100,000 elements.
* **Dynamic Differential Fuzzer (`differential_fuzzer_test.cpp`)**: A native
C++ `cc_test` that dynamically generates randomized string data, processes
it simultaneously through both the C++ architecture and the Java ZetaSketch
reference, and asserts strict serialization parity. It executes 16 distinct
`CREATE` operations (yielding 16 sketches) and 12 distinct `MERGE`
operations (yielding an additional 64 intermediate sketches).

### 2. Configuration Diversity
The testing matrix iterates over four structural configurations, defined by
the Normal Precision (NP) and Sparse Precision (SP):
* `NP=15, SP=20`: High precision, large sparse mode capacity.
* `NP=10, SP=15`: Low precision, standard sparse mode capacity.
* `NP=15, SP=0`: Sparse mode explicitly disabled (forces immediate dense
allocation).
* `NP=10, SP=0`: Low precision, sparse mode disabled.

The element populations injected into these configurations are `[10, 100, 1000,
5000]`, with the golden corpus extending to `10000` and `100000`.

### 3. API Functions Exercised
The testing framework explicitly invokes and asserts the results of the
following C++ API methods against the Java equivalents:
* `HyperLogLogPlusPlus::Create(normal_precision, sparse_precision)`
* `HyperLogLogPlusPlus::Add(std::string_view)`
* `HyperLogLogPlusPlus::FromBytes(std::span<const uint8_t>)`
* `HyperLogLogPlusPlus::Merge(HyperLogLogPlusPlus&&)`
* `HyperLogLogPlusPlus::Serialize()`
* `HyperLogLogPlusPlus::Result()` (Cardinality estimation parity verified in
`golden_corpus_test.cpp`).

### 4. State Machine Cartesian Product Coverage
The `HyperLogLog++` architecture transitions between `SparseRepresentation` and
`NormalRepresentation`. The `Merge` function uses `std::visit` to handle the
Cartesian product of these representations. The testing regimen explicitly
targets all edges of this state machine:

* **Sparse + Sparse**: Merging multiple small sketches keeps the state
below the SP thresholds.
* **Sparse + Sparse -> Promotion to Normal**: Triggered by merging multiple
sparse sketches whose combined unique elements exceed the maximum sparse
threshold during the merge operation.
* **Normal + Normal**: Merging sketches initialized with large element counts
forces early promotion to the normal representation prior to the merge.
* **Sparse + Normal / Normal + Sparse**: Merging sketches sitting near the
capacity threshold. Across multiple sketches, variations in hash collisions
result in a mix of sparse and normal structures, forcing the cross-mode
variant visitation branches.
* **Edge Cases**: The static corpus explicitly verifies `POP0` (completely
empty initialization states) and configurations with `SP=0` (which entirely
bypass the sparse state machine and initialize as normal).

### 5. Endianness Validation
Dedicated unit tests confirm that the integrated FarmHash implementation
produces outputs identical to the Java reference across differing hardware
architectures, including x86_64 and ARM64.

## Performance and Code Hygiene
## Architectural Design and Precision

We have designed this library with a strict adherence to the performance
constraints required by RowKeyDB. Most notably, there is zero memory allocation
on the hot path. We avoid dynamic allocation during active sketch mutations,
relying instead upon pre-allocated, fixed-capacity arrays managed exclusively
through Resource Acquisition Is Initialization (RAII).
The primary objective of this project is to achieve byte-identical
serialization with the original Java implementation. Discrepancies in the
serialized byte array render the output incompatible with the existing Cloud
Bigtable ecosystem. To ensure this conformity, our architectural strategy
integrates existing reference logic.

We have incorporated the exact Protocol Buffer definitions (`hllplusplus.proto`)
directly from Google's repository. We compile these definitions using the
`protoc` compiler, ensuring that the structural encoding remains accurate.
Furthermore, we have copied the exact `farmhash.cc` and `farmhash.h` source
files from Google's FarmHash repository. This guarantees that the
`Fingerprint64` hashing operations produce results identical to the original
implementation.

For the internal state machine, which governs the transitions between sparse
and dense representations, we translated the logic from the `zetasketch-rs`
Rust codebase. The Rust implementation uses explicit memory management and
type systems that map directly to C++ constructs, whereas the original Java
architecture utilizes class inheritance.

The repository maintains a high standard of code hygiene. Our continuous
integration pipeline executes hermetic builds within Docker containers, ensuring
determinism across all environments. The code is subjected to rigorous cross-
translation-unit (CTU) static analysis using `clang-tidy`, and it is
continuously monitored by an array of runtime sanitizers (Address, Memory,
Thread, and Undefined Behavior).
## Performance and Code Hygiene

## Verification of Bit-Exactness of Output Compared to Google's Zetasketch
We have designed this library in adherence to the performance constraints
required by RowKeyDB. There is zero memory allocation on the hot path. We
avoid dynamic allocation during active sketch mutations, relying upon
pre-allocated, fixed-capacity arrays managed through Resource Acquisition Is
Initialization (RAII).

The implementation is verified against the upstream Java reference library
([google/zetasketch](https://github.com/google/zetasketch)) to ensure exact
byte-for-byte serialization compatibility. The testing apparatus utilizes the
following specific methodologies:

* **Endianness Validation:** Dedicated unit tests confirm that the integrated
FarmHash implementation produces outputs identical to the Java reference
across differing hardware architectures, including x86_64 and ARM64.
* **Golden File Verification:** Binary Protocol Buffer files generated by the
Java library serve as the baseline. The C++ output is subjected to an exact
byte-wise comparison against these files across defined operational states:
* Sparse structures prior to the transition threshold.
* The integer boundary marking the sparse-to-dense transition.
* Dense structures following the transition.
* Merged outputs of distinct combinations (Sparse-Sparse, Sparse-Dense,
and Dense-Dense).
* The correct enforcement of Protocol Buffers explicit-set semantics,
verifying that explicitly assigned zero values are not omitted.
* **Differential Fuzzing:** The testing matrix generates randomized string
inputs, processes them simultaneously through both the C++ architecture and
the Java ZetaSketch command-line interface, and asserts strict parity to
identify deviations during dynamic state transitions.
The repository continuous integration pipeline executes hermetic builds within
Docker containers, ensuring determinism across environments. The code is
subjected to cross-translation-unit (CTU) static analysis using `clang-tidy`,
and it is continuously monitored by an array of runtime sanitizers (Address,
Memory, Thread, and Undefined Behavior).

## License and Copyright

Expand All @@ -100,8 +137,6 @@ RowKeyDB (2026).
[static-link]: https://github.com/rowkeydb-com/zetasketch-cpp/actions/workflows/static_analysis.yml
[sanitizer-badge]: https://github.com/rowkeydb-com/zetasketch-cpp/actions/workflows/sanitizers.yml/badge.svg
[sanitizer-link]: https://github.com/rowkeydb-com/zetasketch-cpp/actions/workflows/sanitizers.yml
[fuzzing-badge]: https://github.com/rowkeydb-com/zetasketch-cpp/actions/workflows/fuzzing.yml/badge.svg
[fuzzing-link]: https://github.com/rowkeydb-com/zetasketch-cpp/actions/workflows/fuzzing.yml
[coverage-badge]: https://github.com/rowkeydb-com/zetasketch-cpp/actions/workflows/coverage.yml/badge.svg
[coverage-link]: https://github.com/rowkeydb-com/zetasketch-cpp/actions/workflows/coverage.yml
[codecov-badge]: https://codecov.io/gh/rowkeydb-com/zetasketch-cpp/graph/badge.svg
Expand Down
12 changes: 8 additions & 4 deletions src/hyperloglogplusplus.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
#include "zetasketch/hll/sparse_representation.h"
#include "zetasketch/hll/state.h"
#include "zetasketch/utils/buffer_traits.h"
#include "src/farmhash/fingerprint2011.h"

namespace zetasketch {

std::expected<HyperLogLogPlusPlus, utils::Error> HyperLogLogPlusPlus::Create(
int32_t normal_precision, int32_t sparse_precision) {
hll::State state;
state.type = HYPERLOGLOG_PLUS_UNIQUE;
state.encoding_version = 2;
state.value_type = hll::ValueType::kBytesOrUtf8String;
state.precision = normal_precision;
state.sparse_precision = sparse_precision;

Expand All @@ -40,10 +43,11 @@ std::expected<HyperLogLogPlusPlus, utils::Error> HyperLogLogPlusPlus::Create(

// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
void HyperLogLogPlusPlus::Add(std::string_view value) {
(void)value;
// Stub for now. Hashing is usually done via FarmHash.
// Not strictly needed to test serialization round-tripping if we use
// Add(int64_t).
const uint64_t hash = Fingerprint2011(value.data(), value.size());
auto res = AddHash(hash);
if (!res.has_value()) {
// Ignore error for now, as in original stub
}
}

// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
Expand Down
Loading
Loading