leptris 1.9.386.0-ppc64le-linux → 1.9.388.0-ppc64le-linux

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: 693b3140d5bc11495cb1fe2569a09502e30a93ffbd0174352428f5fa8db73843
4
- data.tar.gz: b68d2e270256b7d92056189a417100fd01c0d71982e6f69a81b9f83cfeac8495
3
+ metadata.gz: 238b6117d6d53b2e74db8530678f08b444e405ed9569cc0d5c794bc1539de2db
4
+ data.tar.gz: 6e3a5e2de83a00123bc4ea19fd4818526809f05248b41e0aa6d7a9d9532d71a2
5
5
  SHA512:
6
- metadata.gz: 526a5ff1640e8adc2c107e0f5289e388eb9cd236fe6c47d8bc54c57a9e0bbf31dde18e0ed3d03ce9a8301f004cb51fef481d9c3b4bb820d9da418b338c96eed2
7
- data.tar.gz: 6b0e71839d07aa3734bff6c110da078a7cfa597404bb891e14590756b230928a6058968d265bf6af1c30a24ec34ec01a588a703830d036a23b288de9d897d2a0
6
+ metadata.gz: ae6c2a635dce0825ae7f0d85ecfaf343a912f551ce7a88e62f5686de2dbfa1ed736aa8c512a3c33ffa3ab236de554ff1ac7ecc316d419cda9bf2e7ec6c68db91
7
+ data.tar.gz: d96b043f56180f0fc6c79a9a25c1678d1161d099b544ed0ba4ebfa5a220fec998b287b591d6535305d3a42b5bb7eada48576f0276ea930b4cfc5db4313589da5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Changed — 1.9.388.0 (rides libleptris 1.9.388)
4
+
5
+ - Engine pin 1.9.386 -> 1.9.388: carries the one-pass text-run scan
6
+ (leptris/leptris#1895, #1842 — text shapes 764-804 -> 1370-1448 MB/s,
7
+ pugixml gap 2.9x -> 1.70x) and the arena retain-pool size-class split
8
+ (leptris/leptris#1893, #1846), plus the 1.9.387 wave (ns-declaration
9
+ snapshot, packed op-blob build, children_batch + name_compare,
10
+ XSD skip-wildcards / attributeGroup order / length facets / builtin
11
+ lexical table / schema-set compile_set / anyType default — the
12
+ schema-set faces this pin un-gates in spec/xml/xsd_spec.rb).
13
+
3
14
  ### Added — 1.9.382.0 (rides libleptris 1.9.382)
4
15
 
