Skip to content

Reduce key-path COW copies in Runner.Plan helpers#1730

Open
inju2403 wants to merge 1 commit into
swiftlang:mainfrom
inju2403:inju2403/inout-key-path-pattern-runner-plan
Open

Reduce key-path COW copies in Runner.Plan helpers#1730
inju2403 wants to merge 1 commit into
swiftlang:mainfrom
inju2403:inju2403/inout-key-path-pattern-runner-plan

Conversation

@inju2403
Copy link
Copy Markdown
Contributor

@inju2403 inju2403 commented May 27, 2026

Motivation

After landing #1725, I went back through the rest of the codebase looking for the same "key-path COW" shape applied elsewhere — i.e. recursive functions that build a per-descent collection with something + [key] and pass it by value to the recursion.

_recursivelySynthesizeSuites in Sources/Testing/Running/Runner.Plan.swift is one such instance: it threads a nameComponents: [String] path down to every node and rebuilds it on every descent with nameComponents + [key].

This is the same shape that #1725 fixed in Graph._forEach / Graph._compactMapValues:

Each recursive call builds the descendant's key path with var childKeyPath = keyPath; childKeyPath.append(key), and because childKeyPath is COW-shared with keyPath, the append always triggers a copy of the whole key path array. For a graph with N nodes at average depth d, this turns traversal into O(N · d) instead of the O(N) it should be.

This runs at plan construction (not during the test run), so the absolute savings are smaller than #1725 — but the pattern is the same, the fix is the same, and the existing tests cover the call site.

Modifications

Pure mechanical match for #1725: thread nameComponents through an inner recursive function as inout. The for loop pushes the key, recurses, pops the key. By the time control returns to the lines that consume nameComponents (the suite-synthesis block at the bottom), the array is back to the value it had on entry.

No public API changes.

Measurements

Isolated micro-benchmark of the recursive pattern at depths 3–6 with branching 3–5:

depth branch nodes OLD NEW speedup
3 3 40 0.011 ms 0.002 ms 4.47×
4 4 341 0.086 ms 0.016 ms 5.42×
5 4 1,365 0.355 ms 0.062 ms 5.68×
6 4 5,461 1.219 ms 0.215 ms 5.67×
4 5 781 0.151 ms 0.030 ms 5.10×
6 3 1,093 0.228 ms 0.043 ms 5.32×

The speedup grows with depth, as expected from O(N · d) → O(N).

This is a plan-construction helper, called once per test run — so the absolute wins are small. The change is the same kind of cleanup as #1725 in spirit.

All existing tests in PlanTests, Runner.Plan.SnapshotTests, RunnerTests, and the wider suite/trait/tag tests pass unchanged.

Checklist

  • Code and documentation follow the Style Guide
  • If public symbols are renamed or modified, DocC references should be updated — no public API changes

Following the same pattern as swiftlang#1725, thread `nameComponents` through `_recursivelySynthesizeSuites`'s inner recursion as `inout` with a push/pop discipline. Avoids the COW copy that `nameComponents + [key]` triggered on every recursive descent.

In isolated micro-benchmarks at depths 3-6 with branching 3-5, the new version is 4.47x-5.68x faster -- same caliber and shape as swiftlang#1725. The helper runs once per test run during plan construction, so absolute savings are small (sub-millisecond on typical test plans), but the change removes one more instance of the pattern there.

All existing tests in `PlanTests`, `Runner.Plan.SnapshotTests`, `RunnerTests`, and the wider suite/trait/tag tests pass.
@inju2403
Copy link
Copy Markdown
Contributor Author

Hi @grynspan @harlanhaskins, sorry to bother you both — reviewers didn't auto-assign on this PR (unlike on my previous one). I'm not sure why, but I noticed a 500 error from GitHub when I opened the PR, so maybe that's the cause? No urgency at all, just flagging in case it needs a manual nudge. Thanks!

@harlanhaskins
Copy link
Copy Markdown
Contributor

Thanks for looking further into this! In this specific case, looking at the microbenchmarks, I think the complexity difference might not be worth the perf improvements given we're dealing in sub-ms times. But I'm curious what @grynspan thinks.

@inju2403
Copy link
Copy Markdown
Contributor Author

Thanks for the honest read — agreed the sub-ms perf wins on their own don't really justify the extra complexity in commit 2.

I will say the motivation was as much about code consistency as raw speed: both helpers had the same COW shape that #1725 had just cleaned up in Graph, so leaving them in the older style felt incomplete. But that's a soft argument and totally up to you both.

If it'd help, I'm happy to drop commit 2 and keep just commit 1 (which is a mechanical match for #1725 and has no real complexity tradeoff), or close this PR entirely. Whatever you both think is best.

@grynspan
Copy link
Copy Markdown
Contributor

Let's try to land the first commit, and you can follow up with the second commit in a separate PR.

@inju2403 inju2403 force-pushed the inju2403/inout-key-path-pattern-runner-plan branch from 5460e63 to 45f5935 Compare May 28, 2026 23:34
@inju2403
Copy link
Copy Markdown
Contributor Author

@grynspan
Thanks! Force-pushed to drop the second commit and updated the description. I'll open a follow-up PR for _recursivelyApplyTraits once this one lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants