#1302

refactor: replace unmaintained bincode with postcard + borsh

OpenMrCroxxwants to merge intofrom

Summary

Replace the unmaintained bincode crate (RUSTSEC-2025-0141) with two actively maintained serialization backends behind feature gates.

Changes

Dependencies

  • Removed: bincode = "1" (workspace)
  • Added: postcard = "1" (serde-native, community standard replacement)
  • Added: borsh = { version = "1", features = ["derive"] } (NEAR Protocol, high performance)

Feature Gates

FeaturesActive blanket Code impl
(none)Manual impls for numeric types, bool, Vec<u8>, String, Bytes
serde onlyimpl Code for T: Serialize + DeserializeOwned via postcard
borsh onlyimpl Code for T: BorshSerialize + BorshDeserialize via borsh
serde + borshborsh takes priority (avoids trait coherence conflicts)

Modified Files

  • Cargo.toml — workspace deps
  • foyer-common/Cargo.toml — features and dependencies
  • foyer-common/src/code.rs — three mutually exclusive Code impl paths with #[cfg]
  • foyer-common/src/error.rs — bincode_error() -> postcard_error()
  • foyer/Cargo.toml — add borsh pass-through feature
  • examples/Cargo.toml — add borsh pass-through feature

New Benchmark

foyer-common/benches/bench_serde/ rewritten to directly compare postcard, borsh, and manual encoding:

  • postcard.rs — postcard backend (#[cfg(feature = "serde")])
  • borsh.rs — borsh backend (#[cfg(feature = "borsh")])
  • manual.rs — hand-written LE encoding (always available)

Benchmark Results

Test machine: Linux 6.8, 100 samples per benchmark, 3s warmup. Test data: struct Entry { id: u64, label: String, payload: Vec<u8> }

Encode

Sizemanualborshpostcard
64 KiB539 ns1.04 us (1.9x)16.8 us (31x)
4 MiB51.9 us101 us (1.9x)1.11 ms (21x)
64 MiB3.83 ms19.3 ms (5.0x)20.6 ms (5.4x)

Decode

Sizemanualborshpostcard
64 KiB541 ns866 ns (1.6x)27.0 us (50x)
4 MiB50.7 us76.9 us (1.5x)1.74 ms (34x)
64 MiB12.3 ms14.6 ms (1.2x)38.0 ms (3.1x)

Encoded Size (raw = id + label + payload)

Sizerawmanualborshpostcard
64 KiB65552 B65568 B (+16)65560 B (+8)65557 B (+5)
4 MiB4194320 B4194336 B (+16)4194328 B (+8)4194326 B (+6)
64 MiB67108880 B67108896 B (+16)67108888 B (+8)67108887 B (+7)

Key Findings

  1. borsh stays close to manual — only 1.5-2x slower than raw memcpy for both encode and decode across all sizes
  2. postcard is significantly slower for large payloads: 21x slower encode and 34x slower decode than manual at 4 MiB. The varint encoding overhead dominates for bulk data.
  3. At 64 MiB the gap narrows as memory bandwidth becomes the bottleneck — borsh ~5x, postcard ~5.4x vs manual for encode
  4. All three produce nearly identical encoded sizes — only 5-16 bytes of metadata overhead regardless of payload size
  5. manual encoding is always fastest but requires handwritten encode/decode — the baseline cost is essentially memcpy

Migration Notes

  • Wire format change: postcard and bincode produce different byte representations. Disk caches written with bincode cannot be read with postcard. Users with persistent disk caches must clear or migrate them.
  • Error::bincode_error() has been replaced with Error::postcard_error() (available when the serde feature is active and borsh is not).
  • Users who derived Serialize + DeserializeOwned for Code continue to work with the serde feature, no code changes needed.
  • Users who want maximum performance should consider the borsh feature and derive BorshSerialize + BorshDeserialize.

Generated with Claude Code

Activity

6
M
2223d21
codecov[bot]4mo ago

Codecov Report

:x: Patch coverage is 10.00000% with 18 lines in your changes missing coverage. Please review.

Files with missing linesCoverage Δ
foyer-common/src/error.rs75.26% <0.00%> (+1.16%):arrow_up:
foyer-common/src/code.rs80.82% <12.50%> (-5.31%):arrow_down:

... and 2 files with indirect coverage changes

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.