diff --git a/CHANGELOG.md b/CHANGELOG.md index 24c4cc6..30f0a20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Scanner-side validity findings are now located structured values (validation-framework audit follow-up): `DocumentType.validityFindings` carries `ValidityFinding` values (reason plus the declared name they are about) instead of strings, `DTDSchema.declarationErrors` holds ready `ValidationError`s, and every declaration-level finding renders at its declaration's coding path (`x`, `r/@a`) instead of "at root of document", matching the XSD consistency-errors precedent. Finding texts are unchanged. +### Changed + +- First measured performance pass (#139): Reader.matches() no longer materializes an Array of the literal per call (it runs on every scan decision and was the hottest comparison in the parse profile); parse time on the 4.2 MB benchmark corpus drops 16 percent (0.49 s to 0.41 s, libxml2 ratio 30x to 26x). The benchmark harness, baseline, profile findings, and the optimization log live in docs/benchmarks.md. + ### Fixed - Stylesheet composition cycles no longer crash (#130 critic loop): an include or import cycle (a.xsl includes b.xsl includes a.xsl) recursed without bound and killed the host with a stack overflow, a denial of service reachable from stylesheet data. The compile chain now tracks the active load chain by resolved href and drops re-entry, while diamond composition (two units legitimately loading the same sheet) keeps loading both copies with their own import precedences, per 2.6.2. id() also gains a per-document index built on first use instead of re-walking the whole document per call, matching key(). Cycle and diamond regression tests added. diff --git a/Sources/Parsing/Reader.swift b/Sources/Parsing/Reader.swift index 40012e3..49c885d 100644 --- a/Sources/Parsing/Reader.swift +++ b/Sources/Parsing/Reader.swift @@ -112,11 +112,16 @@ extension PureXML.Parsing { /// True if the upcoming characters equal `literal`, without consuming. mutating func matches(_ literal: String) -> Bool { - let target = Array(literal) - ensure(target.count) - guard buffer.count >= target.count else { return false } - for index in target.indices where buffer[index] != target[index] { - return false + // No per-call Array materialization: literals are short constants + // and this runs on every scan decision (the hottest comparison in + // the parse profile). + let count = literal.count + ensure(count) + guard buffer.count >= count else { return false } + var index = 0 + for character in literal { + if buffer[index] != character { return false } + index += 1 } return true } diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 9c665c6..c4f5bd9 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -20,6 +20,25 @@ cross-implementation correctness check. These are the honest starting numbers the #139 optimization passes burn down; the table updates as passes land, never silently. +## Optimization log + +| pass | change | parse | xpath | serialize | +|---|---|---|---|---| +| baseline | - | 0.494 s (30x) | 0.413 s (37x) | 0.061 s (5.5x) | +| 1 | allocation-free Reader.matches() | 0.414 s (26x) | unchanged | unchanged | + +## Profile findings (first sample, parse path) + +Top of stack, release build: String.Iterator.next (the per-character pull +closure chain), _stringCompareWithSmolCheck (Character equality in +matches/peek), String._uncheckedFromUTF8 and _allASCII (small-string +materialization), bridgeObjectRelease/swift_release (ARC on Character +buffers), UnicodeScalarView.distance. The Reader's architecture: one +closure call per scalar, [Character] lookahead, Character-typed +classification. The structural fix is a byte-level (UTF-8) scanner with +scalar classification tables; that is its own #139 pass, designed, not +patched. + ## Known structure of the gap - Parse and XPath dominate; serialize is closest.