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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76bc800fd5065540397d957ca2162d2491474a086eda5b08436f016daa8f65e4
4
- data.tar.gz: 683259ac45cfb084722337af80c066890bdf56f2608843343ebf8951589eafa8
3
+ metadata.gz: 288c8f3951e7cd252310e271ed666996442d66d90f04fbdefc8fde57fc122826
4
+ data.tar.gz: d52fb5127d673914f553b47449dbf2b57b2d1b581c5e0636c40c5faf99141222
5
5
  SHA512:
6
- metadata.gz: 3b2be1bc057ee5a059e80ac087000afd2cee325473d834db34bd0f898318634e7b9be95dce9b523f909ae6884a56c9ede549a0e7142184eddf4cc96a13a11cb9
7
- data.tar.gz: e95683f3b475a0dca70488a43cdf23a249d2bcd49c440b4c75bf31914265c0eae9fd2992d1bf6c0ebfa5b0487b796f1a24505e42b4715840c1731ce2af845a4c
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
- JSON.parse min 0.670 med 0.966 mean 0.910 ms
75
- Yeptris::JSON min 0.637 med 0.674 mean 0.680 ms
76
- mean 0.75x faster, head-to-head 370/400 (92.5%)
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
- /* FNV-1a 64 cheap, good enough for short keys */
43
- uint64_t h = 14695981039346656037ull;
44
- for (long i = 0; i < sl; i++) {
45
- h ^= (unsigned char)sp[i];
46
- h *= 1099511628211ull;
47
- }
48
- h ^= (uint64_t)sl;
49
- return h;
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 (pn + 2 > pcap) {
144
- size_t ncap = pcap * 2;
145
- VALUE* nv = malloc(ncap * sizeof(VALUE));
146
- if (!nv) { j->err = -1; goto out; }
147
- memcpy(nv, pv, pn * sizeof(VALUE));
148
- if (pv != pairs) { free(pv); heap_cap = 1; }
149
- pv = nv; pcap = ncap;
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
- rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
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 = rb_gc_disable();
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.0".freeze
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.13.0
4
+ version: 0.1.13.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.