leptris 1.9.229.0-x86_64-linux → 1.9.231.0-x86_64-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 +21 -0
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +49 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/benchmarks/serialization/bench_leptris_serialize.c +4 -4
- data/vendor-src/libleptris/benchmarks/workflows/bench_end_to_end.c +1 -1
- data/vendor-src/libleptris/src/leptris/dom/root_doc_map.c +120 -36
- data/vendor-src/libleptris/src/leptris/dom/root_doc_map.h +3 -1
- data/vendor-src/libleptris/src/leptris/xpath/functions.c +220 -31
- data/vendor-src/libleptris/test/CMakeLists.txt +3 -0
- data/vendor-src/libleptris/test/concurrency/test_root_doc_cross_thread.cpp +104 -0
- data/vendor-src/libleptris/test/perf/test_perf.cpp +20 -3
- data/vendor-src/libleptris/test/xquery/qt3/fn/boolean.xml +1342 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/normalize-space.xml +429 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/string-length.xml +349 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/substring-after.xml +594 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/substring-before.xml +597 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/translate.xml +410 -0
- data/vendor-src/libleptris/test/xquery/test_qt3.cpp +38 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +8 -22
- data/vendor-src/libleptris/benchmarks/results/BENCHMARK_REPORT.md +0 -226
- data/vendor-src/libleptris/benchmarks/results/baseline_report.md +0 -219
- data/vendor-src/libleptris/benchmarks/results/benchmark_20251223_134550.json +0 -13
- data/vendor-src/libleptris/benchmarks/results/benchmark_20260129_225844.json +0 -13
- data/vendor-src/libleptris/benchmarks/results/leptris_dom_output.txt +0 -24
- data/vendor-src/libleptris/benchmarks/results/leptris_modify.json +0 -36
- data/vendor-src/libleptris/benchmarks/results/leptris_modify_optimized.json +0 -36
- data/vendor-src/libleptris/benchmarks/results/leptris_modify_session1.json +0 -36
- data/vendor-src/libleptris/benchmarks/results/leptris_modify_session2.json +0 -36
- data/vendor-src/libleptris/benchmarks/results/leptris_serialize.json +0 -34
- data/vendor-src/libleptris/benchmarks/results/leptris_xpath_output.txt +0 -24
- data/vendor-src/libleptris/benchmarks/results/libxml2_dom_output.txt +0 -24
- data/vendor-src/libleptris/benchmarks/results/libxml2_xpath_output.txt +0 -24
- data/vendor-src/libleptris/benchmarks/results/phase16_session2.json +0 -21
- data/vendor-src/libleptris/benchmarks/results/phase17.5_session1.json +0 -30
- data/vendor-src/libleptris/benchmarks/results/phase17_session1.json +0 -30
- data/vendor-src/libleptris/benchmarks/results/pugixml_dom_output.txt +0 -51
- data/vendor-src/libleptris/benchmarks/results/xpath_axes_leptris.json +0 -318
- data/vendor-src/libleptris/benchmarks/results/xpath_axes_libxml2.json +0 -318
- data/vendor-src/libleptris/benchmarks/results/xpath_functions_leptris.json +0 -222
- data/vendor-src/libleptris/benchmarks/results/xpath_functions_libxml2.json +0 -222
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 012e5580dc974f792458c6943f8059a5d7e3aa467a9805c7eae937f1039e5a83
|
|
4
|
+
data.tar.gz: f0c40fc816995d4f545988578dd70f66b62844bb4ff4fd6dcb43182efb023c1f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44c3ed7dced5f3bc3700458535e214366ca35be93ea4a8312801660598ade70c64254ad9ddf9b57c7647a3d23d28209b2ef6e61dd2e7d632d92c073c116964c8
|
|
7
|
+
data.tar.gz: b10d3216646b6974c356e2a55384ab33985badde6e36499050427d1802730bf282c050237603bbfde6b64a72fb0df4832e9b8e37115b7d2922c7cb4e8d846442
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ 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.231.0] - 2026-09-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Vendored libleptris 1.9.231: the root-doc map is process-global
|
|
13
|
+
now — a document freed on a thread other than the one that
|
|
14
|
+
registered its roots (binding GC finalizers run on arbitrary
|
|
15
|
+
threads) no longer leaves a stale `{root -> doc}` entry in the
|
|
16
|
+
owner thread's map. Pre-fix, address-matched walks dereferenced
|
|
17
|
+
(and rewired) the freed document: the
|
|
18
|
+
`leptris_root_doc_unregister+0x6c` segfault under heavy parse
|
|
19
|
+
churn on macOS Ruby 3.3 (#321), plus silent freed-document
|
|
20
|
+
resolution through lookups and the per-thread memo. The memo
|
|
21
|
+
stays lock-free, validated by a global atomic generation.
|
|
22
|
+
- Also riding 1.9.230's string-length fix (continuation-count
|
|
23
|
+
table, ac6f9738): the old lead-byte table over-advanced past
|
|
24
|
+
the NUL, and the NFD spec expectation enshrined the phantom
|
|
25
|
+
byte (4.0). Re-pinned to the correct contracts — verbatim
|
|
26
|
+
one-argument passthrough (2 codepoints) and two-argument NFD
|
|
27
|
+
(splits the precomposed é, 3 codepoints).
|
|
28
|
+
|
|
8
29
|
## [1.9.229.0] - 2026-09-23
|
|
9
30
|
|
|
10
31
|
### Performance — engine sync (1.9.229)
|
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.231"
|
|
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
|
data/lib/leptris/version.rb
CHANGED
data/lib/libleptris.so
CHANGED
|
Binary file
|
data/vendor-src/README.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.231] - 2026-09-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- root-doc map is process-global — cross-thread document death poisoned owner-thread state (dom)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [1.9.230] - 2026-09-23
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **xquery: QT3 stage-2 corpus slice — 228 newly gated conformance
|
|
16
|
+
cases across six core families** (translate, normalize-space,
|
|
17
|
+
substring-before, substring-after, boolean, string-length),
|
|
18
|
+
vendored from the W3C qt3tests suite with the driver's exact
|
|
19
|
+
`agree == run == pinned` gate. fn:number is deliberately not
|
|
20
|
+
adopted: its string assertions want canonical E-notation doubles,
|
|
21
|
+
which the number formatter deliberately does not print
|
|
22
|
+
(libxml2 xmlXPathFormatNumber parity is load-bearing for the
|
|
23
|
+
libxslt suite).
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **xpath: `fn:translate` is now codepoint-correct.** The old
|
|
28
|
+
implementation built a 256-entry byte-level table, so every
|
|
29
|
+
multi-byte UTF-8 character matched byte-by-byte and astral
|
|
30
|
+
codepoints could not be addressed at all. The rewrite decodes
|
|
31
|
+
`from`/`to` into codepoints (first occurrence governs, per
|
|
32
|
+
F&O), supports cross-width replacements, and emits into a
|
|
33
|
+
growable buffer.
|
|
34
|
+
- **xpath: `fn:string-length` counts codepoints, not bytes** —
|
|
35
|
+
`string-length("𐀂")` returned 4; the walk now steps by
|
|
36
|
+
lead-byte length.
|
|
37
|
+
- **xpath: `substring-before`/`substring-after` accept the 3-arg
|
|
38
|
+
collation overload.** The codepoint-collation URI (under which
|
|
39
|
+
the collation is the default byte comparison) and the empty URI
|
|
40
|
+
are accepted; any other URI raises the unsupported-collation
|
|
41
|
+
error.
|
|
42
|
+
- **bench: the CI benchmark legs' fixture/result paths unified
|
|
43
|
+
under one convention** (harnesses read `fixtures/` and write
|
|
44
|
+
`results/` relative to their working directory; CI stages both
|
|
45
|
+
once). SAX and XPath benchmark results that were silently
|
|
46
|
+
skipped or failing to write now produce real data, and 21 stale
|
|
47
|
+
committed result files (including a nine-month-old comparison
|
|
48
|
+
report) are gone from the tree.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
3
52
|
## [1.9.229] - 2026-09-23
|
|
4
53
|
|
|
5
54
|
### Performance
|
|
@@ -35,7 +35,7 @@ static char* load_file(const char* path, size_t* size) {
|
|
|
35
35
|
// Benchmark: Serialize small document
|
|
36
36
|
static void bench_serialize_small(void) {
|
|
37
37
|
size_t size;
|
|
38
|
-
char* xml = load_file("
|
|
38
|
+
char* xml = load_file("fixtures/small.xml", &size);
|
|
39
39
|
if (!xml) {
|
|
40
40
|
printf("ERROR: Could not load small.xml\n");
|
|
41
41
|
return;
|
|
@@ -68,7 +68,7 @@ static void bench_serialize_small(void) {
|
|
|
68
68
|
// Benchmark: Serialize medium document
|
|
69
69
|
static void bench_serialize_medium(void) {
|
|
70
70
|
size_t size;
|
|
71
|
-
char* xml = load_file("
|
|
71
|
+
char* xml = load_file("fixtures/medium.xml", &size);
|
|
72
72
|
if (!xml) {
|
|
73
73
|
printf("ERROR: Could not load medium.xml\n");
|
|
74
74
|
return;
|
|
@@ -101,7 +101,7 @@ static void bench_serialize_medium(void) {
|
|
|
101
101
|
// Benchmark: Serialize large document
|
|
102
102
|
static void bench_serialize_large(void) {
|
|
103
103
|
size_t size;
|
|
104
|
-
char* xml = load_file("
|
|
104
|
+
char* xml = load_file("fixtures/large.xml", &size);
|
|
105
105
|
if (!xml) {
|
|
106
106
|
printf("ERROR: Could not load large.xml\n");
|
|
107
107
|
return;
|
|
@@ -134,7 +134,7 @@ static void bench_serialize_large(void) {
|
|
|
134
134
|
// Benchmark: Pretty-print vs compact
|
|
135
135
|
static void bench_format_comparison(void) {
|
|
136
136
|
size_t size;
|
|
137
|
-
char* xml = load_file("
|
|
137
|
+
char* xml = load_file("fixtures/small.xml", &size);
|
|
138
138
|
if (!xml) {
|
|
139
139
|
printf("ERROR: Could not load small.xml\n");
|
|
140
140
|
return;
|
|
@@ -513,7 +513,7 @@ int main(int argc, char** argv) {
|
|
|
513
513
|
snprintf(size_medium, sizeof(size_medium), "%.1fKB", medium_len / 1024.0);
|
|
514
514
|
snprintf(size_large, sizeof(size_large), "%.1fKB", large_len / 1024.0);
|
|
515
515
|
|
|
516
|
-
write_json_results("
|
|
516
|
+
write_json_results("results/phase17.5_session1.json",
|
|
517
517
|
size_small, size_medium, size_large,
|
|
518
518
|
&small_results, &medium_results, &large_results,
|
|
519
519
|
iterations);
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
/* dom/root_doc_map.c —
|
|
1
|
+
/* dom/root_doc_map.c — root-element → document mapping.
|
|
2
2
|
*
|
|
3
3
|
* TODO 155 Phase A: the `document` field was removed from struct
|
|
4
4
|
* leptris_element to fit it in one 64-byte cache line. Non-root
|
|
5
5
|
* elements reach their document by walking parent_off to the root,
|
|
6
|
-
* then looking up the root in this
|
|
6
|
+
* then looking up the root in this hash table.
|
|
7
|
+
*
|
|
8
|
+
* leptris-ruby#321: the map (buckets, entries, generation) was
|
|
9
|
+
* THREAD-LOCAL, but document death is not — binding finalizers free
|
|
10
|
+
* documents on arbitrary threads. A cross-thread free could not
|
|
11
|
+
* reach the owner thread's buckets, leaving a stale {root -> freed
|
|
12
|
+
* doc} entry and a memo the owner's TLS generation still trusted:
|
|
13
|
+
* the next owner-thread walk that address-matched the stale entry
|
|
14
|
+
* dereferenced (and rewired) the freed document, and lookups handed
|
|
15
|
+
* the freed document to callers. The map state is process-global
|
|
16
|
+
* now, guarded by one mutex; the per-thread (root, doc) memo stays
|
|
17
|
+
* lock-free, validated against a GLOBAL atomic generation that any
|
|
18
|
+
* thread's mutation bumps.
|
|
7
19
|
*
|
|
8
20
|
* TODO 157 (perf): uses a free-list to avoid per-parse malloc/free.
|
|
9
21
|
* After warmup, register and unregister are O(1) with zero heap ops. */
|
|
@@ -39,7 +51,12 @@ typedef struct root_doc_entry {
|
|
|
39
51
|
struct root_doc_entry* doc_next; /* owning doc's chain */
|
|
40
52
|
} RootDocEntry;
|
|
41
53
|
|
|
42
|
-
static
|
|
54
|
+
static RootDocEntry* g_root_doc_buckets[ROOT_DOC_BUCKETS];
|
|
55
|
+
|
|
56
|
+
/* One lock guards buckets, entry chunks/free-list, and doc chains.
|
|
57
|
+
* Uncontended acquisition is the whole per-op cost single-threaded
|
|
58
|
+
* callers pay; the memo fast path below needs no lock at all. */
|
|
59
|
+
static leptris_mutex_t g_root_doc_lock = LEPTRIS_MUTEX_INIT;
|
|
43
60
|
|
|
44
61
|
/* Doc-entry chain helpers: every register pushes the entry onto its
|
|
45
62
|
* document's list (doc->map_entries), so unregister_doc walks exactly
|
|
@@ -62,7 +79,7 @@ static void doc_chain_unlink(struct leptris_document* doc, RootDocEntry* e) {
|
|
|
62
79
|
|
|
63
80
|
/* Free-list: recycled entries from unregistered roots. Eliminates
|
|
64
81
|
* malloc/free churn on the parse→free cycle. */
|
|
65
|
-
static
|
|
82
|
+
static RootDocEntry* g_free_list;
|
|
66
83
|
|
|
67
84
|
/* Entry chunks (lane 18 P0): one TLS bump chunk serves fresh
|
|
68
85
|
* entries so a create-heavy document pays no malloc per element —
|
|
@@ -74,9 +91,9 @@ typedef struct root_doc_chunk {
|
|
|
74
91
|
struct root_doc_chunk* next;
|
|
75
92
|
RootDocEntry entries[ROOTMAP_CHUNK];
|
|
76
93
|
} RootDocChunk;
|
|
77
|
-
static
|
|
78
|
-
static
|
|
79
|
-
static
|
|
94
|
+
static RootDocChunk* g_entry_chunks;
|
|
95
|
+
static RootDocEntry* g_entry_cursor;
|
|
96
|
+
static RootDocEntry* g_entry_end;
|
|
80
97
|
|
|
81
98
|
static RootDocEntry* entry_take(void) {
|
|
82
99
|
if (g_free_list) {
|
|
@@ -128,13 +145,33 @@ static LEPTRIS_THREAD_LOCAL struct leptris_document* g_memo_doc;
|
|
|
128
145
|
* lookups only, so the memo keeps hitting there; any churn —
|
|
129
146
|
* precisely the strike interleaving — kills it deterministically
|
|
130
147
|
* instead of by allocator luck. */
|
|
131
|
-
|
|
148
|
+
/* Global: a mutation on ANY thread must invalidate EVERY thread's
|
|
149
|
+
* memo snapshot (the TLS counter let a foreign document death go
|
|
150
|
+
* unseen — leptris-ruby#321). Portability per arena.c's lock
|
|
151
|
+
* precedent: __atomic on GCC/Clang, _Interlocked on MSVC. */
|
|
152
|
+
static uint64_t g_rootmap_generation;
|
|
132
153
|
static LEPTRIS_THREAD_LOCAL uint64_t g_memo_generation;
|
|
133
154
|
|
|
134
|
-
|
|
155
|
+
#if defined(_MSC_VER)
|
|
156
|
+
#include <intrin.h>
|
|
157
|
+
static uint64_t rootmap_generation_load(void) {
|
|
158
|
+
return _InterlockedCompareExchange64(&g_rootmap_generation, 0, 0);
|
|
159
|
+
}
|
|
160
|
+
static uint64_t rootmap_generation_bump(void) {
|
|
161
|
+
return _InterlockedIncrement64(&g_rootmap_generation) - 1;
|
|
162
|
+
}
|
|
163
|
+
#else
|
|
164
|
+
static uint64_t rootmap_generation_load(void) {
|
|
165
|
+
return __atomic_load_n(&g_rootmap_generation, __ATOMIC_ACQUIRE);
|
|
166
|
+
}
|
|
167
|
+
static uint64_t rootmap_generation_bump(void) {
|
|
168
|
+
return __atomic_add_fetch(&g_rootmap_generation, 1, __ATOMIC_ACQ_REL);
|
|
169
|
+
}
|
|
170
|
+
#endif
|
|
135
171
|
|
|
136
172
|
void leptris_root_doc_register(LeptrisElement root, struct leptris_document* doc) {
|
|
137
173
|
if (!root || !doc) return;
|
|
174
|
+
LEPTRIS_MUTEX_LOCK(&g_root_doc_lock);
|
|
138
175
|
rootmap_generation_bump();
|
|
139
176
|
size_t idx = bucket_index(root);
|
|
140
177
|
if (rootmap_marked(root)) {
|
|
@@ -149,48 +186,55 @@ void leptris_root_doc_register(LeptrisElement root, struct leptris_document* doc
|
|
|
149
186
|
e->doc = doc;
|
|
150
187
|
doc_chain_push(doc, e);
|
|
151
188
|
}
|
|
189
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
152
190
|
return;
|
|
153
191
|
}
|
|
154
192
|
}
|
|
155
193
|
} else {
|
|
156
194
|
/* Never registered: prepend directly, no duplicate walk. */
|
|
157
195
|
RootDocEntry* e = entry_take();
|
|
158
|
-
if (!e)
|
|
196
|
+
if (!e) {
|
|
197
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
159
200
|
e->root = root; e->doc = doc;
|
|
160
201
|
e->next = g_root_doc_buckets[idx];
|
|
161
202
|
g_root_doc_buckets[idx] = e;
|
|
162
203
|
doc_chain_push(doc, e);
|
|
163
204
|
rootmap_set(root, 1);
|
|
205
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
164
206
|
return;
|
|
165
207
|
}
|
|
166
208
|
/* Pop from free-list, or the bump chunk. */
|
|
167
209
|
RootDocEntry* e = entry_take();
|
|
168
|
-
if (!e)
|
|
210
|
+
if (!e) {
|
|
211
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
169
214
|
e->root = root; e->doc = doc;
|
|
170
215
|
e->next = g_root_doc_buckets[idx];
|
|
171
216
|
g_root_doc_buckets[idx] = e;
|
|
172
217
|
doc_chain_push(doc, e);
|
|
218
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
173
219
|
}
|
|
174
220
|
|
|
175
|
-
/* TODO.concurrency/08
|
|
176
|
-
*
|
|
177
|
-
*
|
|
221
|
+
/* TODO.concurrency/08, adjusted for #321: entry chunks are
|
|
222
|
+
* process-global and reused for the life of the process (they were
|
|
223
|
+
* per-thread and never torn down either). A dying thread now has
|
|
224
|
+
* only its memo to drop. */
|
|
178
225
|
void leptris_root_doc_drain_thread_caches(void) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
while (g_entry_chunks) {
|
|
184
|
-
RootDocChunk* next = g_entry_chunks->next;
|
|
185
|
-
free(g_entry_chunks);
|
|
186
|
-
g_entry_chunks = next;
|
|
187
|
-
}
|
|
188
|
-
g_entry_cursor = NULL;
|
|
189
|
-
g_entry_end = NULL;
|
|
226
|
+
g_memo_root = NULL;
|
|
227
|
+
g_memo_doc = NULL;
|
|
228
|
+
g_memo_generation = rootmap_generation_load();
|
|
190
229
|
}
|
|
191
230
|
|
|
192
231
|
void leptris_root_doc_unregister(LeptrisElement root) {
|
|
193
232
|
if (!root) return;
|
|
233
|
+
/* The flag read touches element storage; elements die only with
|
|
234
|
+
* their document, and a cross-thread document death cannot run
|
|
235
|
+
* concurrently with THIS thread's unregister for the same tree
|
|
236
|
+
* (the binding never touches a freed document), so the probe is
|
|
237
|
+
* safe outside the lock. */
|
|
194
238
|
if (!rootmap_marked(root)) return; /* never registered: O(1) out */
|
|
195
239
|
/* #1242: any map mutation kills the memo (generation bump).
|
|
196
240
|
* The address-match clear below stays as the immediate drop;
|
|
@@ -201,6 +245,7 @@ void leptris_root_doc_unregister(LeptrisElement root) {
|
|
|
201
245
|
g_memo_root = NULL;
|
|
202
246
|
g_memo_doc = NULL;
|
|
203
247
|
}
|
|
248
|
+
LEPTRIS_MUTEX_LOCK(&g_root_doc_lock);
|
|
204
249
|
size_t idx = bucket_index(root);
|
|
205
250
|
RootDocEntry** pp = &g_root_doc_buckets[idx];
|
|
206
251
|
while (*pp) {
|
|
@@ -214,10 +259,12 @@ void leptris_root_doc_unregister(LeptrisElement root) {
|
|
|
214
259
|
freed->next = g_free_list;
|
|
215
260
|
g_free_list = freed;
|
|
216
261
|
rootmap_set(root, 0);
|
|
262
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
217
263
|
return;
|
|
218
264
|
}
|
|
219
265
|
pp = &(*pp)->next;
|
|
220
266
|
}
|
|
267
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
221
268
|
}
|
|
222
269
|
|
|
223
270
|
/* #1038: root-doc map entries must die with their document. The
|
|
@@ -238,6 +285,7 @@ void leptris_root_doc_unregister(LeptrisElement root) {
|
|
|
238
285
|
* preserving the adopted-pool rule above. */
|
|
239
286
|
size_t leptris_root_doc_unregister_doc(struct leptris_document* doc) {
|
|
240
287
|
if (!doc) return 0;
|
|
288
|
+
LEPTRIS_MUTEX_LOCK(&g_root_doc_lock);
|
|
241
289
|
size_t removed = 0;
|
|
242
290
|
/* #1242: doc sweep mutates the map — kill the memo. */
|
|
243
291
|
rootmap_generation_bump();
|
|
@@ -266,16 +314,20 @@ size_t leptris_root_doc_unregister_doc(struct leptris_document* doc) {
|
|
|
266
314
|
removed++;
|
|
267
315
|
e = next;
|
|
268
316
|
}
|
|
317
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
269
318
|
return removed;
|
|
270
319
|
}
|
|
271
320
|
|
|
272
321
|
struct leptris_document* leptris_root_doc_lookup(LeptrisElement root) {
|
|
273
322
|
if (!root) return NULL;
|
|
323
|
+
LEPTRIS_MUTEX_LOCK(&g_root_doc_lock);
|
|
274
324
|
size_t idx = bucket_index(root);
|
|
325
|
+
struct leptris_document* found = NULL;
|
|
275
326
|
for (RootDocEntry* e = g_root_doc_buckets[idx]; e; e = e->next) {
|
|
276
|
-
if (e->root == root)
|
|
327
|
+
if (e->root == root) { found = e->doc; break; }
|
|
277
328
|
}
|
|
278
|
-
|
|
329
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
330
|
+
return found;
|
|
279
331
|
}
|
|
280
332
|
|
|
281
333
|
/* #682 2x lever: an ambient document hint set by the transform
|
|
@@ -293,9 +345,24 @@ struct leptris_document* leptris_root_doc_lookup(LeptrisElement root) {
|
|
|
293
345
|
void leptris_root_doc_memo_prime(LeptrisElement root,
|
|
294
346
|
struct leptris_document* doc) {
|
|
295
347
|
if (!root || !doc) return;
|
|
348
|
+
LEPTRIS_MUTEX_LOCK(&g_root_doc_lock);
|
|
349
|
+
/* Verify the pair is still live under the lock: a document that
|
|
350
|
+
* died on another thread between the caller's registration
|
|
351
|
+
* guarantee and this prime must not seed a trusted memo. */
|
|
352
|
+
size_t idx = bucket_index(root);
|
|
353
|
+
struct leptris_document* live = NULL;
|
|
354
|
+
for (RootDocEntry* e = g_root_doc_buckets[idx]; e; e = e->next) {
|
|
355
|
+
if (e->root == root) { live = e->doc; break; }
|
|
356
|
+
}
|
|
357
|
+
if (live != doc) {
|
|
358
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
uint64_t gen = rootmap_generation_load();
|
|
362
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
296
363
|
g_memo_root = root;
|
|
297
364
|
g_memo_doc = doc;
|
|
298
|
-
g_memo_generation =
|
|
365
|
+
g_memo_generation = gen;
|
|
299
366
|
}
|
|
300
367
|
|
|
301
368
|
void leptris_root_doc_memo_invalidate(const struct leptris_document* doc) {
|
|
@@ -310,7 +377,7 @@ void leptris_root_doc_memo_invalidate(const struct leptris_document* doc) {
|
|
|
310
377
|
* when the memo is generation-stale (primed before a map
|
|
311
378
|
* mutation), even if the pair fields still hold values. */
|
|
312
379
|
LeptrisElement leptris_root_doc_memo_root_for_tests(void) {
|
|
313
|
-
if (g_memo_generation !=
|
|
380
|
+
if (g_memo_generation != rootmap_generation_load()) return NULL;
|
|
314
381
|
return g_memo_root;
|
|
315
382
|
}
|
|
316
383
|
|
|
@@ -334,7 +401,8 @@ struct leptris_document* leptris_element_get_document(LeptrisElement elem) {
|
|
|
334
401
|
* address memo. A freed root's address recycled by the
|
|
335
402
|
* allocator made the memo hit with a stale document
|
|
336
403
|
* before the authoritative namebp was consulted. */
|
|
337
|
-
if (cur == memo_root &&
|
|
404
|
+
if (cur == memo_root &&
|
|
405
|
+
memo_generation == rootmap_generation_load() &&
|
|
338
406
|
!leptris_elem_has_namebp(cur))
|
|
339
407
|
return g_memo_doc;
|
|
340
408
|
LeptrisElement parent = leptris_elem_parent(cur);
|
|
@@ -346,12 +414,28 @@ struct leptris_document* leptris_element_get_document(LeptrisElement elem) {
|
|
|
346
414
|
if (!parent) break;
|
|
347
415
|
cur = parent;
|
|
348
416
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
417
|
+
/* Locked bucket walk with an UNDER-THE-LOCK generation
|
|
418
|
+
* snapshot: a death that acquires the lock after we release
|
|
419
|
+
* bumps the global generation past our snapshot, so the primed
|
|
420
|
+
* pair is untrusted on the next read. Priming from outside the
|
|
421
|
+
* lock (the old shape) could snapshot the NEW generation for a
|
|
422
|
+
* pair whose document was freed in between (#321). */
|
|
423
|
+
{
|
|
424
|
+
struct leptris_document* d = NULL;
|
|
425
|
+
uint64_t now;
|
|
426
|
+
LEPTRIS_MUTEX_LOCK(&g_root_doc_lock);
|
|
427
|
+
size_t ridx = bucket_index(cur);
|
|
428
|
+
for (RootDocEntry* e = g_root_doc_buckets[ridx]; e; e = e->next) {
|
|
429
|
+
if (e->root == cur) { d = e->doc; break; }
|
|
430
|
+
}
|
|
431
|
+
now = rootmap_generation_load();
|
|
432
|
+
LEPTRIS_MUTEX_UNLOCK(&g_root_doc_lock);
|
|
433
|
+
if (d) {
|
|
434
|
+
g_memo_root = cur;
|
|
435
|
+
g_memo_doc = d;
|
|
436
|
+
g_memo_generation = now;
|
|
437
|
+
return d;
|
|
438
|
+
}
|
|
355
439
|
}
|
|
356
440
|
/* Round 21: unattached mutation elements carry their doc in the
|
|
357
441
|
* name slot backpointer — a stateless fallback that replaced the
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
/* dom/root_doc_map.h —
|
|
1
|
+
/* dom/root_doc_map.h — root-element → document mapping (process-global
|
|
2
|
+
* state behind one mutex; per-thread memo validated by a global
|
|
3
|
+
* generation — see leptris-ruby#321). */
|
|
2
4
|
#ifndef LEPTRIS_DOM_ROOT_DOC_MAP_H
|
|
3
5
|
#define LEPTRIS_DOM_ROOT_DOC_MAP_H
|
|
4
6
|
|