Repository navigation
perf(swift/core): check each updateComponents against what it changes, and run message_processor_scaling.yaml - #3376
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces an incremental component graph validation optimization in the Swift core (MessageProcessor) using a new ComponentGraphBound class. When the validation configuration allows dangling references and missing roots, the processor now validates only the updated batch of components against the cached bounds of the existing graph, rather than re-validating the entire surface. This reduces the validation cost to scale with the batch size rather than the total surface size. The PR also adds comprehensive unit tests for this batch validation logic, performance/scaling conformance tests, and updates the documentation. There are no review comments, so I have no feedback to provide.
|
Hi @karolkulesza, thank you for raising the PR to fix #3375. We have checked the changes and confirmed that they resolve the issue. Before merging, please rebase onto main to fix the merge conflict in conformance/README.md. Also, in MessageProcessor, please use a change counter on SurfaceComponentsModel instead of bound.count == held.count (so same-count direct updates don't leave bound stale), and clear graphBounds in dispose(). Could you please update the PR and let us know? Thank you for your contribution and understanding. |
92e4bc8 to
78cc5b2
Compare
|
@Varun-S10 thanks, done in 78cc5b2, and rebased onto main:
|
78cc5b2 to
7743ea3
Compare
Adds MessageProcessorScalingConformanceTests, which writes each measure_growth payload at its two sizes, times processMessages on a new processor once untimed and then five times in turn, and expects the ratio of the median times to be at most the case's max_ratio. The cases run only when A2UI_RUN_TIMING=1, so the default swift test skips them, and a new performance job in swift_build_and_test.yml runs them in a release build. The composition case is listed as a known gap: SurfaceViewModel resolves the whole node tree after each message, which a2ui-project#3231 is expected to change. Refs a2ui-project#3375
Under a validation config that allows dangling references and a missing root, as .relaxed does, MessageProcessor copied every stored component and ran GraphTopologyValidator on the whole surface for each batch, so a surface arriving in n messages took time quadratic in n. MessageProcessor now keeps a ComponentGraphBound per surface, following the Dart core (a2ui-project#3314, a2ui-project#3345): the references each component holds, the components holding each id, and an upper bound on the longest chain below each component. A batch is checked for self-references, cycles and the depth limit by raising the bounds above its components, and for composition constraints on the references from and to its components. When the bound cannot show the batch passes, or under any other config, the whole surface is checked as before, so the error raised is that of the whole-surface check. message_processor_scaling.yaml, release build, median of 5 (M2 Pro): relaxed v1.0 200/800 goes from 2.02 s / 17.57 s (ratio 8.71) to 1.51 s / 9.49 s (ratio 6.28). Fixes a2ui-project#3375
…graph bound The processor used its graph bound for a surface only while the bound held as many components as the surface, so a change that kept the count, such as host code replacing a component through applyBatch or addComponent, left the bound describing a graph the surface no longer had. SurfaceComponentsModel now counts its changes in an internal revision, raised by addComponent, removeComponent and applyBatch. The bound records the revision the processor's own applyBatch leaves, and a batch is checked against the bound only while the surface is still at that revision; otherwise the whole surface is checked and the bound rebuilt. dispose() clears the bounds. Tests: a same-count change made outside the processor is checked again, through applyBatch and addComponent, and a cycle closed through an array of ComponentId is rejected in a batch as in a check of the whole surface.
7743ea3 to
555766d
Compare
…l-update-checks # Conflicts: # conformance/README.md
|
Hi @karolkulesza, thank you for making the requested updates and rebasing onto main. We have verified the new revision counter on SurfaceComponentsModel and the dispose cleanup, and everything works now. I will escalate this PR to the team for final review. Thank you for your patience and understanding. |
…l-update-checks # Conflicts: # swift/core/Sources/A2UICore/Processing/MessageProcessor.swift
|
Merged
I also switched |
Spell out component in validateComponentGraph, drop empty parent sets in ComponentGraphBound so parents only holds live references, and use guard and max where the rest of the type does.
The scaling runner now requires every case to parse and at least one to run, and ConformanceTiming.compare throws on runs below 1. The batch check tests assert the outcomes of the rewritten-container case and cover a duplicate id in a batch while a bound is cached.
Description
Swift leg of the scaling work done for Dart (#3314, #3345), Python (#3326) and web_core (#3327).
Fixes #3375.
Timing runner
MessageProcessorScalingConformanceTestsrunsconformance/core/message_processor_scaling.yaml:it writes each payload at its two sizes, times
processMessageson a new processor once untimedand then five times in turn, and expects the ratio of the median times to be at most
max_ratio.It runs only when
A2UI_RUN_TIMING=1(A2UI_TIMING_RUNSandA2UI_TIMING_CASEnarrow a localrun), so
swift testskips it. It fails when the suite has a case it can't parse, or when nocase ran (a typo in
A2UI_TIMING_CASE, say). A newperformancejob inswift_build_and_test.ymlruns it in arelease build.
The composition case is skipped as a known gap:
SurfaceViewModelresolves the whole node treeafter every components change, which grows with n squared and dominates at 1000/4000. #3231
reworks resolution, so this PR leaves it alone.
Per-batch graph check
Under a config that allows dangling references and a missing root (as
.relaxed, the default,does), the whole-surface graph check can only fail on a self-reference, a cycle, a chain deeper
than the depth limit, or a composition constraint. The surface passed the check before the batch,
so
MessageProcessorkeeps aComponentGraphBoundper surface, as the Dart core does:none rises, so a cycle (which must pass through a batch component) or a chain over the limit
exceeds the limit;
component and its children, and for each holder outside the batch and the batch component it
holds.
When the bound cannot show the batch passes, the whole surface is checked as before, so the error,
its type and message are those of the whole-surface check. Other configs (strict, or root
required) always use the whole-surface check. The bound is dropped on deleteSurface, and is only
used while it belongs to the same
SurfaceViewModeland that surface's components are still atthe
SurfaceComponentsModelrevision the bound describes.GraphTopologyValidatoronly changesmaxGlobalDepthandlookupComponentAPIfrom private tointernal.
Numbers
M2 Pro,
swift test -c release -Xswiftc -enable-testing, median processMessages time:Re-run on the same machine after merging main on 2026-10-30 (3 runs): relaxed 0.24 s / 1.46 s,
ratio 6.02; no config 0.31 s / 1.24 s, ratio 4.03. Absolute times dropped with main's changes;
both ratios stay under 8.
Tests
ComponentGraphBatchCheckTestssends each batch to a processor under.relaxedand checks thesame surface with the batch applied through
GraphTopologyValidator.validate; both must acceptit, or both reject it with the same kind of error, and the surface must hold the same components.
Cases: a child after its container, a container after its child, a disallowed child added later,
a container rewritten to drop a disallowed child, a held child whose type changes, a holder the
child doesn't allow, a root the surface may not hold, a cycle closed by a later message, a
self-reference, a chain over the depth limit arriving from its end, two chains joined in the
middle, a cycle closed through an array of ComponentId, a component replaced outside the
processor, a duplicate id in one batch while a bound is cached, and 200 random sequences of 20 batches with and without composition constraints.
Only the kind of error is compared, because when a surface breaks several rules the whole-surface
check reports the first one in dictionary order. Disabling the holder check makes 6 of these
fail. Existing validator and message-processor conformance suites pass unchanged.
Pre-launch Checklist