From 1963d150adbc906ad246f37cc2764965220414db Mon Sep 17 00:00:00 2001 From: Lee Rhodes Date: Tue, 10 Nov 2026 16:45:34 -0800 Subject: [PATCH 1/4] Count-Min: write serialization version 2, keep reading version 1 New sketches use the cross-language Count-Min format, version 2, defined in the TCK (docs/count-min-format.md): - row seeds: rowSeed[i] = XXH64(LE64(i), seed) - items hashed with XxHash64: longs as 8 little-endian bytes, strings as UTF-8, byte arrays as given - bucket = (hash >>> 1) % numBuckets - serial version byte 2; the byte layout is unchanged A sketch read from a version 1 image keeps the version 1 hashing (Random seeds, MurmurHash3, floorMod) and is written back as version 1. Merging version 1 with version 2 throws, and other serial versions are rejected. A null byte array is now ignored, like an empty one. Tests check every test vector in the spec (XXH64, row seeds, buckets and both images), and the version 1 path against an independent computation of the legacy hashing. Co-Authored-By: Claude Opus 5.5 --- .../datasketches/count/CountMinSketch.java | 112 ++++++++--- .../count/CountMinSketchTest.java | 187 ++++++++++++++++++ 2 files changed, 277 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/apache/datasketches/count/CountMinSketch.java b/src/main/java/org/apache/datasketches/count/CountMinSketch.java index 5354eb702..f5840673e 100644 --- a/src/main/java/org/apache/datasketches/count/CountMinSketch.java +++ b/src/main/java/org/apache/datasketches/count/CountMinSketch.java @@ -21,6 +21,8 @@ import static java.lang.foreign.ValueLayout.JAVA_BYTE; import static java.lang.foreign.ValueLayout.JAVA_LONG_UNALIGNED; +import static org.apache.datasketches.hash.XxHash64.hash; +import static org.apache.datasketches.hash.XxHash64.hashBytes; import java.lang.foreign.MemorySegment; import java.nio.charset.StandardCharsets; @@ -42,8 +44,19 @@ * providing approximate counts with configurable error bounds. * * Reference: http://dimacs.rutgers.edu/~graham/pubs/papers/cm-full.pdf + * + *

New sketches are written in serialization version 2, the cross-language format defined in + * + * the DataSketches TCK. Version 2 hashes items with XxHash64, so its images give the same results + * in every language. A sketch read from a version 1 image keeps the version 1 hashing, which used + * MurmurHash3 and seeds from {@link Random}, and is written back as version 1. + * Version 1 and version 2 sketches can't be merged.

