Skip to content

Commit bb5248e

Browse files
authored
feat: add borsh::object_length helper (#236)
* feat: add `borsh::object_length` helper * chore: add bench to illustrate diff * chore: check overflow on `usize` addition
1 parent d2c63ac commit bb5248e

File tree

6 files changed

+145
-2
lines changed

6 files changed

+145
-2
lines changed

benchmarks/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,9 @@ harness = false
3636
name = "maps_sets_inner_de"
3737
harness = false
3838

39+
[[bench]]
40+
name = "object_length"
41+
harness = false
42+
3943
[features]
4044
default = ["borsh/std", "borsh/derive"]

benchmarks/benches/object_length.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use benchmarks::{Generate, ValidatorStake};
2+
use borsh::{to_vec, BorshSerialize};
3+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
4+
use rand::SeedableRng;
5+
6+
fn ser_obj_length<T>(group_name: &str, num_samples: usize, c: &mut Criterion)
7+
where
8+
for<'a> T: Generate + BorshSerialize + 'static,
9+
{
10+
let mut rng = rand_xorshift::XorShiftRng::from_seed([0u8; 16]);
11+
let mut group = c.benchmark_group(group_name);
12+
13+
let objects: Vec<_> = (0..num_samples).map(|_| T::generate(&mut rng)).collect();
14+
let borsh_datas: Vec<Vec<u8>> = objects.iter().map(|t| to_vec(t).unwrap()).collect();
15+
let borsh_sizes: Vec<_> = borsh_datas.iter().map(|d| d.len()).collect();
16+
17+
for i in 0..objects.len() {
18+
let size = borsh_sizes[i];
19+
let obj = &objects[i];
20+
assert_eq!(
21+
borsh::to_vec(obj).unwrap().len(),
22+
borsh::object_length(obj).unwrap()
23+
);
24+
25+
let benchmark_param_display = format!("idx={}; size={}", i, size);
26+
27+
group.throughput(Throughput::Bytes(size as u64));
28+
group.bench_with_input(
29+
BenchmarkId::new(
30+
"borsh::to_vec(obj).unwrap().len()",
31+
benchmark_param_display.clone(),
32+
),
33+
obj,
34+
|b, d| {
35+
b.iter(|| borsh::to_vec(d).unwrap().len());
36+
},
37+
);
38+
group.bench_with_input(
39+
BenchmarkId::new(
40+
"borsh::object_length(obj).unwrap()",
41+
benchmark_param_display.clone(),
42+
),
43+
obj,
44+
|b, d| {
45+
b.iter(|| borsh::object_length(d).unwrap());
46+
},
47+
);
48+
}
49+
group.finish();
50+
}
51+
fn ser_length_validator_stake(c: &mut Criterion) {
52+
ser_obj_length::<ValidatorStake>("ser_account", 3, c);
53+
}
54+
criterion_group!(ser_length, ser_length_validator_stake,);
55+
criterion_main!(ser_length);

borsh/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ pub use schema::BorshSchema;
9393
pub use schema_helpers::{
9494
max_serialized_size, schema_container_of, try_from_slice_with_schema, try_to_vec_with_schema,
9595
};
96-
pub use ser::helpers::{to_vec, to_writer};
96+
pub use ser::helpers::{object_length, to_vec, to_writer};
9797
pub use ser::BorshSerialize;
9898
pub mod error;
9999

borsh/src/nostd_io.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ pub enum ErrorKind {
151151
/// particular number of bytes but only a smaller number of bytes could be
152152
/// read.
153153
UnexpectedEof,
154+
155+
/// An operation could not be completed, because it failed
156+
/// to allocate enough memory.
157+
OutOfMemory,
154158
}
155159

156160
impl ErrorKind {
@@ -174,6 +178,7 @@ impl ErrorKind {
174178
ErrorKind::Interrupted => "operation interrupted",
175179
ErrorKind::Other => "other os error",
176180
ErrorKind::UnexpectedEof => "unexpected end of file",
181+
ErrorKind::OutOfMemory => "out of memory",
177182
}
178183
}
179184
}