5
16
  - **Schema#version** (moxml#460 item 2): the effective schema
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.9.387"
11
+ LIBLEPTRIS_VERSION = "1.9.388"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.386.0"
4
+ VERSION = "1.9.388.0"
5
5
  end
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.387
4
+ - libleptris v1.9.388
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,33 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.388] - 2026-10-29
4
+
5
+ ### Fixed
6
+
7
+ - **Arena retain pool split by size class (leptris#1893, leptris#1846).**
8
+ Mid-sized spans (256 KB - 2 MB) could evict all four large-span retain
9
+ slots, so mixed-traffic workloads lost their warm large spans and
10
+ re-faulted them every parse. The retain pool is now two tiers (mid /
11
+ large at the 2 MB boundary) under the same shared ceiling, and a
12
+ pointer-identity test pins same-size reuse across mixed traffic.
13
+
14
+ ### Performance
15
+
16
+ - **One-pass text-run scan (leptris#1895, leptris#1842).** The direct
17
+ parser's text path found `<` with a byte-at-a-time inline loop over
18
+ the first 48 bytes, then paid up to three libc memchr calls per run
19
+ (`<`, `&`, CR) — about 90ns of scan per ~55-byte run, capping
20
+ text-heavy documents near 800 MB/s while pugixml ran one vectorized
21
+ pass (profile: 72% of parse samples in the inline loop, 9% in the
22
+ three memchr sites). The new `leptris_text_scan_run` kernel (scalar +
23
+ NEON + AVX2, dispatched like the other simd_text primitives) finds
24
+ the first `<` and flags `&`/CR over the prefix in a single memory
25
+ pass; the recover-mode unescaped-`<` re-scan uses the same kernel.
26
+ Text shapes: 764-804 → 1370-1448 MB/s; gap vs pugixml 2.9x → 1.70x
27
+ (1.30x at 5 MB). Attr shapes unchanged.
28
+
29
+
30
+
3
31
  ## [1.9.387] - 2026-10-28
4
32
 
5
33
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.387
6
+ VERSION 1.9.388
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -0,0 +1,40 @@
1
+ #!/bin/sh
2
+ # arena_ab.sh — interleaved A/B for parse-arena sizing changes
3
+ # (leptris#1846 discipline: same-process-pair, order-alternating,
4
+ # median of per-pair ratios — cross-wave deltas are NOT a valid
5
+ # referee, the 2026-10-25 wave swung both directions at noise
6
+ # magnitude).
7
+ #
8
+ # Usage: arena_ab.sh <base-build-dir> <patch-build-dir> <fixture> [pairs]
9
+ # Both build dirs must contain benchmarks/parse_ab (the file-mode
10
+ # bench_parse_ab). Each pair runs BASE then PATCH, then PATCH then
11
+ # BASE (ABBA), so thermal/load drift cancels; the reported number is
12
+ # the median of patch/base MB/s ratios.
13
+ set -e
14
+ BASE=$(ls "$1"/benchmarks/parse_ab "$1"/benchmarks/bench_parse_ab 2>/dev/null | head -1)
15
+ PATCH=$(ls "$2"/benchmarks/parse_ab "$2"/benchmarks/bench_parse_ab 2>/dev/null | head -1)
16
+ FIX="$3"
17
+ N="${4:-10}"
18
+ if [ -z "$BASE" ] || [ -z "$PATCH" ] || [ -z "$FIX" ]; then
19
+ echo "usage: $0 <base-build-dir> <patch-build-dir> <fixture> [pairs]" >&2
20
+ exit 1
21
+ fi
22
+ run_one() {
23
+ "$1" "$FIX" | awk '{print $4}'
24
+ }
25
+ ratios=""
26
+ i=0
27
+ while [ "$i" -lt "$N" ]; do
28
+ a=$(run_one "$BASE"); b=$(run_one "$PATCH")
29
+ b2=$(run_one "$PATCH"); a2=$(run_one "$BASE")
30
+ ratios="$ratios
31
+ $(echo "$b $a" | awk '{printf "%.4f", $1/$2}')
32
+ $(echo "$b2 $a2" | awk '{printf "%.4f", $1/$2}')"
33
+ i=$((i + 1))
34
+ done
35
+ echo "$ratios" | sort -n | awk '
36
+ { v[NR] = $1 }
37
+ END {
38
+ m = (NR % 2) ? v[(NR + 1) / 2] : (v[NR / 2] + v[NR / 2 + 1]) / 2
39
+ printf "pairs=%d median patch/base = %.4fx\n", NR, m
40
+ }'
@@ -5,6 +5,13 @@
5
5
  * runners; the interleaved lane's slices are gated on THESE
6
6
  * numbers, never a loaded dev machine (2026-09-20: a Mac at load
7
7
  * 340 measured a 472 MB/s build at 46-85).
8
+ *
9
+ * leptris#1846: `argv[1]` switches to file mode — parse+free one
10
+ * fixture in a loop (median of N in-process runs, best-of-5
11
+ * batches) and print one `FILE <path> <bytes> <MB/s>` row. The
12
+ * arena-sizing A/B runner (arena_ab.sh) interleaves two builds of
13
+ * this binary and takes the median of per-pair ratios, so runner
14
+ * load cannot fake or hide the effect.
8
15
  */
9
16
  #include <stdio.h>
10
17
  #include <stdlib.h>
@@ -22,6 +29,45 @@ static double now_us(void) {
22
29
  return ts.tv_sec * 1e6 + ts.tv_nsec / 1e3;
23
30
  }
24
31
 
32
+ static int file_mode(const char* path) {
33
+ FILE* f = fopen(path, "rb");
34
+ if (!f) { fprintf(stderr, "cannot open %s\n", path); return 1; }
35
+ fseek(f, 0, SEEK_END);
36
+ long sz = ftell(f);
37
+ fseek(f, 0, SEEK_SET);
38
+ char* doc = (char*)malloc((size_t)sz);
39
+ if (!doc || fread(doc, 1, (size_t)sz, f) != (size_t)sz) {
40
+ fprintf(stderr, "cannot read %s\n", path);
41
+ fclose(f);
42
+ return 1;
43
+ }
44
+ fclose(f);
45
+ /* LEPTRIS_BENCH_ITERS: profiling escape hatch (sample(1) needs a
46
+ * long-running process, not a 10ms one). */
47
+ const char* iters_env = getenv("LEPTRIS_BENCH_ITERS");
48
+ int iters = iters_env ? atoi(iters_env) : 5;
49
+ double best = 1e18;
50
+ double sum = 0;
51
+ for (int r = 0; r < iters; r++) {
52
+ double a = now_us();
53
+ LeptrisStatus st = LEPTRIS_OK;
54
+ LeptrisDocument d = leptris_parse_string(doc, (size_t)sz, &st);
55
+ double b = now_us();
56
+ if (!d) { printf("PARSE FAILED\n"); return 1; }
57
+ leptris_document_free(d);
58
+ if (b - a < best) best = b - a;
59
+ sum += b - a;
60
+ }
61
+ /* #1846: the MEAN prices in the cold parses (fresh span + full
62
+ * first-touch) that a best-of hides — the warm-span retention fix
63
+ * lives exactly there. */
64
+ printf("FILE %s %ld %.1f MB/s mean %.1f MB/s\n", path, sz,
65
+ sz / 1048576.0 / (best / 1e6),
66
+ sz / 1048576.0 / ((sum / (double)iters) / 1e6));
67
+ free(doc);
68
+ return 0;
69
+ }
70
+
25
71
  static size_t gen(char* out, size_t target, int text_shape) {
26
72
  size_t len = 0;
27
73
  len += (size_t)sprintf(out + len, "<root>");
@@ -40,7 +86,8 @@ static size_t gen(char* out, size_t target, int text_shape) {
40
86
  return len;
41
87
  }
42
88
 
43
- int main(void) {
89
+ int main(int argc, char** argv) {
90
+ if (argc > 1) return file_mode(argv[1]);
44
91
  const size_t sizes[3] = {1500 * 1024, 5000 * 1024, 13000 * 1024};
45
92
  const char* names[2] = {"attr", "text"};
46
93
  printf("%-6s %9s %12s %12s\n", "shape", "bytes", "leptris", "pugixml");
@@ -63,6 +63,29 @@ static void (*g_count3_high_fn)(const char*, size_t, char, char, char,
63
63
  int*) = NULL;
64
64
  static void (*g_copy3_fn)(char*, const char*, size_t, char, char, char,
65
65
  size_t*, size_t*, size_t*) = NULL;
66
+ static ptrdiff_t (*g_scan_run_fn)(const char*, size_t, char, char, char,
67
+ int*, int*) = NULL;
68
+
69
+ /* #1842: one pass, three byte classes — the direct parser's text path
70
+ * (find '<', flag '&'/CR) drops its 48-byte inline loop and up to
71
+ * three per-run memchr calls for this. */
72
+ static ptrdiff_t scan_run_scalar(const char* s, size_t len, char term,
73
+ char f1, char f2,
74
+ int* saw_f1, int* saw_f2) {
75
+ int a = 0, b = 0;
76
+ const char* p = s;
77
+ const char* end = s + len;
78
+ while (p < end) {
79
+ char ch = *p;
80
+ if (ch == term) break;
81
+ a |= (ch == f1);
82
+ b |= (ch == f2);
83
+ p++;
84
+ }
85
+ *saw_f1 = a;
86
+ *saw_f2 = b;
87
+ return (p < end) ? (ptrdiff_t)(p - s) : -1;
88
+ }
66
89
 
67
90
  static size_t count_scalar(const char* s, size_t len, char c) {
68
91
  size_t n = 0;
@@ -108,6 +131,7 @@ static void dispatch_init(void) {
108
131
  g_find3_fn = leptris_text_find3_avx2;
109
132
  g_count_fn = leptris_text_count_char_avx2;
110
133
  g_count3_high_fn = count3_high_scalar;
134
+ g_scan_run_fn = leptris_text_scan_run_avx2;
111
135
  return;
112
136
  }
113
137
  #endif
@@ -119,6 +143,7 @@ static void dispatch_init(void) {
119
143
  g_count3_fn = leptris_text_count3_neon;
120
144
  g_count3_high_fn = leptris_text_count3_high_neon;
121
145
  g_copy3_fn = leptris_copy_count3_neon;
146
+ g_scan_run_fn = leptris_text_scan_run_neon;
122
147
  return;
123
148
  }
124
149
  #endif
@@ -127,6 +152,7 @@ static void dispatch_init(void) {
127
152
  g_find3_fn = find3_scalar;
128
153
  g_count_fn = count_scalar;
129
154
  g_count3_high_fn = count3_high_scalar;
155
+ g_scan_run_fn = scan_run_scalar;
130
156
  }
131
157
 
132
158
  /* Scalar kernel: one byte loop counts all three chars and detects
@@ -213,6 +239,13 @@ void leptris_copy_count3(char* dst, const char* src, size_t len,
213
239
  leptris_text_count3(src, len, c0, c1, c2, n0, n1, n2);
214
240
  }
215
241
 
242
+ ptrdiff_t leptris_text_scan_run(const char* s, size_t len, char term,
243
+ char f1, char f2,
244
+ int* saw_f1, int* saw_f2) {
245
+ if (!g_scan_run_fn) dispatch_init();
246
+ return g_scan_run_fn(s, len, term, f1, f2, saw_f1, saw_f2);
247
+ }
248
+
216
249
  /* ---- Structural span scan (TODO 193 Phase 1) ---- */
217
250
 
218
251
  unsigned char leptris_dp_class_table[256];
@@ -66,6 +66,17 @@ void leptris_copy_count3(char* dst, const char* src, size_t len,
66
66
  char c0, char c1, char c2,
67
67
  size_t* n0, size_t* n1, size_t* n2);
68
68
 
69
+ /* One-pass text-run scan (#1842): find the first `term` byte in
70
+ * [s, s+len) and report whether f1/f2 occur strictly before it.
71
+ * Returns the term offset, or -1 when absent (flags then cover the
72
+ * whole span). saw_f1/saw_f2 are always written (0 or 1). Replaces
73
+ * the direct parser's 48-byte inline loop plus up to three per-run
74
+ * memchr calls with a single memory pass. term/f1/f2 must be
75
+ * distinct. */
76
+ ptrdiff_t leptris_text_scan_run(const char* s, size_t len, char term,
77
+ char f1, char f2,
78
+ int* saw_f1, int* saw_f2);
79
+
69
80
  /* ---- Structural span scan (TODO 193 Phase 1: parser v3) ----
70
81
  *
71
82
  * Records every byte of the XML structural set in [buf, buf+len)
@@ -121,6 +132,12 @@ void leptris_copy_count3_neon(char* dst, const char* src, size_t len,
121
132
  size_t* ca, size_t* cb, size_t* cc);
122
133
  size_t leptris_text_scan_events_neon(const char* buf, size_t len,
123
134
  LeptrisScanEvent* events, size_t max);
135
+ ptrdiff_t leptris_text_scan_run_avx2(const char* s, size_t len, char term,
136
+ char f1, char f2,
137
+ int* saw_f1, int* saw_f2);
138
+ ptrdiff_t leptris_text_scan_run_neon(const char* s, size_t len, char term,
139
+ char f1, char f2,
140
+ int* saw_f1, int* saw_f2);
124
141
 
125
142
  /* Append events for every structural byte to out (capacity max).
126
143
  * Returns the number of events written; returns max unchanged as
@@ -103,3 +103,53 @@ size_t leptris_text_count_char_avx2(const char* s, size_t len, char c) {
103
103
  }
104
104
 
105
105
  #endif /* LEPTRIS_ARCH_X86 && __AVX2__ */
106
+
107
+ /* #1842: one-pass text-run scan — AVX2 mirror of the NEON kernel:
108
+ * full 32-byte chunks OR-accumulate the f1/f2 movemasks; the term
109
+ * mask hits and the first lane is ctz-resolved with a scalar prefix
110
+ * fold. */
111
+ ptrdiff_t leptris_text_scan_run_avx2(const char* s, size_t len, char term,
112
+ char f1, char f2,
113
+ int* saw_f1, int* saw_f2) {
114
+ const char* p = s;
115
+ const char* end = s + len;
116
+ const __m256i kt = _mm256_set1_epi8(term);
117
+ const __m256i k1 = _mm256_set1_epi8(f1);
118
+ const __m256i k2 = _mm256_set1_epi8(f2);
119
+ uint32_t acc1 = 0, acc2 = 0;
120
+
121
+ while (end - p >= 32) {
122
+ __m256i v = _mm256_loadu_si256((const __m256i*)p);
123
+ uint32_t mt =
124
+ (uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, kt));
125
+ if (mt) {
126
+ unsigned idx = (unsigned)LEPTRIS_CTZ(mt);
127
+ int a = (acc1 != 0);
128
+ int b = (acc2 != 0);
129
+ for (unsigned j = 0; j < idx; j++) {
130
+ a |= (p[j] == f1);
131
+ b |= (p[j] == f2);
132
+ }
133
+ *saw_f1 = a;
134
+ *saw_f2 = b;
135
+ return (ptrdiff_t)(p + idx - s);
136
+ }
137
+ acc1 |= (uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, k1));
138
+ acc2 |= (uint32_t)_mm256_movemask_epi8(_mm256_cmpeq_epi8(v, k2));
139
+ p += 32;
140
+ }
141
+ {
142
+ int a = (acc1 != 0);
143
+ int b = (acc2 != 0);
144
+ while (p < end) {
145
+ char ch = *p;
146
+ if (ch == term) break;
147
+ a |= (ch == f1);
148
+ b |= (ch == f2);
149
+ p++;
150
+ }
151
+ *saw_f1 = a;
152
+ *saw_f2 = b;
153
+ return (p < end) ? (ptrdiff_t)(p - s) : -1;
154
+ }
155
+ }
@@ -328,3 +328,55 @@ size_t leptris_text_scan_events_neon(const char* buf, size_t len,
328
328
  }
329
329
  return n;
330
330
  }
331
+
332
+ /* #1842: one-pass text-run scan — first `term` byte plus f1/f2
333
+ * seen-flags over the prefix. Full chunks OR-accumulate the f1/f2
334
+ * compare masks; a term hit resolves its lane scalar (once per run,
335
+ * at most 15 bytes of prefix re-check) and returns without folding
336
+ * the post-hit lanes. */
337
+ ptrdiff_t leptris_text_scan_run_neon(const char* s, size_t len, char term,
338
+ char f1, char f2,
339
+ int* saw_f1, int* saw_f2) {
340
+ const char* p = s;
341
+ const char* end = s + len;
342
+ const uint8x16_t kt = vdupq_n_u8((uint8_t)term);
343
+ const uint8x16_t k1 = vdupq_n_u8((uint8_t)f1);
344
+ const uint8x16_t k2 = vdupq_n_u8((uint8_t)f2);
345
+ uint8x16_t acc1 = vdupq_n_u8(0);
346
+ uint8x16_t acc2 = vdupq_n_u8(0);
347
+
348
+ while (end - p >= 16) {
349
+ uint8x16_t v = vld1q_u8((const uint8_t*)p);
350
+ uint8x16_t eqt = vceqq_u8(v, kt);
351
+ if (vmaxvq_u8(eqt) != 0) {
352
+ int i = 0;
353
+ while (p[i] != term) i++;
354
+ int a = vmaxvq_u8(acc1) != 0;
355
+ int b = vmaxvq_u8(acc2) != 0;
356
+ for (int j = 0; j < i; j++) {
357
+ a |= (p[j] == f1);
358
+ b |= (p[j] == f2);
359
+ }
360
+ *saw_f1 = a;
361
+ *saw_f2 = b;
362
+ return (ptrdiff_t)(p + i - s);
363
+ }
364
+ acc1 = vorrq_u8(acc1, vceqq_u8(v, k1));
365
+ acc2 = vorrq_u8(acc2, vceqq_u8(v, k2));
366
+ p += 16;
367
+ }
368
+ {
369
+ int a = vmaxvq_u8(acc1) != 0;
370
+ int b = vmaxvq_u8(acc2) != 0;
371
+ while (p < end) {
372
+ char ch = *p;
373
+ if (ch == term) break;
374
+ a |= (ch == f1);
375
+ b |= (ch == f2);
376
+ p++;
377
+ }
378
+ *saw_f1 = a;
379
+ *saw_f2 = b;
380
+ return (p < end) ? (ptrdiff_t)(p - s) : -1;
381
+ }
382
+ }
@@ -1726,44 +1726,31 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1726
1726
  if (q >= p.end) break;
1727
1727
  if (*q == '<') { p.pos = q; continue; }
1728
1728
  }
1729
- /* Text content — FUSED scan (TODO 184): one inline loop
1730
- * finds '<' AND counts '\n' (issue #223 line tracking),
1731
- * replacing the memchr + dp_advance_line re-walk (two
1732
- * passes; memchr pays ~10ns setup on short text — the
1733
- * same finding as the attr-value scan). First 48 bytes
1734
- * inline, SIMD fallback for longer spans. */
1729
+ /* Text content — one-pass scan (#1842): leptris_text_
1730
+ * scan_run finds '<' AND flags '&'/'\r' in a single SIMD
1731
+ * pass, replacing the 48-byte inline loop plus up to
1732
+ * three per-run memchr calls (the three setup costs
1733
+ * dominated on text-heavy shapes: ~90ns of scan per
1734
+ * ~55-byte run). Identical semantics: flags cover
1735
+ * [text_start, lt) only. */
1735
1736
  char* text_start = p.pos;
1736
1737
  uint32_t text_off =
1737
1738
  p.line_offsets_ok
1738
1739
  ? (uint32_t)(text_start - p.buf) + 1u : 0u;
1739
1740
  char* lt = NULL;
1740
- /* #1285 slice 4: '&' tracking fused into the scan (one
1741
- * predicted compare per byte) — the flush below needs
1742
- * it and a separate memchr per run cost ~7% parse on
1743
- * entity-free docs. The memchr fallback covers the
1744
- * >48-byte tail region only. */
1745
1741
  int has_amp = 0;
1746
- /* §2.11 (#326): literal-CR tracking rides the same fused
1747
- * loop — CR-bearing runs take the pool-copy collapse,
1748
- * CR-free runs (the norm) keep the zero-copy carve. */
1742
+ /* §2.11 (#326): literal-CR tracking rides the same pass —
1743
+ * CR-bearing runs take the pool-copy collapse, CR-free
1744
+ * runs (the norm) keep the zero-copy carve. */
1749
1745
  int has_cr = 0;
1750
1746
  {
1751
- const char* q = p.pos;
1752
- const char* probe_end = p.probe_slack
1753
- ? q + 48
1754
- : ((p.end - q > 48) ? q + 48 : p.end);
1755
- while (q < probe_end) {
1756
- char ch = *q;
1757
- if (ch == '<') { lt = (char*)q; break; }
1758
- has_amp |= (ch == '&');
1759
- has_cr |= (ch == '\r');
1760
- q++;
1761
- }
1762
- if (q >= p.end) goto text_done;
1763
- lt = (char*)memchr(q, '<', p.end - q);
1764
- if (!lt) lt = p.end;
1765
- has_amp |= memchr(q, '&', (size_t)((char*)lt - q)) != NULL;
1766
- has_cr |= memchr(q, '\r', (size_t)((char*)lt - q)) != NULL;
1747
+ int saw_amp = 0, saw_cr = 0;
1748
+ ptrdiff_t t_off = leptris_text_scan_run(
1749
+ p.pos, (size_t)(p.end - p.pos), '<', '&', '\r',
1750
+ &saw_amp, &saw_cr);
1751
+ lt = (t_off >= 0) ? (char*)(p.pos + t_off) : NULL;
1752
+ has_amp |= saw_amp;
1753
+ has_cr |= saw_cr;
1767
1754
  }
1768
1755
  /* leptris#1748: a '<' that opens no markup construct
1769
1756
  * ('!', '?', '/', name-start) is unescaped text. Strict
@@ -1783,14 +1770,13 @@ static struct leptris_document* direct_parse_internal(char* buf, size_t len,
1783
1770
  LEPTRIS_DIAG_UNESCAPED_LT, NULL,
1784
1771
  "Unescaped < not allowed in a text node");
1785
1772
  const char* nq = lt + 1;
1786
- const char* nlt =
1787
- memchr(nq, '<', (size_t)(p.end - nq));
1788
- const char* span_end = nlt ? nlt : p.end;
1789
- has_amp |= memchr(nq, '&',
1790
- (size_t)(span_end - nq)) != NULL;
1791
- has_cr |= memchr(nq, '\r',
1792
- (size_t)(span_end - nq)) != NULL;
1793
- lt = (char*)nlt;
1773
+ int saw_amp2 = 0, saw_cr2 = 0;
1774
+ ptrdiff_t n_off = leptris_text_scan_run(
1775
+ nq, (size_t)(p.end - nq), '<', '&', '\r',
1776
+ &saw_amp2, &saw_cr2);
1777
+ has_amp |= saw_amp2;
1778
+ has_cr |= saw_cr2;
1779
+ lt = (n_off >= 0) ? (char*)(nq + n_off) : NULL;
1794
1780
  }
1795
1781
  text_done:
1796
1782
  p.pos = lt ? lt : p.end;
@@ -6,6 +6,7 @@
6
6
  #include "../common/port.h" /* LEPTRIS_THREAD_LOCAL */
7
7
  #include <stdint.h> /* uintptr_t: was transitive via glibc's mman graph */
8
8
  #include <stdlib.h>
9
+ #include <stdio.h>
9
10
  #include <string.h>
10
11
  #if defined(__linux__)
11
12
  #include <sys/mman.h> /* madvise(MADV_HUGEPAGE) — Linux only */
@@ -53,6 +54,14 @@ static inline size_t align_up(size_t n) {
53
54
  * twice per document lifetime, so contention is irrelevant. */
54
55
  #define ARENA_RETAIN_MIN (256u * 1024u)
55
56
  #define ARENA_RETAIN_MAX_BLOCKS 4u
57
+ /* #1846 slice 4: the global tier is split by size class. With ONE
58
+ * 4-slot pool, every parked block kind competes for the same slots —
59
+ * and parse input copies (freed at document close, never taken back)
60
+ * permanently occupy slots, so arena spans hit the FULL free path and
61
+ * the next same-size parse lands on a cold mapping (fresh mmap +
62
+ * full first-touch: 2 of 5 takes missed on the 306KB fixture). Mid
63
+ * [256KB, 2MB) and large [2MB, ..) now churn among themselves. */
64
+ #define ARENA_RETAIN_LARGE_MIN (2u * 1024u * 1024u)
56
65
  /* 96MB: a large-document parse arena (~4x input with the
57
66
  * pre-sized estimate) must survive the free to be reused by the
58
67
  * next parse — at the old 32MB ceiling, every big-doc cycle
@@ -73,7 +82,12 @@ static struct {
73
82
  size_t size[ARENA_RETAIN_MAX_BLOCKS];
74
83
  size_t count;
75
84
  size_t bytes;
76
- } g_retain;
85
+ } g_retain_mid, g_retain_large;
86
+
87
+ /* Shared byte ceiling across both tiers (the footprint bound). */
88
+ static size_t retain_bytes_total(void) {
89
+ return g_retain_mid.bytes + g_retain_large.bytes;
90
+ }
77
91
 
78
92
  #if defined(_MSC_VER)
79
93
  #include <intrin.h>
@@ -171,18 +185,43 @@ static char* retain_take(size_t request, size_t* capacity) {
171
185
  char* found = NULL;
172
186
  size_t found_size = 0;
173
187
  retain_lock();
174
- for (size_t i = 0; i < g_retain.count; i++) {
175
- if (g_retain.size[i] >= request &&
176
- (found_size == 0 || g_retain.size[i] < found_size)) {
177
- found = g_retain.base[i];
178
- found_size = g_retain.size[i];
179
- g_retain.base[i] = g_retain.base[g_retain.count - 1];
180
- g_retain.size[i] = g_retain.size[g_retain.count - 1];
181
- g_retain.count--;
188
+ /* #1846: search ONLY the request's size class — mid blocks can't
189
+ * serve large requests, and a large block must never serve a mid
190
+ * request (it would burn the only large slots on input-copy
191
+ * churn). */
192
+ struct {
193
+ char** base;
194
+ size_t* size;
195
+ size_t* count;
196
+ } t;
197
+ size_t tcount;
198
+ if (request >= ARENA_RETAIN_LARGE_MIN) {
199
+ t.base = g_retain_large.base;
200
+ t.size = g_retain_large.size;
201
+ tcount = g_retain_large.count;
202
+ } else {
203
+ t.base = g_retain_mid.base;
204
+ t.size = g_retain_mid.size;
205
+ tcount = g_retain_mid.count;
206
+ }
207
+ for (size_t i = 0; i < tcount; i++) {
208
+ if (t.size[i] >= request &&
209
+ (found_size == 0 || t.size[i] < found_size)) {
210
+ found = t.base[i];
211
+ found_size = t.size[i];
212
+ t.base[i] = t.base[tcount - 1];
213
+ t.size[i] = t.size[tcount - 1];
214
+ tcount--;
182
215
  break;
183
216
  }
184
217
  }
185
- g_retain.bytes -= found_size;
218
+ if (request >= ARENA_RETAIN_LARGE_MIN) {
219
+ g_retain_large.count = tcount;
220
+ g_retain_large.bytes -= found_size;
221
+ } else {
222
+ g_retain_mid.count = tcount;
223
+ g_retain_mid.bytes -= found_size;
224
+ }
186
225
  retain_unlock();
187
226
  if (found) *capacity = found_size;
188
227
  return found;
@@ -203,16 +242,25 @@ static void retain_give(char* base, size_t size) {
203
242
  return;
204
243
  }
205
244
  retain_lock();
206
- if (g_retain.count >= ARENA_RETAIN_MAX_BLOCKS ||
207
- g_retain.bytes + size > ARENA_RETAIN_MAX_BYTES) {
245
+ const int large_class = size >= ARENA_RETAIN_LARGE_MIN;
246
+ size_t count = large_class ? g_retain_large.count : g_retain_mid.count;
247
+ if (count >= ARENA_RETAIN_MAX_BLOCKS ||
248
+ retain_bytes_total() + size > ARENA_RETAIN_MAX_BYTES) {
208
249
  retain_unlock();
209
250
  leptris_free_hook(base);
210
251
  return;
211
252
  }
212
- g_retain.base[g_retain.count] = base;
213
- g_retain.size[g_retain.count] = size;
214
- g_retain.count++;
215
- g_retain.bytes += size;
253
+ if (large_class) {
254
+ g_retain_large.base[count] = base;
255
+ g_retain_large.size[count] = size;
256
+ g_retain_large.count = count + 1;
257
+ g_retain_large.bytes += size;
258
+ } else {
259
+ g_retain_mid.base[count] = base;
260
+ g_retain_mid.size[count] = size;
261
+ g_retain_mid.count = count + 1;
262
+ g_retain_mid.bytes += size;
263
+ }
216
264
  retain_unlock();
217
265
  }
218
266
 
@@ -231,10 +279,14 @@ void leptris_arena_retain_drain(void) {
231
279
  g_small.bytes = 0;
232
280
 
233
281
  retain_lock();
234
- for (size_t i = 0; i < g_retain.count; i++)
235
- leptris_free_hook(g_retain.base[i]);
236
- g_retain.count = 0;
237
- g_retain.bytes = 0;
282
+ for (size_t i = 0; i < g_retain_mid.count; i++)
283
+ leptris_free_hook(g_retain_mid.base[i]);
284
+ g_retain_mid.count = 0;
285
+ g_retain_mid.bytes = 0;
286
+ for (size_t i = 0; i < g_retain_large.count; i++)
287
+ leptris_free_hook(g_retain_large.base[i]);
288
+ g_retain_large.count = 0;
289
+ g_retain_large.bytes = 0;
238
290
  retain_unlock();
239
291
  }
240
292
 
@@ -139,4 +139,141 @@ TEST(SimdText, Find3TerminatorAtVeryEnd) {
139
139
  EXPECT_EQ(leptris_text_find3(u.data(), u.size(), '-', '-', '>'), -1);
140
140
  }
141
141
 
142
+ /* ---- leptris_text_scan_run (#1842): one-pass text-run scan ----
143
+ *
144
+ * Parser contract: find the first `term` byte and report whether f1/f2
145
+ * occur strictly BEFORE it. Replaces the 48-byte inline loop plus up to
146
+ * three per-run memchr calls in the direct parser's text path. */
147
+
148
+ TEST(SimdText, ScanRunFindsTermEveryPosition) {
149
+ std::string base = "abcdefghijklmnopqrstuvwxyz";
150
+ for (size_t len = 1; len <= 80; len++) {
151
+ std::string s;
152
+ for (size_t i = 0; i < len; i++) s += base[i % base.size()];
153
+ for (size_t pos = 0; pos < len; pos++) {
154
+ std::string t = s;
155
+ t[pos] = '<';
156
+ int saw_amp = -1, saw_cr = -1;
157
+ ptrdiff_t off = leptris_text_scan_run(
158
+ t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
159
+ EXPECT_EQ(off, (ptrdiff_t)pos)
160
+ << "len=" << len << " pos=" << pos;
161
+ EXPECT_EQ(saw_amp, 0) << "len=" << len << " pos=" << pos;
162
+ EXPECT_EQ(saw_cr, 0) << "len=" << len << " pos=" << pos;
163
+ }
164
+ }
165
+ }
166
+
167
+ TEST(SimdText, ScanRunFlagsCountPrefixOnly) {
168
+ /* '&'/CR before the term set the flags; the same bytes after it
169
+ * must NOT (the parser's entity/CR handling only covers the run
170
+ * it carves). */
171
+ {
172
+ std::string t = "ab&cd<ef&gh";
173
+ int saw_amp = -1, saw_cr = -1;
174
+ ptrdiff_t off = leptris_text_scan_run(
175
+ t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
176
+ EXPECT_EQ(off, 5);
177
+ EXPECT_EQ(saw_amp, 1);
178
+ EXPECT_EQ(saw_cr, 0);
179
+ }
180
+ {
181
+ std::string t = "ab<cd&"; /* flag byte after the term is skipped */
182
+ int saw_amp = -1, saw_cr = -1;
183
+ ptrdiff_t off = leptris_text_scan_run(
184
+ t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
185
+ EXPECT_EQ(off, 2);
186
+ EXPECT_EQ(saw_amp, 0);
187
+ EXPECT_EQ(saw_cr, 0);
188
+ }
189
+ {
190
+ std::string t = "a\r\rb<c\r";
191
+ int saw_amp = -1, saw_cr = -1;
192
+ ptrdiff_t off = leptris_text_scan_run(
193
+ t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
194
+ EXPECT_EQ(off, 4);
195
+ EXPECT_EQ(saw_amp, 0);
196
+ EXPECT_EQ(saw_cr, 1);
197
+ }
198
+ {
199
+ std::string t = "&x\ry<z";
200
+ int saw_amp = -1, saw_cr = -1;
201
+ ptrdiff_t off = leptris_text_scan_run(
202
+ t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
203
+ EXPECT_EQ(off, 4);
204
+ EXPECT_EQ(saw_amp, 1);
205
+ EXPECT_EQ(saw_cr, 1);
206
+ }
207
+ }
208
+
209
+ TEST(SimdText, ScanRunNoTermFlagsWholeSpan) {
210
+ std::string base = "The quick brown fox jumps over the lazy dog ";
211
+ for (size_t len = 0; len <= 200; len++) {
212
+ std::string s;
213
+ for (size_t i = 0; i < len; i++) s += base[i % base.size()];
214
+ int saw_amp = -1, saw_cr = -1;
215
+ ptrdiff_t off = leptris_text_scan_run(
216
+ s.data(), s.size(), '<', '&', '\r', &saw_amp, &saw_cr);
217
+ EXPECT_EQ(off, -1) << "len=" << len;
218
+ EXPECT_EQ(saw_amp, 0) << "len=" << len;
219
+ EXPECT_EQ(saw_cr, 0) << "len=" << len;
220
+ }
221
+ /* Flags fire across chunk boundaries when the term is absent. */
222
+ std::string t(15, 'x');
223
+ t += '&';
224
+ t.append(20, 'x');
225
+ t += '\r';
226
+ t.append(40, 'x');
227
+ int saw_amp = -1, saw_cr = -1;
228
+ EXPECT_EQ(leptris_text_scan_run(t.data(), t.size(), '<', '&', '\r',
229
+ &saw_amp, &saw_cr),
230
+ -1);
231
+ EXPECT_EQ(saw_amp, 1);
232
+ EXPECT_EQ(saw_cr, 1);
233
+ }
234
+
235
+ TEST(SimdText, ScanRunTermAtVeryEnd) {
236
+ std::string t(40, 'y');
237
+ t += '<';
238
+ int saw_amp = -1, saw_cr = -1;
239
+ EXPECT_EQ(leptris_text_scan_run(t.data(), t.size(), '<', '&', '\r',
240
+ &saw_amp, &saw_cr),
241
+ 40);
242
+ EXPECT_EQ(saw_amp, 0);
243
+ EXPECT_EQ(saw_cr, 0);
244
+ }
245
+
246
+ TEST(SimdText, ScanRunEmptySpan) {
247
+ int saw_amp = -1, saw_cr = -1;
248
+ EXPECT_EQ(leptris_text_scan_run("", 0, '<', '&', '\r', &saw_amp,
249
+ &saw_cr),
250
+ -1);
251
+ EXPECT_EQ(saw_amp, 0);
252
+ EXPECT_EQ(saw_cr, 0);
253
+ }
254
+
255
+ TEST(SimdText, ScanRunLongBodyEveryPosition) {
256
+ /* Vector-loop coverage: term at every offset of a 300-byte body,
257
+ * with flag bytes seeded before and after each position. */
258
+ std::string body(300, 'x');
259
+ for (size_t pos = 0; pos < body.size(); pos++) {
260
+ std::string t = body;
261
+ if (pos > 0) t[pos / 2] = '&';
262
+ if (pos > 2) t[pos / 3] = '\r';
263
+ t[pos] = '<';
264
+ /* Expected flags derive from the final seeded string. */
265
+ int e_amp = 0, e_cr = 0;
266
+ for (size_t i = 0; i < pos; i++) {
267
+ e_amp |= (t[i] == '&');
268
+ e_cr |= (t[i] == '\r');
269
+ }
270
+ int saw_amp = -1, saw_cr = -1;
271
+ ptrdiff_t off = leptris_text_scan_run(
272
+ t.data(), t.size(), '<', '&', '\r', &saw_amp, &saw_cr);
273
+ ASSERT_EQ(off, (ptrdiff_t)pos) << "pos=" << pos;
274
+ EXPECT_EQ(saw_amp, e_amp) << "pos=" << pos;
275
+ EXPECT_EQ(saw_cr, e_cr) << "pos=" << pos;
276
+ }
277
+ }
278
+
142
279
  } // namespace
@@ -377,3 +377,41 @@ TEST(ArenaSmallRecycle, BulkDestroyParksUpToTheBound) {
377
377
  leptris_arena_retain_drain();
378
378
  }
379
379
 
380
+
381
+ // ---- #1846 slice 4: mixed-size traffic must not evict arena spans ---
382
+ // The single 4-slot global pool mixes every parked block kind (parse
383
+ // input copies never get taken back; they just occupy slots). With
384
+ // more block kinds than slots the FULL path frees ARENA spans and the
385
+ // next same-size create lands on a cold mapping. The fix splits the
386
+ // global tier by size class: mid [256KB, 2MB) and large [2MB, ..),
387
+ // each with its own slots — the four mid kinds churn among
388
+ // themselves and the large span keeps its slot.
389
+ TEST(LeptrisArena, SameSizeReuseSurvivesMixedSizeTraffic) {
390
+ #if defined(ARENA_TESTS_ASAN)
391
+ GTEST_SKIP() << "retain cache disabled under ASAN";
392
+ #endif
393
+ const size_t huge = 4u * 1024u * 1024u;
394
+ void* huge_base = nullptr;
395
+ for (int round = 0; round < 3; round++) {
396
+ /* mid-class traffic: four distinct sizes, one per slot */
397
+ for (size_t mid : {300u * 1024u, 350u * 1024u, 400u * 1024u,
398
+ 450u * 1024u}) {
399
+ LeptrisArena* m = leptris_arena_create(mid);
400
+ ASSERT_NE(m, nullptr);
401
+ memset(leptris_arena_base(m), 1, mid);
402
+ leptris_arena_destroy(m);
403
+ }
404
+ /* the large arena: after warmup its mapping must repeat */
405
+ LeptrisArena* h = leptris_arena_create(huge);
406
+ ASSERT_NE(h, nullptr);
407
+ memset(leptris_arena_base(h), 1, huge);
408
+ if (round == 0) {
409
+ huge_base = leptris_arena_base(h);
410
+ } else {
411
+ EXPECT_EQ(leptris_arena_base(h), huge_base)
412
+ << "round " << round
413
+ << ": the large span was evicted by mid traffic";
414
+ }
415
+ leptris_arena_destroy(h);
416
+ }
417
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
3
3
  "name": "leptris",
4
- "version": "1.9.387",
4
+ "version": "1.9.388",
5
5
  "description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
6
6
  "homepage": "https://github.com/leptris/leptris",
7
7
  "license": "MIT",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.386.0
4
+ version: 1.9.388.0
5
5
  platform: ppc64le-linux
6
6
  authors:
7
7
  - Ribose Inc.
@@ -441,6 +441,7 @@ files:
441
441
  - vendor-src/libleptris/benchmarks/modification/bench_append_split.cpp
442
442
  - vendor-src/libleptris/benchmarks/modification/bench_leptris_modify.c
443
443
  - vendor-src/libleptris/benchmarks/modification/bench_node_creation.c
444
+ - vendor-src/libleptris/benchmarks/parse/arena_ab.sh
444
445
  - vendor-src/libleptris/benchmarks/parse/bench_parse_ab.cpp
445
446
  - vendor-src/libleptris/benchmarks/sax/bench_iterparse.c
446
447
  - vendor-src/libleptris/benchmarks/sax/bench_leptris.c