Skip to content

Commit b49e848

Browse files
authored
Merge pull request #16 from lpgauth/perf/consume-timeslice
Add enif_consume_timeslice to normal-scheduler NIFs
2 parents 6bb6f5c + 7bc2da7 commit b49e848

4 files changed

Lines changed: 71 additions & 45 deletions

File tree

README.md

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Torque provides the fastest JSON encoding and decoding available in the BEAM eco
1010
- Ultra-low memory encoder (64 B per encode vs ~4 KB for OTP `json`/jason)
1111
- Parse-then-get API for selective field extraction via JSON Pointer (RFC 6901)
1212
- Batch field extraction (`get_many/2`) with single NIF call
13-
- Automatic dirty CPU scheduler dispatch for large inputs
13+
- Automatic dirty CPU scheduler dispatch for inputs larger than 20 KB
1414
- jiffy-compatible `{proplist}` encoding
1515

1616
## Installation
@@ -147,64 +147,64 @@ Apple M2 Pro, OTP 28, Elixir 1.19:
147147

148148
| Library | ips | mean | median | p99 | memory |
149149
|---|---|---|---|---|---|
150-
| torque | **264.1K** | **3.79 μs** | **3.63 μs** | **6.83 μs** | **1.56 KB** |
151-
| simdjsone | 182.8K | 5.47 μs | 5.21 μs | 10.00 μs | 1.59 KB |
152-
| jiffy | 145.1K | 6.89 μs | 6.25 μs | 14.50 μs | **1.56 KB** |
153-
| otp json | 132.5K | 7.55 μs | 7.25 μs | 13.21 μs | 7.73 KB |
154-
| jason | 107.9K | 9.26 μs | 8.58 μs | 19.25 μs | 9.54 KB |
150+
| torque | **271.9K** | **3.68 μs** | **3.54 μs** | **6.54 μs** | **1.56 KB** |
151+
| simdjsone | 188.9K | 5.29 μs | 5.00 μs | 9.63 μs | 1.59 KB |
152+
| jiffy | 152.4K | 6.56 μs | 5.79 μs | 15.79 μs | **1.56 KB** |
153+
| otp json | 135.0K | 7.41 μs | 7.04 μs | 13.54 μs | 7.73 KB |
154+
| jason | 106.6K | 9.38 μs | 8.54 μs | 21.75 μs | 9.54 KB |
155155

156156
### Decode (750 KB JSON)
157157

158158
| Library | ips | mean | median | p99 | memory |
159159
|---|---|---|---|---|---|
160-
| torque | **536.0** | **1.87 ms** | **1.67 ms** | **2.22 ms** | **1.56 KB** |
161-
| simdjsone | 435.1 | 2.30 ms | 1.80 ms | 3.31 ms | **1.56 KB** |
162-
| otp json | 199.4 | 5.02 ms | 5.07 ms | 5.83 ms | 2.49 MB |
163-
| jason | 147.2 | 6.80 ms | 6.77 ms | 7.17 ms | 3.55 MB |
164-
| jiffy | 116.6 | 8.58 ms | 8.69 ms | 9.78 ms | 5.53 MB |
160+
| torque | **513.5** | **1.95 ms** | **1.80 ms** | **2.51 ms** | **1.56 KB** |
161+
| simdjsone | 448.9 | 2.23 ms | 1.89 ms | 3.33 ms | **1.56 KB** |
162+
| otp json | 191.5 | 5.22 ms | 5.20 ms | 6.15 ms | 2.49 MB |
163+
| jason | 142.2 | 7.03 ms | 7.01 ms | 7.70 ms | 3.55 MB |
164+
| jiffy | 112.4 | 8.90 ms | 8.92 ms | 10.00 ms | 5.53 MB |
165165

166166
### Encode (1.2 KB JSON)
167167