*/ public class CountMinSketch { + static final byte SERIAL_VERSION_1 = 1; + static final byte SERIAL_VERSION_2 = 2; + + private final byte serialVersion_; private final byte numHashes_; private final int numBuckets_; private final long seed_; @@ -65,13 +78,25 @@ int mask() { /** * Creates a CountMin sketch with given number of hash functions and buckets, - * and a user-specified seed. + * and a user-specified seed. The sketch uses serialization version 2. * * @param numHashes The number of hash functions to apply to items * @param numBuckets Array size for each of the hashing function * @param seed The base hash seed */ CountMinSketch(final byte numHashes, final int numBuckets, final long seed) { + this(numHashes, numBuckets, seed, SERIAL_VERSION_2); + } + + /** + * Creates a CountMin sketch that hashes items as the given serialization version does. + * + * @param numHashes The number of hash functions to apply to items + * @param numBuckets Array size for each of the hashing function + * @param seed The base hash seed + * @param serialVersion 1 for the legacy hashing, 2 for the cross-language hashing + */ + private CountMinSketch(final byte numHashes, final int numBuckets, final long seed, final byte serialVersion) { // Validate numHashes if (numHashes <= 0) { throw new SketchesArgumentException("Number of hash functions must be positive, got: " + numHashes); @@ -102,6 +127,7 @@ int mask() { + "Consider reducing numHashes or numBuckets."); } + serialVersion_ = serialVersion; numHashes_ = numHashes; numBuckets_ = numBuckets; seed_ = seed; @@ -109,9 +135,16 @@ int mask() { sketchArray_ = new long[(int) totalSize]; totalWeight_ = 0; - final Random rand = new Random(seed); - for (int i = 0; i < numHashes; i++) { - hashSeeds_[i] = rand.nextLong(); + if (serialVersion == SERIAL_VERSION_1) { + final Random rand = new Random(seed); + for (int i = 0; i < numHashes; i++) { + hashSeeds_[i] = rand.nextLong(); + } + } else { + // rowSeed[i] = XXH64(LE64(i), seed): XxHash64.hash(long, seed) hashes the 8 little-endian bytes of i + for (int i = 0; i < numHashes; i++) { + hashSeeds_[i] = hash(i, seed); + } } } @@ -128,13 +161,42 @@ private long[] getHashes(final byte[] item) { final long[] updateLocations = new long[numHashes_]; for (int i = 0; i < numHashes_; i++) { - final long[] index = MurmurHash3.hash(item, hashSeeds_[i]); - updateLocations[i] = (i * (long)numBuckets_) + Math.floorMod(index[0], numBuckets_); + updateLocations[i] = (i * (long) numBuckets_) + bucket(item, hashSeeds_[i]); } return updateLocations; } + private long[] getHashes(final long item) { + if (serialVersion_ == SERIAL_VERSION_1) { + return getHashes(longToBytes(item)); + } + final long[] updateLocations = new long[numHashes_]; + for (int i = 0; i < numHashes_; i++) { + // XxHash64.hash(long, seed) hashes the 8 little-endian bytes of the item, as version 2 requires + final long h = hash(item, hashSeeds_[i]); + updateLocations[i] = (i * (long) numBuckets_) + ((h >>> 1) % numBuckets_); + } + return updateLocations; + } + + private long bucket(final byte[] item, final long rowSeed) { + if (serialVersion_ == SERIAL_VERSION_1) { + return Math.floorMod(MurmurHash3.hash(item, rowSeed)[0], numBuckets_); + } + final long h = hashBytes(item, 0, item.length, rowSeed); + return (h >>> 1) % numBuckets_; + } + + /** + * Returns the serialization version this sketch hashes and serializes with: + * 2 for new sketches, or 1 for a sketch read from a version 1 image. + * @return the serialization version + */ + byte getSerialVersion() { + return serialVersion_; + } + /** * Checks if the CountMinSketch has processed any items. * @return True if the sketch is empty, otherwise false. @@ -214,7 +276,7 @@ public static int suggestNumBuckets(final double relativeError) { * @param weight The weight of the item. */ public void update(final long item, final long weight) { - update(longToBytes(item), weight); + updateLocations(getHashes(item), weight); } /** @@ -236,12 +298,14 @@ public void update(final String item, final long weight) { * @param weight The weight of the item. */ public void update(final byte[] item, final long weight) { - if (item.length == 0) { + if ((item == null) || (item.length == 0)) { return; } + updateLocations(getHashes(item), weight); + } + private void updateLocations(final long[] hashLocations, final long weight) { totalWeight_ += weight > 0 ? weight : -weight; - final long[] hashLocations = getHashes(item); for (final long h : hashLocations) { sketchArray_[(int) h] += weight; } @@ -253,7 +317,7 @@ public void update(final byte[] item, final long weight) { * @return Estimated frequency. */ public long getEstimate(final long item) { - return getEstimate(longToBytes(item)); + return estimateAt(getHashes(item)); } /** @@ -276,11 +340,13 @@ public long getEstimate(final String item) { * @return Estimated frequency. */ public long getEstimate(final byte[] item) { - if (item.length == 0) { + if ((item == null) || (item.length == 0)) { return 0; } + return estimateAt(getHashes(item)); + } - final long[] hashLocations = getHashes(item); + private long estimateAt(final long[] hashLocations) { long res = sketchArray_[(int) hashLocations[0]]; // Start from index 1 to avoid processing first element twice for (int i = 1; i < hashLocations.length; i++) { @@ -296,7 +362,7 @@ public long getEstimate(final byte[] item) { * @return Upper bound of estimated frequency. */ public long getUpperBound(final long item) { - return getUpperBound(longToBytes(item)); + return getEstimate(item) + (long) (getRelativeError() * getTotalWeight_()); } /** @@ -319,7 +385,7 @@ public long getUpperBound(final String item) { * @return Upper bound of estimated frequency. */ public long getUpperBound(final byte[] item) { - if (item.length == 0) { + if ((item == null) || (item.length == 0)) { return 0; } @@ -332,7 +398,7 @@ public long getUpperBound(final byte[] item) { * @return Lower bound of estimated frequency. */ public long getLowerBound(final long item) { - return getLowerBound(longToBytes(item)); + return getEstimate(item); } /** @@ -373,6 +439,10 @@ public void merge(final CountMinSketch other) { if (!acceptableConfig) { throw new SketchesException("Incompatible sketch configuration."); } + if (serialVersion_ != other.serialVersion_) { + throw new SketchesException("Cannot merge sketches with different serialization versions: " + + serialVersion_ + " and " + other.serialVersion_); + } for (int i = 0; i < sketchArray_.length; i++) { sketchArray_[i] += other.sketchArray_[i]; @@ -404,8 +474,7 @@ public byte[] toByteArray() { // Long 0 final int preambleLongs = Family.COUNTMIN.getMinPreLongs(); posSeg.setByte((byte) preambleLongs); - final int serialVersion = 1; - posSeg.setByte((byte) serialVersion); + posSeg.setByte(serialVersion_); final int familyId = Family.COUNTMIN.getID(); posSeg.setByte((byte) familyId); final int flagsByte = isEmpty() ? Flag.IS_EMPTY.mask() : 0; @@ -457,10 +526,9 @@ public static CountMinSketch deserialize(final byte[] b, final long seed) { throw new SketchesArgumentException("Preamble longs mismatch: expected " + expectedPreambleLongs + ", actual " + preambleLongs); } - final int expectedSerialVersion = 1; - if (serialVersion != expectedSerialVersion) { - throw new SketchesArgumentException("Serial version mismatch: expected " + expectedSerialVersion - + ", actual " + serialVersion); + if ((serialVersion != SERIAL_VERSION_1) && (serialVersion != SERIAL_VERSION_2)) { + throw new SketchesArgumentException("Serial version mismatch: expected " + SERIAL_VERSION_1 + + " or " + SERIAL_VERSION_2 + ", actual " + serialVersion); } final int expectedFamilyId = Family.COUNTMIN.getID(); if (familyId != expectedFamilyId) { @@ -483,7 +551,7 @@ public static CountMinSketch deserialize(final byte[] b, final long seed) { final long arrayBytes = (long) numHashes * numBuckets * Long.BYTES; Util.checkBounds(preambleBytes, Long.BYTES + Math.max(arrayBytes, 0), b.length); } - final CountMinSketch cms = new CountMinSketch(numHashes, numBuckets, seed); + final CountMinSketch cms = new CountMinSketch(numHashes, numBuckets, seed, serialVersion); if (empty) { return cms; } diff --git a/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java b/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java index 59590d9a6..e5adb8ddd 100644 --- a/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java +++ b/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java @@ -24,10 +24,16 @@ import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; +import java.util.Random; + import org.apache.datasketches.common.AllocationTracker; import org.apache.datasketches.common.ByteArrayUtil; import org.apache.datasketches.common.SketchesArgumentException; import org.apache.datasketches.common.SketchesException; +import org.apache.datasketches.hash.MurmurHash3; +import org.apache.datasketches.hash.XxHash64; import org.testng.annotations.Test; public class CountMinSketchTest { @@ -271,4 +277,185 @@ public void truncatedImageThrows() { assertThrows(SketchesArgumentException.class, () -> CountMinSketch.deserialize(shortBytes, seed)); } + // ---- Serialization version 2: test vectors from the TCK spec docs/count-min-format.md, section 7 ---- + + private static final long SPEC_SEED = 9001L; + + // 7.1 XXH64 and 7.2 row seeds + @Test + public void v2XxHashAndRowSeedVectors() { + final byte[] empty = new byte[0]; + assertEquals(XxHash64.hashBytes(empty, 0, 0, 0), 0xEF46DB3751D8E999L); + assertEquals(XxHash64.hashBytes(utf8("a"), 0, 1, 0), 0xD24EC4F1A98C6E5BL); + assertEquals(XxHash64.hashBytes(utf8("abc"), 0, 3, 0), 0x44BC2CF5AD770999L); + + final long[] seeds9001 = { 0xD71D64972584A288L, 0xEFE8454A6499F311L, 0x23819BE21B1B1D9EL }; + final long[] seeds42 = { 0xB71B47EBDA15746CL, 0x9ED50FD59358D232L, 0xD19FE30002B9A3CDL }; + for (int i = 0; i < 3; i++) { + assertEquals(XxHash64.hash(i, 9001L), seeds9001[i]); + assertEquals(XxHash64.hash(i, 42L), seeds42[i]); + } + } + + // 7.3 item buckets, observed through the counters a single update sets + @Test + public void v2BucketVectors() { + assertBuckets(SPEC_SEED, 1L, 185, 898, 4); + assertBuckets(SPEC_SEED, 9L, 490, 217, 440); + assertBuckets(SPEC_SEED, -1L, 256, 641, 308); + assertBuckets(SPEC_SEED, Long.MIN_VALUE, 951, 0, 298); + assertBuckets(SPEC_SEED, "apple", 130, 747, 144); + assertBuckets(SPEC_SEED, "\u00e9t\u00e9", 806, 491, 163); + assertBuckets(42L, 1L, 468, 459, 696); + assertBuckets(42L, 9L, 841, 428, 322); + } + + // 7.4 images + @Test + public void v2ImageVectors() { + final CountMinSketch empty = new CountMinSketch((byte) 2, 9, SPEC_SEED); + assertEquals(empty.getSerialVersion(), CountMinSketch.SERIAL_VERSION_2); + assertEquals(empty.toByteArray(), hex("02021201000000000900000002cc9300")); + + final CountMinSketch cms = new CountMinSketch((byte) 2, 9, SPEC_SEED); + cms.update(1L, 10); + cms.update(9L, 810); + cms.update("apple", 5); + final byte[] expected = new byte[168]; + System.arraycopy(hex("02021200000000000900000002cc9300"), 0, expected, 0, 16); + ByteArrayUtil.putLongLE(expected, 16, 825); + ByteArrayUtil.putLongLE(expected, 24 + (8 * 0), 810); // row 0, bucket 0 + ByteArrayUtil.putLongLE(expected, 24 + (8 * 4), 5); // row 0, bucket 4 + ByteArrayUtil.putLongLE(expected, 24 + (8 * 6), 10); // row 0, bucket 6 + ByteArrayUtil.putLongLE(expected, 24 + (8 * 10), 5); // row 1, bucket 1 + ByteArrayUtil.putLongLE(expected, 24 + (8 * 11), 810); // row 1, bucket 2 + ByteArrayUtil.putLongLE(expected, 24 + (8 * 16), 10); // row 1, bucket 7 + assertEquals(cms.toByteArray(), expected); + + final CountMinSketch read = CountMinSketch.deserialize(expected, SPEC_SEED); + assertEquals(read.getSerialVersion(), CountMinSketch.SERIAL_VERSION_2); + assertEquals(read.getEstimate(1L), 10); + assertEquals(read.getEstimate(9L), 810); + assertEquals(read.getEstimate("apple"), 5); + assertEquals(read.toByteArray(), expected); + } + + // The long overloads hash the 8 little-endian bytes of the item, like the byte[] overloads + @Test + public void v2LongMatchesLittleEndianBytes() { + final CountMinSketch a = new CountMinSketch((byte) 4, 101, SPEC_SEED); + final CountMinSketch b = new CountMinSketch((byte) 4, 101, SPEC_SEED); + final long[] items = { 0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, 0x0123456789ABCDEFL }; + for (final long item : items) { + final byte[] le = new byte[Long.BYTES]; + ByteArrayUtil.putLongLE(le, 0, item); + a.update(item, 3); + b.update(le, 3); + } + assertEquals(a.toByteArray(), b.toByteArray()); + } + + // Version 1 images keep the legacy hashing and are written back as version 1 + @Test + public void v1ImageKeepsLegacyHashing() { + final long seed = 1234L; + final byte numHashes = 3; + final int numBuckets = 17; + final byte[] emptyV1 = new CountMinSketch(numHashes, numBuckets, seed).toByteArray(); + emptyV1[1] = CountMinSketch.SERIAL_VERSION_1; + + final CountMinSketch v1 = CountMinSketch.deserialize(emptyV1, seed); + assertEquals(v1.getSerialVersion(), CountMinSketch.SERIAL_VERSION_1); + v1.update(7L, 11); + v1.update("legacy", 13); + + final byte[] image = v1.toByteArray(); + assertEquals(image[1], CountMinSketch.SERIAL_VERSION_1); + + // Independent version 1 computation: seeds from Random(seed), MurmurHash3, floorMod, native byte order + final Random rand = new Random(seed); + final long[] rowSeeds = new long[numHashes]; + for (int i = 0; i < numHashes; i++) { rowSeeds[i] = rand.nextLong(); } + final byte[] item7 = java.nio.ByteBuffer.allocate(8).order(java.nio.ByteOrder.nativeOrder()).putLong(7L).array(); + final long[] expected = new long[numHashes * numBuckets]; + for (int i = 0; i < numHashes; i++) { + expected[(i * numBuckets) + Math.floorMod(MurmurHash3.hash(item7, rowSeeds[i])[0], numBuckets)] += 11; + expected[(i * numBuckets) + Math.floorMod(MurmurHash3.hash(utf8("legacy"), rowSeeds[i])[0], numBuckets)] += 13; + } + for (int k = 0; k < expected.length; k++) { + assertEquals(ByteArrayUtil.getLongLE(image, 24 + (8 * k)), expected[k], "counter " + k); + } + + final CountMinSketch reread = CountMinSketch.deserialize(image, seed); + assertEquals(reread.getSerialVersion(), CountMinSketch.SERIAL_VERSION_1); + assertEquals(reread.getEstimate(7L), v1.getEstimate(7L)); + assertEquals(reread.getEstimate("legacy"), v1.getEstimate("legacy")); + } + + @Test + public void mergeAcrossVersionsThrows() { + final long seed = 1234L; + final byte[] emptyV1 = new CountMinSketch((byte) 3, 17, seed).toByteArray(); + emptyV1[1] = CountMinSketch.SERIAL_VERSION_1; + final CountMinSketch v1 = CountMinSketch.deserialize(emptyV1, seed); + final CountMinSketch v2 = new CountMinSketch((byte) 3, 17, seed); + assertThrows(SketchesException.class, () -> v1.merge(v2)); + assertThrows(SketchesException.class, () -> v2.merge(v1)); + } + + @Test + public void unknownSerialVersionThrows() { + final long seed = 1234L; + final byte[] image = new CountMinSketch((byte) 3, 17, seed).toByteArray(); + image[1] = 3; + assertThrows(SketchesArgumentException.class, () -> CountMinSketch.deserialize(image, seed)); + image[1] = 0; + assertThrows(SketchesArgumentException.class, () -> CountMinSketch.deserialize(image, seed)); + } + + @Test + public void nullAndEmptyItemsAreIgnored() { + final CountMinSketch cms = new CountMinSketch((byte) 3, 17, SPEC_SEED); + cms.update((byte[]) null, 5); + cms.update(new byte[0], 5); + cms.update((String) null, 5); + cms.update("", 5); + assertTrue(cms.isEmpty()); + assertEquals(cms.getEstimate((byte[]) null), 0); + assertEquals(cms.getEstimate(new byte[0]), 0); + assertEquals(cms.getEstimate((String) null), 0); + assertEquals(cms.getEstimate(""), 0); + assertEquals(cms.getUpperBound((byte[]) null), 0); + } + + private static void assertBuckets(final long seed, final long item, final int... buckets) { + final CountMinSketch cms = new CountMinSketch((byte) 3, 1000, seed); + cms.update(item, 1); + assertCounters(cms, buckets); + } + + private static void assertBuckets(final long seed, final String item, final int... buckets) { + final CountMinSketch cms = new CountMinSketch((byte) 3, 1000, seed); + cms.update(item, 1); + assertCounters(cms, buckets); + } + + private static void assertCounters(final CountMinSketch cms, final int... buckets) { + final byte[] image = cms.toByteArray(); + for (int row = 0; row < buckets.length; row++) { + for (int b = 0; b < 1000; b++) { + final long counter = ByteArrayUtil.getLongLE(image, 24 + (8 * ((row * 1000) + b))); + assertEquals(counter, b == buckets[row] ? 1 : 0, "row " + row + ", bucket " + b); + } + } + } + + private static byte[] utf8(final String s) { + return s.getBytes(StandardCharsets.UTF_8); + } + + private static byte[] hex(final String s) { + return HexFormat.of().parseHex(s); + } + } From 93908aed8ba3a1171b2ac8b211377a00e7e37b26 Mon Sep 17 00:00:00 2001 From: Lee Rhodes Date: Tue, 10 Nov 2026 16:56:36 -0800 Subject: [PATCH 2/4] Count-Min: public constructors and version 2 cross-language files - CountMinSketch had no public way to create a sketch; only deserialize was public. Add public constructors (numHashes, numBuckets) with the default seed 9001, and (numHashes, numBuckets, seed). - Add CountMinSketchCrossLanguageTest, which generates and checks the version 2 CL files defined in section 8 of the TCK spec: count_min_v2__.sk, for cases n0, n1, n10, n100, n1000, n10000 and seed42_n1000. Files from another language must also be byte-identical to Java's. The C++ version 1 files are checked to load as version 1 sketches. Co-Authored-By: Claude Opus 5.5 --- .../datasketches/count/CountMinSketch.java | 33 ++++- .../CountMinSketchCrossLanguageTest.java | 131 ++++++++++++++++++ .../count/CountMinSketchTest.java | 16 +++ 3 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 src/test/java/org/apache/datasketches/count/CountMinSketchCrossLanguageTest.java diff --git a/src/main/java/org/apache/datasketches/count/CountMinSketch.java b/src/main/java/org/apache/datasketches/count/CountMinSketch.java index f5840673e..c576b0b89 100644 --- a/src/main/java/org/apache/datasketches/count/CountMinSketch.java +++ b/src/main/java/org/apache/datasketches/count/CountMinSketch.java @@ -77,14 +77,37 @@ int mask() { } /** - * Creates a CountMin sketch with given number of hash functions and buckets, - * and a user-specified seed. The sketch uses serialization version 2. + * Creates an empty CountMin sketch with the given number of hash functions and buckets, + * and the default seed, {@link Util#DEFAULT_UPDATE_SEED}. + * The sketch uses serialization version 2. * - * @param numHashes The number of hash functions to apply to items - * @param numBuckets Array size for each of the hashing function + *

{@link #suggestNumHashes(double)} and {@link #suggestNumBuckets(double)} give suitable values + * for a desired confidence and relative error.

+ * + * @param numHashes The number of hash functions (rows), from 1 to 127 + * @param numBuckets The number of buckets per hash function, at least 3. + * The product of numHashes and numBuckets must be less than 230. + * @throws SketchesArgumentException if a parameter is out of range + */ + public CountMinSketch(final byte numHashes, final int numBuckets) { + this(numHashes, numBuckets, Util.DEFAULT_UPDATE_SEED); + } + + /** + * Creates an empty CountMin sketch with the given number of hash functions and buckets, + * and the given seed. The sketch uses serialization version 2. + * + *

{@link #suggestNumHashes(double)} and {@link #suggestNumBuckets(double)} give suitable values + * for a desired confidence and relative error. Sketches can be merged only if they use the same + * seed, and an image must be read back with the seed it was created with.

+ * + * @param numHashes The number of hash functions (rows), from 1 to 127 + * @param numBuckets The number of buckets per hash function, at least 3. + * The product of numHashes and numBuckets must be less than 230. * @param seed The base hash seed + * @throws SketchesArgumentException if a parameter is out of range */ - CountMinSketch(final byte numHashes, final int numBuckets, final long seed) { + public CountMinSketch(final byte numHashes, final int numBuckets, final long seed) { this(numHashes, numBuckets, seed, SERIAL_VERSION_2); } diff --git a/src/test/java/org/apache/datasketches/count/CountMinSketchCrossLanguageTest.java b/src/test/java/org/apache/datasketches/count/CountMinSketchCrossLanguageTest.java new file mode 100644 index 000000000..432e1d327 --- /dev/null +++ b/src/test/java/org/apache/datasketches/count/CountMinSketchCrossLanguageTest.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.datasketches.count; + +import static org.apache.datasketches.common.UtilityIO.CHECK_CPP_FILES; +import static org.apache.datasketches.common.UtilityIO.CHECK_GO_FILES; +import static org.apache.datasketches.common.UtilityIO.CHECK_JAVA_FILES; +import static org.apache.datasketches.common.UtilityIO.CHECK_RUST_FILES; +import static org.apache.datasketches.common.UtilityIO.GENERATE_JAVA_FILES; +import static org.apache.datasketches.common.UtilityIO.getFileBytes; +import static org.apache.datasketches.common.UtilityIO.putBytesToJavaPath; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import org.apache.datasketches.common.UtilityIO.GroupLanguage; +import org.testng.annotations.Test; + +/** + * Generates and checks the Count-Min version 2 cross-language files defined in section 8 of the + * TCK spec docs/count-min-format.md. + */ +public class CountMinSketchCrossLanguageTest { + private static final byte NUM_HASHES = 3; + private static final int NUM_BUCKETS = 1000; + + private static final String[] CASES = { "n0", "n1", "n10", "n100", "n1000", "n10000", "seed42_n1000" }; + + private static long seedOf(final String testCase) { + return testCase.startsWith("seed42_") ? 42L : 9001L; + } + + private static int nOf(final String testCase) { + return Integer.parseInt(testCase.substring(testCase.lastIndexOf('n') + 1)); + } + + private static String fileName(final String testCase, final GroupLanguage lang) { + return "count_min_v2_" + testCase + lang.sfx + ".sk"; + } + + static CountMinSketch build(final String testCase) { + final int n = nOf(testCase); + final CountMinSketch cms = new CountMinSketch(NUM_HASHES, NUM_BUCKETS, seedOf(testCase)); + for (int i = 0; i < n; i++) { + cms.update(i, i + 1L); + cms.update(Integer.toString(i), 1); + } + return cms; + } + + @Test(groups = {GENERATE_JAVA_FILES}, priority = 0) + public void generateCountMinBinaries() { + for (final String testCase : CASES) { + putBytesToJavaPath(fileName(testCase, GroupLanguage.JAVA), build(testCase).toByteArray()); + } + } + + @Test(groups = {CHECK_JAVA_FILES}, priority = 1) + public void checkJava() { + checkCountMinBinaries(GroupLanguage.JAVA); + } + + @Test(groups = {CHECK_CPP_FILES}) + public void checkCpp() { + checkCountMinBinaries(GroupLanguage.CPP); + checkCppVersion1Binaries(); + } + + @Test(groups = {CHECK_GO_FILES}) + public void checkGo() { + checkCountMinBinaries(GroupLanguage.GO); + } + + @Test(groups = {CHECK_RUST_FILES}) + public void checkRust() { + checkCountMinBinaries(GroupLanguage.RUST); + } + + private static void checkCountMinBinaries(final GroupLanguage lang) { + for (final String testCase : CASES) { + final byte[] bytes = getFileBytes(lang.pth, fileName(testCase, lang)); + if (bytes.length == 0) { continue; } + final long seed = seedOf(testCase); + final int n = nOf(testCase); + final CountMinSketch cms = CountMinSketch.deserialize(bytes, seed); + + assertEquals(cms.getSerialVersion(), CountMinSketch.SERIAL_VERSION_2, testCase); + assertEquals(cms.getNumHashes_(), NUM_HASHES, testCase); + assertEquals(cms.getNumBuckets_(), NUM_BUCKETS, testCase); + assertEquals(cms.isEmpty(), n == 0, testCase); + assertEquals(cms.getTotalWeight_(), ((long) n * (n + 1)) / 2 + n, testCase); + for (int i = 0; i < n; i++) { + final String s = Integer.toString(i); + assertTrue(cms.getEstimate(i) >= (i + 1L), testCase + " integer " + i); + assertTrue(cms.getEstimate(i) <= cms.getUpperBound(i), testCase + " integer " + i); + assertTrue(cms.getEstimate(s) >= 1, testCase + " string " + s); + assertTrue(cms.getEstimate(s) <= cms.getUpperBound(s), testCase + " string " + s); + } + // Version 2 is deterministic, so the image must equal the one Java builds for the same case + assertEquals(bytes, build(testCase).toByteArray(), testCase); + } + } + + // Section 8.3: the C++ version 1 files deserialize as version 1. Their estimates aren't checked, + // because version 1 hashing differs between languages. + private static void checkCppVersion1Binaries() { + for (final String name : new String[] { "count_min_empty_cpp.sk", "count_min_non_empty_cpp.sk" }) { + final byte[] bytes = getFileBytes(GroupLanguage.CPP.pth, name); + if (bytes.length == 0) { continue; } + final CountMinSketch cms = CountMinSketch.deserialize(bytes, org.apache.datasketches.common.Util.DEFAULT_UPDATE_SEED); + assertEquals(cms.getSerialVersion(), CountMinSketch.SERIAL_VERSION_1, name); + assertEquals(cms.isEmpty(), name.startsWith("count_min_empty"), name); + } + } +} diff --git a/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java b/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java index e5adb8ddd..c54c2516d 100644 --- a/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java +++ b/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java @@ -428,6 +428,22 @@ public void nullAndEmptyItemsAreIgnored() { assertEquals(cms.getUpperBound((byte[]) null), 0); } + @Test + public void publicConstructorsUseDefaultSeedAndVersion2() { + final CountMinSketch dflt = new CountMinSketch((byte) 2, 9); + assertEquals(dflt.getSeed_(), org.apache.datasketches.common.Util.DEFAULT_UPDATE_SEED); + assertEquals(dflt.getSerialVersion(), CountMinSketch.SERIAL_VERSION_2); + assertEquals(dflt.toByteArray(), new CountMinSketch((byte) 2, 9, SPEC_SEED).toByteArray()); + + final byte numHashes = CountMinSketch.suggestNumHashes(0.95); + final int numBuckets = CountMinSketch.suggestNumBuckets(0.01); + final CountMinSketch cms = new CountMinSketch(numHashes, numBuckets, 42L); + cms.update("x", 3); + assertEquals(cms.getEstimate("x"), 3); + assertThrows(SketchesArgumentException.class, () -> new CountMinSketch((byte) 0, 9)); + assertThrows(SketchesArgumentException.class, () -> new CountMinSketch((byte) 2, 2)); + } + private static void assertBuckets(final long seed, final long item, final int... buckets) { final CountMinSketch cms = new CountMinSketch((byte) 3, 1000, seed); cms.update(item, 1); From 33426116f9cbb8fd5cc894634b78721f7369fd9a Mon Sep 17 00:00:00 2001 From: Lee Rhodes Date: Tue, 10 Nov 2026 17:19:09 -0800 Subject: [PATCH 3/4] Count-Min CL test: define the cases as records instead of parsing names Each case from section 8.1 of the spec now states its name, seed and N directly, so the test no longer parses N out of the case name. This also clears CodeQL alert 995 (possible uncaught NumberFormatException). Co-Authored-By: Claude Opus 5.5 --- .../CountMinSketchCrossLanguageTest.java | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/test/java/org/apache/datasketches/count/CountMinSketchCrossLanguageTest.java b/src/test/java/org/apache/datasketches/count/CountMinSketchCrossLanguageTest.java index 432e1d327..14ce808e8 100644 --- a/src/test/java/org/apache/datasketches/count/CountMinSketchCrossLanguageTest.java +++ b/src/test/java/org/apache/datasketches/count/CountMinSketchCrossLanguageTest.java @@ -40,23 +40,25 @@ public class CountMinSketchCrossLanguageTest { private static final byte NUM_HASHES = 3; private static final int NUM_BUCKETS = 1000; - private static final String[] CASES = { "n0", "n1", "n10", "n100", "n1000", "n10000", "seed42_n1000" }; - - private static long seedOf(final String testCase) { - return testCase.startsWith("seed42_") ? 42L : 9001L; - } - - private static int nOf(final String testCase) { - return Integer.parseInt(testCase.substring(testCase.lastIndexOf('n') + 1)); - } - - private static String fileName(final String testCase, final GroupLanguage lang) { - return "count_min_v2_" + testCase + lang.sfx + ".sk"; + /** One case from the table in section 8.1 of the spec. */ + private record Case(String name, long seed, int n) { } + + private static final Case[] CASES = { + new Case("n0", 9001L, 0), + new Case("n1", 9001L, 1), + new Case("n10", 9001L, 10), + new Case("n100", 9001L, 100), + new Case("n1000", 9001L, 1000), + new Case("n10000", 9001L, 10000), + new Case("seed42_n1000", 42L, 1000) }; + + private static String fileName(final Case testCase, final GroupLanguage lang) { + return "count_min_v2_" + testCase.name() + lang.sfx + ".sk"; } - static CountMinSketch build(final String testCase) { - final int n = nOf(testCase); - final CountMinSketch cms = new CountMinSketch(NUM_HASHES, NUM_BUCKETS, seedOf(testCase)); + static CountMinSketch build(final Case testCase) { + final int n = testCase.n(); + final CountMinSketch cms = new CountMinSketch(NUM_HASHES, NUM_BUCKETS, testCase.seed()); for (int i = 0; i < n; i++) { cms.update(i, i + 1L); cms.update(Integer.toString(i), 1); @@ -66,7 +68,7 @@ static CountMinSketch build(final String testCase) { @Test(groups = {GENERATE_JAVA_FILES}, priority = 0) public void generateCountMinBinaries() { - for (final String testCase : CASES) { + for (final Case testCase : CASES) { putBytesToJavaPath(fileName(testCase, GroupLanguage.JAVA), build(testCase).toByteArray()); } } @@ -93,27 +95,27 @@ public void checkRust() { } private static void checkCountMinBinaries(final GroupLanguage lang) { - for (final String testCase : CASES) { + for (final Case testCase : CASES) { final byte[] bytes = getFileBytes(lang.pth, fileName(testCase, lang)); if (bytes.length == 0) { continue; } - final long seed = seedOf(testCase); - final int n = nOf(testCase); + final long seed = testCase.seed(); + final int n = testCase.n(); final CountMinSketch cms = CountMinSketch.deserialize(bytes, seed); - assertEquals(cms.getSerialVersion(), CountMinSketch.SERIAL_VERSION_2, testCase); - assertEquals(cms.getNumHashes_(), NUM_HASHES, testCase); - assertEquals(cms.getNumBuckets_(), NUM_BUCKETS, testCase); - assertEquals(cms.isEmpty(), n == 0, testCase); - assertEquals(cms.getTotalWeight_(), ((long) n * (n + 1)) / 2 + n, testCase); + assertEquals(cms.getSerialVersion(), CountMinSketch.SERIAL_VERSION_2, testCase.name()); + assertEquals(cms.getNumHashes_(), NUM_HASHES, testCase.name()); + assertEquals(cms.getNumBuckets_(), NUM_BUCKETS, testCase.name()); + assertEquals(cms.isEmpty(), n == 0, testCase.name()); + assertEquals(cms.getTotalWeight_(), ((long) n * (n + 1)) / 2 + n, testCase.name()); for (int i = 0; i < n; i++) { final String s = Integer.toString(i); - assertTrue(cms.getEstimate(i) >= (i + 1L), testCase + " integer " + i); - assertTrue(cms.getEstimate(i) <= cms.getUpperBound(i), testCase + " integer " + i); - assertTrue(cms.getEstimate(s) >= 1, testCase + " string " + s); - assertTrue(cms.getEstimate(s) <= cms.getUpperBound(s), testCase + " string " + s); + assertTrue(cms.getEstimate(i) >= (i + 1L), testCase.name() + " integer " + i); + assertTrue(cms.getEstimate(i) <= cms.getUpperBound(i), testCase.name() + " integer " + i); + assertTrue(cms.getEstimate(s) >= 1, testCase.name() + " string " + s); + assertTrue(cms.getEstimate(s) <= cms.getUpperBound(s), testCase.name() + " string " + s); } // Version 2 is deterministic, so the image must equal the one Java builds for the same case - assertEquals(bytes, build(testCase).toByteArray(), testCase); + assertEquals(bytes, build(testCase).toByteArray(), testCase.name()); } } From 8f1d61d99cce8824dcdfd3c799f2b1ddefbf73df Mon Sep 17 00:00:00 2001 From: Lee Rhodes Date: Wed, 11 Nov 2026 17:06:01 -0800 Subject: [PATCH 4/4] Count-Min: take numHashes as an int in the public constructors numHashes is checked to be from 1 to 127 and cast to a byte only where it is stored, so callers no longer need to write (byte) 3. A byte from suggestNumHashes() still passes directly. getNumHashes_(), the image format and deserialization are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../datasketches/count/CountMinSketch.java | 14 ++--- .../count/CountMinSketchTest.java | 54 +++++++++++-------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/apache/datasketches/count/CountMinSketch.java b/src/main/java/org/apache/datasketches/count/CountMinSketch.java index c576b0b89..ce122ecf9 100644 --- a/src/main/java/org/apache/datasketches/count/CountMinSketch.java +++ b/src/main/java/org/apache/datasketches/count/CountMinSketch.java @@ -89,7 +89,7 @@ int mask() { * The product of numHashes and numBuckets must be less than 230. * @throws SketchesArgumentException if a parameter is out of range */ - public CountMinSketch(final byte numHashes, final int numBuckets) { + public CountMinSketch(final int numHashes, final int numBuckets) { this(numHashes, numBuckets, Util.DEFAULT_UPDATE_SEED); } @@ -107,22 +107,22 @@ public CountMinSketch(final byte numHashes, final int numBuckets) { * @param seed The base hash seed * @throws SketchesArgumentException if a parameter is out of range */ - public CountMinSketch(final byte numHashes, final int numBuckets, final long seed) { + public CountMinSketch(final int numHashes, final int numBuckets, final long seed) { this(numHashes, numBuckets, seed, SERIAL_VERSION_2); } /** * Creates a CountMin sketch that hashes items as the given serialization version does. * - * @param numHashes The number of hash functions to apply to items + * @param numHashes The number of hash functions to apply to items, from 1 to 127 * @param numBuckets Array size for each of the hashing function * @param seed The base hash seed * @param serialVersion 1 for the legacy hashing, 2 for the cross-language hashing */ - private CountMinSketch(final byte numHashes, final int numBuckets, final long seed, final byte serialVersion) { + private CountMinSketch(final int numHashes, final int numBuckets, final long seed, final byte serialVersion) { // Validate numHashes - if (numHashes <= 0) { - throw new SketchesArgumentException("Number of hash functions must be positive, got: " + numHashes); + if ((numHashes < 1) || (numHashes > Byte.MAX_VALUE)) { + throw new SketchesArgumentException("Number of hash functions must be from 1 to 127, got: " + numHashes); } // Validate numBuckets with clear mathematical justification @@ -151,7 +151,7 @@ private CountMinSketch(final byte numHashes, final int numBuckets, final long se } serialVersion_ = serialVersion; - numHashes_ = numHashes; + numHashes_ = (byte) numHashes; //stored as one byte in the image numBuckets_ = numBuckets; seed_ = seed; hashSeeds_ = new long[numHashes]; diff --git a/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java b/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java index c54c2516d..45f99b34f 100644 --- a/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java +++ b/src/test/java/org/apache/datasketches/count/CountMinSketchTest.java @@ -40,8 +40,8 @@ public class CountMinSketchTest { @Test public void createNewCountMinSketchTest() throws Exception { - assertThrows(SketchesArgumentException.class, () -> new CountMinSketch((byte) 5, 1, 123)); - assertThrows(SketchesArgumentException.class, () -> new CountMinSketch((byte) 4, (1 << 28), 123)); + assertThrows(SketchesArgumentException.class, () -> new CountMinSketch(5, 1, 123)); + assertThrows(SketchesArgumentException.class, () -> new CountMinSketch(4, (1 << 28), 123)); final byte numHashes = 3; final int numBuckets = 5; final long seed = 1234567; @@ -110,7 +110,7 @@ public void countMinSketchOneUpdateTest() { @Test public void frequencyCancellationTest() { - final CountMinSketch c = new CountMinSketch((byte) 1, 5, 123456); + final CountMinSketch c = new CountMinSketch(1, 5, 123456); c.update("x", 1); c.update("y", -1); assertEquals(c.getTotalWeight_(), 2); @@ -161,7 +161,7 @@ public void mergeFailTest() { assertThrows("Cannot merge a sketch with itself.", SketchesException.class, () -> s.merge(s)); - final CountMinSketch s1 = new CountMinSketch((byte) (numHashes + 1), numBuckets, seed); + final CountMinSketch s1 = new CountMinSketch(numHashes + 1, numBuckets, seed); final CountMinSketch s2 = new CountMinSketch(numHashes, numBuckets + 1, seed); final CountMinSketch s3 = new CountMinSketch(numHashes, numBuckets, seed + 1); @@ -250,8 +250,8 @@ public void serializeDeserializeTest() { @Test public void emptyImageIsUsable() { final long seed = 123L; - final CountMinSketch cms = CountMinSketch.deserialize(new CountMinSketch((byte) 3, 5, seed).toByteArray(), seed); - final CountMinSketch other = new CountMinSketch((byte) 3, 5, seed); + final CountMinSketch cms = CountMinSketch.deserialize(new CountMinSketch(3, 5, seed).toByteArray(), seed); + final CountMinSketch other = new CountMinSketch(3, 5, seed); cms.merge(other); //both empty assertTrue(cms.isEmpty()); other.update(1L, 2); @@ -266,7 +266,7 @@ public void emptyImageIsUsable() { @Test public void truncatedImageThrows() { final long seed = 123L; - final CountMinSketch cms = new CountMinSketch((byte) 3, 5, seed); + final CountMinSketch cms = new CountMinSketch(3, 5, seed); cms.update(1L, 1); final byte[] bytes = cms.toByteArray(); ByteArrayUtil.putIntLE(bytes, 8, 1 << 28); //numBuckets larger than the image holds @@ -313,11 +313,11 @@ public void v2BucketVectors() { // 7.4 images @Test public void v2ImageVectors() { - final CountMinSketch empty = new CountMinSketch((byte) 2, 9, SPEC_SEED); + final CountMinSketch empty = new CountMinSketch(2, 9, SPEC_SEED); assertEquals(empty.getSerialVersion(), CountMinSketch.SERIAL_VERSION_2); assertEquals(empty.toByteArray(), hex("02021201000000000900000002cc9300")); - final CountMinSketch cms = new CountMinSketch((byte) 2, 9, SPEC_SEED); + final CountMinSketch cms = new CountMinSketch(2, 9, SPEC_SEED); cms.update(1L, 10); cms.update(9L, 810); cms.update("apple", 5); @@ -343,8 +343,8 @@ public void v2ImageVectors() { // The long overloads hash the 8 little-endian bytes of the item, like the byte[] overloads @Test public void v2LongMatchesLittleEndianBytes() { - final CountMinSketch a = new CountMinSketch((byte) 4, 101, SPEC_SEED); - final CountMinSketch b = new CountMinSketch((byte) 4, 101, SPEC_SEED); + final CountMinSketch a = new CountMinSketch(4, 101, SPEC_SEED); + final CountMinSketch b = new CountMinSketch(4, 101, SPEC_SEED); final long[] items = { 0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, 0x0123456789ABCDEFL }; for (final long item : items) { final byte[] le = new byte[Long.BYTES]; @@ -395,10 +395,10 @@ public void v1ImageKeepsLegacyHashing() { @Test public void mergeAcrossVersionsThrows() { final long seed = 1234L; - final byte[] emptyV1 = new CountMinSketch((byte) 3, 17, seed).toByteArray(); + final byte[] emptyV1 = new CountMinSketch(3, 17, seed).toByteArray(); emptyV1[1] = CountMinSketch.SERIAL_VERSION_1; final CountMinSketch v1 = CountMinSketch.deserialize(emptyV1, seed); - final CountMinSketch v2 = new CountMinSketch((byte) 3, 17, seed); + final CountMinSketch v2 = new CountMinSketch(3, 17, seed); assertThrows(SketchesException.class, () -> v1.merge(v2)); assertThrows(SketchesException.class, () -> v2.merge(v1)); } @@ -406,7 +406,7 @@ public void mergeAcrossVersionsThrows() { @Test public void unknownSerialVersionThrows() { final long seed = 1234L; - final byte[] image = new CountMinSketch((byte) 3, 17, seed).toByteArray(); + final byte[] image = new CountMinSketch(3, 17, seed).toByteArray(); image[1] = 3; assertThrows(SketchesArgumentException.class, () -> CountMinSketch.deserialize(image, seed)); image[1] = 0; @@ -415,7 +415,7 @@ public void unknownSerialVersionThrows() { @Test public void nullAndEmptyItemsAreIgnored() { - final CountMinSketch cms = new CountMinSketch((byte) 3, 17, SPEC_SEED); + final CountMinSketch cms = new CountMinSketch(3, 17, SPEC_SEED); cms.update((byte[]) null, 5); cms.update(new byte[0], 5); cms.update((String) null, 5); @@ -430,28 +430,40 @@ public void nullAndEmptyItemsAreIgnored() { @Test public void publicConstructorsUseDefaultSeedAndVersion2() { - final CountMinSketch dflt = new CountMinSketch((byte) 2, 9); + final CountMinSketch dflt = new CountMinSketch(2, 9); assertEquals(dflt.getSeed_(), org.apache.datasketches.common.Util.DEFAULT_UPDATE_SEED); assertEquals(dflt.getSerialVersion(), CountMinSketch.SERIAL_VERSION_2); - assertEquals(dflt.toByteArray(), new CountMinSketch((byte) 2, 9, SPEC_SEED).toByteArray()); + assertEquals(dflt.toByteArray(), new CountMinSketch(2, 9, SPEC_SEED).toByteArray()); final byte numHashes = CountMinSketch.suggestNumHashes(0.95); final int numBuckets = CountMinSketch.suggestNumBuckets(0.01); final CountMinSketch cms = new CountMinSketch(numHashes, numBuckets, 42L); cms.update("x", 3); assertEquals(cms.getEstimate("x"), 3); - assertThrows(SketchesArgumentException.class, () -> new CountMinSketch((byte) 0, 9)); - assertThrows(SketchesArgumentException.class, () -> new CountMinSketch((byte) 2, 2)); + assertThrows(SketchesArgumentException.class, () -> new CountMinSketch(0, 9)); + assertThrows(SketchesArgumentException.class, () -> new CountMinSketch(2, 2)); + } + + @Test + public void numHashesRange() { + // numHashes is an int in the constructors but is stored as one byte, so it must be from 1 to 127 + for (final int bad : new int[] {0, -1, 128, 256 + 3}) { + assertThrows(SketchesArgumentException.class, () -> new CountMinSketch(bad, 9)); + } + final CountMinSketch max = new CountMinSketch(127, 3); + assertEquals(max.getNumHashes_(), (byte) 127); + final CountMinSketch copy = CountMinSketch.deserialize(max.toByteArray(), max.getSeed_()); + assertEquals(copy.getNumHashes_(), (byte) 127); } private static void assertBuckets(final long seed, final long item, final int... buckets) { - final CountMinSketch cms = new CountMinSketch((byte) 3, 1000, seed); + final CountMinSketch cms = new CountMinSketch(3, 1000, seed); cms.update(item, 1); assertCounters(cms, buckets); } private static void assertBuckets(final long seed, final String item, final int... buckets) { - final CountMinSketch cms = new CountMinSketch((byte) 3, 1000, seed); + final CountMinSketch cms = new CountMinSketch(3, 1000, seed); cms.update(item, 1); assertCounters(cms, buckets); }