teptris 0.2.51 → 0.2.52

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: 9378e4039e62c8ef1b8271ccf3b2cc8f07ad8db855b4cb399ce4725d25f1d17f
4
- data.tar.gz: b8012bdfb1f3b070f4a42e966da73d9541bce3376a879c252a801af6208b675e
3
+ metadata.gz: 2537428f82b791a55389c0be4bb2eaa2115f118238839705a913e51da11e6af1
4
+ data.tar.gz: a95d1d2b3f582803ef44c0bacbc1ec9172a9cf044c30cf56a488963f575c4007
5
5
  SHA512:
6
- metadata.gz: 588eb7a305d2c39495de417dd95dd091aa5a3bb1251e62b5feb9e5115c9a9740b202a2d3debc7e8a1934d1ec0459a1d8835c31e01675d5938f59c2be4367d3c9
7
- data.tar.gz: d21e70ded4f0441c8d2f9e5b7d5d7e18d0f6a12c9601f902f70dd4e11a98f154e9f5716cf97839043e2f6247b62af04e6187445523b1b783bd02f231907d0e23
6
+ metadata.gz: 2a8d04664b9cb79842b466a7bc4e877e167ff61aaa58258c2e2419363d601d75ad759913f439c3f722c0b3ceccee603548414dcbcf828aa169acd00793fad92c
7
+ data.tar.gz: 27633d7ecf115edbb86be8b669552b8bdd4ccc427aacd1c775dea017a4a143aa7df267a4dfb0ffa59de3f1e827f0056f6e6989f16e022ee808d277ee2e32bcc8
@@ -1,4 +1,5 @@
1
1
  #include <ruby.h>
2
+ #include <ruby/thread.h>
2
3
  #include <ruby/encoding.h>
3
4
  #include "teptris/teptris.h"
4
5
  #include "teptris/plan.h"
@@ -237,9 +238,36 @@ static VALUE obj_from_node(const teptris_node *n, unsigned flags,
237
238
  }}
238
239
  }
239
240
 
