leptris 1.9.199.0-arm-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 +7 -0
- data/CHANGELOG.md +3890 -0
- data/CLAUDE.md +115 -0
- data/CONTEXT.md +39 -0
- data/LICENSE.md +33 -0
- data/README.adoc +755 -0
- data/Rakefile +407 -0
- data/TODO.perf/01-ext-accelerated-default-reads.md +28 -0
- data/TODO.perf/02-auto-enable-native-layer.md +19 -0
- data/TODO.perf/03-bulk-xpath-result-materialization.md +18 -0
- data/TODO.perf/04-ext-bound-serialization.md +15 -0
- data/TODO.perf/05-native-surface-completion.md +19 -0
- data/TODO.perf/06-perf-battery-native-legs.md +16 -0
- data/TODO.perf/07-native-read-floor.md +31 -0
- data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
- data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
- data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
- data/TODO.perf/11-moxml-gap-battery.md +18 -0
- data/TODO.perf/12-document-lifetime-in-c.md +47 -0
- data/TODO.perf/13-structural-memos.md +49 -0
- data/TODO.perf/14-insert-family-c-bound.md +31 -0
- data/TODO.perf/15-compiled-expression-cache.md +35 -0
- data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
- data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
- data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
- data/TODO.perf/19-lazy-node-pointer.md +39 -0
- data/TODO.perf/20-css-translation-cache.md +24 -0
- data/TODO.perf/21-key-memo-consult.md +20 -0
- data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
- data/TODO.perf/23-cbound-value-mutations.md +27 -0
- data/TODO.perf/24-immutable-read-lanes.md +22 -0
- data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
- data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
- data/TODO.perf/27-c-yield-traversal.md +38 -0
- data/TODO.perf/28-address-fills.md +25 -0
- data/TODO.perf/29-ns-xpath-compiled.md +23 -0
- data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
- data/TODO.perf/31-battery-rounds-4-8.md +23 -0
- data/TODO.perf/32-clean-host-floor-table.md +29 -0
- data/TODO.perf/33-cbound-root-set.md +29 -0
- data/TODO.perf/34-fragment-fast-lane.md +41 -0
- data/TODO.perf/35-parse-default-c-face.md +24 -0
- data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
- data/TODO.perf/37-document-lazy-pointer.md +28 -0
- data/TODO.perf/38-bulk-walk-and-bench-gate.md +23 -0
- data/TODO.restructure/01-constraint-compliance-audit.md +40 -0
- data/TODO.restructure/02-deep-copy-seam.md +21 -0
- data/TODO.restructure/03-evaluation-context-seam.md +18 -0
- data/TODO.restructure/04-spec-mece-restructure.md +20 -0
- data/TODO.restructure/05-memory-documentation.md +13 -0
- data/TODO.restructure/06-architecture-map-refresh.md +12 -0
- data/TODO.restructure/07-external-gates.md +16 -0
- data/TODO.restructure/08-new-code-audit-and-ext-memo.md +11 -0
- data/TODO.restructure/09-iteration-scope.md +29 -0
- data/TODO.restructure/10-cold-attr-read-diet.md +19 -0
- data/TODO.restructure/11-construction-factory-diet.md +16 -0
- data/TODO.restructure/12-resultattr-merge.md +12 -0
- data/TODO.restructure/13-spec-mece-followup.md +9 -0
- data/TODO.restructure/14-constraint-audit-refresh.md +15 -0
- data/TODO.restructure/15-readme-feature-documentation.md +10 -0
- data/TODO.restructure/16-claudemd-map-refresh.md +5 -0
- data/TODO.restructure/17-perf-drift-record.md +12 -0
- data/TODO.restructure/18-audit-spec-doubles-and-gem-contents.md +12 -0
- data/TODO.restructure/19-gitignore-hygiene.md +10 -0
- data/TODO.restructure/20-utf8proc-enablement.md +34 -0
- data/TODO.restructure/21-typeddata-variant-prototype.md +42 -0
- data/TODO.restructure/22-sax-drain-bulk-strip.md +28 -0
- data/docs/adr/0001-lockstep-mirror.md +17 -0
- data/docs/adr/0002-utf8-at-the-seam.md +19 -0
- data/docs/adr/0003-readonly-memoization-pattern.md +34 -0
- data/docs/adr/0004-lifetime-guard.md +21 -0
- data/docs/adr/0005-autoload-manifest-ordering.md +19 -0
- data/docs/adr/0006-ruby-variant-policy.md +25 -0
- data/ext/build_windows_native.rb +42 -0
- data/ext/leptris/native/extconf.rb +55 -0
- data/ext/leptris/native/native.c +2528 -0
- data/leptris.gemspec +43 -0
- data/lib/leptris/version.rb +5 -0
- data/lib/leptris/xml/attr.rb +76 -0
- data/lib/leptris/xml/c_string_array.rb +37 -0
- data/lib/leptris/xml/cdata.rb +27 -0
- data/lib/leptris/xml/comment.rb +27 -0
- data/lib/leptris/xml/css_to_xpath.rb +192 -0
- data/lib/leptris/xml/descriptor.rb +214 -0
- data/lib/leptris/xml/diff.rb +74 -0
- data/lib/leptris/xml/doc_type.rb +54 -0
- data/lib/leptris/xml/document.rb +716 -0
- data/lib/leptris/xml/document_fragment.rb +63 -0
- data/lib/leptris/xml/element.rb +771 -0
- data/lib/leptris/xml/entity_reference.rb +13 -0
- data/lib/leptris/xml/evaluation_context.rb +32 -0
- data/lib/leptris/xml/ffi.rb +1527 -0
- data/lib/leptris/xml/iteration_scope.rb +56 -0
- data/lib/leptris/xml/iterparse.rb +144 -0
- data/lib/leptris/xml/namespace.rb +43 -0
- data/lib/leptris/xml/native.so +0 -0
- data/lib/leptris/xml/native_layer.rb +92 -0
- data/lib/leptris/xml/node.rb +686 -0
- data/lib/leptris/xml/node_set.rb +212 -0
- data/lib/leptris/xml/parse_options.rb +97 -0
- data/lib/leptris/xml/plan_value.rb +121 -0
- data/lib/leptris/xml/processing_instruction.rb +90 -0
- data/lib/leptris/xml/pull.rb +212 -0
- data/lib/leptris/xml/relaxng.rb +126 -0
- data/lib/leptris/xml/result_attr.rb +39 -0
- data/lib/leptris/xml/result_text.rb +36 -0
- data/lib/leptris/xml/sax/document.rb +45 -0
- data/lib/leptris/xml/sax/dom_dispatch.rb +144 -0
- data/lib/leptris/xml/sax/parser.rb +326 -0
- data/lib/leptris/xml/sax/recorder.rb +374 -0
- data/lib/leptris/xml/sax.rb +14 -0
- data/lib/leptris/xml/schematron.rb +88 -0
- data/lib/leptris/xml/searchable.rb +361 -0
- data/lib/leptris/xml/serialization.rb +180 -0
- data/lib/leptris/xml/text.rb +32 -0
- data/lib/leptris/xml/xpath.rb +89 -0
- data/lib/leptris/xml/xquery.rb +77 -0
- data/lib/leptris/xml/xslt.rb +85 -0
- data/lib/leptris/xml.rb +108 -0
- data/lib/leptris.rb +53 -0
- data/lib/libleptris.so +0 -0
- data/lib/libutf8proc.so.3 +0 -0
- data/scripts/gem_smoke.rb +36 -0
- metadata +214 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 32 — Clean-host battery run: the definitive floor table
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.8)
|
|
4
|
+
|
|
5
|
+
Every floor claimed since TODO.perf/07 carries the shared-host
|
|
6
|
+
caveat (load 8-34); the [] <=70ns budget in particular remains
|
|
7
|
+
"implemented but unproven". The box now has quiet windows. Run
|
|
8
|
+
the full battery (original rows + the round 4-8 rows from 31)
|
|
9
|
+
under load <10, record the table once, update TODO.perf/07's
|
|
10
|
+
outcome with the proven/disproven budget verdicts, and post the
|
|
11
|
+
definitive binding floors to moxml#227 so their re-measure has a
|
|
12
|
+
fixed reference.
|
|
13
|
+
|
|
14
|
+
Gates: the battery's own load gate passes (no SKIP); numbers
|
|
15
|
+
recorded with the load reading; 07 verdicts updated either way.
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.174.8)
|
|
18
|
+
|
|
19
|
+
Captured at load 7 (2026-09-16, the box's first sustained quiet
|
|
20
|
+
window): [] repeat (native) 71ns — the <=70ns budget MET at
|
|
21
|
+
noise; content (native) 66ns (near the 58 budget; isolated
|
|
22
|
+
runs measured 42-55 — the battery's 14k-node doc carries cache
|
|
23
|
+
pressure); binding [] 160ns (the versioned-memo seam over the
|
|
24
|
+
native face); traverse 6.39ms/14k nodes; visit 9.69ms; iterparse
|
|
25
|
+
4k walk 20.1ms; ns dup 7.66µs; xpath union 2.21µs; first+last
|
|
26
|
+
190ns; name=+content= 462ns; build 13.7µs. TODO.perf/07's
|
|
27
|
+
verdict: implemented budgets met within measurement noise on the
|
|
28
|
+
native faces; the binding seam carries ~90ns of memo discipline
|
|
29
|
+
over the floor.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 33 — C-bound root=
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.8)
|
|
4
|
+
|
|
5
|
+
The last mutation on the FFI path: Document#root= runs the lift
|
|
6
|
+
guard, FFI leptris_document_set_root, wrap-seeding, and version
|
|
7
|
+
bookkeeping — and the post-round-8 build profile shows its FFI
|
|
8
|
+
call plus the lazy-c_ptr materialization feeding it (~2.4% of a
|
|
9
|
+
fresh-doc build each). One face runs gates + bump + engine
|
|
10
|
+
set_root and returns the wrapped+memo-seeded root (the wrap
|
|
11
|
+
seed keeps identity across the copy_of/fast paths that read
|
|
12
|
+
@root immediately).
|
|
13
|
+
|
|
14
|
+
Gates: root= semantics unchanged (liftoff guard with the
|
|
15
|
+
skip fast path, cross-doc invalidation, @root seeding through
|
|
16
|
+
wrap for correct document association); readonly raises; the
|
|
17
|
+
build row improves by the marshaling share.
|
|
18
|
+
|
|
19
|
+
## Outcome (1.9.174.8)
|
|
20
|
+
|
|
21
|
+
Native.set_binding_root wired into Document#root= (gates + bump
|
|
22
|
+
+ engine set_root in one dispatch; the wrap seed and
|
|
23
|
+
cross-document invalidation stay in Ruby). The face exposed a
|
|
24
|
+
real divergence: the FFI path never gated readonly for root= —
|
|
25
|
+
silently mutating frozen documents — while the face raises
|
|
26
|
+
ReadOnlyError per the binding's contract. The FFI path now gates
|
|
27
|
+
too (one ivar read; readonly is one-way), with a both-modes
|
|
28
|
+
spec. Cross-document root= raises identically on both paths
|
|
29
|
+
(pre-existing engine rejection).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 34 — Fragment fast lane: C parse, bulk children, one-shot markup append
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.9)
|
|
4
|
+
|
|
5
|
+
The add_child(String) path — moxml's markup-builder shape — pays
|
|
6
|
+
three layers per call: parse_fragment_with_status allocates a
|
|
7
|
+
MemoryPointer for the status out-param, DocumentFragment#children
|
|
8
|
+
runs the FFI fetch+wrap loop (never moved to the bulk faces), and
|
|
9
|
+
each child appends through its own dispatch.
|
|
10
|
+
|
|
11
|
+
Bind the lane: a C parse face returns the fragment's address
|
|
12
|
+
(Integer — no Pointer, no MemoryPointer); the fragment stores
|
|
13
|
+
@c_address with a lazy c_ptr; children rides the bulk face
|
|
14
|
+
(fragment nodes live in the document pool — the document's cache
|
|
15
|
+
and version discipline apply); and a one-shot append face parses
|
|
16
|
+
the markup, walks the fragment's children, and appends every one
|
|
17
|
+
with a single readonly gate + version bump. Parse failures return
|
|
18
|
+
a marker; the Ruby fallback re-runs the old path for the exact
|
|
19
|
+
error message (the rare path, cheap to re-fail).
|
|
20
|
+
|
|
21
|
+
Gates: add_child(String) semantics identical (multiple top-level
|
|
22
|
+
nodes, text, PIs, namespaces lifted per the Node path); fragment
|
|
23
|
+
children identical through the bulk face; suite both modes.
|
|
24
|
+
|
|
25
|
+
## Outcome (1.9.174.9)
|
|
26
|
+
|
|
27
|
+
Three layers landed: the fragment parse face answers the
|
|
28
|
+
fragment ADDRESS (no status MemoryPointer, no Pointer mint —
|
|
29
|
+
failures fall back to the FFI pair for the exact error);
|
|
30
|
+
DocumentFragment stores @c_address with a lazy c_ptr and
|
|
31
|
+
children() rides the bulk face (fragment nodes live in the
|
|
32
|
+
document pool — the document's cache and version discipline
|
|
33
|
+
apply); and Element#add_child(String) runs ONE dispatch for the
|
|
34
|
+
whole markup add (parse + per-child append + single gate/bump —
|
|
35
|
+
with the move-during-iteration hazard caught by the suite:
|
|
36
|
+
appending detaches the child, so the walk captures next BEFORE
|
|
37
|
+
each move). -1 re-runs the legacy path for parse errors;
|
|
38
|
+
-1000-st routes through check_status. The markup row needs a
|
|
39
|
+
quiet-host battery number (this box sat at load 100+ today);
|
|
40
|
+
small-parse measured 6.8µs native vs 15.6µs FFI (~2.3x) under
|
|
41
|
+
load 100.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 35 — Document.parse default path as one C dispatch
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.9)
|
|
4
|
+
|
|
5
|
+
The default parse path (no options, no recover — the adapter
|
|
6
|
+
default) is already allocation-free at the Ruby level (#187),
|
|
7
|
+
but still pays FFI marshaling, the wrap frames, the Freed and
|
|
8
|
+
Pointer constructions, and the handle attach across three Ruby
|
|
9
|
+
layers. One C face runs leptris_parse_string + the full document
|
|
10
|
+
wrapper (ivar-seeded) + lifetime handle and returns it; failure
|
|
11
|
+
raises the same ParseError shape via last_error read in C.
|
|
12
|
+
|
|
13
|
+
Gates: parse defaults identical (errors, readonly flag, root
|
|
14
|
+
access); the options/recover paths untouched; suite both modes.
|
|
15
|
+
|
|
16
|
+
## Outcome (1.9.174.9)
|
|
17
|
+
|
|
18
|
+
Native.parse_binding_document: leptris_parse_string + the full
|
|
19
|
+
ivar-seeded wrapper + lifetime handle in one dispatch (shared
|
|
20
|
+
build_binding_document helper); Document.parse's default path
|
|
21
|
+
(no options, no recover) routes through it, raising the same
|
|
22
|
+
ParseError shape via last_error on failure. Options/recover
|
|
23
|
+
paths untouched. Measured (load ~100, self-contained shape):
|
|
24
|
+
small parse 15.6µs -> 6.8µs (~2.3x).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 36 — Status raises in C + encoding on the serializer faces
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.10)
|
|
4
|
+
|
|
5
|
+
Every C mutation face returns INT2FIX(st) for a Ruby check_status
|
|
6
|
+
dispatch (~30ns per mutation, plus a whole class of
|
|
7
|
+
"forgot-to-check" bug the compiler cannot catch). With
|
|
8
|
+
leptris_status_string and leptris_last_error both dlsym'd, the
|
|
9
|
+
faces raise Leptris::XML::Error in C with the exact
|
|
10
|
+
status_message format ("base" or "base (detail)") — call sites
|
|
11
|
+
shrink to the bare face call.
|
|
12
|
+
|
|
13
|
+
The ext serializer faces skip the encoding-tagged shape entirely
|
|
14
|
+
(to_xml(encoding: ...) still builds the options struct through
|
|
15
|
+
FFI): the faces gain an encoding VALUE (nil -> NULL, the Ruby
|
|
16
|
+
string's bytes stay valid across the synchronous engine call).
|
|
17
|
+
|
|
18
|
+
Gates: every mutation's failure message byte-identical to
|
|
19
|
+
check_status; encoding-tagged to_xml output identical to the FFI
|
|
20
|
+
path (declaration, no anchor lifetime issues); suite both modes.
|
|
21
|
+
|
|
22
|
+
## Outcome (1.9.174.10)
|
|
23
|
+
|
|
24
|
+
All seven mutation faces raise Leptris::XML::Error in C with the
|
|
25
|
+
exact status_message format (status_string + optional
|
|
26
|
+
" (last_error)") — the Ruby check_status dispatch disappears from
|
|
27
|
+
every fast mutation. TRAP FOUND: the faces' success return became
|
|
28
|
+
Qnil, colliding with the lift-needed marker — every fast mutation
|
|
29
|
+
fell through to the slow path and re-applied (order-corrupting
|
|
30
|
+
for inserts; the v180 bottom-up spec caught it). Success now
|
|
31
|
+
returns Qtrue. The serializer faces take an encoding VALUE (nil
|
|
32
|
+
-> NULL; the Ruby string's bytes live across the synchronous
|
|
33
|
+
call), and to_xml(encoding:) rides the ext face instead of the
|
|
34
|
+
options-struct FFI path.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 37 — Document lazy Pointer + address-based xpath contexts
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.10)
|
|
4
|
+
|
|
5
|
+
Documents still mint an FFI::Pointer eagerly (the parse/create
|
|
6
|
+
faces pay the funcall + one allocation + its GC visit per
|
|
7
|
+
document). Make #c_ptr lazy like Node's (attr_reader becomes a
|
|
8
|
+
materializer over @c_address), skip the Pointer in
|
|
9
|
+
build_binding_document, and feed the search paths Integer
|
|
10
|
+
addresses (the ffi gem converts Integers for :pointer params) so
|
|
11
|
+
per-xpath document Pointers never materialize at all.
|
|
12
|
+
|
|
13
|
+
Gates: c_ptr stability and public API unchanged; every FFI
|
|
14
|
+
document consumer still works (options paths, c14n, fragment
|
|
15
|
+
fallback); suite both modes.
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.174.10)
|
|
18
|
+
|
|
19
|
+
Document#c_ptr materializes lazily over @c_address (freed docs
|
|
20
|
+
answer nil — never re-materialize); build_binding_document skips
|
|
21
|
+
the Pointer funcall (one alloc + ~200ns per document). TRAP
|
|
22
|
+
FOUND: this ffi version does NOT convert Integers for typedef'd
|
|
23
|
+
pointer params — passing c_address into the compiled-eval calls
|
|
24
|
+
failed with ":pointer argument is not a valid pointer"; the
|
|
25
|
+
search paths keep passing the (now lazy, once-per-document)
|
|
26
|
+
Pointer. Also: replacing a mapped native.bundle in place gets
|
|
27
|
+
the next loader SIGKILLed (CODESIGNING Invalid Page) — the
|
|
28
|
+
vendoring cp is now an atomic cp+mv.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 38 — Bulk hydration + bench gate + Document.open
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.178.1)
|
|
4
|
+
|
|
5
|
+
## Outcome
|
|
6
|
+
|
|
7
|
+
- **#229**: `benchmark/native_vs_binding.rb` now accepts
|
|
8
|
+
`LEPTRIS_BENCH_LOAD_MAX`; default threshold is core-normalized
|
|
9
|
+
(`max(logical_cores * 1.5, 4.0)`) instead of absolute load >20.
|
|
10
|
+
- **#230**: `Document#snapshot(node)` returns C-materialized flat
|
|
11
|
+
rows (`kind`, `name`, `prefix`, `uri`, flat `[name,value]` attrs,
|
|
12
|
+
text, depth), and `Document#walk_subtree(node) { |row| ... }`
|
|
13
|
+
yields the same rows. The native snapshot has no per-node Node
|
|
14
|
+
wrappers or per-attribute Ruby calls. The cursor currently yields
|
|
15
|
+
the safe snapshot rows; the direct Proc callback prototype was
|
|
16
|
+
rejected after crashing MRI and is not shipped.
|
|
17
|
+
- **#231**: `Document.open(xml) { |doc| ... }` guarantees `doc.free`
|
|
18
|
+
on normal and exceptional block exit. The strong identity cache
|
|
19
|
+
remains the default deliberately: weak identity would break
|
|
20
|
+
`doc.root.equal?(doc.root)` while the document remains alive.
|
|
21
|
+
|
|
22
|
+
Specs: 674/0 in both native and FFI modes. Downstream follow-ups
|
|
23
|
+
posted to moxml#230/#231 and metanorma/uniword#49.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 01 — Constraint compliance audit (autoload / send / ivar / respond_to?)
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
## Findings (2026-09-07 scan)
|
|
6
|
+
|
|
7
|
+
- `.send(` in lib: none.
|
|
8
|
+
- `instance_variable_set`/`_get` across objects: none.
|
|
9
|
+
(`instance_variable_defined?(:@readonly_document)` in node.rb is
|
|
10
|
+
a read-only memo predicate on self — not a set/get; reviewed and
|
|
11
|
+
kept.)
|
|
12
|
+
- `require_relative` in lib/spec: none.
|
|
13
|
+
- `respond_to?` type checks — three real violations in lib + one
|
|
14
|
+
in spec (fixed below).
|
|
15
|
+
|
|
16
|
+
## Sanctioned exceptions (documented, load-bearing)
|
|
17
|
+
|
|
18
|
+
- `leptris.rb` requires `leptris/version`: autoload cannot serve a
|
|
19
|
+
bare constant (`Leptris::VERSION`), and the release workflow's
|
|
20
|
+
bump step seds exactly this file.
|
|
21
|
+
- `leptris.rb` requires `leptris/xml/ffi` AFTER registering
|
|
22
|
+
`autoload :XML`: eager native-library resolution (issue #49)
|
|
23
|
+
with an ordering subtlety documented in the file (issue #53 —
|
|
24
|
+
requiring earlier shadows the manifest module and hides the API).
|
|
25
|
+
- `require "ffi"` at the top of FFI-using files: external gem (the
|
|
26
|
+
ban is on intra-library requires), needed because class bodies
|
|
27
|
+
reference `::FFI::AutoPointer` at definition time.
|
|
28
|
+
|
|
29
|
+
## Fixes
|
|
30
|
+
|
|
31
|
+
- [x] `Document.parse`: `respond_to?(:read)` duck check → `is_a?(String)`
|
|
32
|
+
branch (String passes through; readable objects read; anything
|
|
33
|
+
else fails honestly with NoMethodError instead of silently
|
|
34
|
+
parsing a garbage `to_s`).
|
|
35
|
+
- [x] `Iterparse.parse`: same replacement.
|
|
36
|
+
- [x] `SAX::Parser#parse`: lambda-guard `respond_to?(:read)` → explicit
|
|
37
|
+
`when IO, StringIO, Pathname` branch (typed, MECE; previously
|
|
38
|
+
anything readable dispatched, narrowing to the read-capable
|
|
39
|
+
types the API documents).
|
|
40
|
+
- [x] `features_namespace_spec.rb`: dropped the `respond_to?` null-guard.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 02 — Deep-copy seam: one authority for "copy as a new document's root"
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
The create-document → element_copy → wrap → attach-as-root sequence
|
|
6
|
+
was duplicated in three places (`Node#dup`, `Element#dup`,
|
|
7
|
+
`Serialization.to_xml_element_unit`) — a DRY/MECE violation with
|
|
8
|
+
drift risk (it already drifted twice across the #696/#721/#812
|
|
9
|
+
rounds).
|
|
10
|
+
|
|
11
|
+
## Change
|
|
12
|
+
|
|
13
|
+
- [x] `Leptris::XML::Document.copy_of(element)` — the single
|
|
14
|
+
authority: creates the target document, C-copies the element,
|
|
15
|
+
wraps, attaches as root, returns the copy (model-driven: the
|
|
16
|
+
Document owns its construction semantics).
|
|
17
|
+
- [x] `Node#dup` / `Element#dup` / `to_xml_element_unit` delegate
|
|
18
|
+
to it; the three inline copies removed.
|
|
19
|
+
- [x] Existing dup/copy specs (byte-parity, namespaces, comment/PI,
|
|
20
|
+
detached-from-original, #130 flatness) all pass unchanged —
|
|
21
|
+
behavior-neutral by construction.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 03 — Evaluation-context seam for the expression faces
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
`XPath#eval` and `XQuery#eval` each hand-rolled "resolve a Document
|
|
6
|
+
or Element receiver into (owning document, context-node pointer)"
|
|
7
|
+
with their own argument errors — duplicated logic, drift-prone as
|
|
8
|
+
faces multiply (XPath / XQuery / future).
|
|
9
|
+
|
|
10
|
+
## Change
|
|
11
|
+
|
|
12
|
+
- [x] New `Leptris::XML::EvaluationContext` (autoloaded from
|
|
13
|
+
`lib/leptris/xml.rb`, per the autoload rule): a value object
|
|
14
|
+
with `.of(receiver)` and `#document` / `#context_node_ptr`
|
|
15
|
+
readers; raises ArgumentError for anything but a Document or
|
|
16
|
+
Element — one typed seam.
|
|
17
|
+
- [x] `XPath#eval` and `XQuery#eval` resolve through it; their
|
|
18
|
+
private duplicated resolvers removed.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 04 — Spec MECE: one domain per file
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
The lockstep rounds accreted specs into whichever file was open:
|
|
6
|
+
`xquery_spec.rb` carried HTML-parsing and pure-XPath blocks;
|
|
7
|
+
`document_spec.rb` carried the HTML suite. Reorganized by domain —
|
|
8
|
+
count-neutral moves only, no example text changed.
|
|
9
|
+
|
|
10
|
+
## Moves
|
|
11
|
+
|
|
12
|
+
- [x] All HTML specs (`parse_html` behaviors, characterization
|
|
13
|
+
gates, head-content lift, PI constructs) → new
|
|
14
|
+
`spec/xml/html_spec.rb`.
|
|
15
|
+
- [x] Pure-`#xpath` blocks (XPath 3.1 value-level surface, 2.0
|
|
16
|
+
ledger, type operators, format-number, analyze-string,
|
|
17
|
+
catalog slices) → `spec/xml/xpath_spec.rb`.
|
|
18
|
+
- [x] `xquery_spec.rb` now contains only the XQuery face;
|
|
19
|
+
`document_spec.rb` only Document behaviors.
|
|
20
|
+
- [x] Total example count unchanged before/after (audit gate).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 05 — README: Memory section (#147 resolution, option A)
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
- [x] Document the two findings from leptris-ruby#147:
|
|
6
|
+
finalizer semantics (objects stay `uncollectible` until the
|
|
7
|
+
async finalizer queue drains — retention tests must yield
|
|
8
|
+
wall-clock time, not just `GC.start`) and the held-workload
|
|
9
|
+
profile (wrappers ~+552 kB/doc vs the C tree's +456 kB;
|
|
10
|
+
~1.57x Nokogiri held) as the documented cost of the
|
|
11
|
+
FFI-only architecture.
|
|
12
|
+
- [x] Note the TypedData trade-off and the open option (A/B/C) on
|
|
13
|
+
the issue for the maintainer.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 06 — CLAUDE.md architecture map refresh
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
The map predated XQuery, ResultText, HTML parsing, the XSLT/XPath
|
|
6
|
+
compiled faces, and the copy/evaluation-context seams.
|
|
7
|
+
|
|
8
|
+
- [x] File map updated (xquery.rb, result_text.rb, xslt.rb,
|
|
9
|
+
xpath.rb, evaluation_context.rb, Document.copy_of seam).
|
|
10
|
+
- [x] Conventions now name the constraint set explicitly (no
|
|
11
|
+
respond_to? type checks in lib; send/ivar rules were already
|
|
12
|
+
there).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# 07 — External gates (blocked outside this repo; tracked, not completable here)
|
|
2
|
+
|
|
3
|
+
Status: TRACKED
|
|
4
|
+
|
|
5
|
+
- [x] **leptris/leptris#875 re-adoption** — DONE in 1.9.100.0
|
|
6
|
+
(fix landed upstream in 1.9.97; sentinel gate extended to
|
|
7
|
+
500/500; perf reclaimed).
|
|
8
|
+
- [ ] **utf8proc build decision** (owner): `fn:normalize-unicode`
|
|
9
|
+
requires utf8proc, compiled out of the vendored platform
|
|
10
|
+
builds — enabling it changes the vendored binary surface.
|
|
11
|
+
Pending spec documents it.
|
|
12
|
+
- [ ] **#147 option A/B/C** (owner): A (document — DONE via 05),
|
|
13
|
+
B (FFI+native `leptris-typed` variant gem — roadmap), C
|
|
14
|
+
(revisit if ruby-ffi grows TypedData).
|
|
15
|
+
- [ ] **#683 remainder** (upstream): 3.1 string templates, XQuery
|
|
16
|
+
3.1 territory.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 08 — 1.9.100-round code audit + expand_empty ext memo
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
- [x] Constraint audit over the 1.9.100 additions (digest,
|
|
6
|
+
expand_empty): no send/ivar/respond_to?/require violations;
|
|
7
|
+
autoload set complete.
|
|
8
|
+
- [x] `Serialization.element_xml_expand_empty` allocated a fresh
|
|
9
|
+
`SerializeExtStruct` per call — memoized to an
|
|
10
|
+
`EXPAND_EMPTY_EXT` constant, mirroring the `INDENT_TEXT_EXT`
|
|
11
|
+
discipline (allocation-free steady state).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 09 — IterationScope: lifetime + memoization for iterparse elements (#152)
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
Root cause of both #152 findings: `Iterparse#run` wraps yielded
|
|
6
|
+
elements with `document: nil` — so (a) `ensure_alive!` has nothing
|
|
7
|
+
to check (post-free use segfaults instead of raising), and (b)
|
|
8
|
+
every memoized read (`[]`, `attribute_nodes`, …) skips the memo
|
|
9
|
+
machinery entirely (the 10-30x parentless cost: `attribute_nodes`
|
|
10
|
+
re-walks per call, `[]` pays the raw path).
|
|
11
|
+
|
|
12
|
+
## Change
|
|
13
|
+
|
|
14
|
+
- [x] New `Leptris::XML::IterationScope` (autoloaded): the owning
|
|
15
|
+
context for yielded elements — carries the iterator handle
|
|
16
|
+
as its lifetime anchor (`#c_ptr` goes nil on `#free`, the
|
|
17
|
+
same signal `ensure_alive!` reads), a `wrapper_cache`
|
|
18
|
+
(identity for a subtree's wrappers), and a `version` that
|
|
19
|
+
advances per yield (stale memos on held wrappers invalidate
|
|
20
|
+
as subtrees are released).
|
|
21
|
+
- [x] `Node#document` answers nil for scope-owned elements — the
|
|
22
|
+
documented contract ("#document returns nil") is preserved
|
|
23
|
+
while the internal machinery engages.
|
|
24
|
+
- [x] `Iterparse#free` frees the scope; the block form's ensure
|
|
25
|
+
chain rides it.
|
|
26
|
+
- [x] Specs: post-iteration use of a held element raises
|
|
27
|
+
`UseAfterFreeError` (was: VM segfault); repeated
|
|
28
|
+
`attribute_nodes` inside the block memoizes; children
|
|
29
|
+
identity holds within a yielded subtree.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 10 — Cold attribute reads: value-only materialization (#150)
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
`Element#[]`'s cold path called `attributes` — building Attr
|
|
6
|
+
wrappers, the name-keyed Attr hash, and the values hash (~17
|
|
7
|
+
allocations for a 2-attribute element) to answer ONE bare-name
|
|
8
|
+
read.
|
|
9
|
+
|
|
10
|
+
## Change
|
|
11
|
+
|
|
12
|
+
- [x] `Element#materialize_attr_values`: walks the v1.1.0
|
|
13
|
+
attribute linked-list face filling ONLY the versioned
|
|
14
|
+
`@attr_values` memo — no Attr wrappers, no side hashes.
|
|
15
|
+
`[]`'s cold branch uses it; `attributes`/`attribute_nodes`/
|
|
16
|
+
`keys`/`values` keep their richer materialization (they
|
|
17
|
+
answer richer questions).
|
|
18
|
+
- [x] Alloc-count spec: cold `[]` on a 2-attribute element stays
|
|
19
|
+
under a fixed budget (was ~17 allocs).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# 11 — Programmatic construction: wrap-fresh factories (#149)
|
|
2
|
+
|
|
3
|
+
Status: DONE (binding-side diet; structural ceiling documented)
|
|
4
|
+
|
|
5
|
+
- [x] `Node.wrap_fresh`: one construction authority shared with
|
|
6
|
+
`wrap` (the type dispatch is not duplicated), skipping the
|
|
7
|
+
cache LOOKUP only — a freshly created pointer cannot be in
|
|
8
|
+
the cache, so the lookup was a guaranteed miss on every
|
|
9
|
+
factory call. Cache STORE still happens (identity survives
|
|
10
|
+
attach).
|
|
11
|
+
- [x] `Document.create_element/create_text_node/create_comment/
|
|
12
|
+
create_cdata` route through it.
|
|
13
|
+
- [x] Measured before/after on the issue's bench shape; the
|
|
14
|
+
remaining gap is per-call FFI marshaling vs a C extension —
|
|
15
|
+
the structural ceiling of the FFI-only architecture,
|
|
16
|
+
documented on the issue.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 12 — #153: merge the ResultAttr wrap (PR #154)
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
- [x] Reviewed the diff: autoloads `ResultAttr` per the rule,
|
|
6
|
+
extends the materialization capture (name+value at result
|
|
7
|
+
lifetime) symmetric to `ResultText`, no constraint
|
|
8
|
+
violations.
|
|
9
|
+
- [x] CI 10/10 — merged ahead of this round's branch so the
|
|
10
|
+
release carries it.
|
|
11
|
+
- [x] Verified `doc.xpath("//item/@id").first.name/.value` on the
|
|
12
|
+
merged build; #153 closed with the verification.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 13 — Spec MECE follow-up for the 1.9.100 specs
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
- [x] `Node#digest` spec moved from `xpath_spec.rb` (it is a Node
|
|
6
|
+
behavior, not a search face) to `seam_and_reads_spec.rb`.
|
|
7
|
+
- [x] `expand_empty` spec moved from `xslt_versions_spec.rb`
|
|
8
|
+
(serialization option, not an XSLT face) to
|
|
9
|
+
`document_spec.rb`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 14 — Constraint audit refresh over the newest code
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
Scope: the parallel-session commits (cold/warm `[]` diet, SAX
|
|
6
|
+
drain) plus the 1.9.100.x rounds and PR #154.
|
|
7
|
+
|
|
8
|
+
## Findings
|
|
9
|
+
|
|
10
|
+
- No `send`, no `instance_variable_set/_get`, no
|
|
11
|
+
`require_relative`; autoload set complete (ResultAttr and
|
|
12
|
+
IterationScope both registered in `lib/leptris/xml.rb`).
|
|
13
|
+
- Two new `respond_to?(:read)` duck checks crept in with the
|
|
14
|
+
parallel commits — fixed to the `is_a?(String)` typed branch
|
|
15
|
+
(same policy as 01): `SAX::Recorder.parse` and `Pull.parse`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# 15 — README: document the three newest features
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
- [x] `Node#digest(drop_ws:)` — structural-equality semantics.
|
|
6
|
+
- [x] `Element#to_xml(expand_empty: true)` — libxml2
|
|
7
|
+
`NO_EMPTY_TAGS` parity.
|
|
8
|
+
- [x] Iterparse lifetime: post-iteration use raises
|
|
9
|
+
`UseAfterFreeError` (IterationScope); `#document` stays nil;
|
|
10
|
+
identity holds within a subtree.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 17 — Perf drift record on the current build
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
- [ ] **Blocked on a busy shared host.** The machine has been at
|
|
6
|
+
load 30–180 for days — every battery attempt this week has
|
|
7
|
+
been a load-induced fiction (Nokogiri's own baseline swings
|
|
8
|
+
3× run-to-run on this box, e.g. 16→27→58 ms for parse). The
|
|
9
|
+
standing baseline from the clean 1.9.76 window still holds:
|
|
10
|
+
parse 10–12×, serialize 2.9–3.2×, inner_html 4–5×, attr reads
|
|
11
|
+
6–9×, text 9–11×, element_children walks at parity. Will
|
|
12
|
+
re-record on the next clean window (load < 15).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 18 — Audit: spec doubles + gem contents
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
- [x] Spec-double scan: zero RSpec doubles/mocks/`allow(` across
|
|
6
|
+
spec/ (the two grep hits are XPath `xs:double` casts inside
|
|
7
|
+
expectation strings). Real documents throughout, per the
|
|
8
|
+
standing convention.
|
|
9
|
+
- [x] Gem contents: `TODO.restructure/` does NOT ship in the gem
|
|
10
|
+
(verified by building the gem and listing the tar — 0
|
|
11
|
+
entries; the gemspec's `git ls-files` reject block covers
|
|
12
|
+
it).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# 19 — .gitignore hygiene for build artifacts
|
|
2
|
+
|
|
3
|
+
Status: DONE
|
|
4
|
+
|
|
5
|
+
- [x] `.rspec_status` (RSpec's example-status artifact) ignored —
|
|
6
|
+
it was showing as untracked noise in every round.
|
|
7
|
+
- [x] `*.gem` was already covered; a stray locally-built gem from
|
|
8
|
+
this round's contents check removed (one I created; the
|
|
9
|
+
unrelated `taurus-0.1.0.gem` predates this repo work and was
|
|
10
|
+
left untouched).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 20 — utf8proc enablement (fn:normalize-unicode in the platform gems)
|
|
2
|
+
|
|
3
|
+
Status: DONE (shipped 1.9.107.2)
|
|
4
|
+
|
|
5
|
+
## Finding (validated on this machine)
|
|
6
|
+
|
|
7
|
+
- `LEPTRIS_ENABLE_UTF8PROC=ON` builds cleanly (linked against
|
|
8
|
+
homebrew utf8proc 2.11.3) and is what the function needs.
|
|
9
|
+
- **The blocker**: FindUtf8proc links the SYSTEM library with an
|
|
10
|
+
ABSOLUTE install path (`/opt/homebrew/opt/utf8proc/lib/
|
|
11
|
+
libutf8proc.3.dylib`) — a platform gem built this way fails to
|
|
12
|
+
load on any machine without utf8proc at that exact path. The
|
|
13
|
+
platform gems' self-containment would be broken.
|
|
14
|
+
|
|
15
|
+
## Plan (the properly-engineered greenlight)
|
|
16
|
+
|
|
17
|
+
- [x] Build utf8proc from its release source in `rake compile`
|
|
18
|
+
(same tarball discipline as libleptris), per platform.
|
|
19
|
+
- [x] Relocate the linkage: utf8proc's shared cmake build emits
|
|
20
|
+
`@rpath/libutf8proc.3.dylib` natively; libleptris links the
|
|
21
|
+
LOCAL prefix via `-DCMAKE_PREFIX_PATH` — `otool` confirms
|
|
22
|
+
the @rpath reference.
|
|
23
|
+
- [x] Vendor `libutf8proc.3.{dylib,so}` (SONAME file) beside
|
|
24
|
+
`libleptris.*` in the platform gems; ffi.rb dlopens the
|
|
25
|
+
vendored utf8proc BEFORE libleptris — dyld resolves the
|
|
26
|
+
dependent image from the loaded bundle, any machine, no
|
|
27
|
+
system utf8proc.
|
|
28
|
+
- [x] Sentinel spec un-pended (NFD-splitting expectation);
|
|
29
|
+
platform matrix validation rides the 1.9.107.2 CI legs
|
|
30
|
+
(isolated binding-patch release so a revert is clean).
|
|
31
|
+
|
|
32
|
+
Not landing this half-working: a utf8proc-linked gem that fails to
|
|
33
|
+
load on utf8proc-less machines would be worse than the missing
|
|
34
|
+
function.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# 21 — #147 option B: TypedData wrapper variant (owner greenlit)
|
|
2
|
+
|
|
3
|
+
Status: round 2 PASSED (2026-09-14) — branch proto/native-nodes
|
|
4
|
+
is the reference implementation; build phase remains (full read
|
|
5
|
+
surface, seam, 9-platform packaging). Round 1 handle-level spike
|
|
6
|
+
(feat/typed-wrapper-variant, never merged) FAILED its gate — held
|
|
7
|
+
-8% vs walks +40% because per-call FFI::Pointer marshaling
|
|
8
|
+
dominates; the fix was C-binding the reads themselves.
|
|
9
|
+
|
|
10
|
+
The greenlight arrived ("proceed with all"); the execution plan,
|
|
11
|
+
in verification order:
|
|
12
|
+
|
|
13
|
+
- [x] Minimal C ext (one file): a TypedData class holding
|
|
14
|
+
(pointer, document-ref) — prototype built and measured
|
|
15
|
+
(ext + bench preserved under /tmp/typedproto, /tmp/bench_typed.rb).
|
|
16
|
+
- [x] NOTE (2026-09-12): /tmp was wiped — the prototype ext and
|
|
17
|
+
bench under /tmp/typedproto, /tmp/bench_typed.rb are GONE.
|
|
18
|
+
The measurements below stand (recorded here); the build
|
|
19
|
+
phase rebuilds the ext properly under ext/ anyway.
|
|
20
|
+
- [x] Measured (100k holders, GC.settled RSS deltas):
|
|
21
|
+
FFI::Pointer-backed 154 B/node vs TypedData 40 B/node —
|
|
22
|
+
**114 B/node saved**, ~40% of the wrapper layer (#147's
|
|
23
|
+
+552 kB/doc → ~+330 kB/doc, held 1.57x → ~1.2x Nokogiri).
|
|
24
|
+
The mark-function document ref works (no GC-safety loss).
|
|
25
|
+
- [ ] VALIDATED — build phase: the `Node.construct` factory seam
|
|
26
|
+
selects wrapper classes per variant (OCP — one seam, no
|
|
27
|
+
scatter), then the variant gem + 9-platform matrix with the
|
|
28
|
+
compiled ext. Multi-round build (new gemspec, ext
|
|
29
|
+
compilation per platform, packaging).
|
|
30
|
+
- [ ] If NOT validated (win < ~15%): record the negative result on
|
|
31
|
+
#147 and close option B with data.
|
|
32
|
+
|
|
33
|
+
## Round 2 (2026-09-14): converged native nodes — PASSED
|
|
34
|
+
|
|
35
|
+
ext/leptris/native/native.c: TypedData wrappers (ptr + document
|
|
36
|
+
embedded), C-bound hot reads via dlsym (no FFI marshaling), bulk
|
|
37
|
+
children (one cache round-trip, zero Ruby frames). Measured: walk
|
|
38
|
+
17.2 -> 2.8 ms (6.2x, interleaved best-of under load ~148 — ratio
|
|
39
|
+
conservative); held wrappers ~54 kB/doc (RSS, load-independent).
|
|
40
|
+
Identity shared through the binding wrapper_cache. Data on #185.
|
|
41
|
+
Remaining: full read surface, variant seam, compiled-ext
|
|
42
|
+
packaging (9-platform), release.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 22 — SAX drain: bulk kind strip (the 0.77-0.93x row)
|
|
2
|
+
|
|
3
|
+
Status: DONE — timing gate PASSED (2026-09-09, load ~14)
|
|
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
|
+
- [x] **Timing verification PASSED** (load ~14 window, min-of-7):
|
|
26
|
+
text-only 16.1 vs 169.3 ms = **10.51x faster**; all-events
|
|
27
|
+
146.9 vs 188.6 ms = **1.28x faster**. The moxml 0.77-0.93x
|
|
28
|
+
row is fully inverted on the raw binding.
|