yeptris 0.1.13.0 → 0.1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +13 -3
- data/ext/yeptris_native/json_ruby.c +104 -23
- data/ext/yeptris_native/yeptris_native.c +84 -0
- data/lib/yeptris.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 288c8f3951e7cd252310e271ed666996442d66d90f04fbdefc8fde57fc122826
|
|
4
|
+
data.tar.gz: d52fb5127d673914f553b47449dbf2b57b2d1b581c5e0636c40c5faf99141222
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc3a2f57041c9f28ec5133a98cf8573cfd820f9451d321ae6d333e507b1f2b9d7ce51814a0380eadee799904e056d013eea70142ebb6031d9c7480762f91634c
|
|
7
|
+
data.tar.gz: a6f0f5990a19597c1ac060e0dbb5abe2c921cdbdc2e49ec1fe70d04bfa91609c36c7b114e0d4e294e1175613a710fab722b1e524f6a1e5cd73f0340fbc2ead31
|
data/README.adoc
CHANGED
|
@@ -71,10 +71,20 @@ fastest first:
|
|
|
71
71
|
~29k-value corpus, Ruby 3.4:
|
|
72
72
|
+
|
|
73
73
|
....
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
CI referee (gated at 1.05 on both platforms, order-alternating
|
|
75
|
+
interleave, N=200; prefix-hashed token cache):
|
|
76
|
+
ubuntu (x86_64) mean 0.745x vs JSON.parse, 200/200 head-to-head
|
|
77
|
+
macos (arm64) mean 0.759x vs JSON.parse, 169/200 head-to-head
|
|
77
78
|
....
|
|
79
|
+
|
|
80
|
+
The parse-window GC strategy is per-arch and runtime-switchable:
|
|
81
|
+
`Yeptris::Native.gc_mode` / `YEPTRIS_NATIVE_GC` = `:none`
|
|
82
|
+
(steady-state: +0 heap pages, the stdlib's own GC cadence — the
|
|
83
|
+
default on arm64) or `:disable` (no GC during the parse — the
|
|
84
|
+
default on x86_64, faster where pages are free). On a LOADED x86_64
|
|
85
|
+
box prefer `:none`: `:disable` grows ~478 heap pages per 50 parses,
|
|
86
|
+
and page growth under contention is exactly the loaded-box
|
|
87
|
+
regression. `:start` (in-window collection) measured 8x — dead.
|
|
78
88
|
2. **Record-drain fallback** (always available): the strict-JSON
|
|
79
89
|
validator gates, then the value records convert without the Psych
|
|
80
90
|
quirk table — exact parity with engine 1, spec-pinned.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#include <stdlib.h>
|
|
6
6
|
#include <string.h>
|
|
7
7
|
#include "parse/scalars.h"
|
|
8
|
+
#include <yeptris/visit.h>
|
|
8
9
|
#include "scan/json.h"
|
|
9
10
|
|
|
10
11
|
#define YEP_JR_MAX 1000
|
|
@@ -38,15 +39,26 @@ static void jr_ws(jr* j) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/* Token-cache knob (TODO.restructure/37): off = fresh strings
|
|
43
|
+
* everywhere (JSON.parse's shape: every key pays its own aset
|
|
44
|
+
* hashing); on = interned keys/tokens (shared frozen VALUEs
|
|
45
|
+
* memoize their hash). The CI referee A/Bs the net sign per arch. */
|
|
46
|
+
enum { YEP_CACHE_ON = 0, YEP_CACHE_OFF = 1 };
|
|
47
|
+
static int yep_cache_mode = YEP_CACHE_ON;
|
|
48
|
+
|
|
41
49
|
static uint64_t jr_hash(const char* sp, long sl) {
|
|
42
|
-
/*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
/* 8-byte-prefix key (the leptris nametab trick): one safe load
|
|
51
|
+
* of min(8, len) bytes + a multiply mix. Replaced byte-wise FNV
|
|
52
|
+
* (~15-20ns per token on the cached path, ~15k tokens per
|
|
53
|
+
* reference-corpus parse - the x86 materialization cost the
|
|
54
|
+
* decomposition isolated, TODO.restructure/37/38). */
|
|
55
|
+
uint64_t k = 0;
|
|
56
|
+
uint64_t take = (uint64_t)sl < 8 ? (uint64_t)sl : 8;
|
|
57
|
+
memcpy(&k, sp, (size_t)take);
|
|
58
|
+
k ^= (uint64_t)sl * 0x9E3779B97F4A7C15ull;
|
|
59
|
+
k *= 0xC2B2AE3D27D4EB4Full;
|
|
60
|
+
k ^= k >> 29;
|
|
61
|
+
return k;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
static VALUE jr_cached(jr* j, const char* sp, long sl) {
|
|
@@ -86,7 +98,7 @@ static VALUE jr_str(jr* j, int as_key) {
|
|
|
86
98
|
sp = j->p + start + 1;
|
|
87
99
|
sl = (long)(close - start - 1);
|
|
88
100
|
}
|
|
89
|
-
if (as_key || sl <= 24) return jr_cached(j, sp, sl);
|
|
101
|
+
if (yep_cache_mode == YEP_CACHE_ON && (as_key || sl <= 24)) return jr_cached(j, sp, sl);
|
|
90
102
|
return rb_enc_str_new(sp, sl, j->enc);
|
|
91
103
|
}
|
|
92
104
|
|
|
@@ -119,14 +131,18 @@ static VALUE jr_num(jr* j) {
|
|
|
119
131
|
return DBL2NUM(dv);
|
|
120
132
|
}
|
|
121
133
|
|
|
134
|
+
/* Insert strategy (TODO.restructure/34): bulk lands all pairs in one
|
|
135
|
+
* rb_hash_bulk_insert (skips per-pair dispatch) but costs +1.4k
|
|
136
|
+
* intermediate allocations on the reference corpus — the CI referee
|
|
137
|
+
* rules per platform. aset = the per-pair rb_hash_aset loop. */
|
|
138
|
+
enum { YEP_INS_BULK = 0, YEP_INS_ASET = 1 };
|
|
139
|
+
static int yep_ins_mode = YEP_INS_BULK;
|
|
140
|
+
|
|
122
141
|
static VALUE jr_object(jr* j) {
|
|
123
142
|
j->i++; j->depth++;
|
|
124
143
|
VALUE h = rb_hash_new_capa(8);
|
|
125
144
|
jr_ws(j);
|
|
126
145
|
if (j->i < j->len && j->p[j->i] == '}') { j->i++; j->depth--; return h; }
|
|
127
|
-
/* pairs collect into a flat buffer and land in ONE bulk insert:
|
|
128
|
-
* rb_hash_bulk_insert skips the per-pair method dispatch the
|
|
129
|
-
* aset loop pays (TODO.restructure/26's margin work) */
|
|
130
146
|
VALUE pairs[64];
|
|
131
147
|
VALUE* pv = pairs;
|
|
132
148
|
size_t pcap = 64, pn = 0, heap_cap = 0;
|
|
@@ -140,23 +156,29 @@ static VALUE jr_object(jr* j) {
|
|
|
140
156
|
j->i++;
|
|
141
157
|
VALUE val = jr_value(j);
|
|
142
158
|
if (j->err) goto out;
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
if (yep_ins_mode == YEP_INS_ASET) {
|
|
160
|
+
rb_hash_aset(h, key, val);
|
|
161
|
+
} else {
|
|
162
|
+
if (pn + 2 > pcap) {
|
|
163
|
+
size_t ncap = pcap * 2;
|
|
164
|
+
VALUE* nv = malloc(ncap * sizeof(VALUE));
|
|
165
|
+
if (!nv) { j->err = -1; goto out; }
|
|
166
|
+
memcpy(nv, pv, pn * sizeof(VALUE));
|
|
167
|
+
if (pv != pairs) { free(pv); heap_cap = 1; }
|
|
168
|
+
pv = nv; pcap = ncap;
|
|
169
|
+
}
|
|
170
|
+
pv[pn++] = key;
|
|
171
|
+
pv[pn++] = val;
|
|
150
172
|
}
|
|
151
|
-
pv[pn++] = key;
|
|
152
|
-
pv[pn++] = val;
|
|
153
173
|
jr_ws(j);
|
|
154
174
|
if (j->i >= j->len) { j->err = -2; goto out; }
|
|
155
175
|
if (j->p[j->i] == ',') { j->i++; continue; }
|
|
156
176
|
if (j->p[j->i] == '}') { j->i++; break; }
|
|
157
177
|
j->err = -2; goto out;
|
|
158
178
|
}
|
|
159
|
-
|
|
179
|
+
if (yep_ins_mode == YEP_INS_BULK) {
|
|
180
|
+
rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
|
|
181
|
+
}
|
|
160
182
|
out:
|
|
161
183
|
if (pv != pairs) { free(pv); (void)heap_cap; }
|
|
162
184
|
if (j->err) return Qnil;
|
|
@@ -205,16 +227,75 @@ static VALUE jr_value(jr* j) {
|
|
|
205
227
|
j->err = -2; return Qnil;
|
|
206
228
|
}
|
|
207
229
|
|
|
230
|
+
/* GC strategy for the parse window (TODO.restructure/34): JSON.parse
|
|
231
|
+
* pays minor GCs mid-parse and recycles slots continuously; a blanket
|
|
232
|
+
* disable defers every collection — fresh pages each iteration, which
|
|
233
|
+
* is cheap idle and expensive exactly under memory contention (the
|
|
234
|
+
* loaded-box regression). The strategy is a runtime choice so the CI
|
|
235
|
+
* referee can A/B without rebuilds:
|
|
236
|
+
* disable (default) — pause GC for the window
|
|
237
|
+
* none — never pause
|
|
238
|
+
* start — pause, then one gc_start before returning
|
|
239
|
+
* (pay the minor GC in-window, like JSON.parse)
|
|
240
|
+
*/
|
|
241
|
+
enum { YEP_GC_DISABLE = 0, YEP_GC_NONE = 1, YEP_GC_START = 2 };
|
|
242
|
+
/* Default per arch (TODO.restructure/35, two CI rounds of evidence):
|
|
243
|
+
* - aarch64/darwin: NONE — +0 heap pages, the stdlib's own GC cadence
|
|
244
|
+
* (0.745x mean, h2h 91% on mac runners; disable was 0.899x/48%).
|
|
245
|
+
* - x86_64: DISABLE — fresh CI VMs have free pages and cheaper
|
|
246
|
+
* page faults than minor GCs (0.911-0.922x vs none's 1.06-1.20x).
|
|
247
|
+
* The mechanism cuts both ways under load: disable's +478 pages/50
|
|
248
|
+
* iters is exactly what a LOADED x86 box punishes — set
|
|
249
|
+
* YEPTRIS_NATIVE_GC=none there (documented in README). */
|
|
250
|
+
#if defined(__aarch64__) || defined(__arm__) || defined(__ARMEL__) || defined(_M_ARM64)
|
|
251
|
+
#define YEP_GC_DEFAULT YEP_GC_NONE
|
|
252
|
+
#else
|
|
253
|
+
#define YEP_GC_DEFAULT YEP_GC_DISABLE
|
|
254
|
+
#endif
|
|
255
|
+
static int yep_gc_mode = YEP_GC_DEFAULT;
|
|
256
|
+
|
|
257
|
+
int yep_rb_gc_mode(void) { return yep_gc_mode; }
|
|
258
|
+
int yep_rb_ins_mode(void) { return yep_ins_mode; }
|
|
259
|
+
int yep_rb_cache_mode(void) { return yep_cache_mode; }
|
|
260
|
+
void yep_rb_set_cache_mode(int mode) {
|
|
261
|
+
if (mode == YEP_CACHE_ON || mode == YEP_CACHE_OFF) yep_cache_mode = mode;
|
|
262
|
+
}
|
|
263
|
+
void yep_rb_set_ins_mode(int mode) {
|
|
264
|
+
if (mode == YEP_INS_BULK || mode == YEP_INS_ASET) yep_ins_mode = mode;
|
|
265
|
+
}
|
|
266
|
+
void yep_rb_set_gc_mode(int mode) {
|
|
267
|
+
if (mode >= YEP_GC_DISABLE && mode <= YEP_GC_START) {
|
|
268
|
+
yep_gc_mode = mode;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* Pure grammar walk, no materialization (TODO.restructure/37): the
|
|
273
|
+
* same scan kernels through the null vtable. Decomposes scan vs
|
|
274
|
+
* materialize cost. Returns seconds for n iterations. */
|
|
275
|
+
#include <time.h>
|
|
276
|
+
double yep_rb_scan_time(const char* p, size_t len, int n) {
|
|
277
|
+
static const YeptrisVisitVTable none = {0};
|
|
278
|
+
struct timespec t0, t1;
|
|
279
|
+
clock_gettime(CLOCK_MONOTONIC, &t0);
|
|
280
|
+
for (int i = 0; i < n; i++) {
|
|
281
|
+
(void)yeptris_visit_json(p, len, &none, NULL);
|
|
282
|
+
}
|
|
283
|
+
clock_gettime(CLOCK_MONOTONIC, &t1);
|
|
284
|
+
return (double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_nsec - t0.tv_nsec) / 1e9;
|
|
285
|
+
}
|
|
286
|
+
|
|
208
287
|
VALUE yep_rb_parse_json(const char* p, size_t len) {
|
|
209
288
|
jr j;
|
|
210
289
|
memset(&j, 0, sizeof(j));
|
|
211
290
|
j.p = p; j.len = len; j.enc = rb_utf8_encoding();
|
|
212
|
-
VALUE already =
|
|
291
|
+
VALUE already = Qtrue;
|
|
292
|
+
if (yep_gc_mode != YEP_GC_NONE) already = rb_gc_disable();
|
|
213
293
|
VALUE v = jr_value(&j);
|
|
214
294
|
jr_ws(&j);
|
|
215
295
|
if (j.i != len && j.err == 0) j.err = -2;
|
|
216
296
|
free(j.scratch);
|
|
217
297
|
if (already == Qfalse) rb_gc_enable();
|
|
298
|
+
if (yep_gc_mode == YEP_GC_START && j.err == 0) rb_gc_start();
|
|
218
299
|
if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
|
|
219
300
|
if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
|
|
220
301
|
return v;
|
|
@@ -322,6 +322,75 @@ static VALUE native_load_stream(VALUE self, VALUE input, VALUE schema) {
|
|
|
322
322
|
return ctx_result(&s.inner, st == YEPTRIS_OK ? YEPTRIS_ERROR_INTERNAL : st);
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/* GC-strategy surface (TODO.restructure/34): ENV at load sets the
|
|
326
|
+
* default; the setter re-picks at runtime so the CI referee can A/B
|
|
327
|
+
* in-process. Symbols: :disable, :none, :start. */
|
|
328
|
+
extern int yep_rb_gc_mode(void);
|
|
329
|
+
extern void yep_rb_set_gc_mode(int mode);
|
|
330
|
+
extern int yep_rb_ins_mode(void);
|
|
331
|
+
extern void yep_rb_set_ins_mode(int mode);
|
|
332
|
+
extern int yep_rb_cache_mode(void);
|
|
333
|
+
extern void yep_rb_set_cache_mode(int mode);
|
|
334
|
+
extern double yep_rb_scan_time(const char* p, size_t len, int n);
|
|
335
|
+
|
|
336
|
+
static VALUE native_gc_mode(VALUE self) {
|
|
337
|
+
(void)self;
|
|
338
|
+
switch (yep_rb_gc_mode()) {
|
|
339
|
+
case 1: return ID2SYM(rb_intern("none"));
|
|
340
|
+
case 2: return ID2SYM(rb_intern("start"));
|
|
341
|
+
default: return ID2SYM(rb_intern("disable"));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
static VALUE native_ins_mode(VALUE self) {
|
|
346
|
+
(void)self;
|
|
347
|
+
return yep_rb_ins_mode() == 1 ? ID2SYM(rb_intern("aset")) : ID2SYM(rb_intern("bulk"));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
static VALUE native_ins_mode_set(VALUE self, VALUE mode) {
|
|
351
|
+
(void)self;
|
|
352
|
+
Check_Type(mode, T_SYMBOL);
|
|
353
|
+
ID id = rb_sym2id(mode);
|
|
354
|
+
if (id == rb_intern("bulk")) yep_rb_set_ins_mode(0);
|
|
355
|
+
else if (id == rb_intern("aset")) yep_rb_set_ins_mode(1);
|
|
356
|
+
else rb_raise(rb_eArgError, "ins_mode must be :bulk or :aset");
|
|
357
|
+
return mode;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
static VALUE native_cache_mode(VALUE self) {
|
|
361
|
+
(void)self;
|
|
362
|
+
return yep_rb_cache_mode() == 1 ? ID2SYM(rb_intern("off")) : ID2SYM(rb_intern("on"));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
static VALUE native_cache_mode_set(VALUE self, VALUE mode) {
|
|
366
|
+
(void)self;
|
|
367
|
+
Check_Type(mode, T_SYMBOL);
|
|
368
|
+
ID id = rb_sym2id(mode);
|
|
369
|
+
if (id == rb_intern("on")) yep_rb_set_cache_mode(0);
|
|
370
|
+
else if (id == rb_intern("off")) yep_rb_set_cache_mode(1);
|
|
371
|
+
else rb_raise(rb_eArgError, "cache_mode must be :on or :off");
|
|
372
|
+
return mode;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
static VALUE native_scan_time(VALUE self, VALUE input, VALUE count) {
|
|
376
|
+
(void)self;
|
|
377
|
+
StringValue(input);
|
|
378
|
+
int n = NUM2INT(count);
|
|
379
|
+
double secs = yep_rb_scan_time(RSTRING_PTR(input), (size_t)RSTRING_LEN(input), n);
|
|
380
|
+
return DBL2NUM(secs / (double)n);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
static VALUE native_gc_mode_set(VALUE self, VALUE mode) {
|
|
384
|
+
(void)self;
|
|
385
|
+
Check_Type(mode, T_SYMBOL);
|
|
386
|
+
ID id = rb_sym2id(mode);
|
|
387
|
+
if (id == rb_intern("disable")) yep_rb_set_gc_mode(0);
|
|
388
|
+
else if (id == rb_intern("none")) yep_rb_set_gc_mode(1);
|
|
389
|
+
else if (id == rb_intern("start")) yep_rb_set_gc_mode(2);
|
|
390
|
+
else rb_raise(rb_eArgError, "gc_mode must be :disable, :none, or :start");
|
|
391
|
+
return mode;
|
|
392
|
+
}
|
|
393
|
+
|
|
325
394
|
RUBY_FUNC_EXPORTED void Init_native(void) {
|
|
326
395
|
utf8_enc = rb_utf8_encoding();
|
|
327
396
|
VALUE mYep = rb_define_module("Yeptris");
|
|
@@ -329,5 +398,20 @@ RUBY_FUNC_EXPORTED void Init_native(void) {
|
|
|
329
398
|
rb_define_singleton_method(mNat, "load_json", native_load_json, 1);
|
|
330
399
|
rb_define_singleton_method(mNat, "load", native_load, 2);
|
|
331
400
|
rb_define_singleton_method(mNat, "load_stream", native_load_stream, 2);
|
|
401
|
+
rb_define_singleton_method(mNat, "gc_mode", native_gc_mode, 0);
|
|
402
|
+
rb_define_singleton_method(mNat, "gc_mode=", native_gc_mode_set, 1);
|
|
403
|
+
rb_define_singleton_method(mNat, "ins_mode", native_ins_mode, 0);
|
|
404
|
+
rb_define_singleton_method(mNat, "ins_mode=", native_ins_mode_set, 1);
|
|
405
|
+
rb_define_singleton_method(mNat, "cache_mode", native_cache_mode, 0);
|
|
406
|
+
rb_define_singleton_method(mNat, "cache_mode=", native_cache_mode_set, 1);
|
|
407
|
+
rb_define_singleton_method(mNat, "scan_time", native_scan_time, 2);
|
|
332
408
|
rb_define_const(mNat, "AVAILABLE", Qtrue);
|
|
409
|
+
const char* env = getenv("YEPTRIS_NATIVE_GC");
|
|
410
|
+
if (env != NULL && strcmp(env, "none") == 0) yep_rb_set_gc_mode(1);
|
|
411
|
+
else if (env != NULL && strcmp(env, "start") == 0) yep_rb_set_gc_mode(2);
|
|
412
|
+
else if (env != NULL && strcmp(env, "disable") == 0) yep_rb_set_gc_mode(0);
|
|
413
|
+
const char* ins = getenv("YEPTRIS_NATIVE_INSERT");
|
|
414
|
+
if (ins != NULL && strcmp(ins, "aset") == 0) yep_rb_set_ins_mode(1);
|
|
415
|
+
const char* cache = getenv("YEPTRIS_NATIVE_CACHE");
|
|
416
|
+
if (cache != NULL && strcmp(cache, "off") == 0) yep_rb_set_cache_mode(1);
|
|
333
417
|
}
|
data/lib/yeptris.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Yeptris
|
|
4
4
|
# The gem's version lives in the parent namespace's file — the last
|
|
5
5
|
# internal require (yeptris/version) retired with it.
|
|
6
|
-
VERSION = "0.1.13.
|
|
6
|
+
VERSION = "0.1.13.2".freeze
|
|
7
7
|
# The error hierarchy lives in THIS file (the parent namespace's
|
|
8
8
|
# own file): nested constants do not trigger a parent-constant
|
|
9
9
|
# autoload, and the law forbids internal requires — defining the
|