yeptris 0.1.13.0 → 0.1.13.1

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: 17b1311c9327a9986b7bb89f3731fd40ca040c49336fdb97596951fbf87ed1c0
4
+ data.tar.gz: 6c1b2f707d15d469963ffc0ee030fc90287a5e01954d7431996f24765bddf9e3
5
5
  SHA512:
6
- metadata.gz: 3b2be1bc057ee5a059e80ac087000afd2cee325473d834db34bd0f898318634e7b9be95dce9b523f909ae6884a56c9ede549a0e7142184eddf4cc96a13a11cb9
7
- data.tar.gz: e95683f3b475a0dca70488a43cdf23a249d2bcd49c440b4c75bf31914265c0eae9fd2992d1bf6c0ebfa5b0487b796f1a24505e42b4715840c1731ce2af845a4c
6
+ metadata.gz: b04ca9a9d899c00207d8cf09ab1d1aa071c1cc63f2adab17cf87453d0a0646fc84c9df9f3e80f245bbc977a32e67feb7f25e2b562293a59adc102764b80507dc
7
+ data.tar.gz: 25c9033573e619dc69924f4ae15c102b650efdbf7d39c1dcc4d3d73243d1d0ac3037750c7b28b5e6d28562175296fd4b125e39253f4ab9c9ed799529d7fcb4ee
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):
76
+ ubuntu (x86_64, gc=disable) mean 0.961x vs JSON.parse
77
+ macos (arm64, gc=none) mean 0.718x (head-to-head 173/200)
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.
@@ -119,14 +119,18 @@ static VALUE jr_num(jr* j) {
119
119
  return DBL2NUM(dv);
120
120
  }
121
121
 