168168
| Library | ips | mean | median | p99 | memory |
169169
|---|---|---|---|---|---|
170-
| torque: proplist => binary | **1075.2K** | **0.93 μs** | **0.88 μs** | **1.04 μs** | 88 B |
171-
| otp json: map => iodata | 1074.0K | 0.93 μs | **0.88 μs** | 1.29 μs | 3928 B |
172-
| torque: proplist => iodata | 1069.6K | 0.93 μs | **0.88 μs** | 1.08 μs | **64 B** |
173-
| torque: map => iodata | 924.8K | 1.08 μs | 1.04 μs | 1.21 μs | **64 B** |
174-
| torque: map => binary | 914.6K | 1.09 μs | 1.04 μs | 1.25 μs | 88 B |
175-
| otp json: map => binary | 584.0K | 1.71 μs | 1.58 μs | 3.04 μs | 3992 B |
176-
| jiffy: proplist => iodata | 523.8K | 1.91 μs | 1.71 μs | 2.25 μs | 120 B |
177-
| jason: map => iodata | 515.0K | 1.94 μs | 1.54 μs | 14.17 μs | 3848 B |
178-
| jiffy: map => iodata | 431.4K | 2.32 μs | 2.13 μs | 2.83 μs | 824 B |
179-
| simdjsone: proplist => iodata | 424.3K | 2.36 μs | 2.25 μs | 2.83 μs | 184 B |
180-
| jason: map => binary | 352.6K | 2.84 μs | 2.42 μs | 15.13 μs | 3912 B |
181-
| simdjsone: map => iodata | 352.2K | 2.84 μs | 2.71 μs | 3.46 μs | 888 B |
170+
| torque: proplist => binary | **1243.5K** | **0.80 μs** | **0.71 μs** | **1.00 μs** | 88 B |
171+
| torque: proplist => iodata | 1242.6K | 0.80 μs | **0.71 μs** | **1.00 μs** | **64 B** |
172+
| torque: map => iodata | 1046.0K | 0.96 μs | 0.88 μs | 1.17 μs | **64 B** |
173+
| torque: map => binary | 1044.8K | 0.96 μs | 0.88 μs | 1.17 μs | 88 B |
174+
| otp json: map => iodata | 943.3K | 1.06 μs | 0.83 μs | 10.37 μs | 3928 B |
175+
| jason: map => iodata | 625.3K | 1.60 μs | 1.50 μs | 2.63 μs | 3848 B |
176+
| jiffy: proplist => iodata | 563.3K | 1.78 μs | 1.50 μs | 3.83 μs | 120 B |
177+
| otp json: map => binary | 559.1K | 1.79 μs | 1.54 μs | 5.13 μs | 3992 B |
178+
| jiffy: map => iodata | 493.5K | 2.03 μs | 1.83 μs | 2.54 μs | 824 B |
179+
| simdjsone: proplist => iodata | 458.1K | 2.18 μs | 2.04 μs | 2.71 μs | 184 B |
180+
| jason: map => binary | 384.3K | 2.60 μs | 2.38 μs | 6.04 μs | 3912 B |
181+
| simdjsone: map => iodata | 379.8K | 2.63 μs | 2.38 μs | 5.79 μs | 888 B |
182182

183183
### Encode (750 KB JSON)
184184

