Skip to content

Commit 5f73831

Browse files
authored
Merge pull request #30 from lpgauth/encode-thread-local-buffer
Reuse a thread-local scratch buffer in the JSON encoder
2 parents b4bcee2 + a811127 commit 5f73831

1 file changed

Lines changed: 72 additions & 39 deletions

File tree

native/torque_nif/src/encoder.rs

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,29 @@ use rustler::sys::{
77
ErlNifCharEncoding, ErlNifEnv, ERL_NIF_TERM,
88
};
99
use rustler::{schedule, Env, MapIterator, NewBinary, Term, TermType};
10+
use std::cell::RefCell;
1011
use std::mem::MaybeUninit;
1112

1213
const BYTES_PER_REDUCTION: usize = 20;
1314
const REDUCTION_COUNT: usize = 4000;
1415

16+
/// Below this output size the work is sub-microsecond, so the
17+
/// enif_consume_timeslice call costs more than the scheduler accounting is
18+
/// worth. The BEAM's fixed per-NIF-call reduction charge already covers it.
19+
const TIMESLICE_MIN_BYTES: usize = 4096;
20+
21+
/// Cap on the retained thread-local scratch buffer, so a one-off huge document
22+
/// doesn't pin a large allocation on a scheduler thread indefinitely.
23+
const BUF_RETAIN_CAP: usize = 1 << 20;
24+
25+
thread_local! {
26+
/// Reused across encode calls on each scheduler thread. Avoids a
27+
/// malloc/free per call, which is the dominant per-call cost for small
28+
/// payloads. NIFs run to completion without preemption and the encoder
29+
/// never re-enters this NIF, so the borrow is never nested.
30+
static ENCODE_BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(2048));
31+
}
32+
1533
/// Compute a timeslice percentage (1–100) proportional to bytes processed.
1634
#[inline]
1735
fn timeslice_percent(bytes: usize) -> i32 {
@@ -29,6 +47,30 @@ enum EncodeError {
2947
InvalidUtf8,
3048
}
3149

50+
#[inline]
51+
fn error_reason(e: EncodeError) -> ERL_NIF_TERM {
52+
match e {
53+
EncodeError::DepthExceeded => atoms::nesting_too_deep().as_c_arg(),
54+
EncodeError::UnsupportedType => atoms::unsupported_type().as_c_arg(),
55+
EncodeError::NonFiniteFloat => atoms::non_finite_float().as_c_arg(),
56+
EncodeError::InvalidKey => atoms::invalid_key().as_c_arg(),
57+
EncodeError::MalformedProplist => atoms::malformed_proplist().as_c_arg(),
58+
EncodeError::InvalidUtf8 => atoms::invalid_utf8().as_c_arg(),
59+
}
60+
}
61+
62+
/// Hand the finished scratch buffer to a freshly allocated Erlang binary,
63+
/// reporting the work to the scheduler only when it's large enough to matter.
64+
#[inline]
65+
fn buf_to_binary<'a>(env: Env<'a>, buf: &[u8]) -> Term<'a> {
66+
if buf.len() >= TIMESLICE_MIN_BYTES {
67+
schedule::consume_timeslice(env, timeslice_percent(buf.len()));
68+
}
69+
let mut binary = NewBinary::new(env, buf.len());
70+
binary.as_mut_slice().copy_from_slice(buf);
71+
binary.into()
72+
}
73+
3274
/// Read an atom's name into a stack buffer without heap allocation.
3375
#[inline]
3476
unsafe fn atom_to_stack_buf(
@@ -62,55 +104,46 @@ unsafe fn atom_to_stack_buf(
62104

63105
#[rustler::nif]
64106
fn encode<'a>(env: Env<'a>, term: Term<'a>) -> Term<'a> {
65-
let mut buf: Vec<u8> = Vec::with_capacity(2048);
66107
let env_raw = env.as_c_arg();
67-
match encode_term(env, env_raw, term, &mut buf, MAX_DEPTH) {
68-
Ok(()) => {
69-
schedule::consume_timeslice(env, timeslice_percent(buf.len()));
70-
let mut binary = NewBinary::new(env, buf.len());
71-
binary.as_mut_slice().copy_from_slice(&buf);
72-
let bin_term: Term = binary.into();
73-
make_tuple2(env, atoms::ok().as_c_arg(), bin_term.as_c_arg())
74-
}
75-
Err(e) => {
76-
let reason = match e {
77-
EncodeError::DepthExceeded => atoms::nesting_too_deep().as_c_arg(),
78-
EncodeError::UnsupportedType => atoms::unsupported_type().as_c_arg(),
79-
EncodeError::NonFiniteFloat => atoms::non_finite_float().as_c_arg(),
80-
EncodeError::InvalidKey => atoms::invalid_key().as_c_arg(),
81-
EncodeError::MalformedProplist => atoms::malformed_proplist().as_c_arg(),
82-
EncodeError::InvalidUtf8 => atoms::invalid_utf8().as_c_arg(),
83-
};
84-
make_tuple2(env, atoms::error().as_c_arg(), reason)
108+
ENCODE_BUF.with(|cell| {
109+
let mut buf = cell.borrow_mut();
110+
buf.clear();
111+
let result = match encode_term(env, env_raw, term, &mut buf, MAX_DEPTH) {
112+
Ok(()) => {
113+
let bin_term = buf_to_binary(env, &buf);
114+
make_tuple2(env, atoms::ok().as_c_arg(), bin_term.as_c_arg())
115+
}
116+
Err(e) => make_tuple2(env, atoms::error().as_c_arg(), error_reason(e)),
117+
};
118+
if buf.capacity() > BUF_RETAIN_CAP {
119+
buf.shrink_to(BUF_RETAIN_CAP);
85120
}
86-
}
121+
result
122+
})
87123
}
88124

89125
/// Returns the raw binary on success, raises on error.
90126
/// Skips the {:ok, binary} tuple wrapping for maximum throughput.
91127
#[rustler::nif]
92128
fn encode_iodata<'a>(env: Env<'a>, term: Term<'a>) -> Term<'a> {
93-
let mut buf: Vec<u8> = Vec::with_capacity(2048);
94129
let env_raw = env.as_c_arg();
95-
match encode_term(env, env_raw, term, &mut buf, MAX_DEPTH) {
96-
Ok(()) => {
97-
schedule::consume_timeslice(env, timeslice_percent(buf.len()));
98-
let mut binary = NewBinary::new(env, buf.len());
99-
binary.as_mut_slice().copy_from_slice(&buf);
100-
binary.into()
130+
ENCODE_BUF.with(|cell| {
131+
let mut buf = cell.borrow_mut();
132+
buf.clear();
133+
let result = match encode_term(env, env_raw, term, &mut buf, MAX_DEPTH) {
134+
Ok(()) => buf_to_binary(env, &buf),
135+
Err(e) => unsafe {
136+
Term::new(
137+
env,
138+
rustler::sys::enif_raise_exception(env_raw, error_reason(e)),
139+
)
140+
},
141+
};
142+
if buf.capacity() > BUF_RETAIN_CAP {
143+
buf.shrink_to(BUF_RETAIN_CAP);
101144
}
102-
Err(e) => unsafe {
103-
let reason = match e {
104-
EncodeError::DepthExceeded => atoms::nesting_too_deep().as_c_arg(),
105-
EncodeError::UnsupportedType => atoms::unsupported_type().as_c_arg(),
106-
EncodeError::NonFiniteFloat => atoms::non_finite_float().as_c_arg(),
107-
EncodeError::InvalidKey => atoms::invalid_key().as_c_arg(),
108-
EncodeError::MalformedProplist => atoms::malformed_proplist().as_c_arg(),
109-
EncodeError::InvalidUtf8 => atoms::invalid_utf8().as_c_arg(),
110-
};
111-
Term::new(env, rustler::sys::enif_raise_exception(env_raw, reason))
112-
},
113-
}
145+
result
146+
})
114147
}
115148

116149
#[inline]

0 commit comments

Comments
 (0)