Skip to content

Commit 3d1d060

Browse files
authored
Optimize DAG-CBOR decode by reading Py_GetRecursionLimit once per call (#112)
1 parent d8bee00 commit 3d1d060

2 files changed

Lines changed: 26 additions & 14 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ test.py
3333
__pycache__
3434
.benchmarks
3535
.pytest_cache
36+
.codspeed
3637

3738
# Build artifacts
3839
*.so

src/lib.rs

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -349,23 +349,31 @@ where
349349
.ok_or_else(|| anyhow!("end of data"))
350350
}
351351

352+
// Snapshot `sys.getrecursionlimit()` once per top-level decode call and pass
353+
// it through. Calling `ffi::Py_GetRecursionLimit()` from the hot path costs
354+
// ~5–10 ns per recursive step, which dominates on scalar-dense payloads
355+
// (canada makes 111k+ recursive calls, one per float).
356+
#[inline]
357+
fn current_recursion_limit() -> usize {
358+
unsafe { ffi::Py_GetRecursionLimit() as usize }
359+
}
360+
352361
fn decode_dag_cbor_to_pyobject<'de, R: dec::Read<'de>>(
353362
py: Python,
354363
r: &mut R,
355364
depth: usize,
365+
max_depth: usize,
356366
) -> Result<Py<PyAny>>
357367
where
358368
R::Error: Send + Sync,
359369
{
360-
unsafe {
361-
if depth > ffi::Py_GetRecursionLimit() as usize {
362-
PyErr::new::<pyo3::exceptions::PyRecursionError, _>(
363-
"RecursionError: maximum recursion depth exceeded in DAG-CBOR decoding",
364-
)
365-
.restore(py);
370+
if depth > max_depth {
371+
PyErr::new::<pyo3::exceptions::PyRecursionError, _>(
372+
"RecursionError: maximum recursion depth exceeded in DAG-CBOR decoding",
373+
)
374+
.restore(py);
366375

367-
return Err(anyhow!("Maximum recursion depth exceeded"));
368-
}
376+
return Err(anyhow!("Maximum recursion depth exceeded"));
369377
}
370378

371379
let byte = peek_one(r)?;
@@ -398,7 +406,7 @@ where
398406
ffi::PyList_SET_ITEM(
399407
ptr,
400408
i,
401-
decode_dag_cbor_to_pyobject(py, r, depth + 1)?.into_ptr(),
409+
decode_dag_cbor_to_pyobject(py, r, depth + 1, max_depth)?.into_ptr(),
402410
);
403411
}
404412

@@ -437,7 +445,7 @@ where
437445
let (key_ptr, key_hash) = unsafe { key_cache::intern_key(py, key)? };
438446
let key_bound: Bound<'_, PyAny> = unsafe { Bound::from_owned_ptr(py, key_ptr) };
439447

440-
let value_py = decode_dag_cbor_to_pyobject(py, r, depth + 1)?;
448+
let value_py = decode_dag_cbor_to_pyobject(py, r, depth + 1, max_depth)?;
441449

442450
#[cfg(CPython)]
443451
unsafe {
@@ -714,9 +722,10 @@ where
714722
fn decode_dag_cbor_multi<'py>(py: Python<'py>, data: &[u8]) -> PyResult<Bound<'py, PyList>> {
715723
let mut reader = SliceReader::new(data);
716724
let decoded_parts = PyList::empty(py);
725+
let max_depth = current_recursion_limit();
717726

718727
loop {
719-
let py_object = decode_dag_cbor_to_pyobject(py, &mut reader, 0);
728+
let py_object = decode_dag_cbor_to_pyobject(py, &mut reader, 0, max_depth);
720729
if let Ok(py_object) = py_object {
721730
decoded_parts.append(py_object)?;
722731
} else {
@@ -766,14 +775,15 @@ where
766775
#[pyfunction]
767776
pub fn decode_car<'py>(py: Python<'py>, data: &[u8]) -> PyResult<(Py<PyAny>, Bound<'py, PyDict>)> {
768777
let buf = &mut SliceReader::new(data);
778+
let max_depth = current_recursion_limit();
769779

770780
if read_u64_leb128(buf).is_err() {
771781
return Err(get_err(
772782
"Failed to read CAR header",
773783
"Invalid uvarint".to_string(),
774784
));
775785
}
776-
let Ok(header_obj) = decode_dag_cbor_to_pyobject(py, buf, 0) else {
786+
let Ok(header_obj) = decode_dag_cbor_to_pyobject(py, buf, 0, max_depth) else {
777787
return Err(get_err(
778788
"Failed to read CAR header",
779789
"Invalid DAG-CBOR".to_string(),
@@ -841,7 +851,7 @@ pub fn decode_car<'py>(py: Python<'py>, data: &[u8]) -> PyResult<(Py<PyAny>, Bou
841851
buf.advance(consumed);
842852
let cid_raw = &cid_bytes_before[..consumed];
843853

844-
let block_result = decode_dag_cbor_to_pyobject(py, buf, 0);
854+
let block_result = decode_dag_cbor_to_pyobject(py, buf, 0, max_depth);
845855
let Ok(block) = block_result else {
846856
return Err(get_err(
847857
"Failed to read CAR block",
@@ -859,7 +869,8 @@ pub fn decode_car<'py>(py: Python<'py>, data: &[u8]) -> PyResult<(Py<PyAny>, Bou
859869
#[pyfunction]
860870
pub fn decode_dag_cbor(py: Python, data: &[u8]) -> PyResult<Py<PyAny>> {
861871
let mut reader = SliceReader::new(data);
862-
let py_object = decode_dag_cbor_to_pyobject(py, &mut reader, 0);
872+
let max_depth = current_recursion_limit();
873+
let py_object = decode_dag_cbor_to_pyobject(py, &mut reader, 0, max_depth);
863874
if let Ok(py_object) = py_object {
864875
// check for any remaining data in the reader
865876
if reader.fill(1)?.as_ref().is_empty() {

0 commit comments

Comments
 (0)