Skip to content
Open
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
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ exclude = ["website/*"]
[workspace.dependencies]
allocator-api2 = "0.2"
anyhow = "1"
bincode = "1"
borsh = { version = "1", features = ["derive"] }
postcard = "1"
bitflags = "2"
bytes = "1"
bytesize = { package = "foyer-bytesize", version = "2" }
Expand Down
1 change: 1 addition & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ publish = false

[features]
serde = ["foyer/serde"]
borsh = ["foyer/borsh"]
jaeger = ["fastrace-jaeger"]
ot = [
"fastrace-opentelemetry",
Expand Down
6 changes: 4 additions & 2 deletions foyer-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,21 @@ development = ["criterion", "serde_bytes"]

[features]
default = ["tokio/runtime-tokio"]
serde = ["dep:serde", "dep:bincode"]
serde = ["dep:serde", "dep:postcard"]
borsh = ["dep:borsh"]
strict_assertions = []
tracing = ["dep:fastrace"]

[dependencies]
anyhow = { workspace = true }
bincode = { workspace = true, optional = true }
borsh = { workspace = true, optional = true, features = ["derive"] }
bytes = { workspace = true }
cfg-if = { workspace = true }
fastrace = { workspace = true, optional = true }
mixtrics = { workspace = true }
parking_lot = { workspace = true }
pin-project = { workspace = true }
postcard = { workspace = true, optional = true, features = ["use-std"] }
serde = { workspace = true, optional = true }
tokio = { workspace = true, features = ["rt"] }
twox-hash = { workspace = true, features = ["xxhash64"] }
Expand Down
59 changes: 59 additions & 0 deletions foyer-common/benches/bench_serde/borsh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2026 foyer Project Authors
//
// Licensed 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.

use borsh::{BorshDeserialize, BorshSerialize};
use criterion::Criterion;

use crate::{Entry, run_encode_decode_bench};

#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
struct BorshEntry {
id: u64,
label: String,
payload: Vec<u8>,
}

impl From<Entry> for BorshEntry {
fn from(e: Entry) -> Self {
Self {
id: e.id,
label: e.label,
payload: e.payload,
}
}
}

impl From<BorshEntry> for Entry {
fn from(e: BorshEntry) -> Self {
Self {
id: e.id,
label: e.label,
payload: e.payload,
}
}
}

fn borsh_encode(entry: &Entry, buf: &mut Vec<u8>) {
let be: BorshEntry = entry.clone().into();
borsh::to_writer(buf, &be).unwrap();
}

fn borsh_decode(bytes: &[u8]) -> Entry {
let be: BorshEntry = borsh::from_reader(&mut &bytes[..]).unwrap();
be.into()
}

pub fn bench(c: &mut Criterion) {
run_encode_decode_bench(c, "borsh", borsh_encode, borsh_decode, Entry::create);
}
125 changes: 105 additions & 20 deletions foyer-common/benches/bench_serde/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,130 @@

use std::time::Instant;

use criterion::Bencher;
use foyer_common::code::StorageValue;
use criterion::{Bencher, Criterion, criterion_group, criterion_main};

#[cfg(feature = "borsh")]
mod borsh;
mod manual;
#[cfg(feature = "serde")]
mod serde;
#[cfg(feature = "serde")]
criterion::criterion_group!(benches, serde::bench_encode, serde::bench_decode);
#[cfg(feature = "serde")]
criterion::criterion_main!(benches);
mod postcard;

#[cfg(not(feature = "serde"))]
mod no_serde;
#[cfg(not(feature = "serde"))]
criterion::criterion_group!(benches, no_serde::bench_encode, no_serde::bench_decode);
#[cfg(not(feature = "serde"))]
criterion::criterion_main!(benches);
/// A representative cache entry: an id, a label, and a payload.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Entry {
pub id: u64,
pub label: String,
pub payload: Vec<u8>,
}

impl Entry {
pub fn create(payload_size: usize, label_len: usize) -> Self {
let mut payload = vec![0; payload_size];
rand::fill(&mut payload[..]);
let label = "x".repeat(label_len);
Self {
id: rand::random(),
label,
payload,
}
}
}

const K: usize = 1 << 10;
const M: usize = 1 << 20;

