Skip to content
Closed
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ signals a backwards-compatible change.
worker's seed and the aggregator's from one seed. The unseeded constructors
draw from the thread generator. The seed is not serialized, and the ASAPv1
payload is unchanged.
- **ASAPv1 golden fixture for Coco** (`0c 00`): `asapv1_golden/coco_3x7.hex`,
a 3x7 bucket table set directly, checked by `tests/asapv1_golden.rs`.

### Changed

Expand All @@ -88,6 +90,14 @@ signals a backwards-compatible change.
as an empty window like any other, and that window takes a place in
`max_windows`.

### Removed

- **Coco's protobuf format.** `proto/cocosketch/cocosketch.proto` and the
generated `asap_sketchlib::proto::sketchlib::CocoSketchState` are gone, and
the `SketchEnvelope` oneof field `coco` (17) is reserved, so
`sketch_envelope::SketchState::Coco` is gone too. Coco serializes only as
ASAPv1 (`Coco::serialize_to_bytes` / `deserialize_from_bytes`).

### Fixed

- `KLLDynamic` panicked on every quantile query once a NaN had entered the
Expand Down
4 changes: 2 additions & 2 deletions docs/asapv1_wire_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -1341,14 +1341,14 @@ Good direction (more compact, higher fidelity, less Rust-internal duplication),
2. **Golden byte-vector fixtures** in one shared repo, [`sketchlib-golden-bytes`](https://github.com/ProjectASAP/sketchlib-golden-bytes), which each implementation mounts at `asapv1_golden/`; both languages decode and re-encode them byte-identically. These replace the `portable`-as-oracle round-trip test.
3. **This registry**, mirrored, never independently allocated.

Fixtures exist for six `kind_id`s — HLL's three estimators, Count-Min, Count Sketch and compact KLL — and `sketchlib-go` mirrors those. Every other kind here has a payload and **no fixture**, so this document is its only contract; `asapv1_golden/README.md` lists the gap. `portable` carries its own Go goldens.
`asapv1_golden/README.md`'s Coverage section lists which `kind_id`s have fixtures; for every other kind this document is the only contract. `portable` carries its own Go goldens.

**Hash profile on the Go side.**
Rust derives the hash spec from a generic `HashProfile` bound on the hasher type; Go has no generic hasher type, so there is nothing to derive from.
On the Go side the profile is simply **written into** the metadata on encode and **read from** it on decode.
Go MUST validate the profile it reads (same fail-closed intent as Rust): a sketch is only mergeable/queryable if its `hash_profile_id` + seeds match the profile Go is prepared to reproduce.

Sequencing: (2) covers six `kind_id`s, and the one `native bytes == portable bytes` test is HLL-only, since no other `portable` type emits ASAPv1 bytes.
Sequencing: (2) covers the `kind_id`s `asapv1_golden/README.md`'s Coverage section lists, and the one `native bytes == portable bytes` test is HLL-only, since no other `portable` type emits ASAPv1 bytes.
Retire `portable` once the fixtures cover the rest.

---
Expand Down
2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ MessagePack (`rmp-serde`) support. **serde support** means the type derives `Ser
| EHSketchList | Yes | Yes |
| ExponentialHistogram | No | Yes |

Protobuf (prost): `.proto` definitions exist for CountMin, Count, HLL, DDSketch, KLL, Elastic, Coco, Hydra, and UnivMon. Rust conversion code is in progress.
Protobuf (prost): `.proto` definitions exist for CountMin, Count, HLL, DDSketch, KLL, Elastic, Hydra, and UnivMon. Rust conversion code is in progress.

### API Stability

Expand Down
7 changes: 3 additions & 4 deletions docs/message_pack_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,9 @@ decode → re-encode them byte-identically. The `kind_id` registry is mirrored
verbatim with Go's `wire/asapmsgpack/magic_ids.go`, never independently
allocated.

That proof covers the six `kind_id`s a fixture exists for — HLL's three
estimators, Count-Min, Count Sketch and compact KLL. Every other implemented
kind has **no ASAPv1 golden and therefore no ASAPv1 drift guard**;
[its README](https://github.com/ProjectASAP/sketchlib-golden-bytes#coverage) lists what is covered.
That proof covers only the `kind_id`s that have a fixture;
[its README's Coverage section](https://github.com/ProjectASAP/sketchlib-golden-bytes#coverage)
lists which. A kind without one has **no ASAPv1 drift guard**.
The older `portable` path is guarded separately, by the `sketchlib-go` goldens
of its own listed above.

Expand Down
3 changes: 2 additions & 1 deletion docs/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -2123,6 +2123,7 @@ Test file: [`tests/asapv1_golden.rs`](../tests/asapv1_golden.rs)
| --- | --- | --- |
| `cms_f64_fast_2x3_matches_golden` | A known f64 fast-path Count-Min matrix serializes to its checked-in golden bytes. | Verifies `CountMin::<Vector2D<f64>, FastPath>::from_storage` over the `2x3` matrix `[[0.0, 1.5, 2.25], [3.75, 4.125, 5.0625]]` serializes to `asapv1_golden/cms_f64_fast_2x3.hex`, and that decoding that fixture recovers the six cells in row-major order and re-serializes to the same bytes. |
| `cms_i64_regular_2x3_matches_golden` | A known i64 regular-path Count-Min matrix serializes to its checked-in golden bytes. | Verifies `CountMin::<Vector2D<i64>, RegularPath>::from_storage` over the `2x3` matrix `[[0, 1, 127], [128, 300, 65536]]` serializes to `asapv1_golden/cms_i64_regular_2x3.hex`, and that decoding the fixture reports `rows()` `2`, `cols()` `3`, the six cells in row-major order, and a byte-identical re-serialization. |
| `coco_3x7_matches_golden` | A known Coco bucket table serializes to its checked-in golden bytes. | Sets 15 of the 21 buckets of `Coco::init_with_size(7, 3)` directly — each key in the column its row hashes it to folded `% 7`, a width that is not a power of two so a mask fold misplaces the keys; values spanning positive fixint through uint64 at both ends of each width; keys including `""`, a 31-byte and a 32-byte key and UTF-8 code points of two, three and four bytes; and `"zero"` holding `0` — verifies it serializes to `asapv1_golden/coco_3x7.hex`, and that decoding the fixture reports `d` `3`, `w` `7`, all 21 buckets in row-major order with the other six unoccupied, and a byte-identical re-serialization. |
| `cs_i32_regular_2x4_matches_golden` | The i32 counter type reaches the metadata, and an i32 golden is not an i64 sketch. | Verifies `Count::<Vector2D<i32>, RegularPath>::from_storage` over `[[0, 127, 128, 65536], [-1, -33, -32768, -2147483648]]` cast to `i32` serializes to `asapv1_golden/cs_i32_regular_2x4.hex`, that decoding it recovers the eight cells and re-serializes identically, that the i32 and i64 regular fixtures have equal length yet differ, and that the i32 fixture fails to decode as `Count<Vector2D<i64>, RegularPath>`. |
| `cs_i64_fast_2x4_matches_golden` | The fast-path mode reaches the bytes, so the same matrix encodes differently per path. | Verifies `Count::<Vector2D<i64>, FastPath>::from_storage` over `[[0, 127, 128, 65536], [-1, -33, -32768, -2147483648]]` serializes to `asapv1_golden/cs_i64_fast_2x4.hex`, that decoding it recovers the eight cells and re-serializes identically, and that the fixture differs from `cs_i64_regular_2x4.hex` built from the same matrix. |
| `cs_i64_regular_2x4_matches_golden` | A known signed Count-Sketch matrix serializes to its checked-in golden bytes. | Verifies `Count::<Vector2D<i64>, RegularPath>::from_storage` over `[[0, 127, 128, 65536], [-1, -33, -32768, -2147483648]]` — sweeping the msgpack integer widths in both directions — serializes to `asapv1_golden/cs_i64_regular_2x4.hex`, and that decoding the fixture reports `rows()` `2`, `cols()` `4`, the eight cells, and a byte-identical re-serialization. |
Expand Down Expand Up @@ -2202,7 +2203,7 @@ Test file: [`tests/xtest_consumer.rs`](../tests/xtest_consumer.rs)

| test_name | test_description | what_is_tested |
| --- | --- | --- |
| `cross_language_proto` | Protobuf envelopes read from the cross-language fixture directory answer their point queries in band. | Asserts nothing unless fixtures are available: with no `XTEST_DIR` and no discoverable producer directory holding `tests/cross_language/xtest_producer_test.go`, it prints a skip line and returns. With fixtures it decodes nine `SketchEnvelope` files and records a pass or failure for each — `countmin.pb` min-frequency for `item:42` at least `101`, `kll.pb` p50 within 5% of `5000` and p99 within 5% of `9900`, `ddsketch.pb` the same two quantiles within 2%, `hll.pb` an ErtlMLE cardinality inside `[40_000, 65_000]`, `countsketch.pb` `cs:hot` at least `200`, `coco.pb` `coco:hot` at least `500`, `elastic.pb` `elephant` at least `900`, `univmon.pb` a cardinality inside `[1_000, 15_000]`, `hydra.pb` `hydra:42` at least `51` — using the hash seeds `0xcafe3553`, `0x6a09e667` and `0xbb67ae85`, and, when `countmin_sampled.pb` and `hll_sampled.pb` exist, requires `effective_sample_p` of `0.1` and a `rescale_count` by `1/p` within 5% of `100_000` and 6% of `200_000`. A single `assert!` at the end fails if any check failed; a missing or mistyped envelope panics on the spot. |
| `cross_language_proto` | Protobuf envelopes read from the cross-language fixture directory answer their point queries in band. | Asserts nothing unless fixtures are available: with no `XTEST_DIR` and no discoverable producer directory holding `tests/cross_language/xtest_producer_test.go`, it prints a skip line and returns. With fixtures it decodes eight `SketchEnvelope` files and records a pass or failure for each — `countmin.pb` min-frequency for `item:42` at least `101`, `kll.pb` p50 within 5% of `5000` and p99 within 5% of `9900`, `ddsketch.pb` the same two quantiles within 2%, `hll.pb` an ErtlMLE cardinality inside `[40_000, 65_000]`, `countsketch.pb` `cs:hot` at least `200`, `elastic.pb` `elephant` at least `900`, `univmon.pb` a cardinality inside `[1_000, 15_000]`, `hydra.pb` `hydra:42` at least `51` — using the hash seeds `0xcafe3553`, `0x6a09e667` and `0xbb67ae85`, and, when `countmin_sampled.pb` and `hll_sampled.pb` exist, requires `effective_sample_p` of `0.1` and a `rescale_count` by `1/p` within 5% of `100_000` and 6% of `200_000`. A single `assert!` at the end fails if any check failed; a missing or mistyped envelope panics on the spot. |

### HLL Custom Precision

Expand Down
35 changes: 0 additions & 35 deletions proto/cocosketch/cocosketch.proto

This file was deleted.

5 changes: 3 additions & 2 deletions proto/sketchlib.proto
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import "kll/kll.proto";
import "ddsketch/ddsketch.proto";
import "univmon/univmon.proto";
import "hydra/hydra.proto";
import "cocosketch/cocosketch.proto";
import "elasticsketch/elasticsketch.proto";

option go_package = "github.com/ProjectASAP/sketchlib-go/proto/sketch_envelope;sketch_envelopepb";
Expand Down Expand Up @@ -73,10 +72,12 @@ message SketchEnvelope {
DDSketchState ddsketch = 14;
UnivMonState univmon = 15;
HydraState hydra = 16;
CocoSketchState coco = 17;
ElasticState elastic = 18;
}

reserved 17;
reserved "coco";

reserved 19 to 31; // Reserved for future sketch types.

// Geometric / NitroSketch sampling probability for the producing sketch.
Expand Down
27 changes: 1 addition & 26 deletions src/proto/generated/sketchlib.v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,29 +800,6 @@ impl HydraCounterType {
}
}
}
/// CocoSketchState is the portable state of a CocoSketch flow-size estimator.
///
/// The bucket table is stored flat in row-major order (d rows × width cols).
/// Element [r][c] is at index r * width + c.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CocoSketchState {
/// Number of hash rows.
#[prost(uint32, tag = "1")]
pub d: u32,
/// Number of buckets per row.
#[prost(uint32, tag = "2")]
pub width: u32,
/// Full hash stored in each bucket, length = d * width.
/// Zero means the bucket is empty (has_keys\[i\] == false).
#[prost(uint64, repeated, tag = "3")]
pub hashes: ::prost::alloc::vec::Vec<u64>,
/// Cumulative count value per bucket, length = d * width.
#[prost(uint64, repeated, tag = "4")]
pub vals: ::prost::alloc::vec::Vec<u64>,
/// Whether each bucket holds a valid key entry, length = d * width.
#[prost(bool, repeated, tag = "5")]
pub has_keys: ::prost::alloc::vec::Vec<bool>,
}
/// ElasticState is the portable state of an Elastic Sketch.
///
/// The Elastic Sketch has a heavy part (one bucket per index) and a light
Expand Down Expand Up @@ -892,7 +869,7 @@ pub struct SketchEnvelope {
/// The sketch payload. Exactly one field must be set.
#[prost(
oneof = "sketch_envelope::SketchState",
tags = "10, 11, 12, 13, 14, 15, 16, 17, 18"
tags = "10, 11, 12, 13, 14, 15, 16, 18"
)]
pub sketch_state: ::core::option::Option<sketch_envelope::SketchState>,
}
Expand All @@ -915,8 +892,6 @@ pub mod sketch_envelope {
Univmon(super::UnivMonState),
#[prost(message, tag = "16")]
Hydra(super::HydraState),
#[prost(message, tag = "17")]
Coco(super::CocoSketchState),
#[prost(message, tag = "18")]
Elastic(super::ElasticState),
}
Expand Down
65 changes: 63 additions & 2 deletions tests/asapv1_golden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
//! See `asapv1_golden/README.md`.

use asap_sketchlib::{
Classic, Count, CountMin, ErtlMLE, FastPath, HllSketch, HllVariant, HyperLogLogHIPP12,
HyperLogLogP12, KLL, MessagePackCodec, RegularPath, Vector2D,
Classic, Coco, CocoBucket, Count, CountMin, ErtlMLE, FastPath, HllSketch, HllVariant,
HyperLogLogHIPP12, HyperLogLogP12, KLL, MessagePackCodec, RegularPath, Vector2D,
};

fn decode_hex(s: &str) -> Vec<u8> {
Expand All @@ -32,6 +32,7 @@ const GOLDEN_CS_FAST: &str = include_str!("../asapv1_golden/cs_i64_fast_2x4.hex"
const GOLDEN_CS_I32: &str = include_str!("../asapv1_golden/cs_i32_regular_2x4.hex");
const GOLDEN_KLL_F64: &str = include_str!("../asapv1_golden/kll_f64_k200.hex");
const GOLDEN_KLL_I64: &str = include_str!("../asapv1_golden/kll_i64_k200.hex");
const GOLDEN_COCO: &str = include_str!("../asapv1_golden/coco_3x7.hex");

/// The known P12 register pattern shared by all three HLL fixtures.
fn p12_registers() -> Vec<u8> {
Expand Down Expand Up @@ -287,3 +288,63 @@ fn kll_i64_k200_matches_golden() {
assert_eq!(decoded.quantile(0.0), 1.0);
assert_eq!(decoded.quantile(1.0), 50.0);
}

// ---------------------------------------------------------------------------
// Coco: build a known bucket table -> serialize == golden, round-trips.
// ---------------------------------------------------------------------------

/// The occupied buckets of the 3x7 Coco fixture as `(row, col, key, value)`;
/// every other bucket is unoccupied. Each key sits in the column its row hashes
/// it to, folded `% 7`, which the codec checks on both sides.
const COCO_CELLS: [(usize, usize, &str, u64); 15] = [
(0, 0, "uint32-min", 65536),
(0, 1, "uint16-max", 65535),
(0, 2, "fixint-max", 127),
(0, 3, "", 1),
(0, 4, "uint8-max", 255),
(0, 5, "emoji-😀", 5),
(1, 0, "uint16-min", 256),
(1, 1, "str8-min-32-bytes-0123456789abcd", 3),
(1, 2, "uint8-min", 128),
(1, 3, "clé-ünïcode-流量", 4),
(1, 4, "fixstr-max-31-bytes-0123456789a", 2),
(1, 5, "uint64-min", 4294967296),
(1, 6, "zero", 0),
(2, 1, "uint64-max", u64::MAX),
(2, 5, "uint32-max", 4294967295),
];

/// The fixture's 21 buckets in row-major order.
fn coco_buckets() -> Vec<(Option<String>, u64)> {
let mut buckets = vec![(None, 0); 21];
for (r, c, key, val) in COCO_CELLS {
buckets[r * 7 + c] = (Some(key.to_string()), val);
}
buckets
}

#[test]
fn coco_3x7_matches_golden() {
let want = decode_hex(GOLDEN_COCO);

let mut sketch: Coco = Coco::init_with_size(7, 3);
for (r, c, key, val) in COCO_CELLS {
sketch.table[r][c] = CocoBucket {
full_key: Some(key.to_string()),
val,
};
}
let got = sketch.serialize_to_bytes().expect("serialize");
assert_eq!(got, want, "Coco bytes diverge from golden");

let decoded: Coco = Coco::deserialize_from_bytes(&want).expect("decode");
assert_eq!((decoded.d, decoded.w), (3, 7));
let cells: Vec<(Option<String>, u64)> = decoded
.table
.as_slice()
.iter()
.map(|b| (b.full_key.clone(), b.val))
.collect();
assert_eq!(cells, coco_buckets());
assert_eq!(decoded.serialize_to_bytes().expect("re-serialize"), want);
}
Loading
Loading