leptris 1.9.209.1-arm-linux → 1.9.210.1-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 +4 -4
- data/CHANGELOG.md +44 -0
- data/README.adoc +49 -11
- data/Rakefile +70 -1
- data/TODO.restructure/17-perf-drift-record.md +15 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/descriptor.rb +59 -2
- data/lib/leptris/xml/plan_value.rb +53 -2
- data/lib/leptris.rb +7 -4
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +14 -0
- data/vendor-src/libleptris/CHANGELOG.md +23 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/src/CMakeLists.txt +1 -0
- data/vendor-src/libleptris/src/leptris/core.c +2 -0
- data/vendor-src/libleptris/src/leptris/memory/arena.c +34 -1
- data/vendor-src/libleptris/src/leptris/memory/arena.h +3 -0
- data/vendor-src/libleptris/src/leptris/memory/doc_block.c +108 -0
- data/vendor-src/libleptris/src/leptris/memory/doc_block.h +61 -0
- data/vendor-src/libleptris/test/CMakeLists.txt +1 -0
- data/vendor-src/libleptris/test/memory/test_arena.cpp +21 -0
- data/vendor-src/libleptris/test/memory/test_doc_block.cpp +94 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1ea51f9fab577cb642f3d97041e358f9cd7bb24683f00efeda95d030d23f271
|
|
4
|
+
data.tar.gz: e5daf20fd58e184304892ae0524609a1f75421fcbd1b281c7b8b1b14b1434d14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 748f76cccbf61b98d4c8c59f9a9c334496945e148a54421cdcbacbc0fd0e065a6d8174ffb535bd2f698aea595ce401dbef3dff5ba1ad183689665d6ffa101230
|
|
7
|
+
data.tar.gz: a95bccff70898f0ebe1879d9d3d6a25216e4e64eeec499671cf5fb87ba8a7e00852bbd926ed7e01b26eab0c268f19e43a46f01569f9124ef4ebfdb660f6753b0
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,50 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added — Descriptor typed scalars + the fused materialize entry (#230's consumer contract)
|
|
11
|
+
|
|
12
|
+
- **Typed scalars**: attribute and child plan rows accept
|
|
13
|
+
`type: :string | :integer | :float | :boolean`. The tag travels
|
|
14
|
+
through the plan ABI (host-defined, echoed verbatim) and
|
|
15
|
+
`PlanValue#to_ruby` returns the cast value — Integer / Float /
|
|
16
|
+
true / false — on element scalars AND collected attributes, so
|
|
17
|
+
per-value cast probes disappear from the consumer's loop
|
|
18
|
+
(lutaml-model's `cast_element_present`/`apply_value_map` class
|
|
19
|
+
of cost). Lenient on unparseable input (raw String wins);
|
|
20
|
+
`#string_value` stays the raw escape hatch regardless of tag.
|
|
21
|
+
- **`Descriptor#materialize(source)`** — the fused loop, pointed
|
|
22
|
+
at the parse: source bytes → typed rows in ONE call (parse,
|
|
23
|
+
plan walk, document free; the standalone PlanValue tree is all
|
|
24
|
+
that surfaces). Byte-parity with parse + `#walk(root)`.
|
|
25
|
+
- Mirrors the yeptris schema-ABI shape (leptris/yeptris#238:
|
|
26
|
+
SCALAR + type_tag STR|INT|FLOAT|BOOL, whole-document results)
|
|
27
|
+
so a framework compiles one descriptor vocabulary and runs it
|
|
28
|
+
on every engine. Native in-pass int/float parsing (no string
|
|
29
|
+
round-trip) is the recorded engine-side headroom.
|
|
30
|
+
|
|
31
|
+
## [1.9.210.0] - 2026-09-20
|
|
32
|
+
|
|
33
|
+
### Performance — engine sync (1.9.210)
|
|
34
|
+
|
|
35
|
+
- Vendored libleptris 1.9.210: the node-layout fork's document
|
|
36
|
+
block (slice 1, #1253) — two contiguous geometrically-grown
|
|
37
|
+
regions per document (dense node array + string arena) with
|
|
38
|
+
region-relative byte-offset edges, so growth never invalidates
|
|
39
|
+
an offset; and the parse-arena retain ceiling raised 32 → 96MB
|
|
40
|
+
with a thread-cleanup drain (#1222/#1238 lane): repeated large
|
|
41
|
+
parses stop freeing/re-mallocing tens of MB per cycle. Measured
|
|
42
|
+
engine-side: 13MB attr shape 341 → 472 MB/s (+38%); the text
|
|
43
|
+
shape's size-decline eliminated (~605-627 flat across
|
|
44
|
+
1.5-13MB). No binding-facing surface changes; audit holds.
|
|
45
|
+
- Note: #1242 was closed upstream before our strikes 7-10
|
|
46
|
+
forensics landed; reopened with the post-fix evidence (the
|
|
47
|
+
1.9.208 memo clear is incomplete — direct-call NULL + empty
|
|
48
|
+
chain on a live element; set_root cross-document false
|
|
49
|
+
positive on windows-4.0). The ubuntu-3.4 gate and armed
|
|
50
|
+
forensics stay until the engine ships the residual fix.
|
|
51
|
+
|
|
8
52
|
## [1.9.209.0] - 2026-09-20
|
|
9
53
|
|
|
10
54
|
### Performance — engine sync (1.9.209)
|
data/README.adoc
CHANGED
|
@@ -67,8 +67,11 @@ vendors precompiled `libleptris` + `libutf8proc` binaries for the
|
|
|
67
67
|
common engine platforms under `lib/leptris/vendor/`:
|
|
68
68
|
|
|
69
69
|
- `arm64-darwin`, `x86_64-darwin`
|
|
70
|
-
- `x86_64-linux`, `aarch64-linux` (glibc
|
|
71
|
-
tried as fallback for Alpine hosts)
|
|
70
|
+
- `x86_64-linux`, `aarch64-linux` (glibc) and `x86_64-linux-musl`,
|
|
71
|
+
`aarch64-linux-musl` (tried as fallback for Alpine hosts)
|
|
72
|
+
- `arm-linux`, `arm-linux-musl` (32-bit ARM EABI5)
|
|
73
|
+
- `ppc64le-linux`, `s390x-linux`, `s390x-linux-musl` (POWER8+ LE
|
|
74
|
+
and IBM Z, qemu-built)
|
|
72
75
|
|
|
73
76
|
`gem install leptris` just works on both engines — the FFI layer
|
|
74
77
|
selects the matching vendored binary at require time (the binding
|
|
@@ -127,7 +130,10 @@ Leptris::XML::FFI.leptris_last_error # => "..."
|
|
|
127
130
|
`Leptris::XML.parse_html(html)` parses tolerant HTML4/5 into a
|
|
128
131
|
standard Document — the same nodes, pool, serializer, and
|
|
129
132
|
XPath/XSLT/XQuery machinery as XML (libleptris 1.9.75, the last
|
|
130
|
-
Nokogiri capability gap)
|
|
133
|
+
Nokogiri capability gap). The `Leptris::HTML`, `Leptris::HTML4`,
|
|
134
|
+
and `Leptris::HTML5` module facades provide the
|
|
135
|
+
Nokogiri-shaped entry points over the same engine
|
|
136
|
+
(`Leptris::HTML5.parse(html)`, `Leptris::HTML(html)`):
|
|
131
137
|
|
|
132
138
|
[source,ruby]
|
|
133
139
|
----
|
|
@@ -345,18 +351,45 @@ puts doc.to_xml
|
|
|
345
351
|
`Element#replace(node)` / `#swap(node)` :: replace in parent.
|
|
346
352
|
`Element#wrap(node_or_markup)` :: wrap this element in a new one.
|
|
347
353
|
`Node#unlink` :: detach from the tree.
|
|
354
|
+
`Element#attribute_pairs` :: bulk read-only attribute listing — `[[name, value], ...]` in document order, duplicates included; one C crossing on the native layer (libleptris 1.9.210 era).
|
|
355
|
+
|
|
356
|
+
Document diffing (`Leptris::XML.diff(a, b)`) yields an op list with
|
|
357
|
+
`#ops`, per-kind counts via `#summary`
|
|
358
|
+
(`{update_attr: 1, insert: 1}`, `{}` when identical), and `#to_json`
|
|
359
|
+
for the op list — the `leptris diff --summary/--json` CLI modes'
|
|
360
|
+
library face.
|
|
348
361
|
|
|
349
362
|
=== Building from scratch (no parse)
|
|
350
363
|
|
|
364
|
+
`Document.create` starts an empty document; build up from there
|
|
365
|
+
(document-level prolog parts included — PIs, comments, the XML
|
|
366
|
+
declaration, and the DOCTYPE are all first-class):
|
|
367
|
+
|
|
351
368
|
[source,ruby]
|
|
352
369
|
----
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
doc.
|
|
357
|
-
|
|
370
|
+
doc = Leptris::XML::Document.create
|
|
371
|
+
book = doc.create_element("book")
|
|
372
|
+
book[:id] = "b3"
|
|
373
|
+
book << doc.create_element("title")
|
|
374
|
+
doc.root = doc.create_element("catalog")
|
|
375
|
+
doc.root << book
|
|
376
|
+
doc.add_comment("built programmatically") # epilog comment
|
|
377
|
+
doc.set_doctype("catalog", system_id: "catalog.dtd")
|
|
378
|
+
|
|
379
|
+
puts doc.to_xml
|
|
358
380
|
----
|
|
359
381
|
|
|
382
|
+
[horizontal]
|
|
383
|
+
`Document.create` :: empty document (no root yet).
|
|
384
|
+
`Document#root=` :: attach the root element (bottom-up construction).
|
|
385
|
+
`Document#set_doctype(name, public_id:, system_id:)` :: programmatic DOCTYPE.
|
|
386
|
+
`Document#doctype` (alias `#internal_subset`) :: the `DocType` reader (name/root_name/public_id/system_id/internal_subset).
|
|
387
|
+
`Document#clear_declaration` :: un-set the XML declaration (serialize as if the input had none; idempotent).
|
|
388
|
+
`Document#remove_doctype` :: un-set the DOCTYPE (true when removed; the DocType stays readable until `#free`).
|
|
389
|
+
`Document#add_pi(target, data)` / `#remove_pi(target_or_index)` :: document-level processing instructions.
|
|
390
|
+
`Document#add_comment(str)` :: epilog comment.
|
|
391
|
+
`Document#create_entity_reference(name)` :: `&name;` node, serializes verbatim once attached.
|
|
392
|
+
|
|
360
393
|
== Namespaces
|
|
361
394
|
|
|
362
395
|
[horizontal]
|
|
@@ -711,11 +744,16 @@ Notable differences:
|
|
|
711
744
|
past Document disposal will see the error; silence-replace-UAF patterns
|
|
712
745
|
from Nokogiri do not apply.
|
|
713
746
|
* No `Nokogiri::CSS` parser. HTML parsing is supported
|
|
714
|
-
(`Leptris::
|
|
747
|
+
(`Leptris::HTML` / `Leptris::HTML4` / `Leptris::HTML5` facades
|
|
748
|
+
over `Leptris::XML.parse_html`, libleptris 1.9.75); Nokogiri's
|
|
715
749
|
HTML-specific node subclasses have no equivalent.
|
|
716
|
-
*
|
|
750
|
+
* RelaxNG and DTD validation ARE supported (`Leptris::XML::RelaxNG`,
|
|
751
|
+
`Leptris::XML::DTD` — parse once, validate many, structured
|
|
752
|
+
errors). No W3C XML Schema and no schema caching (XSLT 1.0–3.0
|
|
717
753
|
and an XQuery 1.0 core are supported — see above).
|
|
718
|
-
*
|
|
754
|
+
* TruffleRuby and JRuby ARE supported out of the box (#160): the
|
|
755
|
+
`ruby`-platform variant vendors precompiled binaries and the
|
|
756
|
+
binding is FFI-based (no MRI C extension required to load).
|
|
719
757
|
|
|
720
758
|
== Performance
|
|
721
759
|
|
data/Rakefile
CHANGED
|
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
8
8
|
# Pin for `rake compile` and the platform-gem builds. Keep in lockstep
|
|
9
9
|
# with .github/workflows/build.yml (which calls `rake compile`) and the
|
|
10
10
|
# CHANGELOG when libleptris releases.
|
|
11
|
-
LIBLEPTRIS_VERSION = "1.9.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.210"
|
|
12
12
|
# Vendored alongside libleptris for fn:normalize-unicode (TODO
|
|
13
13
|
# .restructure/20): built per platform with a RELOCATABLE @rpath
|
|
14
14
|
# install name, loaded by ffi.rb before libleptris so the
|
|
@@ -117,6 +117,35 @@ task :compile do
|
|
|
117
117
|
rm_rf([File.join(dest, "build"), File.join(dest, "prefix")])
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
# Rebuild notes ride every gem beside the sources (packaging
|
|
121
|
+
# doctrine): the exact pins and the commands to reproduce the
|
|
122
|
+
# vendored libraries. The ruby variant's extconf runs these
|
|
123
|
+
# trees automatically at install time.
|
|
124
|
+
File.write(File.join(vsrc, "README.md"), <<~README)
|
|
125
|
+
Sources vendored in this gem (recompile rights; the ruby
|
|
126
|
+
variant builds them at install via extconf.rb):
|
|
127
|
+
|
|
128
|
+
- libleptris v#{LIBLEPTRIS_VERSION}
|
|
129
|
+
- utf8proc v#{UTF8PROC_VERSION}
|
|
130
|
+
|
|
131
|
+
Rebuild by hand:
|
|
132
|
+
|
|
133
|
+
cmake -B vendor-src/utf8proc/build -S vendor-src/utf8proc \
|
|
134
|
+
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
|
|
135
|
+
-DUTF8PROC_ENABLE_TESTING=OFF
|
|
136
|
+
cmake --build vendor-src/utf8proc/build --config Release
|
|
137
|
+
cmake --install vendor-src/utf8proc/build --prefix <prefix>
|
|
138
|
+
|
|
139
|
+
cmake -B vendor-src/libleptris/build -S vendor-src/libleptris \
|
|
140
|
+
-DCMAKE_BUILD_TYPE=Release \
|
|
141
|
+
-DLEPTRIS_BUILD_SHARED=ON -DLEPTRIS_BUILD_STATIC=OFF \
|
|
142
|
+
-DBUILD_TESTING=OFF -DLEPTRIS_BUILD_CLI=OFF \
|
|
143
|
+
-DLEPTRIS_BUILD_BENCHMARKS=OFF -DLEPTRIS_BUILD_MAN_PAGES=OFF \
|
|
144
|
+
-DLEPTRIS_ENABLE_UTF8PROC=ON \
|
|
145
|
+
-DCMAKE_PREFIX_PATH=<utf8proc-prefix>
|
|
146
|
+
cmake --build vendor-src/libleptris/build --config Release
|
|
147
|
+
README
|
|
148
|
+
|
|
120
149
|
# libleptris 1.9.18's xslt_functions.c:270 assigns LeptrisElement
|
|
121
150
|
# to LeptrisNodeRef — GCC 14 (Alpine/musl) makes incompatible
|
|
122
151
|
# pointer types an error by default and the musl platform gems
|
|
@@ -357,6 +386,46 @@ namespace :audit do
|
|
|
357
386
|
end
|
|
358
387
|
end
|
|
359
388
|
|
|
389
|
+
# Packaging doctrine (#271): every BUILT gem carries the engine
|
|
390
|
+
# source (vendor-src/libleptris build inputs) and the ext sources;
|
|
391
|
+
# platform gems additionally carry the prebuilt libraries and must
|
|
392
|
+
# NOT carry extensions; the pure ruby gem compiles at install
|
|
393
|
+
# (extensions present). Run over pkg/*.gem before publishing —
|
|
394
|
+
# the gate is what keeps the doctrine from regressing silently.
|
|
395
|
+
desc "Audit built gems for the packaging doctrine (source + binaries + extension policy)"
|
|
396
|
+
task :doctrine do
|
|
397
|
+
gems = Dir.glob("pkg/*.gem")
|
|
398
|
+
abort "audit:doctrine: no gems in pkg/ — build first (rake gem:native:...)" if gems.empty?
|
|
399
|
+
|
|
400
|
+
gems.each do |path|
|
|
401
|
+
spec = Gem::Package.new(path).spec
|
|
402
|
+
files = spec.files.map { |f| f.gsub("\\", "/") }
|
|
403
|
+
|
|
404
|
+
missing = []
|
|
405
|
+
missing << "vendor-src/libleptris/CMakeLists.txt" unless
|
|
406
|
+
files.include?("vendor-src/libleptris/CMakeLists.txt")
|
|
407
|
+
missing << "vendor-src/README.md" unless files.include?("vendor-src/README.md")
|
|
408
|
+
missing << "ext/leptris/native/extconf.rb" unless
|
|
409
|
+
files.include?("ext/leptris/native/extconf.rb")
|
|
410
|
+
|
|
411
|
+
if spec.platform.to_s == "ruby"
|
|
412
|
+
missing << "extensions (compile-at-install)" if spec.extensions.empty?
|
|
413
|
+
else
|
|
414
|
+
binary = files.any? { |f| f =~ %r{\Alib/libleptris\.(so|dylib|dll)\z} } ||
|
|
415
|
+
files.any? { |f| f.start_with?("lib/leptris/vendor/") }
|
|
416
|
+
missing << "prebuilt engine binary" unless binary
|
|
417
|
+
missing << "extensions (platform gems never rebuild)" unless spec.extensions.empty?
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
if missing.empty?
|
|
421
|
+
puts "audit:doctrine: #{File.basename(path)} OK " \
|
|
422
|
+
"(#{spec.platform}, #{files.length} files)"
|
|
423
|
+
else
|
|
424
|
+
abort "audit:doctrine: #{File.basename(path)} MISSING: #{missing.join(', ')}"
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
360
429
|
# @api private
|
|
361
430
|
def nm_available?
|
|
362
431
|
system("nm", "--version", out: File::NULL, err: File::NULL)
|
|
@@ -10,3 +10,18 @@ Status: DONE
|
|
|
10
10
|
parse 10–12×, serialize 2.9–3.2×, inner_html 4–5×, attr reads
|
|
11
11
|
6–9×, text 9–11×, element_children walks at parity. Will
|
|
12
12
|
re-record on the next clean window (load < 15).
|
|
13
|
+
|
|
14
|
+
## Re-recorded 2026-09-21 — clean window (load ~12, 1.9.210.0, 84k-element/1.3MB catalog, interleaved best-of-5, nokogiri 1.19.4)
|
|
15
|
+
|
|
16
|
+
- [x] Standing harness (MEDIUM fixture): parse medium **11.1x** /
|
|
17
|
+
small 1.88x; xpath 2.4-20x (predicate 15-20x, complex
|
|
18
|
+
11-14x); traverse 1.5-1.7x; serialize 2.25-3.13x
|
|
19
|
+
- [x] Warm `children` walks **2.06-2.08x** (was 1.86x at 1.9.152)
|
|
20
|
+
- [x] Cold parse+first-touch walks **0.58-0.74x (avg ~0.66x)** —
|
|
21
|
+
the loss nearly HALVED from 0.34x; the engine's
|
|
22
|
+
document-block/arena work (1.9.210) shows here
|
|
23
|
+
- [x] Attr cold row (fresh wrappers) 1.18x
|
|
24
|
+
- [ ] **Open front: cold TEXT row 0.66x** — fresh text-node
|
|
25
|
+
materialization (`element_children.each(&:content)` cold)
|
|
26
|
+
loses where warm text aggregation was 6.18x. Next perf
|
|
27
|
+
round's target.
|
data/lib/leptris/version.rb
CHANGED
|
@@ -28,7 +28,33 @@ require "ffi"
|
|
|
28
28
|
# Namespace binding per plan: +ns:+ is :none (default), :any, or
|
|
29
29
|
# { exact: "urn:..." }; +flags: [:mixed_content, :ordered, :cdata,
|
|
30
30
|
# :ns_lenient].
|
|
31
|
+
#
|
|
32
|
+
# == Typed scalars (#230's fused-consumer contract, mirroring the
|
|
33
|
+
# yeptris schema ABI)
|
|
34
|
+
#
|
|
35
|
+
# Attribute and child rows accept +type: :string (default),
|
|
36
|
+
# :integer, :float, or :boolean+. The tag travels through the plan
|
|
37
|
+
# ABI (host-defined, echoed verbatim) and PlanValue#to_ruby returns
|
|
38
|
+
# the cast value — Integer / Float / true / false — so per-value
|
|
39
|
+
# cast probes disappear from the consumer. Unparseable values fall
|
|
40
|
+
# back to the raw String (lenient, first-wins house style);
|
|
41
|
+
# #string_value always returns the raw String regardless of tag.
|
|
42
|
+
#
|
|
43
|
+
# descriptor = Leptris::XML::Descriptor.build(
|
|
44
|
+
# name: "catalog",
|
|
45
|
+
# children: [
|
|
46
|
+
# { name: "price", kind: :scalar, type: :float },
|
|
47
|
+
# { name: "in_stock", kind: :scalar, type: :boolean },
|
|
48
|
+
# ])
|
|
49
|
+
# descriptor.materialize(xml_source).to_ruby # typed, one call
|
|
31
50
|
class Leptris::XML::Descriptor
|
|
51
|
+
TYPE_TAGS = {
|
|
52
|
+
string: 0,
|
|
53
|
+
integer: 1,
|
|
54
|
+
float: 2,
|
|
55
|
+
boolean: 3,
|
|
56
|
+
}.freeze
|
|
57
|
+
private_constant :TYPE_TAGS
|
|
32
58
|
class Handle < ::FFI::AutoPointer
|
|
33
59
|
def self.release(ptr)
|
|
34
60
|
Leptris::XML::FFI.leptris_plan_free(ptr)
|
|
@@ -136,7 +162,7 @@ class Leptris::XML::Descriptor
|
|
|
136
162
|
ap = Leptris::XML::FFI::AttrPlan.new(attr_memory[j])
|
|
137
163
|
ap[:wire_name] = anchor_string(anchors, row.fetch(:name))
|
|
138
164
|
ap[:kind] = kind_code(row[:kind] || :scalar)
|
|
139
|
-
ap[:type_tag] = row
|
|
165
|
+
ap[:type_tag] = type_tag_code(row)
|
|
140
166
|
end
|
|
141
167
|
ep[:attribute_count] = attrs.size
|
|
142
168
|
ep[:attribute_plans] = attr_memory
|
|
@@ -148,7 +174,7 @@ class Leptris::XML::Descriptor
|
|
|
148
174
|
cp = Leptris::XML::FFI::ChildPlan.new(child_memory[j])
|
|
149
175
|
cp[:wire_name] = anchor_string(anchors, row.fetch(:name))
|
|
150
176
|
cp[:kind] = kind_code(row[:kind] || :scalar)
|
|
151
|
-
cp[:type_tag] = row
|
|
177
|
+
cp[:type_tag] = type_tag_code(row)
|
|
152
178
|
cp[:child_plan_index] = row[:child_plan_index] || -1
|
|
153
179
|
# Rule-level ns form (libleptris 1.9.178, #1115): siblings
|
|
154
180
|
# under one parent can require different URIs when set;
|
|
@@ -189,6 +215,20 @@ class Leptris::XML::Descriptor
|
|
|
189
215
|
end
|
|
190
216
|
private_class_method :kind_code
|
|
191
217
|
|
|
218
|
+
# +type: :string/:integer/:float/:boolean+ (or a raw numeric
|
|
219
|
+
# type_tag passthrough for hosts with their own vocabulary).
|
|
220
|
+
# Public: PlanValue's attribute casts share the vocabulary.
|
|
221
|
+
def self.type_tag_code(row)
|
|
222
|
+
if row.key?(:type)
|
|
223
|
+
TYPE_TAGS.fetch(row[:type]) do
|
|
224
|
+
raise ArgumentError,
|
|
225
|
+
"type must be one of #{TYPE_TAGS.keys.inspect}, got #{row[:type].inspect}"
|
|
226
|
+
end
|
|
227
|
+
else
|
|
228
|
+
row[:type_tag] || 0
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
192
232
|
def initialize(handle, plans, root_index)
|
|
193
233
|
@handle = handle
|
|
194
234
|
@plans = plans
|
|
@@ -211,4 +251,21 @@ class Leptris::XML::Descriptor
|
|
|
211
251
|
Leptris::XML::PlanValue.new(ResultHandle.new(raw),
|
|
212
252
|
owner: true, plans: @plans, plan: @plans[0])
|
|
213
253
|
end
|
|
254
|
+
|
|
255
|
+
# The fused loop (#230, pointed at the parse): source bytes →
|
|
256
|
+
# typed rows in ONE call — parse, walk the compiled plan, free
|
|
257
|
+
# the document; the returned PlanValue tree is standalone and
|
|
258
|
+
# the Document never surfaces. Byte-for-byte parity with
|
|
259
|
+
# #walk on the same source's root element.
|
|
260
|
+
def materialize(source)
|
|
261
|
+
document = Leptris::XML::Document.parse(source)
|
|
262
|
+
begin
|
|
263
|
+
root = document.root
|
|
264
|
+
raise Leptris::XML::Error,
|
|
265
|
+
"materialize: document has no root element" if root.nil?
|
|
266
|
+
walk(root)
|
|
267
|
+
ensure
|
|
268
|
+
document.free
|
|
269
|
+
end
|
|
270
|
+
end
|
|
214
271
|
end
|
|
@@ -94,12 +94,40 @@ class Leptris::XML::PlanValue
|
|
|
94
94
|
when :callback
|
|
95
95
|
{ value: string_value, position: position, type_tag: type_tag }
|
|
96
96
|
else
|
|
97
|
-
|
|
97
|
+
typed_string_value
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
private
|
|
102
102
|
|
|
103
|
+
# SCALAR honoring the plan row's type tag (#230's typed-scalars
|
|
104
|
+
# contract): 1=Integer, 2=Float, 3=boolean. Lenient on
|
|
105
|
+
# unparseable input — the raw String wins (first-wins house
|
|
106
|
+
# style; strict validation belongs to the consumer's callback
|
|
107
|
+
# rows). #string_value stays the raw escape.
|
|
108
|
+
def typed_string_value
|
|
109
|
+
case type_tag
|
|
110
|
+
when 1
|
|
111
|
+
s = string_value
|
|
112
|
+
Integer(s, 10) rescue s
|
|
113
|
+
when 2
|
|
114
|
+
s = string_value
|
|
115
|
+
begin
|
|
116
|
+
Float(s)
|
|
117
|
+
rescue ArgumentError, TypeError
|
|
118
|
+
s
|
|
119
|
+
end
|
|
120
|
+
when 3
|
|
121
|
+
case string_value
|
|
122
|
+
when "true", "1" then true
|
|
123
|
+
when "false", "0" then false
|
|
124
|
+
else string_value
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
string_value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
103
131
|
# The element plan that a child row with this wire_name recurses
|
|
104
132
|
# into (nil for non-nested rows — their values carry no plan).
|
|
105
133
|
def child_row_plan(child_wire_name)
|
|
@@ -114,8 +142,31 @@ class Leptris::XML::PlanValue
|
|
|
114
142
|
result = {}
|
|
115
143
|
((@plan && @plan[:attributes]) || []).each do |row|
|
|
116
144
|
v = attribute(row[:name])
|
|
117
|
-
result[row[:name]] = v unless v.nil?
|
|
145
|
+
result[row[:name]] = cast_attribute(v, row) unless v.nil?
|
|
118
146
|
end
|
|
119
147
|
result
|
|
120
148
|
end
|
|
149
|
+
|
|
150
|
+
# Attribute values come back as plain Strings from the walk;
|
|
151
|
+
# apply the row's type tag with the same lenient semantics as
|
|
152
|
+
# typed_string_value.
|
|
153
|
+
def cast_attribute(value, row)
|
|
154
|
+
tag = Leptris::XML::Descriptor.type_tag_code(row)
|
|
155
|
+
case tag
|
|
156
|
+
when 1 then Integer(value, 10) rescue value
|
|
157
|
+
when 2
|
|
158
|
+
begin
|
|
159
|
+
Float(value)
|
|
160
|
+
rescue ArgumentError, TypeError
|
|
161
|
+
value
|
|
162
|
+
end
|
|
163
|
+
when 3
|
|
164
|
+
case value
|
|
165
|
+
when "true", "1" then true
|
|
166
|
+
when "false", "0" then false
|
|
167
|
+
else value
|
|
168
|
+
end
|
|
169
|
+
else value
|
|
170
|
+
end
|
|
171
|
+
end
|
|
121
172
|
end
|
data/lib/leptris.rb
CHANGED
|
@@ -29,10 +29,13 @@ begin
|
|
|
29
29
|
rescue LoadError => e
|
|
30
30
|
raise LoadError, <<~MSG
|
|
31
31
|
Leptris could not load the native libleptris library.
|
|
32
|
-
The ruby-platform gem
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
The ruby-platform gem carries the engine sources under
|
|
33
|
+
vendor-src/ and builds them at install time — a failed build
|
|
34
|
+
here usually means cmake or a C toolchain is missing (check
|
|
35
|
+
the gem install output). Alternatively install the
|
|
36
|
+
platform-specific variant for your system (e.g. `gem install
|
|
37
|
+
leptris --platform=arm64-darwin`), or set LEPTRIS_LIB_PATH to
|
|
38
|
+
a libleptris.{so,dylib,dll} file.
|
|
36
39
|
(Underlying error: #{e.message})
|
|
37
40
|
MSG
|
|
38
41
|
end
|
data/lib/libleptris.so
CHANGED
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Sources vendored in this gem (recompile rights; the ruby
|
|
2
|
+
variant builds them at install via extconf.rb):
|
|
3
|
+
|
|
4
|
+
- libleptris v1.9.210
|
|
5
|
+
- utf8proc v2.11.0
|
|
6
|
+
|
|
7
|
+
Rebuild by hand:
|
|
8
|
+
|
|
9
|
+
cmake -B vendor-src/utf8proc/build -S vendor-src/utf8proc -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DUTF8PROC_ENABLE_TESTING=OFF
|
|
10
|
+
cmake --build vendor-src/utf8proc/build --config Release
|
|
11
|
+
cmake --install vendor-src/utf8proc/build --prefix <prefix>
|
|
12
|
+
|
|
13
|
+
cmake -B vendor-src/libleptris/build -S vendor-src/libleptris -DCMAKE_BUILD_TYPE=Release -DLEPTRIS_BUILD_SHARED=ON -DLEPTRIS_BUILD_STATIC=OFF -DBUILD_TESTING=OFF -DLEPTRIS_BUILD_CLI=OFF -DLEPTRIS_BUILD_BENCHMARKS=OFF -DLEPTRIS_BUILD_MAN_PAGES=OFF -DLEPTRIS_ENABLE_UTF8PROC=ON -DCMAKE_PREFIX_PATH=<utf8proc-prefix>
|
|
14
|
+
cmake --build vendor-src/libleptris/build --config Release
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.210] - 2026-09-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- The node-layout fork's document block (slice 1, #1253): two
|
|
8
|
+
contiguous geometrically-grown regions per document (dense node
|
|
9
|
+
array + string arena) with region-relative byte-offset edges —
|
|
10
|
+
growth never invalidates an offset. Foundation for the pugixml-
|
|
11
|
+
class parse layout (TODO.max-perf/node-layout-fork.md).
|
|
12
|
+
|
|
13
|
+
### Performance
|
|
14
|
+
|
|
15
|
+
- Large-document parse (#1222/#1238 lane, #1255): the parse arena
|
|
16
|
+
(~4x input) exceeded the arena retain cache's 32MB ceiling, so
|
|
17
|
+
every repeated-parse cycle freed and re-malloc'd tens of MB.
|
|
18
|
+
Ceiling raised to 96MB (one document's worth of parking) with a
|
|
19
|
+
thread-cleanup drain and ASAN free-for-real. Measured: 13MB
|
|
20
|
+
attr shape 341 -> 472 MB/s (+38%); the text shape's size-decline
|
|
21
|
+
eliminated (465 -> 163 MB/s falling across 1.5-13MB, now
|
|
22
|
+
~605-627 flat).
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
3
26
|
## [1.9.209] - 2026-09-20
|
|
4
27
|
|
|
5
28
|
### Performance
|
|
@@ -74,9 +74,11 @@ LEPTRIS_API void leptris_thread_cleanup(void) {
|
|
|
74
74
|
extern void leptris_xpath_drain_thread_caches(void);
|
|
75
75
|
extern void leptris_root_doc_drain_thread_caches(void);
|
|
76
76
|
extern void leptris_pool_drain_recycler(void);
|
|
77
|
+
extern void leptris_arena_retain_drain(void);
|
|
77
78
|
leptris_xpath_drain_thread_caches();
|
|
78
79
|
leptris_root_doc_drain_thread_caches();
|
|
79
80
|
leptris_pool_drain_recycler();
|
|
81
|
+
leptris_arena_retain_drain();
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/* ---- Memory management API ---- */
|
|
@@ -51,7 +51,20 @@ static inline size_t align_up(size_t n) {
|
|
|
51
51
|
* twice per document lifetime, so contention is irrelevant. */
|
|
52
52
|
#define ARENA_RETAIN_MIN (256u * 1024u)
|
|
53
53
|
#define ARENA_RETAIN_MAX_BLOCKS 4u
|
|
54
|
-
|
|
54
|
+
/* 96MB: a large-document parse arena (~4x input with the
|
|
55
|
+
* pre-sized estimate) must survive the free to be reused by the
|
|
56
|
+
* next parse — at the old 32MB ceiling, every big-doc cycle
|
|
57
|
+
* freed and re-malloc'd tens of MB (libc madvise showed ~2% of
|
|
58
|
+
* repeated-parse wall time). One document's worth of parking. */
|
|
59
|
+
#define ARENA_RETAIN_MAX_BYTES (96u * 1024u * 1024u)
|
|
60
|
+
|
|
61
|
+
#if defined(__has_feature)
|
|
62
|
+
#if __has_feature(address_sanitizer)
|
|
63
|
+
#define ARENA_RETAIN_ASAN 1
|
|
64
|
+
#endif
|
|
65
|
+
#elif defined(__SANITIZE_ADDRESS__)
|
|
66
|
+
#define ARENA_RETAIN_ASAN 1
|
|
67
|
+
#endif
|
|
55
68
|
|
|
56
69
|
static struct {
|
|
57
70
|
char* base[ARENA_RETAIN_MAX_BLOCKS];
|
|
@@ -106,6 +119,14 @@ static char* retain_take(size_t request, size_t* capacity) {
|
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
static void retain_give(char* base, size_t size) {
|
|
122
|
+
#ifdef ARENA_RETAIN_ASAN
|
|
123
|
+
/* ASAN: retained blocks have no TLS destructor and read as
|
|
124
|
+
* LSan leaks — free for real (the recycle win is measurable
|
|
125
|
+
* only in repeated-parse loops, which ASAN runs don't gate). */
|
|
126
|
+
(void)size;
|
|
127
|
+
leptris_free_hook(base);
|
|
128
|
+
return;
|
|
129
|
+
#endif
|
|
109
130
|
if (size < ARENA_RETAIN_MIN) {
|
|
110
131
|
leptris_free_hook(base);
|
|
111
132
|
return;
|
|
@@ -124,6 +145,18 @@ static void retain_give(char* base, size_t size) {
|
|
|
124
145
|
retain_unlock();
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
/* Host-invoked drain (leptris_thread_cleanup): the retained
|
|
149
|
+
* blocks are process-lifetime parking by design; release them so
|
|
150
|
+
* embedded hosts and leak checkers see a clean exit. */
|
|
151
|
+
void leptris_arena_retain_drain(void) {
|
|
152
|
+
retain_lock();
|
|
153
|
+
for (size_t i = 0; i < g_retain.count; i++)
|
|
154
|
+
leptris_free_hook(g_retain.base[i]);
|
|
155
|
+
g_retain.count = 0;
|
|
156
|
+
g_retain.bytes = 0;
|
|
157
|
+
retain_unlock();
|
|
158
|
+
}
|
|
159
|
+
|
|
127
160
|
char* leptris_arena_buffer_alloc(size_t size) {
|
|
128
161
|
size_t capacity = size;
|
|
129
162
|
char* base = retain_take(size, &capacity);
|
|
@@ -55,6 +55,9 @@ void leptris_arena_reset(LeptrisArena* arena);
|
|
|
55
55
|
* All pointers handed out by the arena become invalid. */
|
|
56
56
|
void leptris_arena_destroy(LeptrisArena* arena);
|
|
57
57
|
|
|
58
|
+
/* Release retained arena blocks (leptris_thread_cleanup drain). */
|
|
59
|
+
void leptris_arena_retain_drain(void);
|
|
60
|
+
|
|
58
61
|
/* Bump-allocate `size` bytes, 8-byte aligned. NOT zeroed.
|
|
59
62
|
* Returns NULL when the request does not fit in remaining space —
|
|
60
63
|
* no fallback allocation, ever (see the contract above). */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* doc_block.c — the node-layout fork's document block (slice 1).
|
|
2
|
+
* See doc_block.h for the design contract. */
|
|
3
|
+
|
|
4
|
+
#include "doc_block.h"
|
|
5
|
+
#include <stdlib.h>
|
|
6
|
+
#include <string.h>
|
|
7
|
+
|
|
8
|
+
#define DOC_BLOCK_MIN_CAP ((size_t)4096)
|
|
9
|
+
#define DOC_BLOCK_ALIGN ((size_t)8)
|
|
10
|
+
|
|
11
|
+
struct LeptrisDocBlock {
|
|
12
|
+
char* nodes; /* node region base (8-aligned) */
|
|
13
|
+
size_t nodes_cap;
|
|
14
|
+
size_t nodes_used;
|
|
15
|
+
char* strs; /* string region base */
|
|
16
|
+
size_t strs_cap;
|
|
17
|
+
size_t strs_used;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
static char* region_grow(char* p, size_t* cap, size_t need) {
|
|
21
|
+
size_t want = *cap ? *cap : DOC_BLOCK_MIN_CAP;
|
|
22
|
+
while (want < need) want *= 2;
|
|
23
|
+
char* grown = (char*)realloc(p, want);
|
|
24
|
+
if (grown) *cap = want;
|
|
25
|
+
return grown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
LeptrisDocBlock* leptris_doc_block_create(size_t initial_capacity) {
|
|
29
|
+
LeptrisDocBlock* b =
|
|
30
|
+
(LeptrisDocBlock*)calloc(1, sizeof(*b));
|
|
31
|
+
if (!b) return NULL;
|
|
32
|
+
if (initial_capacity && initial_capacity < DOC_BLOCK_MIN_CAP)
|
|
33
|
+
initial_capacity = DOC_BLOCK_MIN_CAP;
|
|
34
|
+
if (initial_capacity) {
|
|
35
|
+
b->nodes = (char*)malloc(initial_capacity);
|
|
36
|
+
b->strs = (char*)malloc(initial_capacity);
|
|
37
|
+
if (!b->nodes || !b->strs) {
|
|
38
|
+
free(b->nodes);
|
|
39
|
+
free(b->strs);
|
|
40
|
+
free(b);
|
|
41
|
+
return NULL;
|
|
42
|
+
}
|
|
43
|
+
b->nodes_cap = b->strs_cap = initial_capacity;
|
|
44
|
+
}
|
|
45
|
+
return b;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
void leptris_doc_block_free(LeptrisDocBlock* b) {
|
|
49
|
+
if (!b) return;
|
|
50
|
+
free(b->nodes);
|
|
51
|
+
free(b->strs);
|
|
52
|
+
free(b);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
size_t leptris_doc_block_node_alloc(LeptrisDocBlock* b, size_t sz) {
|
|
56
|
+
if (!b || sz == 0 || sz > (size_t)-1 / 2) return (size_t)-1;
|
|
57
|
+
size_t off = (b->nodes_used + DOC_BLOCK_ALIGN - 1) &
|
|
58
|
+
~(DOC_BLOCK_ALIGN - 1);
|
|
59
|
+
size_t need = off + sz;
|
|
60
|
+
if (need > b->nodes_cap) {
|
|
61
|
+
char* grown = region_grow(b->nodes, &b->nodes_cap, need);
|
|
62
|
+
if (!grown) return (size_t)-1;
|
|
63
|
+
b->nodes = grown;
|
|
64
|
+
}
|
|
65
|
+
b->nodes_used = need;
|
|
66
|
+
return off;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
size_t leptris_doc_block_str(LeptrisDocBlock* b, const char* s,
|
|
70
|
+
size_t len) {
|
|
71
|
+
if (!b || (!s && len)) return (size_t)-1;
|
|
72
|
+
size_t need = b->strs_used + len + 1;
|
|
73
|
+
if (need > b->strs_cap) {
|
|
74
|
+
char* grown = region_grow(b->strs, &b->strs_cap, need);
|
|
75
|
+
if (!grown) return (size_t)-1;
|
|
76
|
+
b->strs = grown;
|
|
77
|
+
}
|
|
78
|
+
size_t off = b->strs_used;
|
|
79
|
+
if (len) memcpy(b->strs + off, s, len);
|
|
80
|
+
b->strs[off + len] = '\0';
|
|
81
|
+
b->strs_used = need;
|
|
82
|
+
return off;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
void* leptris_doc_block_node(const LeptrisDocBlock* b, size_t off) {
|
|
86
|
+
return (b && off < b->nodes_used) ? b->nodes + off : NULL;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
void* leptris_doc_block_nodes_base(const LeptrisDocBlock* b) {
|
|
90
|
+
return b ? b->nodes : NULL;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
size_t leptris_doc_block_nodes_used(const LeptrisDocBlock* b) {
|
|
94
|
+
return b ? b->nodes_used : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const char* leptris_doc_block_str_at(const LeptrisDocBlock* b,
|
|
98
|
+
size_t off) {
|
|
99
|
+
return (b && off < b->strs_used) ? b->strs + off : NULL;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
size_t leptris_doc_block_bytes_used(const LeptrisDocBlock* b) {
|
|
103
|
+
return b ? b->nodes_used + b->strs_used : 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
size_t leptris_doc_block_capacity(const LeptrisDocBlock* b) {
|
|
107
|
+
return b ? b->nodes_cap + b->strs_cap : 0;
|
|
108
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* lib/src/memory/doc_block.h — the node-layout fork's document
|
|
2
|
+
* block (TODO.max-perf/node-layout-fork, slice 1).
|
|
3
|
+
*
|
|
4
|
+
* Two contiguous, geometrically-grown regions per document — a
|
|
5
|
+
* dense node array and a string arena. Every reference to either
|
|
6
|
+
* is a byte OFFSET within its region, so growth (realloc) never
|
|
7
|
+
* invalidates an edge: callers hold offsets, not pointers, and
|
|
8
|
+
* refetch region bases after any allocation.
|
|
9
|
+
*
|
|
10
|
+
* Differences vs memory/pool.c (which stays for mutation paths):
|
|
11
|
+
* the pool chains fixed 32KB pages, scattering one tree across
|
|
12
|
+
* pages; each parse node pays compact-edge encoding and detached
|
|
13
|
+
* nodes pay root-doc registration. Here the tree is contiguous
|
|
14
|
+
* (walk locality), edges are plain offsets (no cp16 overflow
|
|
15
|
+
* table), and the owning document is implied by the region base.
|
|
16
|
+
*/
|
|
17
|
+
#ifndef LEPTRIS_MEMORY_DOC_BLOCK_H
|
|
18
|
+
#define LEPTRIS_MEMORY_DOC_BLOCK_H
|
|
19
|
+
|
|
20
|
+
#include <stddef.h>
|
|
21
|
+
#include <stdint.h>
|
|
22
|
+
|
|
23
|
+
#ifdef __cplusplus
|
|
24
|
+
extern "C" {
|
|
25
|
+
#endif
|
|
26
|
+
|
|
27
|
+
typedef struct LeptrisDocBlock LeptrisDocBlock;
|
|
28
|
+
|
|
29
|
+
/* Create with an initial byte capacity per region (grown
|
|
30
|
+
* geometrically on demand; 0 = lazy). NULL on failure. */
|
|
31
|
+
LeptrisDocBlock* leptris_doc_block_create(size_t initial_capacity);
|
|
32
|
+
|
|
33
|
+
void leptris_doc_block_free(LeptrisDocBlock* b);
|
|
34
|
+
|
|
35
|
+
/* Node region: bump-allocate sz bytes, 8-byte aligned. Returns
|
|
36
|
+
* the node's offset in the region (stable across growth) — never
|
|
37
|
+
* a pointer; resolve via leptris_doc_block_node. (size_t)-1 on
|
|
38
|
+
* failure. Growth may realloc: refetch any held pointers. */
|
|
39
|
+
size_t leptris_doc_block_node_alloc(LeptrisDocBlock* b, size_t sz);
|
|
40
|
+
|
|
41
|
+
/* String region: append len bytes plus a NUL. Returns the
|
|
42
|
+
* string's offset in ITS region (stable across growth) or
|
|
43
|
+
* (size_t)-1 on failure. */
|
|
44
|
+
size_t leptris_doc_block_str(LeptrisDocBlock* b, const char* s,
|
|
45
|
+
size_t len);
|
|
46
|
+
|
|
47
|
+
/* Resolve an offset (region base + off, one add). */
|
|
48
|
+
void* leptris_doc_block_node(const LeptrisDocBlock* b, size_t off);
|
|
49
|
+
|
|
50
|
+
void* leptris_doc_block_nodes_base(const LeptrisDocBlock* b);
|
|
51
|
+
size_t leptris_doc_block_nodes_used(const LeptrisDocBlock* b);
|
|
52
|
+
const char* leptris_doc_block_str_at(const LeptrisDocBlock* b,
|
|
53
|
+
size_t off);
|
|
54
|
+
size_t leptris_doc_block_bytes_used(const LeptrisDocBlock* b);
|
|
55
|
+
size_t leptris_doc_block_capacity(const LeptrisDocBlock* b);
|
|
56
|
+
|
|
57
|
+
#ifdef __cplusplus
|
|
58
|
+
}
|
|
59
|
+
#endif
|
|
60
|
+
|
|
61
|
+
#endif /* LEPTRIS_MEMORY_DOC_BLOCK_H */
|
|
@@ -116,6 +116,7 @@ leptris_add_test(test_simd_spans common/test_simd_spans.cpp)
|
|
|
116
116
|
leptris_add_test(test_pool memory/test_pool.cpp)
|
|
117
117
|
leptris_add_test(test_oom memory/test_oom_injection.cpp)
|
|
118
118
|
leptris_add_test(test_arena memory/test_arena.cpp)
|
|
119
|
+
leptris_add_test(test_doc_block memory/test_doc_block.cpp)
|
|
119
120
|
leptris_add_test(test_pool_arena memory/test_pool_arena.cpp)
|
|
120
121
|
leptris_add_test(test_xpath xpath/test_xpath.cpp)
|
|
121
122
|
leptris_add_test(test_exslt xpath/test_exslt.cpp)
|
|
@@ -192,11 +192,25 @@ TEST(Arena, AllPointersWithinSpanForCompactEncoding) {
|
|
|
192
192
|
|
|
193
193
|
/* ---- Retained-block free list (parse fault fix) ---------------------- */
|
|
194
194
|
|
|
195
|
+
#if defined(__has_feature)
|
|
196
|
+
#if __has_feature(address_sanitizer)
|
|
197
|
+
#define ARENA_TESTS_ASAN 1
|
|
198
|
+
#endif
|
|
199
|
+
#elif defined(__SANITIZE_ADDRESS__)
|
|
200
|
+
#define ARENA_TESTS_ASAN 1
|
|
201
|
+
#endif
|
|
202
|
+
|
|
195
203
|
TEST(LeptrisArena, RetainsAndReusesLargeBlocks) {
|
|
196
204
|
/* Blocks >= 256 KB must round-trip through the free list: the
|
|
197
205
|
* next same-size create gets the SAME mapping back (no munmap,
|
|
198
206
|
* no page faults). Machine-independent by construction: it
|
|
199
207
|
* asserts pointer identity, not timing. */
|
|
208
|
+
/* Under ASAN the retain cache frees for real (no TLS
|
|
209
|
+
* destructor; parked blocks read as LSan leaks) — the reuse
|
|
210
|
+
* contract is deliberately disabled there. */
|
|
211
|
+
#if defined(ARENA_TESTS_ASAN)
|
|
212
|
+
GTEST_SKIP() << "retain cache disabled under ASAN";
|
|
213
|
+
#endif
|
|
200
214
|
const size_t big = 512u * 1024u;
|
|
201
215
|
LeptrisArena* a1 = leptris_arena_create(big);
|
|
202
216
|
ASSERT_NE(a1, nullptr);
|
|
@@ -239,6 +253,13 @@ TEST(LeptrisArena, SmallBlocksStillMallocSemantics) {
|
|
|
239
253
|
}
|
|
240
254
|
|
|
241
255
|
TEST(LeptrisArena, BufferRoundTripReusesMapping) {
|
|
256
|
+
/* Under ASAN the retain cache frees for real (no TLS
|
|
257
|
+
* destructor; parked blocks read as LSan leaks) — the reuse
|
|
258
|
+
* contract is deliberately disabled there. */
|
|
259
|
+
#if defined(ARENA_TESTS_ASAN)
|
|
260
|
+
GTEST_SKIP() << "retain cache disabled under ASAN";
|
|
261
|
+
#endif
|
|
262
|
+
|
|
242
263
|
char* b1 = leptris_arena_buffer_alloc(512u * 1024u);
|
|
243
264
|
ASSERT_NE(b1, nullptr);
|
|
244
265
|
memset(b1, 1, 512u * 1024u);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// test/memory/test_doc_block.cpp - the node-layout fork's document
|
|
2
|
+
// block (slice 1). Falsifiable contract: offsets stay valid across
|
|
3
|
+
// geometric growth, regions stay dense/aligned, boundary inputs
|
|
4
|
+
// are refused.
|
|
5
|
+
|
|
6
|
+
#include <gtest/gtest.h>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
#include <cstdio>
|
|
9
|
+
#include <vector>
|
|
10
|
+
#include "../../src/leptris/memory/doc_block.h"
|
|
11
|
+
|
|
12
|
+
typedef struct {
|
|
13
|
+
uint32_t first_child;
|
|
14
|
+
uint32_t next;
|
|
15
|
+
uint32_t name_off;
|
|
16
|
+
} TNode;
|
|
17
|
+
|
|
18
|
+
TEST(DocBlock, OffsetsSurviveGrowth) {
|
|
19
|
+
LeptrisDocBlock* b = leptris_doc_block_create(0);
|
|
20
|
+
ASSERT_NE(b, nullptr);
|
|
21
|
+
std::vector<uint32_t> offs;
|
|
22
|
+
const int N = 20000;
|
|
23
|
+
for (int i = 0; i < N; i++) {
|
|
24
|
+
size_t off = leptris_doc_block_node_alloc(b, sizeof(TNode));
|
|
25
|
+
ASSERT_NE(off, (size_t)-1);
|
|
26
|
+
ASSERT_EQ(off % 8u, 0u);
|
|
27
|
+
TNode* n = (TNode*)leptris_doc_block_node(b, off);
|
|
28
|
+
ASSERT_NE(n, nullptr);
|
|
29
|
+
n->first_child = 0;
|
|
30
|
+
n->next = i > 0 ? offs[i - 1] : 0;
|
|
31
|
+
n->name_off = 0;
|
|
32
|
+
offs.push_back((uint32_t)off);
|
|
33
|
+
if (i % 1000 == 0) {
|
|
34
|
+
for (size_t k = 1; k < offs.size(); k++) {
|
|
35
|
+
const TNode* p =
|
|
36
|
+
(const TNode*)leptris_doc_block_node(b, offs[k]);
|
|
37
|
+
ASSERT_NE(p, nullptr);
|
|
38
|
+
ASSERT_EQ(p->next, offs[k - 1]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
EXPECT_GT(leptris_doc_block_capacity(b), (size_t)0);
|
|
43
|
+
leptris_doc_block_free(b);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
TEST(DocBlock, StringRegionStableAcrossGrowth) {
|
|
47
|
+
LeptrisDocBlock* b = leptris_doc_block_create(64);
|
|
48
|
+
ASSERT_NE(b, nullptr);
|
|
49
|
+
std::vector<size_t> offs;
|
|
50
|
+
char buf[32];
|
|
51
|
+
for (int i = 0; i < 5000; i++) {
|
|
52
|
+
int n = snprintf(buf, sizeof(buf), "name-%05d", i);
|
|
53
|
+
size_t off = leptris_doc_block_str(b, buf, (size_t)n);
|
|
54
|
+
ASSERT_NE(off, (size_t)-1);
|
|
55
|
+
offs.push_back(off);
|
|
56
|
+
}
|
|
57
|
+
for (int i = 0; i < 5000; i += 500) {
|
|
58
|
+
int n = snprintf(buf, sizeof(buf), "name-%05d", i);
|
|
59
|
+
const char* s = leptris_doc_block_str_at(b, offs[i]);
|
|
60
|
+
ASSERT_NE(s, nullptr);
|
|
61
|
+
EXPECT_EQ(std::memcmp(s, buf, (size_t)n), 0);
|
|
62
|
+
EXPECT_EQ(s[n], '\0');
|
|
63
|
+
}
|
|
64
|
+
leptris_doc_block_free(b);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
TEST(DocBlock, BoundaryInputsRefused) {
|
|
68
|
+
LeptrisDocBlock* b = leptris_doc_block_create(0);
|
|
69
|
+
ASSERT_NE(b, nullptr);
|
|
70
|
+
EXPECT_EQ(leptris_doc_block_node_alloc(b, 0), (size_t)-1);
|
|
71
|
+
EXPECT_EQ(leptris_doc_block_node_alloc(nullptr, 8), (size_t)-1);
|
|
72
|
+
EXPECT_EQ(leptris_doc_block_str(b, nullptr, 3), (size_t)-1);
|
|
73
|
+
size_t off = leptris_doc_block_str(b, "", 0);
|
|
74
|
+
EXPECT_NE(off, (size_t)-1);
|
|
75
|
+
EXPECT_STREQ(leptris_doc_block_str_at(b, off), "");
|
|
76
|
+
EXPECT_EQ(leptris_doc_block_node(b, 1 << 20), nullptr);
|
|
77
|
+
leptris_doc_block_free(b);
|
|
78
|
+
leptris_doc_block_free(nullptr);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
TEST(DocBlock, NodesAndStringsIndependent) {
|
|
82
|
+
LeptrisDocBlock* b = leptris_doc_block_create(0);
|
|
83
|
+
ASSERT_NE(b, nullptr);
|
|
84
|
+
size_t n1 = leptris_doc_block_node_alloc(b, 16);
|
|
85
|
+
size_t s1 = leptris_doc_block_str(b, "abc", 3);
|
|
86
|
+
size_t n2 = leptris_doc_block_node_alloc(b, 16);
|
|
87
|
+
size_t s2 = leptris_doc_block_str(b, "defg", 4);
|
|
88
|
+
EXPECT_NE(n1, n2);
|
|
89
|
+
EXPECT_NE(s1, s2);
|
|
90
|
+
EXPECT_STREQ(leptris_doc_block_str_at(b, s1), "abc");
|
|
91
|
+
EXPECT_STREQ(leptris_doc_block_str_at(b, s2), "defg");
|
|
92
|
+
EXPECT_EQ(leptris_doc_block_nodes_used(b), 32u);
|
|
93
|
+
leptris_doc_block_free(b);
|
|
94
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
|
|
3
3
|
"name": "leptris",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.210",
|
|
5
5
|
"description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
|
|
6
6
|
"homepage": "https://github.com/leptris/leptris",
|
|
7
7
|
"license": "MIT",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.210.1
|
|
5
5
|
platform: arm-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -187,6 +187,7 @@ files:
|
|
|
187
187
|
- lib/libleptris.so
|
|
188
188
|
- lib/libutf8proc.so.3
|
|
189
189
|
- scripts/gem_smoke.rb
|
|
190
|
+
- vendor-src/README.md
|
|
190
191
|
- vendor-src/libleptris/CHANGELOG.md
|
|
191
192
|
- vendor-src/libleptris/CLAUDE.md
|
|
192
193
|
- vendor-src/libleptris/CMakeLists.txt
|
|
@@ -644,6 +645,8 @@ files:
|
|
|
644
645
|
- vendor-src/libleptris/src/leptris/leptris_parse.h
|
|
645
646
|
- vendor-src/libleptris/src/leptris/memory/arena.c
|
|
646
647
|
- vendor-src/libleptris/src/leptris/memory/arena.h
|
|
648
|
+
- vendor-src/libleptris/src/leptris/memory/doc_block.c
|
|
649
|
+
- vendor-src/libleptris/src/leptris/memory/doc_block.h
|
|
647
650
|
- vendor-src/libleptris/src/leptris/memory/pool.c
|
|
648
651
|
- vendor-src/libleptris/src/leptris/memory/pool.h
|
|
649
652
|
- vendor-src/libleptris/src/leptris/rng/parse.c
|
|
@@ -798,6 +801,7 @@ files:
|
|
|
798
801
|
- vendor-src/libleptris/test/html/test_html.cpp
|
|
799
802
|
- vendor-src/libleptris/test/html/test_html5lib.cpp
|
|
800
803
|
- vendor-src/libleptris/test/memory/test_arena.cpp
|
|
804
|
+
- vendor-src/libleptris/test/memory/test_doc_block.cpp
|
|
801
805
|
- vendor-src/libleptris/test/memory/test_oom_injection.cpp
|
|
802
806
|
- vendor-src/libleptris/test/memory/test_pool.cpp
|
|
803
807
|
- vendor-src/libleptris/test/memory/test_pool_arena.cpp
|