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
58 changes: 37 additions & 21 deletions cloudini_lib/include/cloudini_lib/encoding_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,35 +99,51 @@ inline size_t decodeVarint(const uint8_t* buf, size_t max_size, int64_t& val) {
if (max_size == 0) {
throw std::runtime_error("decodeVarint: empty input");
}
uint64_t uval = 0;
uint8_t shift = 0;
const uint8_t* ptr = buf;
while (true) {
if (static_cast<size_t>(ptr - buf) >= max_size) {
throw std::runtime_error("decodeVarint: truncated input");
uint64_t uval;
size_t count;
const uint8_t b0 = buf[0];
if ((b0 & 0x80) == 0) {
// 1-byte varint: by far the most common case for small deltas. Fully safe
// (max_size >= 1 guaranteed above).
uval = b0;
count = 1;
} else if (max_size >= 2 && (buf[1] & 0x80) == 0) {
// 2-byte varint. Max value 0x3FFF, so no overflow is possible. The
// `max_size >= 2` guard is checked before buf[1] so we never read past bound.
uval = static_cast<uint64_t>(b0 & 0x7f) | (static_cast<uint64_t>(buf[1]) << 7);
count = 2;
} else {
// General path for 3+ byte varints: per-byte bounds + overflow checks.
uval = 0;
uint8_t shift = 0;
const uint8_t* ptr = buf;
while (true) {
if (static_cast<size_t>(ptr - buf) >= max_size) {
throw std::runtime_error("decodeVarint: truncated input");
}
uint8_t byte = *ptr;
ptr++;
const uint8_t payload = byte & 0x7f;
if (shift >= 64 || (shift == 63 && payload > 1)) {
throw std::runtime_error("decodeVarint: value overflow");
}
uval |= (static_cast<uint64_t>(payload) << shift);
if ((byte & 0x80) == 0) {
break;
}
if (shift >= 63) {
throw std::runtime_error("decodeVarint: value overflow");
}
shift = static_cast<uint8_t>(shift + 7);
}
uint8_t byte = *ptr;
ptr++;
const uint8_t payload = byte & 0x7f;
if (shift >= 64 || (shift == 63 && payload > 1)) {
throw std::runtime_error("decodeVarint: value overflow");
}
uval |= (static_cast<uint64_t>(payload) << shift);
if ((byte & 0x80) == 0) {
break;
}
if (shift >= 63) {
throw std::runtime_error("decodeVarint: value overflow");
}
shift = static_cast<uint8_t>(shift + 7);
count = static_cast<size_t>(ptr - buf);
}
if (uval == 0) {
throw std::runtime_error("decodeVarint: unexpected NaN marker");
}
uval--;
// Perform zigzag decoding to retrieve the original signed value.
val = static_cast<int64_t>((uval >> 1) ^ static_cast<uint64_t>(-(static_cast<int64_t>(uval & 1))));
const auto count = static_cast<size_t>(ptr - buf);
return count;
}

Expand Down
115 changes: 115 additions & 0 deletions cloudini_lib/test/test_field_encoders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,121 @@ TEST(FieldEncoders, DecodeVarintRejectsTruncatedInputBeforeReadingPastBound) {

namespace {

// Oracle: the pre-optimization, loop-only decodeVarint. Kept here verbatim so
// the optimized fast-path implementation can be differentially compared against
// it. Any divergence (value, byte count, or throw/no-throw) is a regression.
size_t decodeVarintOracle(const uint8_t* buf, size_t max_size, int64_t& val) {
if (max_size == 0) {
throw std::runtime_error("decodeVarint: empty input");
}
uint64_t uval = 0;
uint8_t shift = 0;
const uint8_t* ptr = buf;
while (true) {
if (static_cast<size_t>(ptr - buf) >= max_size) {
throw std::runtime_error("decodeVarint: truncated input");
}
uint8_t byte = *ptr;
ptr++;
const uint8_t payload = byte & 0x7f;
if (shift >= 64 || (shift == 63 && payload > 1)) {
throw std::runtime_error("decodeVarint: value overflow");
}
uval |= (static_cast<uint64_t>(payload) << shift);
if ((byte & 0x80) == 0) {
break;
}
if (shift >= 63) {
throw std::runtime_error("decodeVarint: value overflow");
}
shift = static_cast<uint8_t>(shift + 7);
}
if (uval == 0) {
throw std::runtime_error("decodeVarint: unexpected NaN marker");
}
uval--;
val = static_cast<int64_t>((uval >> 1) ^ static_cast<uint64_t>(-(static_cast<int64_t>(uval & 1))));
return static_cast<size_t>(ptr - buf);
}

// Compare the optimized decodeVarint against the oracle for one (buf, max_size)
// case: both must throw, or both must return identical (count, value).
void expectVarintMatchesOracle(const uint8_t* buf, size_t max_size) {
int64_t opt_val = 0;
size_t opt_count = 0;
bool opt_threw = false;
try {
opt_count = Cloudini::decodeVarint(buf, max_size, opt_val);
} catch (const std::exception&) {
opt_threw = true;
}

int64_t ref_val = 0;
size_t ref_count = 0;
bool ref_threw = false;
try {
ref_count = decodeVarintOracle(buf, max_size, ref_val);
} catch (const std::exception&) {
ref_threw = true;
}

ASSERT_EQ(opt_threw, ref_threw) << "throw mismatch at max_size=" << max_size;
if (!opt_threw) {
ASSERT_EQ(opt_count, ref_count) << "count mismatch at max_size=" << max_size;
ASSERT_EQ(opt_val, ref_val) << "value mismatch at max_size=" << max_size;
}
}

} // namespace

TEST(FieldEncoders, DecodeVarintMatchesOracleExhaustiveAndRandom) {
// Exhaustive over all 1- and 2-byte prefixes (the new fast paths and their
// boundary with the general path) with every truncation length in [0, len].
std::array<uint8_t, 16> buf{};
for (int b0 = 0; b0 < 256; ++b0) {
buf[0] = static_cast<uint8_t>(b0);
for (size_t ms = 0; ms <= 1; ++ms) {
expectVarintMatchesOracle(buf.data(), ms);
}
for (int b1 = 0; b1 < 256; ++b1) {
buf[1] = static_cast<uint8_t>(b1);
for (size_t ms = 0; ms <= 2; ++ms) {
expectVarintMatchesOracle(buf.data(), ms);
}
// 3-byte prefixes: sample b2 at the byte-value boundaries (the general
// path here is the original code verbatim, so full enumeration is
// unnecessary; the randomized sweep below covers the interior).
for (uint8_t b2 : {0x00u, 0x01u, 0x7eu, 0x7fu, 0x80u, 0x81u, 0xfeu, 0xffu}) {
buf[2] = b2;
expectVarintMatchesOracle(buf.data(), 3);
}
}
}

// Randomized sweep over the general (3+ byte) path and all truncation
// lengths, including malformed all-continuation and overflow-edge varints.
// The 1- and 2-byte fast paths are already proven exhaustively above and the
// 3+ byte path is the original code verbatim, so this sweep is supplementary
// coverage of the interior; 200k keeps the ASan/Debug ctest run fast.
std::mt19937_64 rng(0xC10D1217ULL);
for (int iter = 0; iter < 200'000; ++iter) {
const size_t len = 1 + (rng() % 12); // up to 12 bytes (varint64 worst case is 10)
for (size_t i = 0; i < len; ++i) {
// Bias toward continuation bytes so we exercise long/overflowing varints.
const uint32_t r = static_cast<uint32_t>(rng());
uint8_t byte = static_cast<uint8_t>(r);
if ((r >> 8) & 1) {
byte |= 0x80u; // force continuation ~50% of the time
}
buf[i] = byte;
}
const size_t max_size = rng() % (len + 1); // 0 .. len
expectVarintMatchesOracle(buf.data(), max_size);
}
}

namespace {

// Helper: round-trip a sequence of FloatType values through encoder/decoder,
// periodically flushing+resetting both at chunk boundaries to exercise the
// chunk-flush path (the classic bit-packer gotcha).
Expand Down
39 changes: 39 additions & 0 deletions cloudini_lib/tools/src/mcap_codec_benchmark.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ struct DecodeSample {
std::vector<uint8_t> encoded;
};

inline uint64_t fnv1a(uint64_t h, const uint8_t* data, size_t n) {
for (size_t i = 0; i < n; ++i) {
h ^= data[i];
h *= 0x100000001b3ULL;
}
return h;
}

inline uint64_t elapsedNs(Clock::time_point t0, Clock::time_point t1) {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count());
}
Expand Down Expand Up @@ -237,6 +245,7 @@ int main(int argc, char** argv) {
cxxopts::value<uint64_t>()->default_value("1")) //
("profile-sleep-ms", "Sleep after preload and before decode replay so perf can attach", //
cxxopts::value<uint64_t>()->default_value("0")) //
("hash", "Print an FNV-1a fingerprint of decoded output per mode (correctness gate)") //
("explain", "Print the field schema and viz-preprocessing effect for the first message of each topic and exit");
options.parse_positional({"filename"});
options.positional_help("<file.mcap>");
Expand All @@ -263,6 +272,7 @@ int main(int argc, char** argv) {
const bool decode_replay = parse_result.count("decode-replay") > 0;
const uint64_t decode_repeat = std::max<uint64_t>(1, parse_result["decode-repeat"].as<uint64_t>());
const uint64_t profile_sleep_ms = parse_result["profile-sleep-ms"].as<uint64_t>();
const bool do_hash = parse_result.count("hash") > 0;
int only_mode = -1;
if (parse_result.count("mode") > 0) {
only_mode = modeIndexFromName(parse_result["mode"].as<std::string>());
Expand All @@ -280,6 +290,12 @@ int main(int argc, char** argv) {
std::cerr << "Error: --decode-replay requires --mode so the in-memory replay stays bounded\n";
return 1;
}
if (do_hash && !decode_replay) {
// The fingerprint pass lives inside the decode-replay block, so without it
// --hash would silently print nothing — a footgun for a correctness gate.
std::cerr << "Error: --hash requires --decode-replay (and --mode)\n";
return 1;
}

if (!std::filesystem::exists(input_file)) {
std::cerr << "Error: file does not exist: " << input_file << "\n";
Expand Down Expand Up @@ -517,6 +533,29 @@ int main(int argc, char** argv) {
std::cout << "\nDecode replay: samples=" << decode_samples.size()
<< " repeat=" << decode_repeat << " total-decodes=" << replay_count
<< " (MCAP read/decompression excluded)\n";
if (do_hash) {
// Untimed correctness pass: decode each sample once and fingerprint the
// decoded output bytes per mode. A pure performance refactor of the
// deterministic decode path MUST leave these values unchanged.
uint64_t mode_hash[kModeCount];
std::fill(std::begin(mode_hash), std::end(mode_hash), 0xcbf29ce484222325ULL);
for (const auto& sample : decode_samples) {
if (dec_buf[sample.mode].size() < sample.out_bytes_needed) {
dec_buf[sample.mode].resize(sample.out_bytes_needed);
}
Cloudini::ConstBufferView enc_view(sample.encoded.data(), sample.encoded.size());
Cloudini::BufferView out_view(dec_buf[sample.mode].data(), sample.out_bytes_needed);
Cloudini::PointcloudDecoder decoder;
decoder.decode(sample.header_info, enc_view, out_view);
mode_hash[sample.mode] =
fnv1a(mode_hash[sample.mode], dec_buf[sample.mode].data(), sample.out_bytes_needed);
}
std::cout << "Decoded-output fingerprint (FNV-1a):\n";
for (int m = 0; m < kModeCount; ++m) {
if (only_mode >= 0 && m != only_mode) continue;
std::cout << " " << kModeNames[m] << " : 0x" << std::hex << mode_hash[m] << std::dec << "\n";
}
}
if (profile_sleep_ms > 0) {
std::cout << "Sleeping " << profile_sleep_ms << " ms before decode replay"
<< " so perf can attach...\n" << std::flush;
Expand Down
Loading