perf(schema): memoize group expansion in the compiled content model
Compiling a schema with deeply nested, multiply-referenced model groups was exponential: groupReferenceParticle inlined each <xs:group ref> into the Particle content model, so g0=(g1,g1), g1=(g2,g2), ... built 2^K particles (depth 14 = 0.21s, depth 20 ~13-18s). The expansion is now memoized per (group definition, cycle-visiting set) in a per-compile GroupParticleMemo shared across the value copies XSDContext.scoped/visiting make, so sibling references share it and the build drops to O(groups) (depth 40 now ~0.016s; previously infeasible). The shared content term is reused by value (copy-on-write), so the compiled model is identical and validation is unchanged: the XSTS gate holds (invalid-schemas- accepted 15, valid-schemas-rejected 0, instance buckets unchanged) and the streaming-vs-tree fuzz differential still passes. This closes the schema-compile half of the interactive-safety group blowup (companion to the positionCap determinism fix); a depth-40 compile test guards against regression. Full suite, style, namespacing, and WASM all pass.
This commit is contained in:
parent
6f8222a840
commit
bc0638e201
File diff suppressed because one or more lines are too long
22
Sources/Schema/GroupParticleMemo.swift
Normal file
22
Sources/Schema/GroupParticleMemo.swift
Normal file
@ -0,0 +1,22 @@
|
||||
extension PureXML.Schema {
|
||||
/// The key under which an `<xs:group>` reference's expansion is memoized: the
|
||||
/// group DEFINITION node and the cycle-visiting set at the reference site. The
|
||||
/// expansion is determined by these (the namespace scope is fixed per
|
||||
/// definition), so two references that share them share the result.
|
||||
struct GroupParticleMemoKey: Hashable {
|
||||
let definition: ObjectIdentifier
|
||||
let visiting: Set<String>
|
||||
}
|
||||
|
||||
/// Memoizes group-reference expansion during one schema compile. A
|
||||
/// multiply-referenced nested group (`g0=(g1,g1), g1=(g2,g2), …`) otherwise
|
||||
/// rebuilds each subtree per reference, an exponential `2^K` compile; sharing the
|
||||
/// expansion across sibling references drops the build to `O(groups)`. The
|
||||
/// resulting `Particle` is semantically identical (a value type, so the shared
|
||||
/// content term is copy-on-write), only built once. A reference type so it is
|
||||
/// shared across the value copies `XSDContext.scoped`/`visiting` make; one
|
||||
/// instance per compile.
|
||||
final class GroupParticleMemo {
|
||||
var store: [GroupParticleMemoKey: PureXML.Schema.Particle] = [:]
|
||||
}
|
||||
}
|
||||
@ -284,6 +284,7 @@ extension PureXML.Schema {
|
||||
/// refs can be expanded), and a guard against cyclic group references.
|
||||
struct XSDContext {
|
||||
var simpleTypes: [String: SimpleType]
|
||||
let groupMemo = GroupParticleMemo()
|
||||
var attributeGroups: [String: XSDTree]
|
||||
/// Attribute-group definitions from included schemas before a `redefine`
|
||||
/// overlay replaces them; used when a redefinition self-references its
|
||||
|
||||
@ -98,10 +98,19 @@ extension PureXML.Schema.XSDParser {
|
||||
guard let definition = context.groups[name] else {
|
||||
return nil
|
||||
}
|
||||
// Memoize the expansion: it is fixed by the definition and the visiting set
|
||||
// (scope is fixed per definition), so sibling references share it and a
|
||||
// multiply-referenced nested group compiles in O(groups), not 2^K. The
|
||||
// reference site's own occurrence still wraps the shared content term.
|
||||
let key = PureXML.Schema.GroupParticleMemoKey(definition: ObjectIdentifier(definition), visiting: context.visitingGroups)
|
||||
if let cached = context.groupMemo.store[key] {
|
||||
return Particle(occurrenceRange: occurrence, term: cached.term)
|
||||
}
|
||||
let scoped = context.scoped(for: XSDNode.schemaOwner(definition))
|
||||
guard let inner = modelGroup(in: definition, scoped.visiting(name)) else {
|
||||
return nil
|
||||
}
|
||||
context.groupMemo.store[key] = inner
|
||||
return Particle(occurrenceRange: occurrence, term: inner.term)
|
||||
}
|
||||
|
||||
|
||||
@ -11,10 +11,14 @@ import Testing
|
||||
/// both depths still exceed the old 4096-position cap, so the point holds.
|
||||
@Suite("UPA on 2^K nested-group content models")
|
||||
struct SchemaDeterminismGroupBlowupTests {
|
||||
// The compiled content model also memoizes group expansion
|
||||
// (GroupParticleMemo), so even these depths build in O(depth), not 2^depth: a
|
||||
// depth past ~25 is infeasible to inline at all, so it doubles as a regression
|
||||
// guard on that memo. WASI uses a smaller depth (~30x slower runtime).
|
||||
#if os(WASI)
|
||||
private static let depth = 13 // 2^13 = 8192 positions, past the old 4096 cap
|
||||
private static let depth = 16 // 2^16 = 65536 inlined positions, past the old 4096 cap
|
||||
#else
|
||||
private static let depth = 20 // 2^20 = ~1,048,576 inlined positions
|
||||
private static let depth = 40 // 2^40 inlined positions: infeasible without memoization
|
||||
#endif
|
||||
|
||||
/// `g0=(g1,g1) … g{depth-1}=(g{depth},g{depth})`, leaf `g{depth}` = the given model.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user