leptris 1.9.162.8 → 1.9.162.9
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 +4 -4
- data/CHANGELOG.md +20 -0
- data/TODO.perf/01-ext-accelerated-default-reads.md +8 -1
- data/TODO.perf/02-auto-enable-native-layer.md +5 -1
- data/ext/leptris/native/native.c +52 -5
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/element.rb +32 -4
- data/lib/leptris/xml/native_layer.rb +4 -0
- data/lib/leptris.rb +15 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 245dd6ccedb905fe340d1ed3117b8a0fd8cc37cfc39c5cb0d3412c1f84dbee80
|
|
4
|
+
data.tar.gz: f91b66d536994043a4dad8bbf7cf8801b3779bff2d620a2725bf4057f7f3e4bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1447bfb2999b6dbd50053281883fadfb45d3730ea8b67990fecaf24ce35cd219b479523a1e0e7b65b1e8f255769a8a77fdc3f82b7daba3b94e167f847dbd8bac
|
|
7
|
+
data.tar.gz: add08727ca40f2cf397e5e0a256629c2cd5a7f25f5a21ec07fed80241dd4b78b1b89432717cc1edbce081ecae5203a4fbddfd52572220c1a9d37dd5c7ac9b529
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.162.9] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Performance
|
|
11
|
+
|
|
12
|
+
- **TODO.perf/01 — ext-accelerated default reads**: with the
|
|
13
|
+
native layer loaded, the DEFAULT binding classes' hot reads
|
|
14
|
+
(`Element#name`, `#content`, `#prefix`, `#[]`'s engine lookups)
|
|
15
|
+
call the ext directly (one C-API dispatch + UTF-8 string build —
|
|
16
|
+
no FFI marshaling): name 276->84ns, attribute 373->199ns per
|
|
17
|
+
read. Warm loops are memo-served and unaffected; the win lands
|
|
18
|
+
on cold one-shot passes (fresh doc, single read per node):
|
|
19
|
+
13-25% on the #187 workload shape. Scope-owned (iterparse)
|
|
20
|
+
elements stay on the FFI path (pool addresses recycle).
|
|
21
|
+
- **TODO.perf/02 — auto-enable**: `require "leptris"` quietly
|
|
22
|
+
activates the native acceleration when the compiled bundle is
|
|
23
|
+
present (platform gems). Falls back to pure FFI when absent
|
|
24
|
+
(ruby-variant on TruffleRuby/JRuby). `LEPTRIS_NO_NATIVE=1`
|
|
25
|
+
forces the FFI path. The explicit `native_layer` require remains
|
|
26
|
+
the parallel-API entry for NativeNode builders.
|
|
27
|
+
|
|
8
28
|
## [1.9.162.8] - 2026-09-14
|
|
9
29
|
|
|
10
30
|
### Added
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# 01 — Ext-accelerated default reads (no FFI marshaling on hot paths)
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: SHIPPED (1.9.162.9) — name/content/prefix/[]-cold-path ride
|
|
4
|
+
the ext when the bundle is loaded. Per-read 276->84ns (3.3x,
|
|
5
|
+
pre-address) / attr 373->199ns. HONEST GATE FINDING: warm loops
|
|
6
|
+
cannot see it (memos serve after first touch) — the gate is the
|
|
7
|
+
COLD one-shot pass (fresh doc, one read per node): 13-25% on the
|
|
8
|
+
#187 shape. UTF-8 encoding via rb_utf8_str_new_cstr (suite specs
|
|
9
|
+
run both paths in one process: native_layer_spec sets the flag
|
|
10
|
+
mid-suite). Remaining: children bulk for binding classes.
|
|
4
11
|
|
|
5
12
|
The default binding's hot reads each pay FFI per-call marshaling
|
|
6
13
|
(:string returns allocate through FFI, args marshal through the
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# 02 — Auto-enable the native layer when the bundle is present
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: SHIPPED (1.9.162.9) — require "leptris" quietly
|
|
4
|
+
probes for the native bundle and activates when present.
|
|
5
|
+
LEPTRIS_NO_NATIVE=1 forces the FFI path (suite exercises both).
|
|
6
|
+
The explicit native_layer require remains the parallel-API entry
|
|
7
|
+
for NativeNode builders.
|
|
4
8
|
|
|
5
9
|
Today `require "leptris/xml/native_layer"` is opt-in. End-state:
|
|
6
10
|
`require "leptris"` quietly probes for the vendored native.bundle
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -21,6 +21,7 @@ typedef int (*children_ex_fn)(void *, void **, int *, int);
|
|
|
21
21
|
typedef int (*node_type_fn)(void *);
|
|
22
22
|
typedef void *(*next_sibling_fn)(void *);
|
|
23
23
|
typedef void *(*parent_fn)(void *);
|
|
24
|
+
typedef const char *(*elem_prefix_fn)(void *);
|
|
24
25
|
typedef const char *(*element_text_fn)(void *);
|
|
25
26
|
typedef void *(*doc_create_fn)(void);
|
|
26
27
|
typedef void *(*elem_create_fn)(void *, const char *);
|
|
@@ -37,6 +38,7 @@ static children_ex_fn f_children_ex;
|
|
|
37
38
|
static node_type_fn f_node_type;
|
|
38
39
|
static next_sibling_fn f_next_sibling;
|
|
39
40
|
static parent_fn f_parent;
|
|
41
|
+
static elem_prefix_fn f_elem_prefix;
|
|
40
42
|
static element_text_fn f_element_text;
|
|
41
43
|
static doc_create_fn f_doc_create;
|
|
42
44
|
static elem_create_fn f_elem_create;
|
|
@@ -94,6 +96,7 @@ static void resolve_symbols(const char *lib_path)
|
|
|
94
96
|
f_node_type = (node_type_fn)lib_sym(h, "leptris_node_get_type");
|
|
95
97
|
f_next_sibling = (next_sibling_fn)lib_sym(h, "leptris_node_next_sibling");
|
|
96
98
|
f_parent = (parent_fn)lib_sym(h, "leptris_node_parent");
|
|
99
|
+
f_elem_prefix = (elem_prefix_fn)lib_sym(h, "leptris_element_prefix");
|
|
97
100
|
f_element_text = (element_text_fn)lib_sym(h, "leptris_element_text");
|
|
98
101
|
f_doc_create = (doc_create_fn)lib_sym(h, "leptris_document_create");
|
|
99
102
|
f_elem_create = (elem_create_fn)lib_sym(h, "leptris_element_create");
|
|
@@ -105,7 +108,8 @@ static void resolve_symbols(const char *lib_path)
|
|
|
105
108
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
106
109
|
!f_children_ex || !f_node_type || !f_next_sibling || !f_parent ||
|
|
107
110
|
!f_element_text || !f_doc_create || !f_elem_create || !f_text_create ||
|
|
108
|
-
!f_create_child || !f_append_child || !f_set_root || !f_doc_free
|
|
111
|
+
!f_create_child || !f_append_child || !f_set_root || !f_doc_free ||
|
|
112
|
+
!f_elem_prefix)
|
|
109
113
|
rb_raise(rb_eRuntimeError, "libleptris symbols missing");
|
|
110
114
|
}
|
|
111
115
|
|
|
@@ -115,7 +119,7 @@ static VALUE nn_name(VALUE self)
|
|
|
115
119
|
const char *s;
|
|
116
120
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
117
121
|
s = f_elem_name(n->ptr);
|
|
118
|
-
return s ?
|
|
122
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
static VALUE nn_content(VALUE self)
|
|
@@ -127,7 +131,7 @@ static VALUE nn_content(VALUE self)
|
|
|
127
131
|
* nodes carry it directly. */
|
|
128
132
|
s = f_node_type(n->ptr) == 0 ? f_element_text(n->ptr)
|
|
129
133
|
: f_text_content(n->ptr);
|
|
130
|
-
return s ?
|
|
134
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
static VALUE nn_attribute(VALUE self, VALUE name)
|
|
@@ -136,7 +140,7 @@ static VALUE nn_attribute(VALUE self, VALUE name)
|
|
|
136
140
|
const char *s;
|
|
137
141
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
138
142
|
s = f_attr(n->ptr, StringValueCStr(name));
|
|
139
|
-
return s ?
|
|
143
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
static VALUE nn_allocate(VALUE klass)
|
|
@@ -355,6 +359,43 @@ static VALUE nn_address(VALUE self)
|
|
|
355
359
|
return ULL2NUM((uint64_t)(uintptr_t)n->ptr);
|
|
356
360
|
}
|
|
357
361
|
|
|
362
|
+
/* ---- Address-based fast readers (TODO.perf/01) -----------------
|
|
363
|
+
* The DEFAULT binding classes call these when the bundle is
|
|
364
|
+
* loaded: one C-API dispatch + rb_utf8_str_new_cstr — no FFI
|
|
365
|
+
* marshaling. Addresses come from FFI::Pointer#address (a Ruby
|
|
366
|
+
* ivar read on the ffi gem's Pointer). */
|
|
367
|
+
static VALUE nf_name(VALUE self, VALUE addr)
|
|
368
|
+
{
|
|
369
|
+
const char *s;
|
|
370
|
+
(void)self;
|
|
371
|
+
s = f_elem_name((void *)(uintptr_t)NUM2ULL(addr));
|
|
372
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
static VALUE nf_element_text(VALUE self, VALUE addr)
|
|
376
|
+
{
|
|
377
|
+
const char *s;
|
|
378
|
+
(void)self;
|
|
379
|
+
s = f_element_text((void *)(uintptr_t)NUM2ULL(addr));
|
|
380
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
static VALUE nf_attribute(VALUE self, VALUE addr, VALUE name)
|
|
384
|
+
{
|
|
385
|
+
const char *s;
|
|
386
|
+
(void)self;
|
|
387
|
+
s = f_attr((void *)(uintptr_t)NUM2ULL(addr), StringValueCStr(name));
|
|
388
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
static VALUE nf_prefix(VALUE self, VALUE addr)
|
|
392
|
+
{
|
|
393
|
+
const char *s;
|
|
394
|
+
(void)self;
|
|
395
|
+
s = f_elem_prefix((void *)(uintptr_t)NUM2ULL(addr));
|
|
396
|
+
return (s && *s) ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
397
|
+
}
|
|
398
|
+
|
|
358
399
|
static VALUE nn_from(VALUE klass, VALUE document, VALUE element)
|
|
359
400
|
{
|
|
360
401
|
/* Element address comes through the binding's #c_ptr address;
|
|
@@ -383,7 +424,7 @@ static VALUE leptris_native_resolve_rb(VALUE self, VALUE lib_path)
|
|
|
383
424
|
|
|
384
425
|
void Init_native(void)
|
|
385
426
|
{
|
|
386
|
-
VALUE m_leptris, m_xml;
|
|
427
|
+
VALUE m_leptris, m_xml, m_native;
|
|
387
428
|
|
|
388
429
|
m_leptris = rb_define_module("Leptris");
|
|
389
430
|
m_xml = rb_define_module_under(m_leptris, "XML");
|
|
@@ -411,4 +452,10 @@ void Init_native(void)
|
|
|
411
452
|
rb_define_method(c_native_node, "node_type", nn_type_sym, 0);
|
|
412
453
|
rb_define_singleton_method(c_native_node, "resolve!",
|
|
413
454
|
leptris_native_resolve_rb, 1);
|
|
455
|
+
|
|
456
|
+
m_native = rb_define_module_under(m_xml, "Native");
|
|
457
|
+
rb_define_module_function(m_native, "fast_name", nf_name, 1);
|
|
458
|
+
rb_define_module_function(m_native, "fast_element_text", nf_element_text, 1);
|
|
459
|
+
rb_define_module_function(m_native, "fast_attribute", nf_attribute, 2);
|
|
460
|
+
rb_define_module_function(m_native, "fast_prefix", nf_prefix, 1);
|
|
414
461
|
}
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Leptris::XML::Element < Leptris::XML::Node
|
|
4
|
+
# TODO.perf/01: with the native bundle loaded, hot reads go
|
|
5
|
+
# through the ext (one C-API dispatch + rb_str_new_cstr — no FFI
|
|
6
|
+
# marshaling; ~3x per read). defined? is near-free when absent.
|
|
7
|
+
# Scope-owned (iterparse-yield) elements stay on the FFI path:
|
|
8
|
+
# their pool addresses recycle across yields and their lifetime
|
|
9
|
+
# rules ride the IterationScope seam — the ext fast path is for
|
|
10
|
+
# document-owned trees.
|
|
11
|
+
def native_fast?
|
|
12
|
+
defined?(Leptris::XML::NATIVE_FAST) && !scope_owned?
|
|
13
|
+
end
|
|
14
|
+
|
|
4
15
|
def name
|
|
5
16
|
return @name if @name
|
|
6
17
|
ensure_alive!
|
|
7
|
-
@name =
|
|
18
|
+
@name = if native_fast?
|
|
19
|
+
Leptris::XML::Native.fast_name(@c_ptr.address)
|
|
20
|
+
else
|
|
21
|
+
Leptris::XML::FFI.leptris_element_name(@c_ptr)
|
|
22
|
+
end
|
|
8
23
|
end
|
|
9
24
|
alias_method :node_name, :name
|
|
10
25
|
|
|
@@ -19,7 +34,11 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
19
34
|
def content
|
|
20
35
|
return @content if memo_hit?(@content_version)
|
|
21
36
|
ensure_alive!
|
|
22
|
-
result =
|
|
37
|
+
result = if native_fast?
|
|
38
|
+
Leptris::XML::Native.fast_element_text(@c_ptr.address)
|
|
39
|
+
else
|
|
40
|
+
Leptris::XML::FFI.leptris_element_text(@c_ptr)
|
|
41
|
+
end
|
|
23
42
|
if @document
|
|
24
43
|
@content = result
|
|
25
44
|
@content_version = @document.version
|
|
@@ -60,7 +79,11 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
60
79
|
# engine call resolves it, then the name is cached.
|
|
61
80
|
return v if !v.nil? || @attributes
|
|
62
81
|
ensure_alive!
|
|
63
|
-
v =
|
|
82
|
+
v = if native_fast?
|
|
83
|
+
Leptris::XML::Native.fast_attribute(@c_ptr.address, name)
|
|
84
|
+
else
|
|
85
|
+
Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
|
|
86
|
+
end
|
|
64
87
|
values[name] = v
|
|
65
88
|
return v
|
|
66
89
|
end
|
|
@@ -203,7 +226,12 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
203
226
|
# or nil when the element has none.
|
|
204
227
|
def prefix
|
|
205
228
|
ensure_alive!
|
|
206
|
-
|
|
229
|
+
if native_fast?
|
|
230
|
+
Leptris::XML::Native.fast_prefix(@c_ptr.address)
|
|
231
|
+
else
|
|
232
|
+
result = Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
|
|
233
|
+
result if result && !result.empty?
|
|
234
|
+
end
|
|
207
235
|
end
|
|
208
236
|
|
|
209
237
|
# The written qualified name — "foo:child" with a prefix, the
|
|
@@ -37,6 +37,10 @@ module Leptris::XML::Native
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# TODO.perf/01: accelerate the DEFAULT binding classes while the
|
|
41
|
+
# bundle is loaded (name/content/prefix/[] hot reads).
|
|
42
|
+
Leptris::XML::NATIVE_FAST = true
|
|
43
|
+
|
|
40
44
|
candidates = Leptris::XML::Native.lib_candidates
|
|
41
45
|
resolved = candidates.any? do |path|
|
|
42
46
|
begin
|
data/lib/leptris.rb
CHANGED
|
@@ -31,3 +31,18 @@ rescue LoadError => e
|
|
|
31
31
|
(Underlying error: #{e.message})
|
|
32
32
|
MSG
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
# TODO.perf/02: auto-enable the native acceleration layer when the
|
|
36
|
+
# compiled bundle is present (platform gems). Quietly falls back
|
|
37
|
+
# to pure FFI when absent (ruby-variant on TruffleRuby/JRuby,
|
|
38
|
+
# custom builds). LEPTRIS_NO_NATIVE=1 forces the FFI path (the
|
|
39
|
+
# suite uses it to exercise both surfaces). The explicit
|
|
40
|
+
# `require "leptris/xml/native_layer"` remains the parallel-API
|
|
41
|
+
# entry for NativeNode builders.
|
|
42
|
+
unless ENV["LEPTRIS_NO_NATIVE"] == "1"
|
|
43
|
+
begin
|
|
44
|
+
require "leptris/xml/native_layer"
|
|
45
|
+
rescue LoadError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
end
|