185185
| Library | ips | mean | median | p99 | memory |
186186
|---|---|---|---|---|---|
187-
| torque: proplist => iodata | **1058.7** | **0.94 ms** | **0.93 ms** | **1.06 ms** | **64 B** |
188-
| torque: proplist => binary | 1048.4 | 0.95 ms | 0.93 ms | 1.09 ms | 88 B |
189-
| torque: map => iodata | 937.0 | 1.07 ms | 1.05 ms | 1.21 ms | **64 B** |
190-
| torque: map => binary | 933.8 | 1.07 ms | 1.05 ms | 1.22 ms | 88 B |
191-
| jiffy: proplist => iodata | 341.4 | 2.93 ms | 2.87 ms | 4.41 ms | 37.7 KB |
192-
| otp json: map => iodata | 278.9 | 3.59 ms | 3.66 ms | 4.69 ms | 5.40 MB |
193-
| jiffy: map => iodata | 275.8 | 3.63 ms | 3.74 ms | 4.17 ms | 1.06 MB |
194-
| jason: map => iodata | 268.0 | 3.73 ms | 3.43 ms | 5.65 ms | 4.96 MB |
195-
| simdjsone: proplist => iodata | 249.4 | 4.01 ms | 3.95 ms | 5.58 ms | 37.7 KB |
196-
| simdjsone: map => iodata | 211.6 | 4.73 ms | 4.81 ms | 5.44 ms | 1.06 MB |
197-
| otp json: map => binary | 210.7 | 4.75 ms | 4.85 ms | 6.57 ms | 5.40 MB |
198-
| jason: map => binary | 137.9 | 7.25 ms | 6.84 ms | 8.85 ms | 4.96 MB |
187+
| torque: proplist => iodata | **1280.9** | **0.78 ms** | **0.76 ms** | **0.96 ms** | **64 B** |
188+
| torque: proplist => binary | 1270.8 | 0.79 ms | **0.76 ms** | 1.01 ms | 88 B |
189+
| torque: map => iodata | 1109.3 | 0.90 ms | 0.89 ms | 1.10 ms | **64 B** |
190+
| torque: map => binary | 1078.4 | 0.93 ms | 0.90 ms | 1.46 ms | 88 B |
191+
| jiffy: proplist => iodata | 329.1 | 3.04 ms | 2.78 ms | 7.86 ms | 37.7 KB |
192+
| jiffy: map => iodata | 285.8 | 3.50 ms | 3.56 ms | 4.18 ms | 1.06 MB |
193+
| otp json: map => iodata | 263.5 | 3.80 ms | 3.97 ms | 4.91 ms | 5.40 MB |
194+
| simdjsone: proplist => iodata | 257.8 | 3.88 ms | 3.79 ms | 6.54 ms | 37.7 KB |
195+
| jason: map => iodata | 255.5 | 3.91 ms | 3.74 ms | 6.04 ms | 4.96 MB |
196+
| simdjsone: map => iodata | 225.3 | 4.44 ms | 4.31 ms | 5.34 ms | 1.06 MB |
197+
| otp json: map => binary | 180.1 | 5.55 ms | 5.58 ms | 13.46 ms | 5.40 MB |
198+
| jason: map => binary | 141.4 | 7.07 ms | 7.03 ms | 7.94 ms | 4.96 MB |
199199

200200
### Parse + Get (1.2 KB JSON)
201201

202202
| Library | ips | mean | median | p99 | memory |
203203
|---|---|---|---|---|---|
204-
| torque parse+get_many_nil | **493.2K** | **2.03 μs** | **1.75 μs** | **3.21 μs** | **288 B** |
205-
| torque parse+get_many | 472.3K | 2.12 μs | 1.79 μs | 3.67 μs | 408 B |
206-
| torque parse+get | 428.3K | 2.34 μs | 2.00 μs | 5.75 μs | 432 B |
207-
| simdjsone parse+get | 385.0K | 2.60 μs | 1.79 μs | 6.71 μs | 408 B |
204+
| torque parse+get_many_nil | **478.4K** | **2.09 μs** | **1.75 μs** | **4.83 μs** | **288 B** |
205+
| torque parse+get_many | 471.8K | 2.12 μs | 1.75 μs | 4.83 μs | 408 B |
206+
| torque parse+get | 445.9K | 2.24 μs | 1.96 μs | 5.88 μs | 432 B |
207+
| simdjsone parse+get | 445.5K | 2.24 μs | 1.67 μs | 5.17 μs | 408 B |
208208

209209
Run benchmarks locally:
210210