fn encode<V: StorageValue>(b: &mut Bencher, v: V, size: usize) {
/// Sizes to benchmark: (label, payload_size, label_len).
const SIZES: &[(&str, usize, usize)] = &[("64KiB", 64 * K, 8), ("4MiB", 4 * M, 8), ("64MiB", 64 * M, 8)];

/// Helper: measure encode throughput for a given value.
fn bench_encode<V>(b: &mut Bencher, encode: fn(&V, &mut Vec<u8>), v: V, cap: usize)
where
V: Clone,
{
b.iter_custom(|iters| {
let mut buf = vec![0; size * 2];
let mut buf = Vec::with_capacity(cap);
let start = Instant::now();
for _ in 0..iters {
v.encode(&mut &mut buf[..]).unwrap();
buf.clear();
encode(&v, &mut buf);
}
start.elapsed()
});
}

fn decode<V: StorageValue>(b: &mut Bencher, v: V, size: usize) {
/// Helper: measure decode throughput for a given encoded buffer.
fn bench_decode<V: Clone>(b: &mut Bencher, encode: fn(&V, &mut Vec<u8>), decode: fn(&[u8]) -> V, v: V, cap: usize) {
let mut buf = Vec::with_capacity(cap);
encode(&v, &mut buf);
b.iter_custom(|iters| {
let mut buf = vec![0; size * 2];
v.encode(&mut &mut buf[..]).unwrap();
let start = Instant::now();
for _ in 0..iters {
V::decode(&mut &buf[..]).unwrap();
std::hint::black_box(decode(&buf));
}
start.elapsed()
});
}

/// Helper: get the encoded size for a given value.
fn encoded_size<V>(encode: fn(&V, &mut Vec<u8>), v: &V) -> usize {
let mut buf = Vec::new();
encode(v, &mut buf);
buf.len()
}

/// Run encode + decode benchmarks for a given backend, and print encoded sizes.
pub fn run_encode_decode_bench<V: Clone + 'static>(
c: &mut Criterion,
group: &str,
encode: fn(&V, &mut Vec<u8>),
decode: fn(&[u8]) -> V,
create: fn(usize, usize) -> V,
) {
{
let mut grp = c.benchmark_group(format!("{group}/encode"));
for (label, size, label_len) in SIZES {
let entry = create(*size, *label_len);
let cap = size + label_len + 4096;
grp.bench_function(*label, |b| {
bench_encode(b, encode, entry.clone(), cap);
});
}
grp.finish();
}

{
let mut grp = c.benchmark_group(format!("{group}/decode"));
for (label, size, label_len) in SIZES {
let entry = create(*size, *label_len);
let cap = size + label_len + 4096;
grp.bench_function(*label, |b| {
bench_decode(b, encode, decode, entry.clone(), cap);
});
}
grp.finish();
}

// Print encoded sizes (appears in benchmark output).
println!("--- {group} encoded sizes ---");
for (label, size, label_len) in SIZES {
let entry = create(*size, *label_len);
let enc_size = encoded_size(encode, &entry);
let raw_size = std::mem::size_of::<u64>() + *label_len + *size;
println!(
"[{group}] {label}: encoded={enc_size}B raw={raw_size}B ratio={:.2}%",
(enc_size as f64 / raw_size as f64) * 100.0
);
}
}

fn bench_all(c: &mut Criterion) {
#[cfg(feature = "borsh")]
borsh::bench(c);
#[cfg(feature = "serde")]
postcard::bench(c);
manual::bench(c);
}

criterion_group!(benches, bench_all);
criterion_main!(benches);
50 changes: 50 additions & 0 deletions foyer-common/benches/bench_serde/manual.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2026 foyer Project Authors
//
// Licensed 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.

use criterion::Criterion;

use crate::{Entry, run_encode_decode_bench};

/// Manual little-endian encoding: u64 (8 bytes LE) + label_len (u64 LE) + label bytes + payload_len (u64 LE) + payload
/// bytes.
fn manual_encode(entry: &Entry, buf: &mut Vec<u8>) {
buf.extend_from_slice(&entry.id.to_le_bytes());
buf.extend_from_slice(&(entry.label.len() as u64).to_le_bytes());
buf.extend_from_slice(entry.label.as_bytes());
buf.extend_from_slice(&(entry.payload.len() as u64).to_le_bytes());
buf.extend_from_slice(&entry.payload);
}

fn manual_decode(bytes: &[u8]) -> Entry {
let (id_bytes, rest) = bytes.split_at(8);
let id = u64::from_le_bytes(id_bytes.try_into().unwrap());

let (label_len_bytes, rest) = rest.split_at(8);
let label_len = u64::from_le_bytes(label_len_bytes.try_into().unwrap()) as usize;

let (label_bytes, rest) = rest.split_at(label_len);
let label = String::from_utf8(label_bytes.to_vec()).unwrap();

let (payload_len_bytes, rest) = rest.split_at(8);
let payload_len = u64::from_le_bytes(payload_len_bytes.try_into().unwrap()) as usize;

let (payload_bytes, _) = rest.split_at(payload_len);
let payload = payload_bytes.to_vec();

Entry { id, label, payload }
}

pub fn bench(c: &mut Criterion) {
run_encode_decode_bench(c, "manual", manual_encode, manual_decode, Entry::create);
}
30 changes: 30 additions & 0 deletions foyer-common/benches/bench_serde/postcard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2026 foyer Project Authors
//
// Licensed 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.

use criterion::Criterion;

use crate::{Entry, run_encode_decode_bench};

fn postcard_encode(entry: &Entry, buf: &mut Vec<u8>) {
postcard::to_io(entry, buf).unwrap();
}

fn postcard_decode(bytes: &[u8]) -> Entry {
let (entry, _remaining): (Entry, &[u8]) = postcard::take_from_bytes(bytes).unwrap();
entry
}

pub fn bench(c: &mut Criterion) {
run_encode_decode_bench(c, "postcard", postcard_encode, postcard_decode, Entry::create);
}
Loading
Loading