PureXML/Sources/Emitting
Mihaela Mihaljevic 0bda990f80
perf(serialize): scan bytes, not graphemes, to find characters to escape (#297)
The bulk-copy escape loop still iterated Characters and compared each to '&',
'<', '>' (a grapheme comparison per character), the dominant remaining serialize
cost for content that needs escaping. Every escaping trigger is an ASCII marker,
or in ASCII-only mode a non-ASCII lead byte, so find the next one with
value.utf8.firstIndex(where:) and decode no grapheme for the run between markers.

The marker byte always lands on a grapheme boundary: an ASCII byte is its own
scalar and forces a cluster break (so '&', '<', '>', the quote, and whitespace
are never mid-cluster), and after a grapheme-aligned runStart the first byte at
or above 0x80 is a lead byte. So value[marker] and value[runStart ..< marker]
stay valid, and a multi-scalar cluster escapes as a unit (Swift rounds a
mid-grapheme index to its boundary, so the base letter is neither dropped nor
duplicated). The output is byte-identical.

Measured serialize ~23% faster on the 20k-item generated corpus via a same-state
A/B (~2.8x to ~2.2x slower than libxml2). Verified by EscapingBulkCopyTests
(the oracle differential extended with combining-mark, ZWJ, and flag clusters),
the Apache Xalan gold-output and C14N suites, the full suite, the WASM build,
and an adversarial grapheme-alignment review.
2026-06-25 15:39:20 +02:00
..
Escaping.swift perf(serialize): scan bytes, not graphemes, to find characters to escape (#297) 2026-06-25 15:39:20 +02:00
Options.swift feat(emitting): textEscaping round-trip option to preserve carriage returns (#91) 2026-06-09 03:48:20 +02:00
Serializer.swift fix(emit): split ?> in processing-instruction data with a space (#262) 2026-06-24 03:14:01 +02:00
Writer.swift fix(emit): split ?> in processing-instruction data with a space (#262) 2026-06-24 03:14:01 +02:00