241
+ /* the parse is pure C over the buffer with no VM access: run it
242
+ * GVL-free so other threads progress during long parses (and parallel
243
+ * batch loads overlap). The string is locked (pinned) across the
244
+ * release so a concurrent compacting GC cannot relocate the buffer. */
245
+ struct gvl_parse_arg {
246
+ const char *data;
247
+ size_t len;
248
+ teptris_document **doc;
249
+ teptris_status st;
250
+ };
251
+
252
+ static void *parse_without_gvl(void *p)
253
+ {
254
+ struct gvl_parse_arg *a = (struct gvl_parse_arg *)p;
255
+ a->st = teptris_parse(a->data, a->len, NULL, a->doc);
256
+ return NULL;
257
+ }
258
+
240
259
  static teptris_document *parse_or_raise(VALUE str) {
241
260
  teptris_document *doc = NULL;
242
- teptris_status st = teptris_parse(RSTRING_PTR(str), (size_t)RSTRING_LEN(str), NULL, &doc);
261
+ /* frozen strings cannot take the tmp lock (ruby 4.0 raises
262
+ * FrozenError - chilled literals); a dup is movable-page-safe the
263
+ * same way, at one input copy for frozen callers only */
264
+ volatile VALUE pin = OBJ_FROZEN(str) ? rb_str_dup(str) : str;
265
+ rb_str_locktmp(pin);
266
+ struct gvl_parse_arg arg = {RSTRING_PTR(pin), (size_t)RSTRING_LEN(pin),
267
+ &doc, TEPTRIS_OK};
268
+ rb_thread_call_without_gvl(parse_without_gvl, &arg, RUBY_UBF_IO, NULL);
269
+ rb_str_unlocktmp(pin);
270
+ teptris_status st = arg.st;
243
271
  if (st != TEPTRIS_OK) {
244
272
  const teptris_error *e = teptris_document_error(doc);
245
273
  VALUE ex = rb_exc_new(eParseError, e->message, (long)strlen(e->message));
@@ -995,6 +1023,100 @@ static VALUE ext_load_lazy_batch(VALUE self, VALUE strings) {
995
1023
  * as ext_load. opts is parsed once; safe_load semantics apply uniformly.
996
1024
  * The caller's input Array is read-only; its strings stay referenced
997
1025
  * for the duration of the C call (no separate keeper Array). */
1026
+ /* slice-level GVL release: one thread parses K documents under a
1027
+ * single release (no per-document GVL ping-pong), then materializes
1028
+ * them under the GVL with one shared key cache (batch semantics).
1029
+ * All inputs are pinned (dup'd when frozen) across the release. */
1030
+ struct slice_parse_arg {
1031
+ long n;
1032
+ const char **data;
1033
+ size_t *lens;
1034
+ teptris_document **docs;
1035
+ teptris_status *stats;
1036
+ };
1037
+
1038
+ static void *slice_parse_without_gvl(void *p)
1039
+ {
1040
+ struct slice_parse_arg *a = (struct slice_parse_arg *)p;
1041
+ for (long i = 0; i < a->n; i++) {
1042
+ a->docs[i] = NULL;
1043
+ a->stats[i] = teptris_parse(a->data[i], a->lens[i], NULL, &a->docs[i]);
1044
+ }
1045
+ return NULL;
1046
+ }
1047
+
1048
+ static VALUE ext_load_slice(int argc, VALUE *argv, VALUE self) {
1049
+ VALUE strs, opts;
1050
+ rb_scan_args(argc, argv, "11", &strs, &opts);
1051
+ Check_Type(strs, T_ARRAY);
1052
+ long n = RARRAY_LEN(strs);
1053
+ if (n == 0) return rb_ary_new();
1054
+ unsigned flags = flags_from_opts(opts);
1055
+
1056
+ volatile VALUE *pins = ALLOC_N(VALUE, n);
1057
+ const char **data = ALLOC_N(const char *, n);
1058
+ size_t *lens = ALLOC_N(size_t, n);
1059
+ teptris_document **docs = ALLOC_N(teptris_document *, n);
1060
+ teptris_status *stats = ALLOC_N(teptris_status, n);
1061
+ /* the tmp lock is one-per-string: repeated objects in the slice
1062
+ * get a dup before pinning (frozen inputs dup for the same reason
1063
+ * - they cannot take the lock, yet compaction can move them) */
1064
+ VALUE seen = rb_hash_new();
1065
+ long i;
1066
+ for (i = 0; i < n; i++) {
1067
+ VALUE str = rb_ary_entry(strs, i);
1068
+ StringValue(str);
1069
+ VALUE id = rb_obj_id(str);
1070
+ if (!NIL_P(rb_hash_aref(seen, id))) {
1071
+ str = rb_str_dup(str);
1072
+ } else {
1073
+ rb_hash_aset(seen, id, Qtrue);
1074
+ if (OBJ_FROZEN(str)) str = rb_str_dup(str);
1075
+ }
1076
+ pins[i] = str;
1077
+ rb_str_locktmp(pins[i]);
1078
+ data[i] = RSTRING_PTR(pins[i]);
1079
+ lens[i] = (size_t)RSTRING_LEN(pins[i]);
1080
+ }
1081
+ struct slice_parse_arg arg = {n, data, lens, docs, stats};
1082
+ rb_thread_call_without_gvl(slice_parse_without_gvl, &arg,
1083
+ RUBY_UBF_IO, NULL);
1084
+ for (i = 0; i < n; i++) rb_str_unlocktmp(pins[i]);
1085
+
1086
+ VALUE out = rb_ary_new_capa(n);
1087
+ keycache kc;
1088
+ kc_init(&kc);
1089
+ for (i = 0; i < n; i++) {
1090
+ if (stats[i] != TEPTRIS_OK && docs[i] == NULL) {
1091
+ const char *msg = "parse failed (out of memory)";
1092
+ VALUE ex = rb_exc_new(eParseError, msg, (long)strlen(msg));
1093
+ kc_free(&kc);
1094
+ xfree(pins); xfree(data); xfree(lens); xfree(docs); xfree(stats);
1095
+ rb_exc_raise(ex);
1096
+ }
1097
+ const teptris_error *e = teptris_document_error(docs[i]);
1098
+ if (stats[i] != TEPTRIS_OK) {
1099
+ VALUE ex = rb_exc_new(eParseError, e->message,
1100
+ (long)strlen(e->message));
1101
+ rb_iv_set(ex, "@line", SIZET2NUM(e->line));
1102
+ rb_iv_set(ex, "@column", SIZET2NUM(e->column));
1103
+ long j;
1104
+ for (j = i; j < n; j++) teptris_document_free(docs[j]);
1105
+ kc_free(&kc);
1106
+ xfree(pins); xfree(data); xfree(lens); xfree(docs); xfree(stats);
1107
+ rb_exc_raise(ex);
1108
+ }
1109
+ }
1110
+ for (i = 0; i < n; i++) {
1111
+ rb_ary_store(out, i,
1112
+ obj_from_node(teptris_document_root(docs[i]), flags, &kc));
1113
+ teptris_document_free(docs[i]);
1114
+ }
1115
+ kc_free(&kc);
1116
+ xfree(pins); xfree(data); xfree(lens); xfree(docs); xfree(stats);
1117
+ return out;
1118
+ }
1119
+
998
1120
  static VALUE ext_load_batch(int argc, VALUE *argv, VALUE self) {
999
1121
  VALUE strings, opts;
1000
1122
  rb_scan_args(argc, argv, "11", &strings, &opts);
@@ -1052,6 +1174,7 @@ void Init_teptris_ext(void) {
1052
1174
  rb_define_module_function(m, "load", ext_load, -1);
1053
1175
  rb_define_module_function(m, "load_lazy", ext_load_lazy, 1);
1054
1176
  rb_define_module_function(m, "load_batch", ext_load_batch, -1);
1177
+ rb_define_module_function(m, "load_slice", ext_load_slice, -1);
1055
1178
  rb_define_module_function(m, "load_lazy_batch", ext_load_lazy_batch, 1);
1056
1179
  cLazyOwner = rb_define_class_under(m, "LazyOwner", rb_cObject);
1057
1180
  cLazyValue = rb_define_class_under(m, "LazyValue", rb_cObject);
data/lib/teptris/toml.rb CHANGED
@@ -51,9 +51,53 @@ module Teptris
51
51
  # struct across the batch, return one eager Hash per document.
52
52
  # Same datetime contract as load. A failing document raises
53
53
  # Teptris::ParseError carrying the first failure's line/column.
54
- def load_batch(tomls, safe_load: nil, datetime_policy: :native)
54
+ def load_batch(tomls, safe_load: nil, datetime_policy: :native,
55
+ threads: 1)
55
56
  _validate_batch!(tomls, safe_load, datetime_policy)
56
- TeptrisExt.load_batch(tomls, _safe_load_opts(safe_load, datetime_policy))
57
+ opts = _safe_load_opts(safe_load, datetime_policy)
58
+ return TeptrisExt.load_batch(tomls, opts) if threads <= 1
59
+
60
+ # threads > 1: slice across Ruby threads. The C parse releases
61
+ # the GVL, so parses overlap; materialization serializes on the
62
+ # GVL. Contiguous slices keep error order deterministic: the
63
+ # lowest-index failure raises after all threads join.
64
+ n = tomls.size
65
+ threads = [threads, n].min
66
+ return TeptrisExt.load_batch(tomls, opts) if threads <= 1
67
+ # load_slice releases the GVL once per slice (no per-document
68
+ # ping-pong) and shares one key cache across its documents.
69
+ # Repeated objects are dup'd once each HERE: a repeat landing in
70
+ # two slices would double-lock across threads (the slice-level
71
+ # guard cannot see across slices); frozen dup'ing is C-side.
72
+ seen = {}
73
+ tomls = tomls.map do |s|
74
+ next s.dup if seen[s.object_id]
75
+ seen[s.object_id] = true
76
+ s
77
+ end
78
+ per = (n.to_f / threads).ceil
79
+ results = Array.new(n)
80
+ first_error = nil
81
+ mutex = Mutex.new
82
+ threads.times.map do |ti|
83
+ Thread.new(ti) do |i|
84
+ base = i * per
85
+ slice = tomls[base, per] || []
86
+ begin
87
+ loaded = TeptrisExt.load_slice(slice, opts)
88
+ slice.each_with_index { |_, j| results[base + j] = loaded[j] }
89
+ rescue StandardError => e
90
+ mutex.synchronize do
91
+ first_error ||= [base, e]
92
+ end
93
+ end
94
+ end
95
+ end.each(&:join)
96
+ if first_error
97
+ _, err = first_error
98
+ raise err
99
+ end
100
+ results
57
101
  end
58
102
 
59
103
  # Lazy twin of load_batch: parse N inputs eagerly, hand back an
@@ -1,3 +1,3 @@
1
1
  module Teptris
2
- VERSION = "0.2.51"
2
+ VERSION = "0.2.52"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.51
4
+ version: 0.2.52
5
5
  platform: ruby
6
6
  authors:
7
7
  - teptris contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-30 00:00:00.000000000 Z
11
+ date: 2026-10-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A native C extension over libteptris, a C99 TOML 1.1 parser/emitter.
14
14
  API shape mirrors the tomlib gem.