leptris 1.9.197.2-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 +2646 -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,47 @@
|
|
|
1
|
+
# 12 — Document lifetime in C: TypedData handle replaces the ObjectSpace finalizer
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.6)
|
|
4
|
+
|
|
5
|
+
Every document (parsed AND created) registers an ObjectSpace
|
|
6
|
+
finalizer: a per-document proc allocation (~110ns), the
|
|
7
|
+
define_finalizer call (~315ns), the Freed struct, and — the real
|
|
8
|
+
cost — a Ruby finalizer invocation at GC time that must dispatch
|
|
9
|
+
back through FFI to leptris_document_free (~1-2µs per document
|
|
10
|
+
inside GC pauses). Fresh-document build loops measured 7.1µs per
|
|
11
|
+
Document.create in-loop, ~4-5µs of it GC/finalizer amortization
|
|
12
|
+
(stackprof: GC ~19% of a build loop, define_finalizer+proc in the
|
|
13
|
+
top frames).
|
|
14
|
+
|
|
15
|
+
Fix: a TypedData DocHandle (struct { void *doc; }) whose dfree
|
|
16
|
+
calls leptris_document_free directly — no Ruby finalizer, no FFI
|
|
17
|
+
from finalizer context, no proc. The handle is referenced only by
|
|
18
|
+
the Document's @doc_handle ivar, so its lifetime IS the
|
|
19
|
+
Document's. Document#free keeps its explicit path (FFI free +
|
|
20
|
+
release the handle's pointer so dfree no-ops — the same
|
|
21
|
+
double-free protocol Freed enforces today, in C). The proc
|
|
22
|
+
finalizer stays for LEPTRIS_NO_NATIVE mode.
|
|
23
|
+
|
|
24
|
+
Companion: Native.create_binding_document — the full
|
|
25
|
+
Document.create in one C dispatch (engine create + wrapper
|
|
26
|
+
allocation + @c_ptr/@c_address/@freed/@readonly/@version + handle
|
|
27
|
+
attach). Document.create today pays FFI marshaling + wrap frames
|
|
28
|
+
(~250-600ns) on top of the engine call.
|
|
29
|
+
|
|
30
|
+
Gates: lifetime_contract_spec + memory_spec green in both modes;
|
|
31
|
+
double-free impossible (free → dfire no-op, finalizer-mode
|
|
32
|
+
unchanged); Document.create and fresh-doc build rows improve.
|
|
33
|
+
|
|
34
|
+
## Outcome (1.9.163.6)
|
|
35
|
+
|
|
36
|
+
DocHandle TypedData (struct { void *doc; }, RUBY_TYPED_FREE_IMMEDIATELY,
|
|
37
|
+
dfree -> leptris_document_free) attached as @doc_handle on every
|
|
38
|
+
wrap() when native is enabled; Document#free detaches it (dfree
|
|
39
|
+
no-ops). Native.create_binding_document does engine create +
|
|
40
|
+
ivar-seeded wrapper + handle in one dispatch (Document.create
|
|
41
|
+
routes through it). Freed stays the shared free-state; the proc
|
|
42
|
+
finalizer remains for LEPTRIS_NO_NATIVE. 3,000-doc GC-reclamation
|
|
43
|
+
and explicit-free-then-GC specs green; 647/0 both modes. Measured
|
|
44
|
+
under host load 14-30: GC-amortized Document.create ~5.0us (the
|
|
45
|
+
Ruby-finalizer invocation and FFI-dispatch-at-GC are gone from
|
|
46
|
+
every document lifecycle; clean-load numbers pending the battery
|
|
47
|
+
— the box never dropped under load 13 this session).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 13 — Version-stamped structural memos: root/parent/siblings (+ FFI-mode stale-@parent fix)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.6)
|
|
4
|
+
|
|
5
|
+
Document#root re-derives (FFI + wrap) on every call — every
|
|
6
|
+
pipeline entry pays it. Node#parent and the sibling hops
|
|
7
|
+
(next_sibling/previous_sibling) pay FFI + Node.wrap per call, so
|
|
8
|
+
Nokogiri-style sibling-advancement loops pay ~300ns/hop.
|
|
9
|
+
|
|
10
|
+
BUG found while designing this: in FFI mode the children walk
|
|
11
|
+
seeds @parent, and a later move (add_child to another parent)
|
|
12
|
+
leaves the stale @parent — child.parent answers the OLD parent.
|
|
13
|
+
Native mode masks it (the bulk path does not seed @parent). The
|
|
14
|
+
version-stamped memo fixes both modes: derive once, stamp with
|
|
15
|
+
the owning document's mutation version, re-derive after any
|
|
16
|
+
mutation that bumps it.
|
|
17
|
+
|
|
18
|
+
Design: parent memoizes only for document-owned nodes
|
|
19
|
+
(scope-owned iterparse elements keep deriving — a scope element
|
|
20
|
+
adopted into a document moves without its scope's version
|
|
21
|
+
advancing, so a stamp would lie). Cross-document adoption bumps
|
|
22
|
+
BOTH documents' versions (the source document's other nodes hold
|
|
23
|
+
sibling/parent stamps that the move invalidates); the moved
|
|
24
|
+
node's stamps clear at the mutation site. next_sibling /
|
|
25
|
+
previous_sibling memoize the same way. Document#root memoizes
|
|
26
|
+
against the document's own version (root= bumps it; free clears).
|
|
27
|
+
|
|
28
|
+
Gates: parent correct after same-doc and cross-doc moves in BOTH
|
|
29
|
+
modes (the FFI staleness becomes a spec); sibling chains correct
|
|
30
|
+
around unlink/insert; root identity stable; traversal-loop rows
|
|
31
|
+
improve.
|
|
32
|
+
|
|
33
|
+
## Outcome (1.9.163.6)
|
|
34
|
+
|
|
35
|
+
Version-stamped memos on Document#root (pure ivar compare, ~82ns
|
|
36
|
+
under load 17), Node#parent / next_sibling / previous_sibling
|
|
37
|
+
(~72-76ns memo hits vs ~300-500ns deriving; @structure_memoizable
|
|
38
|
+
computed once at construction — including the C-constructed
|
|
39
|
+
wrappers — keeping the hit path free of method dispatch). The
|
|
40
|
+
FFI-mode stale-@parent bug is FIXED (spec-pinned: parent correct
|
|
41
|
+
after same-doc and cross-doc moves; the seed stamps only
|
|
42
|
+
constructor-KNOWN parents — nil constructor parent means unknown,
|
|
43
|
+
because the engine may attach during creation, e.g. create_child).
|
|
44
|
+
Cross-document moves clear the moved node's stamps and advance
|
|
45
|
+
the SOURCE document's version. The adoption-lift ancestor walk
|
|
46
|
+
uses Node#unstamped_parent: deriving a stamped parent between the
|
|
47
|
+
version bump and the engine move would record post-bump versions
|
|
48
|
+
carrying pre-move truth. Scope-owned iterparse elements never
|
|
49
|
+
memoize.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 14 — C-bound insert family + cold-[] native fill
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.6)
|
|
4
|
+
|
|
5
|
+
add_child and []= are C-bound (TODO.perf/08-09); prepend_child,
|
|
6
|
+
add_next_sibling, and add_previous_sibling still run the full
|
|
7
|
+
Ruby path (ensure_writable + eager-namespace guard ordering +
|
|
8
|
+
FFI). One C face with a mode int covers all engine insertions
|
|
9
|
+
(append/prepend/insert_after/insert_before — same
|
|
10
|
+
int(void*, void*) ABI): gates + predicate + version bump + engine
|
|
11
|
+
insert in one dispatch, Qnil fallback when the child needs the
|
|
12
|
+
namespace lift, exactly like append_binding_child.
|
|
13
|
+
|
|
14
|
+
Also: the completely-cold branch of Element#[] fills through
|
|
15
|
+
leptris_element_attribute (FFI) while the partial-memo branch
|
|
16
|
+
above it uses the native face — the cold path should ride the
|
|
17
|
+
native face too (~150ns per first-touch attribute read).
|
|
18
|
+
|
|
19
|
+
Gates: insert semantics unchanged (specs exist for sibling
|
|
20
|
+
inserts); namespaced children still lift; readonly raises.
|
|
21
|
+
|
|
22
|
+
## Outcome (1.9.163.6)
|
|
23
|
+
|
|
24
|
+
Native.insert_binding_child(document, anchor, child, mode): 1
|
|
25
|
+
prepend / 2 after / 3 before — gates + predicate + version bump +
|
|
26
|
+
engine insert in one dispatch, Qnil fallback to the Ruby lift
|
|
27
|
+
path. prepend_child / add_next_sibling / add_previous_sibling
|
|
28
|
+
carry the fast branch + cross-document invalidation. The
|
|
29
|
+
completely-cold Element#[] first-touch fill rides the native face
|
|
30
|
+
(matching the partial-memo branch). Specs: ordering, lift
|
|
31
|
+
fallback, readonly raise.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 15 — Compiled-expression cache for Searchable#xpath / #at_xpath
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.6)
|
|
4
|
+
|
|
5
|
+
Searchable#xpath passes the expression STRING to
|
|
6
|
+
leptris_xpath_eval each call. The engine caches compiled
|
|
7
|
+
expressions by string (measured: repeat-string eval 7.0µs vs
|
|
8
|
+
varied-string 34.98µs — the ~28µs is per-call parsing), but the
|
|
9
|
+
string path still pays cache lookup + hashing per call. A
|
|
10
|
+
compiled handle evaluated directly measured 4.58µs on the same
|
|
11
|
+
shape — 34% under the string path.
|
|
12
|
+
|
|
13
|
+
The binding already exposes XPath.compile (parse once, eval
|
|
14
|
+
many). Add a bounded LRU keyed on the expression string inside
|
|
15
|
+
Searchable: plain (no version pin, no ns bindings) xpath/at_xpath
|
|
16
|
+
evaluations route through cached compiled handles. Cache only
|
|
17
|
+
successful compiles; bounded (~64 entries, Hash#shift eviction);
|
|
18
|
+
GVL makes the Hash ops safe.
|
|
19
|
+
|
|
20
|
+
Gates: xpath/at_xpath results identical (nodesets, scalars,
|
|
21
|
+
empty results); invalid expressions still raise XPathError;
|
|
22
|
+
namespace-bound and version-pinned paths unchanged; repeat-call
|
|
23
|
+
rows improve ~30%.
|
|
24
|
+
|
|
25
|
+
## Outcome (1.9.163.6)
|
|
26
|
+
|
|
27
|
+
Searchable.compiled_expression: bounded LRU (64 entries, Hash#shift
|
|
28
|
+
eviction, LRU refresh on hit) keyed on the expression string;
|
|
29
|
+
plain xpath/at_xpath evaluate cached compiled handles via
|
|
30
|
+
XPath#eval_ptrs (leptris_xpath_compiled_eval). Failed compiles
|
|
31
|
+
never cache (fall back to the string entry so the error surface
|
|
32
|
+
is unchanged); version-pinned and namespace-bound paths keep
|
|
33
|
+
their dedicated entries. Measured: repeat at_xpath 3.7us ->
|
|
34
|
+
1.9us under load 17-30 (~2x); CSS rides it automatically
|
|
35
|
+
(deterministic translation -> same key).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 16 — C-bound at_xpath single-result seam
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.7)
|
|
4
|
+
|
|
5
|
+
at_xpath is the hottest query shape (adapters picking one node),
|
|
6
|
+
and its result path is five FFI calls per hit: result_type,
|
|
7
|
+
get_node(0), node_kind(0), Node.wrap (pointer + construct), and
|
|
8
|
+
result_free — plus the Ruby case dispatch between them. The bulk
|
|
9
|
+
machinery from TODO.perf/03 already knows how to materialize a
|
|
10
|
+
result entry as a binding wrapper in C (identity cache, kind
|
|
11
|
+
dispatch, attribute/text value capture).
|
|
12
|
+
|
|
13
|
+
Add Native.at_xpath_first(document, result_ptr): one C dispatch
|
|
14
|
+
checks the result type; nodesets materialize entry 0 through the
|
|
15
|
+
bulk machinery (get_nodes_ex with capacity 1 carries pointer +
|
|
16
|
+
kind in one call), free the result handle, and return the binding
|
|
17
|
+
node (or Qnil on empty); non-nodeset results return Qundef so the
|
|
18
|
+
existing Ruby scalar path keeps its exact semantics. Searchable's
|
|
19
|
+
wrap_xpath_first_result routes through the face when native is
|
|
20
|
+
enabled.
|
|
21
|
+
|
|
22
|
+
Gates: at_xpath parity for elements, attributes (ResultAttr name/
|
|
23
|
+
value capture), text-kind results, empty results, and scalars;
|
|
24
|
+
the result handle always freed exactly once.
|
|
25
|
+
|
|
26
|
+
## Outcome (1.9.163.7)
|
|
27
|
+
|
|
28
|
+
Native.at_xpath_first(document, result_ptr): one dispatch — type
|
|
29
|
+
check, entry-0 materialization through the extracted
|
|
30
|
+
materialize_xp_entry (shared with the TODO.perf/03 bulk loop:
|
|
31
|
+
identity cache, kind dispatch, attribute/text value capture),
|
|
32
|
+
result free. Non-nodeset results return the Native module object
|
|
33
|
+
(Qundef must never cross into Ruby) and keep the exact Ruby
|
|
34
|
+
scalar path. Measured: repeat at_xpath 3.7µs baseline -> 1.9µs
|
|
35
|
+
(TODO.perf/15) -> ~1.3µs with the seam (2.8x total, load 8-30;
|
|
36
|
+
the same-run Nokogiri comparison on this box is anomalous and not
|
|
37
|
+
reported). Specs pin element identity, attribute capture, text
|
|
38
|
+
kinds, nil misses, scalars, and xpath().first agreement.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 17 — Precomputed fast-path flags + set_attribute tightening
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.7)
|
|
4
|
+
|
|
5
|
+
The native guards on hot paths are method chains:
|
|
6
|
+
native_fast_children? resolves as defined?(NATIVE_FAST) plus
|
|
7
|
+
scope_owned? (another method) — paid per mutation ([]=, add_child,
|
|
8
|
+
the insert family) and per cold read fill. @structure_memoizable
|
|
9
|
+
(TODO.perf/13) already carries "document-owned and alive" as an
|
|
10
|
+
ivar; NATIVE_FAST availability is fixed at load time before any
|
|
11
|
+
node exists, so the conjunction is construct-time constant.
|
|
12
|
+
|
|
13
|
+
Add @native_fast to every wrapper construction (Ruby initialize
|
|
14
|
+
and every C construction site): one ivar read replaces the chain
|
|
15
|
+
at every gate. Fold in the set_attribute shaves: the C face reads
|
|
16
|
+
attribute NAMES via StringValue+RSTRING_PTR (names cannot contain
|
|
17
|
+
NULs — same rationale as nn_attribute); VALUES keep StringValueCStr
|
|
18
|
+
so an embedded-NUL value still raises rather than truncating.
|
|
19
|
+
|
|
20
|
+
Gates: suite green in both modes (the FFI fallback semantics must
|
|
21
|
+
be unchanged for nodes constructed before a late native require —
|
|
22
|
+
worst case they stay on the FFI path); set_attribute row improves.
|
|
23
|
+
|
|
24
|
+
## Outcome (1.9.163.7)
|
|
25
|
+
|
|
26
|
+
@native_fast computed once at construction (Ruby initialize and
|
|
27
|
+
every C construction site — the bundle running implies the
|
|
28
|
+
constant), replacing the defined?+scope_owned? method chain at
|
|
29
|
+
every gate (native_fast?/native_fast_children? are ivar readers
|
|
30
|
+
now). []= additionally reads attribute names via
|
|
31
|
+
StringValue+RSTRING_PTR (values keep StringValueCStr — an
|
|
32
|
+
embedded-NUL value still raises; spec-pinned). Measured (load
|
|
33
|
+
17-31): binding []= 454ns mandate baseline -> 236ns (~2x on the
|
|
34
|
+
row; 0.88x vs Nokogiri's 209ns in the same run — effectively
|
|
35
|
+
parity).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 18 — inner_html in one C pass
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.7)
|
|
4
|
+
|
|
5
|
+
Element#inner_html walks children (bulk since TODO.perf/01) and
|
|
6
|
+
then serializes EACH child from Ruby: element children get one
|
|
7
|
+
ext dispatch each, text/comment/PI children are string-built in
|
|
8
|
+
Ruby and joined. An N-child element pays N dispatches plus the
|
|
9
|
+
case ladder plus the join.
|
|
10
|
+
|
|
11
|
+
Bind the whole pass in C: iterate the child chain once, appending
|
|
12
|
+
each child's serialization into one growable buffer — elements
|
|
13
|
+
through leptris_element_serialize_into (the fast_serialize
|
|
14
|
+
protocol), text nodes XML-escaped (& < > — the binding's
|
|
15
|
+
escape_text set), CDATA wrapped raw, comments and PIs wrapped.
|
|
16
|
+
One dispatch, one string mint, no Ruby per child.
|
|
17
|
+
|
|
18
|
+
Gates: inner_html parity for every child kind (elements, text
|
|
19
|
+
with entities, CDATA, comments, PIs, mixed), empty elements,
|
|
20
|
+
large children past the 4096-byte stack probe (growth path).
|
|
21
|
+
|
|
22
|
+
## Outcome (1.9.163.7)
|
|
23
|
+
|
|
24
|
+
Native.fast_inner_xml: one pass over the child chain into a
|
|
25
|
+
grow-only scratch — elements through leptris_element_serialize_into
|
|
26
|
+
(needed-1 protocol, grow+retry past capacity), text escaped with
|
|
27
|
+
the binding's entity set (& < > \r), CDATA raw-wrapped, comments
|
|
28
|
+
and PIs wrapped (PI data leading-whitespace-stripped, read_pi_data
|
|
29
|
+
parity). Byte-identical to the Ruby loop for every child kind and
|
|
30
|
+
past the 4KB growth floor (specs). Measured (load 17-31): 30-child
|
|
31
|
+
inner_html ~4.9µs (~160ns/child vs the loop's ~500-800ns/child
|
|
32
|
+
estimate).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 19 — Lazy FFI::Pointer on wrappers
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.3)
|
|
4
|
+
|
|
5
|
+
Every binding wrapper stores an FFI::Pointer (@c_ptr), and the C
|
|
6
|
+
construction faces mint one per node via
|
|
7
|
+
rb_funcall(FFI::Pointer, :new) — ~200ns plus a Ruby object and
|
|
8
|
+
its GC visit per node, paid at creation even though most nodes'
|
|
9
|
+
Pointers are never read (the hot reads are memoized or go through
|
|
10
|
+
the address-based native faces).
|
|
11
|
+
|
|
12
|
+
Store @c_address (Integer) as the canonical truth on every
|
|
13
|
+
wrapper; `c_ptr` becomes a lazy materializer
|
|
14
|
+
(@c_ptr ||= FFI::Pointer.new(@c_address)) preserving the public
|
|
15
|
+
API. Internal call sites: `@c_ptr.address` collapses to
|
|
16
|
+
@c_address (also skipping the #address dispatch), and remaining
|
|
17
|
+
internal @c_ptr reads go through the c_ptr reader (same cost as
|
|
18
|
+
the old attr_reader). The C faces stop constructing Pointers
|
|
19
|
+
entirely — they already hold the address.
|
|
20
|
+
|
|
21
|
+
Gates: suite green both modes (a missed site fails loudly — nil
|
|
22
|
+
to FFI raises TypeError); wrapper identity and c_ptr stability
|
|
23
|
+
unchanged; creation faces drop ~200ns and one allocation per
|
|
24
|
+
node; fresh-doc build row improves accordingly.
|
|
25
|
+
|
|
26
|
+
## Outcome (1.9.174.3)
|
|
27
|
+
|
|
28
|
+
@c_address is the canonical truth on every Node wrapper; c_ptr
|
|
29
|
+
materializes lazily (@c_ptr ||= FFI::Pointer.new(@c_address)),
|
|
30
|
+
preserving the public API and identity. All internal @c_ptr
|
|
31
|
+
reads became c_ptr reader calls; the 15 @c_ptr.address sites
|
|
32
|
+
collapsed to @c_address (also skipping the #address dispatch).
|
|
33
|
+
The C construction faces (create element/text, bulk children,
|
|
34
|
+
every xpath materialization) stopped minting Pointers entirely —
|
|
35
|
+
one allocation and ~200ns saved per node, and proportionally less
|
|
36
|
+
GC. Document keeps its eager reader (documents are few).
|
|
37
|
+
Measured under host load 22-25: GC-amortized Document.create
|
|
38
|
+
~5-6µs -> ~4.0µs; NativeNode#[] 78ns (budget 70 — clear at
|
|
39
|
+
load <10), content 45ns (budget 58 met).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 20 — CSS translation cache
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.3)
|
|
4
|
+
|
|
5
|
+
Searchable#css/#at_css run CssToXPath.convert on every call —
|
|
6
|
+
pure Ruby regex/string work (~300ns+) — before the compiled-
|
|
7
|
+
expression cache (TODO.perf/15) can hit on the result. The
|
|
8
|
+
translation is a pure function of (selector, prefix); cache it
|
|
9
|
+
with the same bounded-LRU shape (64 entries, Hash#shift
|
|
10
|
+
eviction, GVL-safe), so repeat selectors skip straight to the
|
|
11
|
+
compiled handle.
|
|
12
|
+
|
|
13
|
+
Gates: css/at_css results identical; repeat-call rows improve by
|
|
14
|
+
the translation cost; the join in xpath()'s expression build is
|
|
15
|
+
also skipped for the single-String-path shape (join minted a
|
|
16
|
+
fresh String per call even for one path).
|
|
17
|
+
|
|
18
|
+
## Outcome (1.9.174.3)
|
|
19
|
+
|
|
20
|
+
Searchable.css_expression: bounded LRU (64, shared limit) keyed
|
|
21
|
+
on prefix+selector; css/at_css translate through it so repeat
|
|
22
|
+
selectors skip the regex machinery straight to the compiled-
|
|
23
|
+
expression cache. The single-String-path shape of xpath/at_xpath
|
|
24
|
+
no longer joins (join minted a fresh String per call).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 21 — key? consults the attribute memo
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.3)
|
|
4
|
+
|
|
5
|
+
Element#key?/has_attribute? pays an engine FFI round-trip per
|
|
6
|
+
call even when the versioned @attr_values memo already proves
|
|
7
|
+
presence. Consult the memo first (hit = true; only a miss falls
|
|
8
|
+
through to the engine + written-name fallback — the memo cannot
|
|
9
|
+
prove ABSENCE for a partial fill).
|
|
10
|
+
|
|
11
|
+
Gates: key? true for memoized names, unchanged miss semantics
|
|
12
|
+
(undeclared-prefix fallback intact), readonly/namespace specs
|
|
13
|
+
green.
|
|
14
|
+
|
|
15
|
+
## Outcome (1.9.174.3)
|
|
16
|
+
|
|
17
|
+
Element#key? consults the versioned @attr_values memo first —
|
|
18
|
+
presence-proven hits skip the engine round-trip; partial-fill
|
|
19
|
+
misses still fall through to the engine + written-name fallback
|
|
20
|
+
(the memo cannot prove absence).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 22 — Eager-in-C nodeset materialization (drop the per-call AutoPointer)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.4)
|
|
4
|
+
|
|
5
|
+
Every xpath() nodeset result pays an FFI::AutoPointer + a Method
|
|
6
|
+
object + finalizer registration (~400-600ns plus their GC) just
|
|
7
|
+
to keep the C result handle alive until the lazy NodeSet
|
|
8
|
+
materializes — and virtually every consumer materializes anyway
|
|
9
|
+
(each/size/to_a). Materialize eagerly in C instead: one face runs
|
|
10
|
+
the bulk pass AND frees the result handle, returning the Ruby
|
|
11
|
+
Array; Searchable wraps it in the eager NodeSet constructor (an
|
|
12
|
+
Array-backed set has no handle at all). Exotic node kinds (the
|
|
13
|
+
module-object sentinel) fall back to the existing lazy path with
|
|
14
|
+
the handle intact.
|
|
15
|
+
|
|
16
|
+
Gates: xpath results identical for every kind (including
|
|
17
|
+
sentinel-graded kinds); laziness is internal — the public NodeSet
|
|
18
|
+
behaviors (indexing, iteration, size) unchanged; the xpath row
|
|
19
|
+
drops the AutoPointer cost per call.
|
|
20
|
+
|
|
21
|
+
## Outcome (1.9.174.4)
|
|
22
|
+
|
|
23
|
+
Native.materialize_xpath: the bulk pass AND the result free in C;
|
|
24
|
+
Searchable's nodeset branch wraps the returned Array in the eager
|
|
25
|
+
NodeSet (no AutoPointer, no Method object, no finalizer per
|
|
26
|
+
xpath call). Sentinel scans happen in C (a Ruby include? would
|
|
27
|
+
cost microseconds on large sets) — any exotic kind returns Qnil
|
|
28
|
+
with the handle alive for the lazy fallback. Measured (load
|
|
29
|
+
~11): full materialization of a 2000-node xpath set 55.9µs vs
|
|
30
|
+
Nokogiri 57.3µs — parity on the heavy shape.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 23 — C-bound name= / content= / unlink
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.4)
|
|
4
|
+
|
|
5
|
+
The value mutations still run ensure_writable! + FFI marshaling
|
|
6
|
+
per call (~450ns): Element#name=, Element#content=,
|
|
7
|
+
Text#content=, and Node#unlink (transforms rename, rewrite, and
|
|
8
|
+
remove nodes constantly). Same face shape as set_binding_attribute:
|
|
9
|
+
gates + version bump + engine write in one dispatch —
|
|
10
|
+
set_binding_name (element names), set_binding_text (element text
|
|
11
|
+
or text-node content, kind-dispatched in C), and
|
|
12
|
+
unlink_binding_node (plus the Ruby-side @parent clear and memo
|
|
13
|
+
invalidations that ride the version bump).
|
|
14
|
+
|
|
15
|
+
Gates: rename/content/unlink semantics unchanged (memos, sibling
|
|
16
|
+
stamps, path memo all invalidate through the version bump);
|
|
17
|
+
readonly raises through every face.
|
|
18
|
+
|
|
19
|
+
## Outcome (1.9.174.4)
|
|
20
|
+
|
|
21
|
+
Native.set_binding_name / set_binding_text (kind-dispatched
|
|
22
|
+
element-vs-text) / unlink_binding_node — gates + bump + engine
|
|
23
|
+
write in one dispatch, wired into Element#name=, #content=,
|
|
24
|
+
Text#content=, and Node#unlink (@parent clear and memo
|
|
25
|
+
invalidation ride the version bump). Measured (load ~11):
|
|
26
|
+
name= 237ns, content= 298ns, unlink+re-add 1.74µs; readonly
|
|
27
|
+
raises verified through every face.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 24 — Immutable read lanes: line/byte_offset memo + precomputed public #document
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.4)
|
|
4
|
+
|
|
5
|
+
Node#line and #byte_offset pay an FFI round-trip per call, but a
|
|
6
|
+
node's source position never changes — memoize unconditionally
|
|
7
|
+
(first read wins). The public Node#document walks scope_owned? (a
|
|
8
|
+
method chain) on every call though scope ownership is fixed at
|
|
9
|
+
construction — precompute @pub_document once (nil for
|
|
10
|
+
scope-owned) including at every C construction site.
|
|
11
|
+
|
|
12
|
+
Gates: line/byte_offset stable across mutations (they must be —
|
|
13
|
+
the engine reports source positions); document nil for
|
|
14
|
+
scope-owned yields exactly as before; suite both modes.
|
|
15
|
+
|
|
16
|
+
## Outcome (1.9.174.4)
|
|
17
|
+
|
|
18
|
+
line/byte_offset memoize unconditionally (a node's source
|
|
19
|
+
position never changes; 40ns hits). Node#document answers the
|
|
20
|
+
constructor-precomputed @pub_document (nil for scope-owned
|
|
21
|
+
yields) — the scope_owned? method chain disappears from one of
|
|
22
|
+
the most-called readers; every C construction site sets it.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 25 — Scope-owned (iterparse) elements on the bulk/native read path
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.5)
|
|
4
|
+
|
|
5
|
+
iterparse is moxml's streaming core, yet scope-owned elements
|
|
6
|
+
ride the FFI read path (two-call fetch + per-child Ruby wrap
|
|
7
|
+
frames; #4ca5b3c kept them off deliberately). The IterationScope
|
|
8
|
+
exposes exactly the seams the C faces read — @wrapper_cache
|
|
9
|
+
(reset per yield by new_subtree!, killing recycled-address
|
|
10
|
+
entries) and @version (advances per yield and on mutation) — so
|
|
11
|
+
the bulk children faces can take the SCOPE as their "document"
|
|
12
|
+
argument safely, provided the constructed wrappers are stamped
|
|
13
|
+
scope-aware: @structure_memoizable/@native_fast false,
|
|
14
|
+
@pub_document nil.
|
|
15
|
+
|
|
16
|
+
Pure address-based reads (fast_attribute / element text / name)
|
|
17
|
+
touch no cache — scope elements can use them via a construction
|
|
18
|
+
-time @addr_reads_fast that ignores scope ownership.
|
|
19
|
+
|
|
20
|
+
Gates: iterparse children/element_children identical via the C
|
|
21
|
+
path; identity holds within a yielded subtree and resets across
|
|
22
|
+
yields (the new_subtree! discipline); document stays nil for
|
|
23
|
+
scope yields; mutations on scope elements keep the Ruby path;
|
|
24
|
+
suite both modes.
|
|
25
|
+
|
|
26
|
+
## Outcome (1.9.174.5)
|
|
27
|
+
|
|
28
|
+
The bulk children faces take the IterationScope itself as their
|
|
29
|
+
"document" (its @wrapper_cache resets per yield via new_subtree!
|
|
30
|
+
— recycled pool addresses cannot collide — and its @version
|
|
31
|
+
advances per yield and on mutation); the C face stamps
|
|
32
|
+
scope-aware (@structure_memoizable/@native_fast false,
|
|
33
|
+
@pub_document nil) while document-owned stamps stay Qtrue.
|
|
34
|
+
Pure address-based reads (fast_attribute / element text / name)
|
|
35
|
+
switch to @addr_reads_fast — true for scope elements too (they
|
|
36
|
+
touch no cache). Mutations on scope elements keep the Ruby path.
|
|
37
|
+
Measured (load ~6): the 20k-record iterparse walk (attr + child
|
|
38
|
+
name + content per record) 124.8ms (FFI) -> 100.5ms (native
|
|
39
|
+
bulk) — ~20%.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 26 — Post-mutation memo seeding
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.5)
|
|
4
|
+
|
|
5
|
+
Every mutation invalidates the read memos through the version
|
|
6
|
+
bump, so a write-then-read cycle pays a full re-derivation even
|
|
7
|
+
when the new value is known at write time. Seed it: Element#[]=
|
|
8
|
+
re-stamps @attr_values at the new version and writes the value
|
|
9
|
+
into it (a partial fill stays partial — same rules); content=
|
|
10
|
+
stores the new text as the @content memo at the new version;
|
|
11
|
+
name= already seeds @name.
|
|
12
|
+
|
|
13
|
+
Gates: []=/content=/name= followed by reads answers from the
|
|
14
|
+
seeded memo (spec-observable via identity for strings? — no,
|
|
15
|
+
values only); mixed attribute sets then attributes() still
|
|
16
|
+
rebuild the full face on demand; invalidation semantics
|
|
17
|
+
unchanged for other mutators.
|
|
18
|
+
|
|
19
|
+
## Outcome (1.9.174.5)
|
|
20
|
+
|
|
21
|
+
Element#[]= seeds the version-stamped @attr_values at the new
|
|
22
|
+
version (a just-invalidated valid partial memo extends in place;
|
|
23
|
+
a full face or anything older restarts partial, mirroring the
|
|
24
|
+
cold-[] rules); content= seeds @content; name= seeds @name
|
|
25
|
+
(already). Measured: the []= + [] read-back cycle runs ~545ns
|
|
26
|
+
(write + read combined).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 27 — C-yield traversal (traverse/visit without the FFI::Function closure)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.6)
|
|
4
|
+
|
|
5
|
+
Node#traverse and #visit allocate an FFI::Function closure per
|
|
6
|
+
call (~1µs) and dispatch every visited node back through the
|
|
7
|
+
Ruby closure — the dominant per-node cost of whole-subtree
|
|
8
|
+
iteration, paid even on small subtrees where the walk itself is
|
|
9
|
+
microseconds.
|
|
10
|
+
|
|
11
|
+
Bind both to C callbacks that rb_yield directly: the ext's
|
|
12
|
+
traverse face keeps the abort-at-self post-order contract and
|
|
13
|
+
the exception discipline (rb_protect in the callback stashes,
|
|
14
|
+
aborts the walk, re-raises after — exactly the Ruby version's
|
|
15
|
+
contract); visit keeps its (node, entering, depth) callback
|
|
16
|
+
shape with the same stash-and-raise treatment. Node wrapping per
|
|
17
|
+
visit rides the shared binding construction (identity cache +
|
|
18
|
+
kind dispatch), and the walk state lives on the C stack — no
|
|
19
|
+
globals, nested traversals safe.
|
|
20
|
+
|
|
21
|
+
Gates: post-order sequence identical (receiver last); the
|
|
22
|
+
subtree boundary holds (abort-at-self); block exceptions
|
|
23
|
+
re-raise after the walk stops; enum_for forms unchanged; suite
|
|
24
|
+
both modes.
|
|
25
|
+
|
|
26
|
+
## Outcome (1.9.174.6)
|
|
27
|
+
|
|
28
|
+
Native.traverse_binding / visit_binding: C callbacks that
|
|
29
|
+
rb_yield directly (block forwarded explicitly — rb_yield needs
|
|
30
|
+
it on the C entry's frame). traverse keeps post-order +
|
|
31
|
+
abort-at-self + stash-abort-raise (rb_protect in the callback,
|
|
32
|
+
walk state on the C stack — nested traversals independent);
|
|
33
|
+
visit keeps (node, entering, depth) with the engine's
|
|
34
|
+
(user, node, ...) callback order. Two bugs found by the suite:
|
|
35
|
+
the visit callback's argument order (user first — the segfault
|
|
36
|
+
spec caught it) and the block forwarding. Measured (load 34):
|
|
37
|
+
traverse over 8k nodes 1663.5µs (FFI closure) -> 379.9µs
|
|
38
|
+
(4.4x; 6.81x Nokogiri); visit 2288.6µs -> 545.4µs (4.2x).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 28 — Address-based first-touch fills for the remaining reads
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.6)
|
|
4
|
+
|
|
5
|
+
The memoized content readers still fill through FFI marshaling on
|
|
6
|
+
first touch — Text/Comment/CDATA/PI content, and Node#path
|
|
7
|
+
(owned-string read + free) — and the bulk attribute faces exclude
|
|
8
|
+
scope-owned elements though they touch no document state. Add the
|
|
9
|
+
trivial address faces (fast_text/comment/cdata content, pi
|
|
10
|
+
target/data, node path with the free protocol) and switch the
|
|
11
|
+
fills to @addr_reads_fast (scope-eligible); the attributes bulk
|
|
12
|
+
path follows.
|
|
13
|
+
|
|
14
|
+
Gates: identical values for every kind (UTF-8, escapes, nils);
|
|
15
|
+
scope-owned fills answer the same; path memo unchanged; suite
|
|
16
|
+
both modes.
|
|
17
|
+
|
|
18
|
+
## Outcome (1.9.174.6)
|
|
19
|
+
|
|
20
|
+
fast_text/comment/cdata content, fast_pi_target/data (the
|
|
21
|
+
read_pi_data strip preserved), and fast_path (owned-string copy
|
|
22
|
+
+ engine free) — every first-touch content fill now rides the
|
|
23
|
+
address faces via @addr_reads_fast (scope-eligible), and the
|
|
24
|
+
bulk attribute faces accept scope-owned elements (they touch no
|
|
25
|
+
document state; the scope's version stamps the memo).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 29 — Namespace-bound xpath on the compiled + eager path
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.7)
|
|
4
|
+
|
|
5
|
+
TODO.perf/15 excluded namespace-bound evaluations from the
|
|
6
|
+
compiled-expression cache — they still pay the engine's string
|
|
7
|
+
parse per call (~2-3µs) on top of an already-cached ns set. The
|
|
8
|
+
engine has leptris_xpath_compiled_eval_ns; route the ns branch
|
|
9
|
+
of xpath/at_xpath through the compiled cache (the handle is
|
|
10
|
+
ns-independent — compile once) and the cached ns set, then let
|
|
11
|
+
the eager materializer (TODO.perf/22) take the result.
|
|
12
|
+
|
|
13
|
+
Gates: ns-bound results identical (prefix resolution, missing
|
|
14
|
+
prefixes raising as today); the version-pinned path untouched;
|
|
15
|
+
suite both modes.
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.174.7)
|
|
18
|
+
|
|
19
|
+
The ns branch of xpath/at_xpath rides the compiled-expression
|
|
20
|
+
cache (the handle is ns-independent) + the cached ns set via
|
|
21
|
+
XPath#eval_ns_ptrs (leptris_xpath_compiled_eval_ns); results
|
|
22
|
+
flow into the eager materializer. Measured (load ~16): repeat
|
|
23
|
+
ns-xpath 7.9µs -> 3.1µs (2.5x).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 30 — dup one-dispatch + first/last element child faces
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.7)
|
|
4
|
+
|
|
5
|
+
Node#dup runs create + FFI element_copy + root= (three dispatches
|
|
6
|
+
plus the lift-guard walk) for an engine-copier op that is already
|
|
7
|
+
2.3x — bind the whole copy: engine create + handle + element_copy
|
|
8
|
+
+ root wrap in one face. And the element-child readers pay O(N):
|
|
9
|
+
first_element_child scans siblings with two FFI calls per
|
|
10
|
+
non-element (text-heavy parents), last_element_children batches
|
|
11
|
+
ALL children to keep one — one C walk each answers in a single
|
|
12
|
+
dispatch.
|
|
13
|
+
|
|
14
|
+
Gates: dup semantics (comment/PI children, namespaces — the
|
|
15
|
+
copy_of seam contract #696/#721/#812); first/last on mixed
|
|
16
|
+
content, empty elements, document receivers; suite both modes.
|
|
17
|
+
|
|
18
|
+
## Outcome (1.9.174.7)
|
|
19
|
+
|
|
20
|
+
Native.copy_binding_element: engine create + lifetime handle +
|
|
21
|
+
document wrapper + element_copy + rooted+memo-seeded wrap in one
|
|
22
|
+
dispatch; the namespace-lift decision stays in Ruby (skip_
|
|
23
|
+
adoption_lift? + lift — the copy_of seam's #696/#721/#812
|
|
24
|
+
semantics), and the return shape matches copy_of's contract (the
|
|
25
|
+
copied root ELEMENT — my first version returned the document and
|
|
26
|
+
the probe caught it). Native.first/last_element_child: one C
|
|
27
|
+
walk each (the FFI scan paid two calls per skipped sibling; the
|
|
28
|
+
batch fetch materialized every child to keep one). Measured
|
|
29
|
+
(load ~16): ns-bearing element dup 37.0µs -> 9.6µs (3.9x);
|
|
30
|
+
first_element_child over 1000 mixed children 98ns (memo-hit
|
|
31
|
+
shape), last 3.2µs cold-walk.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 31 — Battery rows for the round 4-8 surfaces
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.8)
|
|
4
|
+
|
|
5
|
+
The drift battery (benchmark/native_vs_binding.rb) predates
|
|
6
|
+
TODO.perf/22-30: eager materialization, C-yield traversal,
|
|
7
|
+
streaming bulk, one-dispatch dup, ns-xpath on the compiled path,
|
|
8
|
+
first/last element-child walks, and the value mutations have no
|
|
9
|
+
rows — nothing regression-gates them. Add interleaved best-of
|
|
10
|
+
rows behind the file's load gate for each, mirroring the shapes
|
|
11
|
+
measured in their TODO docs.
|
|
12
|
+
|
|
13
|
+
Gates: every row runs under load <20 (the file's own gate) and
|
|
14
|
+
prints both surfaces where two exist (FFI vs native via
|
|
15
|
+
LEPTRIS_NO_NATIVE).
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.174.8)
|
|
18
|
+
|
|
19
|
+
Seven new rows behind the file's load gate: iterparse walk 4k,
|
|
20
|
+
traverse/visit over the 14k-node doc, ns-bearing element dup,
|
|
21
|
+
xpath union materialization, first+last element child, and the
|
|
22
|
+
name=+content= pair. The round-4-8 surfaces are now
|
|
23
|
+
regression-gated.
|