Benchmark coverage: missing constructions, wellformed levels, eight entry points - #140
Merged
Conversation
Its escaping path had no caller and its vocabulary is ASCII letters, so no generated document carried a reference, a comment, a CDATA section, a processing instruction, a DOCTYPE, or an attribute value to normalize. `Features` turns each on and is all off by default: the default document stays byte-identical to the one behind the published figures. Placement is deterministic and draws nothing from the random stream, so a twin keeps the plain document's elements and attributes in the same order. On factor 1.0 the escaped twin has the plain document's 882,026 nodes; the markup twin has two more per marked element plus the DOCTYPE and its line end, 898,028. Assisted-by: Claude (Anthropic)
Section (6) generates two documents beside the XMark corpus through the generator's opt-in features, checks that each keeps the corpus's elements in the same order, and measures every reader over the three: an escaped twin, where one drawn word in ten carries a predefined entity or a character reference and every item and person has a note attribute to decode or normalize, and a markup twin with a CDATA section, a comment and a processing instruction per item and person, under a DOCTYPE of the schema's declarations, which parse_dtd reads. Section (7) measures the three wellformed levels on the corpus, on the escaped twin and on the corpus's character data alone: the measurement the :strict note of PERFORMANCE-v0.4.md had no script for. The generator is included whether or not the corpus exists, since the twins need it either way. Assisted-by: Claude (Anthropic)
sourcespan, splicetext, issamenode, depth, siblings, foreach_attr and xpath are measured on one item element, the 1000th of the XMark corpus, addressed in every reader; the setup line checks that the three readers hold the same node and that xpath finds it. The cells print at nanosecond resolution with their allocation counts: most of these answer in a few nanoseconds, and the ones that search from a Node root or copy the document show it in the same columns. Assisted-by: Claude (Anthropic)
PERFORMANCE-v0.4.md gains three subsections before "Choosing": the readers over the XMark corpus and its escaped and markup twins (Table 6), what :strict adds by document shape (Table 7), and the entry points no other table covered (Table 8). The :strict note they replace said ~1.1× on the corpus and up to ~20× on a pure-text document, and described one check where the level runs two; measured, 1.2× and 14×, and the reference check allocates seven times per reference. The README points to the tables and the CHANGELOG records the coverage. Assisted-by: Claude (Anthropic)
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #140 +/- ##
=======================================
Coverage 96.47% 96.47%
=======================================
Files 15 15
Lines 2494 2494
=======================================
Hits 2406 2406
Misses 88 88 🚀 New features to boost your workflow:
|
XMark generates one document at a scaling factor and its literature calls it that: a corpus is a collection. The generator here follows the XMark DTD without being xmlgen, so the document is XMark-style. Both words are replaced in PERFORMANCE-v0.4.md, the README, the CHANGELOG and the benchmark scripts, released sections included; the conformance testset keeps "corpus" for the W3C suite, a collection. PERFORMANCE-v0.4.md now cites the VLDB 2002 paper and the Encyclopedia of Database Systems entry at the first mention, and the paper's §4.4 where the twins are introduced: the constructions they add are the ones XMark leaves out by design. Assisted-by: Claude (Anthropic)
The document read as one flat list of access patterns, with the twin, wellformed-level and entry-point tables looking like three more of them. Its sections now say what each part is: the readers on the XMark-style document by access pattern; what the twins and the wellformed levels add beyond that document; the other entry points; and choosing a reader. No figure moves. Assisted-by: Claude (Anthropic)
Two EzXML rows join Table 6, the reader and the DOM build over the three documents, and a libxml2 reference line follows Table 7. The C library's DOM build reproduces within a few percent on the plain document and on pure text, where it parses, checks and builds in half the time of the :strict character scan alone, but not on the twins: across five runs its median lay between 55 and 94 ms on the escaped one, so those cells carry the range. Tables 6 and 7 are republished from the run that added the rows. Assisted-by: Claude (Anthropic)
EzXML attaches its finalizer to a document's node, not to the document, so the finalize(doc) the cells relied on freed nothing, and a benchmark loop allocates too little in Julia to wake the collector: the trees piled up by the gigabyte within a cell until the machine compressed and swapped, which made the libxml2 cells of Table 6 drift between runs and every libxml2 figure pessimistic. Every C-tree cell of the three scripts now frees its tree per sample outside the timing, LightXML's extraction cell moves its free out of the timed region, and README.md and PERFORMANCE-v0.4.md are republished from one campaign: libxml2's build of the 14 MB document goes from 47.6 to 37.3 ms, its extraction from 65 to 58 ms, LightXML's from 66 to 47 ms; the XML.jl figures move within noise. Table 7's ratios are taken net of the GC share, and its caption says so. Assisted-by: Claude (Anthropic)
mathieu17g
marked this pull request as ready for review
September 2, 2026 12:23
This was referenced Sep 2, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #138.
The tables of
PERFORMANCE-v0.4.mdand the README's access-pattern table rest on one generated XMark-style document, and it carries none of these:XMarkGenerator.jlnow has an opt-in feature for each. Placement is deterministic, every k-th word,itemorperson, and draws nothing from the random stream, so a generated twin keeps the XMark-style document's elements and attributes in the same order: a difference between the two belongs to the construction alone. The default document is byte-identical to the one behind the published tables. One correction to the issue's diagnosis: the generator's escaping path had no caller, so nothing would have been escaped had the vocabulary needed it; the special words now reach the document through that path.What the scripts gain, after the entity twin of #130 in section (6) of
profile.jl:profile.jl, section (7): every reader over the XMark-style document and two twins. The escaped twin replaces one drawn word in ten by one carrying a predefined entity or a character reference, and gives everyitemandpersonanoteattribute to decode or to normalize. The markup twin has a CDATA section, a comment and a processing instruction peritemandperson, under a DOCTYPE holding the schema's 74 element and 14 attribute-list declarations and no entity, whichparse_dtdreads.profile.jl, section (8): the threewellformedlevels on the XMark-style document, on the escaped twin, and on the XMark-style document's character data alone.flatnode_bench.jl: the other seven entry points that had no cell, on the XMark-style document's 1000thitemaddressed in every reader.benchmarks.jl: nothing measured changes, but every cell that builds a C tree now frees it per sample, outside the timing (see below), andbenchmarks_results.mdis regenerated.PERFORMANCE-v0.4.md: Tables 6 to 8, replacing the:strictnote that no script reproduced; the four parts its tables fall into, as section headings; the XMark paper and the XML Benchmarks survey cited at the first mention, and the paper's §4.4 where the twins are introduced; and "XMark-style document" for what the text called a corpus, a word the XMark literature does not use for a single document. Every figure of the document and of the README is republished from one campaign, with the C trees freed.What the twins show
Cursorstreamparse→Nodeparse→Node{SubString}LazyNodewalkLazyNodeattribute sweepFlatNodewalkStreamReader(libxml2)parsexml(libxml2)Cursor,LazyNodeandFlatNodefor the 46,583 text tokens carrying a reference.unescapecopies its argument to aString, runs a regular-expressionreplaceand copies the result.Node{SubString}does not decode; its +12,801 are the §3.3.3 normalization of the 3,200 attribute values carrying a literal tab or newline, four allocations each.&e;should resolve in content and attribute values #130 measured on a document with no DOCTYPE at all.parse→Node, and takes the markup twin's comments, processing instructions, CDATA sections and DOCTYPE for 7 ms over it, whereparse→Nodeshows no difference beyond noise.What the levels show
parse(…, Node; wellformed = …):structural:strict:strictruns two: the character-range scan, in proportion to the text share, and the reference check, only on tokens that carry a&, which the XMark-style document never had.:strictcharacter-range scan by itself.The entry points
The issue's eight entry points had no cell, so nothing said what any of them costs, or which reader pays for what. They are now measured on one
itemelement, the 1000th of the XMark-style document, addressed in every reader. The allocation counts are the absolutes: most of these answer in nanoseconds and allocate nothing, so any later change that makes one of them allocate will show.sourcespanFlatNodesourcespanLazyNodesplicetextFlatNodesplicetextLazyNodeissamenodeFlatNodeissamenodeLazyNodedepthFlatNodedepth(child, root)Nodesiblings(child, root)Nodeforeach_attrLazyNodeeachattributeLazyNodexpath,/site/regions/asia/item[500]Nodexpath,//item[@featured='yes']Nodeparse_dtd, the schema's 88 declarationsWhat the numbers say:
FlatNodestores each node's span and parent, sosourcespananddepthare one lookup.LazyNodekeeps only the source, sosourcespanhas to scan to the element's end.Nodekeeps no parent link, sodepthandsiblingssearch the whole tree down from the root: 4.32 ms whereFlatNodeanswers in 1.8 ns.splicetextreturns the document with one node replaced, so it costs a copy of the document whichever reader asks: the 13.5 MiB in the allocation column is the result itself.LazyNodeare allocation-free:foreach_attryields raw tokens,eachattributedecoded pairs, and on attributes that need no decoding the pair costs about twice the tokens, 70 ns against 36.xpathover the descendant axis allocates 17 MiB for a query that returns 303 elements; the positional path, which returns one, allocates 26 times.A measurement defect found on the way
The libxml2 rows first added to Table 6 did not reproduce: the same DOM build read 55 ms in one section and 94 ms in the next. The cause is not libxml2, which is stable through a bare
xmlReadMemory/xmlFreeDocloop, nor the Julia collector, whichgcsample = truerules out. EzXML attaches its finalizer to the document's node, not to the document, so thefinalize(doc)the benchmark cells relied on frees nothing:xmlReadMemory/xmlFreeDocbyccallEzXML.parsexml+finalize(doc)EzXML.parsexml+finalize(doc.node)A benchmark loop allocates two Julia objects per sample, so the collector never runs inside a cell and the orphaned trees pile up: about 8 GiB per 5 s cell, tens of gigabytes over a run, until macOS compresses and swaps. The cells that allocate most, the escaped and markup twins, were the first to suffer; every libxml2 cell was pessimistic. Every C-tree cell of the three scripts now frees its tree per sample, outside the timing, through the document's node (
benchmarks.jl's comment described exactly this risk and believedfinalize(doc)covered it), and LightXML's extraction cell moves itsfreeout of the timed region for the same reason. Every figure ofREADME.mdandPERFORMANCE-v0.4.mdis republished from one campaign with the fix in place:freeout of the timingParse, medium, EzXMLThe XML.jl figures move within noise.
Left out, and why
The real-world documents the issue lists as candidates are not added:
:strictclaim needed: the XMark-style document's character data alone gives it;noteattributes give the normalization its values;Three of the findings above are candidates for issues of their own rather than fixes here: the decode path's six allocations per value, the reference check's seven per reference, and a character-range scan that alone takes longer than libxml2's full parse of the same text.