122
+ /* Insert strategy (TODO.restructure/34): bulk lands all pairs in one
123
+ * rb_hash_bulk_insert (skips per-pair dispatch) but costs +1.4k
124
+ * intermediate allocations on the reference corpus — the CI referee
125
+ * rules per platform. aset = the per-pair rb_hash_aset loop. */
126
+ enum { YEP_INS_BULK = 0, YEP_INS_ASET = 1 };
127
+ static int yep_ins_mode = YEP_INS_BULK;
128
+
122
129
  static VALUE jr_object(jr* j) {
123
130
  j->i++; j->depth++;
124
131
  VALUE h = rb_hash_new_capa(8);
125
132
  jr_ws(j);
126
133
  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
134
  VALUE pairs[64];
131
135
  VALUE* pv = pairs;
132
136
  size_t pcap = 64, pn = 0, heap_cap = 0;
@@ -140,23 +144,29 @@ static VALUE jr_object(jr* j) {
140
144
  j->i++;
141
145
  VALUE val = jr_value(j);
142
146
  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;
147
+ if (yep_ins_mode == YEP_INS_ASET) {
148
+ rb_hash_aset(h, key, val);
149
+ } else {
150
+ if (pn + 2 > pcap) {
151
+ size_t ncap = pcap * 2;
152
+ VALUE* nv = malloc(ncap * sizeof(VALUE));
153
+ if (!nv) { j->err = -1; goto out; }
154
+ memcpy(nv, pv, pn * sizeof(VALUE));
155
+ if (pv != pairs) { free(pv); heap_cap = 1; }
156
+ pv = nv; pcap = ncap;
157
+ }
158
+ pv[pn++] = key;
159
+ pv[pn++] = val;
150
160
  }
151
- pv[pn++] = key;
152
- pv[pn++] = val;
153
161
  jr_ws(j);
154
162
  if (j->i >= j->len) { j->err = -2; goto out; }
155
163
  if (j->p[j->i] == ',') { j->i++; continue; }
156
164
  if (j->p[j->i] == '}') { j->i++; break; }
157
165
  j->err = -2; goto out;
158
166
  }
159
- rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
167
+ if (yep_ins_mode == YEP_INS_BULK) {
168
+ rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
169
+ }
160
170
  out:
161
171
  if (pv != pairs) { free(pv); (void)heap_cap; }
162
172
  if (j->err) return Qnil;
@@ -205,16 +215,56 @@ static VALUE jr_value(jr* j) {
205
215
  j->err = -2; return Qnil;
206
216
  }
207
217
 
218
+ /* GC strategy for the parse window (TODO.restructure/34): JSON.parse
219
+ * pays minor GCs mid-parse and recycles slots continuously; a blanket
220
+ * disable defers every collection — fresh pages each iteration, which
221
+ * is cheap idle and expensive exactly under memory contention (the
222
+ * loaded-box regression). The strategy is a runtime choice so the CI
223
+ * referee can A/B without rebuilds:
224
+ * disable (default) — pause GC for the window
225
+ * none — never pause
226
+ * start — pause, then one gc_start before returning
227
+ * (pay the minor GC in-window, like JSON.parse)
228
+ */
229
+ enum { YEP_GC_DISABLE = 0, YEP_GC_NONE = 1, YEP_GC_START = 2 };
230
+ /* Default per arch (TODO.restructure/35, two CI rounds of evidence):
231
+ * - aarch64/darwin: NONE — +0 heap pages, the stdlib's own GC cadence
232
+ * (0.745x mean, h2h 91% on mac runners; disable was 0.899x/48%).
233
+ * - x86_64: DISABLE — fresh CI VMs have free pages and cheaper
234
+ * page faults than minor GCs (0.911-0.922x vs none's 1.06-1.20x).
235
+ * The mechanism cuts both ways under load: disable's +478 pages/50
236
+ * iters is exactly what a LOADED x86 box punishes — set
237
+ * YEPTRIS_NATIVE_GC=none there (documented in README). */
238
+ #if defined(__aarch64__) || defined(__arm__) || defined(__ARMEL__) || defined(_M_ARM64)
239
+ #define YEP_GC_DEFAULT YEP_GC_NONE
240
+ #else
241
+ #define YEP_GC_DEFAULT YEP_GC_DISABLE
242
+ #endif
243
+ static int yep_gc_mode = YEP_GC_DEFAULT;
244
+
245
+ int yep_rb_gc_mode(void) { return yep_gc_mode; }
246
+ int yep_rb_ins_mode(void) { return yep_ins_mode; }
247
+ void yep_rb_set_ins_mode(int mode) {
248
+ if (mode == YEP_INS_BULK || mode == YEP_INS_ASET) yep_ins_mode = mode;
249
+ }
250
+ void yep_rb_set_gc_mode(int mode) {
251
+ if (mode >= YEP_GC_DISABLE && mode <= YEP_GC_START) {
252
+ yep_gc_mode = mode;
253
+ }
254
+ }
255
+
208
256
  VALUE yep_rb_parse_json(const char* p, size_t len) {
209
257
  jr j;
210
258
  memset(&j, 0, sizeof(j));
211
259
  j.p = p; j.len = len; j.enc = rb_utf8_encoding();
212
- VALUE already = rb_gc_disable();
260
+ VALUE already = Qtrue;
261
+ if (yep_gc_mode != YEP_GC_NONE) already = rb_gc_disable();
213
262
  VALUE v = jr_value(&j);
214
263
  jr_ws(&j);
215
264
  if (j.i != len && j.err == 0) j.err = -2;
216
265
  free(j.scratch);
217
266
  if (already == Qfalse) rb_gc_enable();
267
+ if (yep_gc_mode == YEP_GC_START && j.err == 0) rb_gc_start();
218
268
  if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
219
269
  if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
220
270
  return v;
@@ -322,6 +322,49 @@ 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
+
333
+ static VALUE native_gc_mode(VALUE self) {
334
+ (void)self;
335
+ switch (yep_rb_gc_mode()) {
336
+ case 1: return ID2SYM(rb_intern("none"));
337
+ case 2: return ID2SYM(rb_intern("start"));
338
+ default: return ID2SYM(rb_intern("disable"));
339
+ }
340
+ }
341
+
342
+ static VALUE native_ins_mode(VALUE self) {
343
+ (void)self;
344
+ return yep_rb_ins_mode() == 1 ? ID2SYM(rb_intern("aset")) : ID2SYM(rb_intern("bulk"));
345
+ }
346
+
347
+ static VALUE native_ins_mode_set(VALUE self, VALUE mode) {
348
+ (void)self;
349
+ Check_Type(mode, T_SYMBOL);
350
+ ID id = rb_sym2id(mode);
351
+ if (id == rb_intern("bulk")) yep_rb_set_ins_mode(0);
352
+ else if (id == rb_intern("aset")) yep_rb_set_ins_mode(1);
353
+ else rb_raise(rb_eArgError, "ins_mode must be :bulk or :aset");
354
+ return mode;
355
+ }
356
+
357
+ static VALUE native_gc_mode_set(VALUE self, VALUE mode) {
358
+ (void)self;
359
+ Check_Type(mode, T_SYMBOL);
360
+ ID id = rb_sym2id(mode);
361
+ if (id == rb_intern("disable")) yep_rb_set_gc_mode(0);
362
+ else if (id == rb_intern("none")) yep_rb_set_gc_mode(1);
363
+ else if (id == rb_intern("start")) yep_rb_set_gc_mode(2);
364
+ else rb_raise(rb_eArgError, "gc_mode must be :disable, :none, or :start");
365
+ return mode;
366
+ }
367
+
325
368
  RUBY_FUNC_EXPORTED void Init_native(void) {
326
369
  utf8_enc = rb_utf8_encoding();
327
370
  VALUE mYep = rb_define_module("Yeptris");
@@ -329,5 +372,15 @@ RUBY_FUNC_EXPORTED void Init_native(void) {
329
372
  rb_define_singleton_method(mNat, "load_json", native_load_json, 1);
330
373
  rb_define_singleton_method(mNat, "load", native_load, 2);
331
374
  rb_define_singleton_method(mNat, "load_stream", native_load_stream, 2);
375
+ rb_define_singleton_method(mNat, "gc_mode", native_gc_mode, 0);
376
+ rb_define_singleton_method(mNat, "gc_mode=", native_gc_mode_set, 1);
377
+ rb_define_singleton_method(mNat, "ins_mode", native_ins_mode, 0);
378
+ rb_define_singleton_method(mNat, "ins_mode=", native_ins_mode_set, 1);
332
379
  rb_define_const(mNat, "AVAILABLE", Qtrue);
380
+ const char* env = getenv("YEPTRIS_NATIVE_GC");
381
+ if (env != NULL && strcmp(env, "none") == 0) yep_rb_set_gc_mode(1);
382
+ else if (env != NULL && strcmp(env, "start") == 0) yep_rb_set_gc_mode(2);
383
+ else if (env != NULL && strcmp(env, "disable") == 0) yep_rb_set_gc_mode(0);
384
+ const char* ins = getenv("YEPTRIS_NATIVE_INSERT");
385
+ if (ins != NULL && strcmp(ins, "aset") == 0) yep_rb_set_ins_mode(1);
333
386
  }
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.1".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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.