leptris 1.9.107.0 → 1.9.115.0

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: 64e0e821d66fb8c8d25debf605648a6d533f432e0882f1a7cecf58b2e2a19e52
4
- data.tar.gz: 70bb6cf535b69d0ae0c470459f183d6420e87db4a8bef12c89152f1d739c793b
3
+ metadata.gz: 8fb86a086496ee90110e37c8049ccb26b472d7a21f0f774a381e7611deb1999a
4
+ data.tar.gz: 1ec17ae04d09a31ef6e38ead74151be098373d1e0863b8e59755c2ebdb01cc3a
5
5
  SHA512:
6
- metadata.gz: eadc86a700848a3a0980d0e2af292ded87619a4bd958a0ea897a310c35caf00dfca7d4c1a997e1760d48d18872cb84458440601bc437a120131e6f4344a614b0
7
- data.tar.gz: 9d6a4fbc7dc3b88e96c896a4b55d10583b9764979a4d7f8eb3d0a51be3422d870f758a805c566529e85c94c2bcd7512b5715479e4d1e04295ce1936ff78599ab
6
+ metadata.gz: 4733922a9794ada70fba4b50783b87a4748a04145131bba696eeb1b2ab3ae33d8f76fbcabb531f8fbc93be51f3a53634841f3488f42c238a5d93b22bdd732a75
7
+ data.tar.gz: e1ffd73a63379edd8c23018ab1477f0627e783a85c52ec8210b82e7efcad16caf063a01bddf661e2b05cae26315bb800484c2560f5841b47d093432d40db9ed7
data/CHANGELOG.md CHANGED
@@ -5,6 +5,35 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.115.0] - 2026-09-09
9
+
10
+ ### Changed
11
+
12
+ - Lockstep with libleptris 1.9.115: the RELAX NG arc completes —
13
+ the validator (Jing conformance gate 38/38), `<include>` with
14
+ Jing's override rules, `<param>` datatype facets (portable
15
+ matcher), and schema errors on `leptris_last_error`. Required by
16
+ the RelaxNG binding (#165).
17
+
18
+ ## [1.9.107.1] - 2026-09-08
19
+
20
+ ### Improved
21
+
22
+ - **SAX drain: bulk kind strip** (TODO.restructure/22 — the moxml
23
+ 0.77-0.93x row): the recorder's dispatch loop read the event
24
+ kind with a `get_uint8` per EVENT (a memory crossing for every
25
+ record, dispatched or not); the strip now reads in ONE bulk
26
+ `get_bytes` + ONE `unpack` per drain via the memoized
27
+ per-count template — kind codes 0..10 unpack to immediate
28
+ Fixnums, so the change strictly removes ~count FFI crossings
29
+ with zero new allocations. Allocation profile (load-independent
30
+ measurement) is far ahead either way: text-only 32,381 vs
31
+ Nokogiri's 400,061 allocs/parse (12x fewer); all-events 112,440
32
+ vs 400,034 (3.6x fewer) — the remaining gap is pure per-event
33
+ CPU, which this addresses. Timing verification gated on a clean
34
+ host window (bench staged; the shared machine sat at load
35
+ 40-400 today).
36
+
8
37
  ## [1.9.107.0] - 2026-09-08
9
38
 
10
39
  ### Changed
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.107"
11
+ LIBLEPTRIS_VERSION = "1.9.115"
12
12
 
13
13
  CMAKE_FLAGS = %w[
14
14
  -DCMAKE_BUILD_TYPE=Release
@@ -0,0 +1,28 @@
1
+ # 22 — SAX drain: bulk kind strip (the 0.77-0.93x row)
2
+
3
+ Status: CODE DONE — timing verification gated on a clean window
4
+
5
+ The moxml table shows SAX at 0.77-0.93x Nokogiri (it fully won at
6
+ the 1.9.40-era battery). Investigation under load-404 conditions
7
+ (timing unusable; allocation profiling is load-independent):
8
+
9
+ - **Allocations are NOT the gap** — measured GC-stat deltas:
10
+ text-only 32,381 vs Nokogiri's 400,061 allocs/parse (12x fewer);
11
+ all-events 112,440 vs 400,034 (3.6x fewer).
12
+ - **The time cost was per-event FFI crossings in the drain**: the
13
+ kind strip was read with a `get_uint8` per EVENT (all events,
14
+ dispatched or not — a memory crossing each) after the
15
+ whole-record-unpack was retired for its 2.25M-Integer profile.
16
+ - **Fix**: the kind strip reads in ONE `get_bytes` + ONE `unpack`
17
+ per drain through the already-defined memoized
18
+ `KIND_TEMPLATE_FOR` — kind codes 0..10 unpack to IMMEDIATE
19
+ Fixnums (zero allocation), so this strictly removes ~count FFI
20
+ crossings per drain with no new allocations. Per-dispatched-event
21
+ field reads (2-3 `get_uint32`) remain.
22
+
23
+ - [x] Bulk strip restored; SAX suite green; allocation profile
24
+ re-verified (32k/112k vs 400k/400k).
25
+ - [ ] **Timing verification** (the actual 0.77-0.93x row): bench
26
+ staged at /tmp/bench_sax.rb — requires load < 20 (the host
27
+ has been at 40-400). Gate: text-only and all-events SAX at
28
+ parity or better vs Nokogiri on the raw binding.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.107.0"
4
+ VERSION = "1.9.115.0"
5
5
  end
@@ -42,6 +42,7 @@ module Leptris
42
42
  typedef :pointer, :leptris_xpath_result
43
43
  typedef :pointer, :leptris_xpath_ns_set
44
44
  typedef :pointer, :leptris_xpath_compiled
45
+ typedef :pointer, :leptris_relaxng
45
46
  typedef :pointer, :leptris_xslt
46
47
  typedef :pointer, :leptris_xquery
47
48
  typedef :pointer, :leptris_sax_recorder
@@ -733,6 +734,18 @@ attach_function :leptris_parse_string,
733
734
  attach_function :leptris_str_has_nonstandard_entity,
734
735
  [:string, :size_t], :int
735
736
 
737
+ attach_function :leptris_rng_parse,
738
+ [:string, :size_t, :pointer], :leptris_relaxng
739
+ attach_function :leptris_rng_parse_file,
740
+ [:string, :pointer], :leptris_relaxng
741
+ attach_function :leptris_rng_free,
742
+ [:leptris_relaxng], :void
743
+ # 1 valid; 0 invalid (detail via leptris_rng_error)
744
+ attach_function :leptris_rng_validate,
745
+ [:leptris_relaxng, :leptris_document], :int
746
+ attach_function :leptris_rng_error,
747
+ [:leptris_relaxng], :string
748
+
736
749
  attach_function :leptris_xslt_parse,
737
750
  [:string, :size_t], :leptris_xslt
738
751
  attach_function :leptris_xslt_parse_file,
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Leptris::XML::RelaxNG
6
+ module_function
7
+
8
+ def parse(schema_xml)
9
+ Schema.parse(schema_xml)
10
+ end
11
+
12
+ def parse_file(path)
13
+ Schema.parse_file(path)
14
+ end
15
+
16
+ # A compiled RELAX NG schema (libleptris >= 1.9.115): Jing-parity
17
+ # validation against the standard DOM — parse once, validate any
18
+ # number of documents. Schema errors publish to the thread-global
19
+ # error channel, so failures raise with the engine's detail
20
+ # ("unknown pattern element: ...", "cannot open schema file ...").
21
+ #
22
+ # rng = Leptris::XML::RelaxNG.parse(schema_xml)
23
+ # rng.valid?(doc) # => true/false
24
+ # rng.validate(doc) # => [] or ["1:0: error: element ..."]
25
+ #
26
+ class Schema
27
+ # GC-managed compiled handle.
28
+ class Handle < ::FFI::AutoPointer
29
+ def self.release(ptr)
30
+ Leptris::XML::FFI.leptris_rng_free(ptr)
31
+ end
32
+ end
33
+
34
+ # Compile a schema from XML text. Raises Error on a schema
35
+ # error (detail via leptris_last_error).
36
+ def self.parse(schema_xml)
37
+ xml = schema_xml.to_s
38
+ raw = Leptris::XML::FFI.leptris_rng_parse(xml, xml.bytesize, nil)
39
+ if raw.null?
40
+ raise Leptris::XML::Error,
41
+ "schema parse failed: #{Leptris::XML::FFI.leptris_last_error}"
42
+ end
43
+ new(Handle.new(raw))
44
+ end
45
+
46
+ # Compile from a file; <include href="..."> resolves relative to
47
+ # the file's directory (Jing's override rules apply).
48
+ def self.parse_file(path)
49
+ raw = Leptris::XML::FFI.leptris_rng_parse_file(path.to_s, nil)
50
+ if raw.null?
51
+ raise Leptris::XML::Error,
52
+ "schema parse failed: #{Leptris::XML::FFI.leptris_last_error}"
53
+ end
54
+ new(Handle.new(raw))
55
+ end
56
+
57
+ def initialize(handle)
58
+ @handle = handle
59
+ end
60
+
61
+ # Nokogiri shape: an array of error strings (the engine emits
62
+ # Jing's "line:col: error: message" form), empty when valid.
63
+ def validate(document)
64
+ ok = Leptris::XML::FFI.leptris_rng_validate(@handle,
65
+ document.c_ptr)
66
+ return [] if ok != 0
67
+ [Leptris::XML::FFI.leptris_rng_error(@handle)].compact
68
+ end
69
+
70
+ def valid?(document)
71
+ Leptris::XML::FFI.leptris_rng_validate(@handle,
72
+ document.c_ptr) != 0
73
+ end
74
+ end
75
+ end
@@ -159,16 +159,20 @@ class Leptris::XML::SAX::Recorder
159
159
  @handle, len_ptr)
160
160
  arena = arena_ptr.read_bytes(len_ptr.read_uint64)
161
161
  count = count_ptr.read_uint64
162
- # Kind is the first byte of each record — read it off the C
163
- # pointer. The previous path copied every record into Ruby
164
- # and unpacked a Fixnum array just for the kind strip
165
- # (2.25M Integers on a single-feed drain); one get_uint8 per
166
- # event replaces both.
162
+ # Kind strip in ONE bulk read + ONE unpack per drain: the
163
+ # per-count template reads the first byte of each record,
164
+ # skipping the stride (KIND_TEMPLATE_FOR is memoized per
165
+ # count). Kind codes are 0..10 — unpack yields immediate
166
+ # Fixnums, so the strip allocates nothing. This replaces a
167
+ # get_uint8 crossing per EVENT (all events, dispatched or
168
+ # not) with one memory read per drain — the SAX gap to
169
+ # Nokogiri's per-event C callback lives exactly here.
170
+ kinds = records_ptr.get_bytes(0, count * RECORD_STRIDE)
171
+ .unpack(KIND_TEMPLATE_FOR[count])
167
172
  one_arg_start = dispatched[:start_element] == :one_arg
168
173
  i = 0
169
174
  while i < count
170
- code = records_ptr.get_uint8(i * RECORD_STRIDE)
171
- kind = KIND_BY_CODE[code]
175
+ kind = kinds[i] && KIND_BY_CODE[kinds[i]]
172
176
  if kind && dispatched[kind]
173
177
  base = i * RECORD_STRIDE
174
178
  case kind
data/lib/leptris/xml.rb CHANGED
@@ -28,6 +28,7 @@ module Leptris
28
28
  autoload :ResultAttr, "leptris/xml/result_attr"
29
29
  autoload :XSLT, "leptris/xml/xslt"
30
30
  autoload :XQuery, "leptris/xml/xquery"
31
+ autoload :RelaxNG, "leptris/xml/relaxng"
31
32
  autoload :Pull, "leptris/xml/pull"
32
33
  autoload :Iterparse, "leptris/xml/iterparse"
33
34
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.107.0
4
+ version: 1.9.115.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-08 00:00:00.000000000 Z
11
+ date: 2026-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -90,6 +90,7 @@ files:
90
90
  - TODO.restructure/19-gitignore-hygiene.md
91
91
  - TODO.restructure/20-utf8proc-enablement.md
92
92
  - TODO.restructure/21-typeddata-variant-prototype.md
93
+ - TODO.restructure/22-sax-drain-bulk-strip.md
93
94
  - docs/adr/0001-lockstep-mirror.md
94
95
  - docs/adr/0002-utf8-at-the-seam.md
95
96
  - docs/adr/0003-readonly-memoization-pattern.md
@@ -119,6 +120,7 @@ files:
119
120
  - lib/leptris/xml/parse_options.rb
120
121
  - lib/leptris/xml/processing_instruction.rb
121
122
  - lib/leptris/xml/pull.rb
123
+ - lib/leptris/xml/relaxng.rb
122
124
  - lib/leptris/xml/result_attr.rb
123
125
  - lib/leptris/xml/result_text.rb
124
126
  - lib/leptris/xml/sax.rb