borsh/src/ser/helpers.rs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::BorshSerialize;
22
use crate::__private::maybestd::vec::Vec;
3-
use crate::io::{Result, Write};
3+
use crate::io::{ErrorKind, Result, Write};
44

55
pub(super) const DEFAULT_SERIALIZER_CAPACITY: usize = 1024;
66

@@ -21,3 +21,35 @@ where
2121
{
2222
value.serialize(&mut writer)
2323
}
24+
25+
/// Serializes an object without allocation to compute and return its length
26+
pub fn object_length<T>(value: &T) -> Result<usize>
27+
where
28+
T: BorshSerialize + ?Sized,
29+
{
30+
// copy-paste of solution provided by @matklad
31+
// in https://github.com/near/borsh-rs/issues/23#issuecomment-816633365
32+
struct LengthWriter {
33+
len: usize,
34+
}
35+
impl Write for LengthWriter {
36+
#[inline]
37+
fn write(&mut self, buf: &[u8]) -> Result<usize> {
38+
let res = self.len.checked_add(buf.len());
39+
self.len = match res {
40+
Some(res) => res,
41+
None => {
42+
return Err(ErrorKind::OutOfMemory.into());
43+
}
44+
};
45+
Ok(buf.len())
46+
}
47+
#[inline]
48+
fn flush(&mut self) -> Result<()> {
49+
Ok(())
50+
}
51+
}
52+
let mut w = LengthWriter { len: 0 };
53+
value.serialize(&mut w)?;
54+
Ok(w.len)
55+
}

borsh/tests/test_simple_structs.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,50 @@ fn test_ultimate_combined_all_features() {
176176
assert_eq!(decoded_f2.aa.len(), 2);
177177
assert!(decoded_f2.aa.iter().all(|f2_a| f2_a == &expected_a));
178178
}
179+
180+
#[test]
181+
fn test_object_length() {
182+
let mut map: BTreeMap<String, String> = BTreeMap::new();
183+
map.insert("test".into(), "test".into());
184+
let mut set: BTreeSet<u64> = BTreeSet::new();
185+
set.insert(u64::MAX);
186+
set.insert(100);
187+
set.insert(103);
188+
set.insert(109);
189+
let cow_arr = [
190+
borrow::Cow::Borrowed("Hello1"),
191+
borrow::Cow::Owned("Hello2".to_string()),
192+
];
193+
let a = A {
194+
x: 1,
195+
b: B {
196+
x: 2,
197+
y: 3,
198+
c: C::C5(D { x: 1 }),
199+
},
200+
y: 4.0,
201+
z: "123".to_string(),
202+
t: ("Hello".to_string(), 10),
203+
btree_map_string: map.clone(),
204+
btree_set_u64: set.clone(),
205+
linked_list_string: vec!["a".to_string(), "b".to_string()].into_iter().collect(),
206+
vec_deque_u64: vec![1, 2, 3].into_iter().collect(),
207+
bytes: vec![5, 4, 3, 2, 1].into(),
208+
bytes_mut: BytesMut::from(&[1, 2, 3, 4, 5][..]),
209+
v: vec!["qwe".to_string(), "zxc".to_string()],
210+
w: vec![0].into_boxed_slice(),
211+
box_str: Box::from("asd"),
212+
i: [4u8; 32],
213+
u: Ok("Hello".to_string()),
214+
lazy: Some(5),
215+
c: borrow::Cow::Borrowed("Hello"),
216+
cow_arr: borrow::Cow::Borrowed(&cow_arr),
217+
range_u32: 12..71,
218+
skipped: Some(6),
219+
};
220+
let encoded_a_len = to_vec(&a).unwrap().len();
221+
222+
let len_helper_result = borsh::object_length(&a).unwrap();
223+
224+
assert_eq!(encoded_a_len, len_helper_result);
225+
}

0 commit comments

Comments
 (0)