lib/torque.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ defmodule Torque do
1717
keys), lists, binaries, numbers, booleans, `nil`, and jiffy-style
1818
`{proplist}` tuples.
1919
20-
Inputs larger than 10 KB are automatically scheduled on a dirty CPU scheduler
20+
Inputs larger than 20 KB are automatically scheduled on a dirty CPU scheduler
2121
to avoid blocking normal BEAM schedulers.
2222
"""
2323

24-
@timeslice_bytes 10_240
24+
@timeslice_bytes 20_480
2525

2626
# --- Decoding ---
2727

native/torque_nif/src/decoder.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ use crate::serde_decode;
44
use crate::types::{value_to_term, MAX_DEPTH};
55
use crate::ParsedDocument;
66
use rustler::sys::{enif_make_list_from_array, ERL_NIF_TERM};
7-
use rustler::{Binary, Encoder, Env, ListIterator, ResourceArc, Term};
7+
use rustler::{schedule, Binary, Encoder, Env, ListIterator, ResourceArc, Term};
88
use sonic_rs::{JsonContainerTrait, JsonValueTrait};
99

1010
const GET_MANY_STACK: usize = 64;
11+
const BYTES_PER_REDUCTION: usize = 20;
12+
const REDUCTION_COUNT: usize = 4000;
13+
14+
/// Compute a timeslice percentage (1–100) proportional to bytes processed.
15+
#[inline]
16+
fn timeslice_percent(bytes: usize) -> i32 {
17+
let reds = bytes / BYTES_PER_REDUCTION;
18+
((reds * 100 / REDUCTION_COUNT) as i32).clamp(1, 100)
19+
}
1120

1221
/// Returns the last value for `key` in an object, matching the last-value-wins
1322
/// behaviour of `value_to_term` / `build_map_dedup` for duplicate keys.
@@ -97,7 +106,10 @@ fn do_parse(bytes: &[u8]) -> Result<ResourceArc<ParsedDocument>, String> {
97106
#[rustler::nif]
98107
fn parse<'a>(env: Env<'a>, json: Binary) -> Term<'a> {
99108
match do_parse(json.as_slice()) {
100-
Ok(resource) => make_tuple2(env, atoms::ok().as_c_arg(), resource.encode(env).as_c_arg()),
109+
Ok(resource) => {
110+
schedule::consume_timeslice(env, timeslice_percent(json.len()));
111+
make_tuple2(env, atoms::ok().as_c_arg(), resource.encode(env).as_c_arg())
112+
}
101113
Err(reason) => make_tuple2(
102114
env,
103115
atoms::error().as_c_arg(),
@@ -234,7 +246,9 @@ fn array_length<'a>(env: Env<'a>, doc: ResourceArc<ParsedDocument>, path: &str)
234246
#[rustler::nif]
235247
fn decode<'a>(env: Env<'a>, json: Binary<'a>) -> Term<'a> {
236248
let input_term = json.encode(env).as_c_arg();
237-
serde_decode::decode_to_term(env, input_term, json.as_slice())
249+
let result = serde_decode::decode_to_term(env, input_term, json.as_slice());
250+
schedule::consume_timeslice(env, timeslice_percent(json.len()));
251+
result
238252
}
239253

240254
#[rustler::nif(schedule = "DirtyCpu")]

native/torque_nif/src/encoder.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,19 @@ use rustler::sys::{
66
enif_get_list_cell, enif_get_tuple, enif_get_uint64, enif_inspect_binary, ErlNifBinary,
77
ErlNifCharEncoding, ErlNifEnv, ERL_NIF_TERM,
88
};
9-
use rustler::{Env, MapIterator, NewBinary, Term, TermType};
9+
use rustler::{schedule, Env, MapIterator, NewBinary, Term, TermType};
1010
use std::mem::MaybeUninit;
1111

12+
const BYTES_PER_REDUCTION: usize = 20;
13+
const REDUCTION_COUNT: usize = 4000;
14+
15+
/// Compute a timeslice percentage (1–100) proportional to bytes processed.
16+
#[inline]
17+
fn timeslice_percent(bytes: usize) -> i32 {
18+
let reds = bytes / BYTES_PER_REDUCTION;
19+
((reds * 100 / REDUCTION_COUNT) as i32).clamp(1, 100)
20+
}
21+
1222
#[derive(Debug)]
1323
enum EncodeError {
1424
UnsupportedType,
@@ -56,6 +66,7 @@ fn encode<'a>(env: Env<'a>, term: Term<'a>) -> Term<'a> {
5666
let env_raw = env.as_c_arg();
5767
match encode_term(env, env_raw, term, &mut buf, MAX_DEPTH) {
5868
Ok(()) => {
69+
schedule::consume_timeslice(env, timeslice_percent(buf.len()));
5970
let mut binary = NewBinary::new(env, buf.len());
6071
binary.as_mut_slice().copy_from_slice(&buf);
6172
let bin_term: Term = binary.into();
@@ -83,6 +94,7 @@ fn encode_iodata<'a>(env: Env<'a>, term: Term<'a>) -> Term<'a> {
8394
let env_raw = env.as_c_arg();
8495
match encode_term(env, env_raw, term, &mut buf, MAX_DEPTH) {
8596
Ok(()) => {
97+
schedule::consume_timeslice(env, timeslice_percent(buf.len()));
8698
let mut binary = NewBinary::new(env, buf.len());
8799
binary.as_mut_slice().copy_from_slice(&buf);
88100
binary.into()

0 commit comments

Comments
 (0)