Skip to content

Reuse the traversal key path in TestFilter's precomputed filter#1744

Open
inju2403 wants to merge 1 commit into
swiftlang:mainfrom
inju2403:inju2403/reuse-traversal-keypath-in-test-filter
Open

Reuse the traversal key path in TestFilter's precomputed filter#1744
inju2403 wants to merge 1 commit into
swiftlang:mainfrom
inju2403:inju2403/reuse-traversal-keypath-in-test-filter

Conversation

@inju2403
Copy link
Copy Markdown
Contributor

@inju2403 inju2403 commented Jun 5, 2026

Reuse the key path the graph traversal already provides instead of recomputing it from each test's ID while filtering.

Motivation

Following the key-path work in #1725 and #1730 — which made the Graph traversal thread its key path efficiently — this change makes a consumer of that traversal actually use it.

When a precomputed (ID-based) Configuration.TestFilter is applied, Operation.apply(to:) walks the test graph with mapValues, which hands each closure the node's key path. The .precomputed branches discard it (mapValues { _, item in ... }) and instead call selection.contains(item.test), which reconstructs item.test.id and its keyPathRepresentation for every node — allocating a fresh [String] and formatting the source location into a "file:line:column" string each time.

That work is redundant: the graph is built by inserting each test at test.id.keyPathRepresentation (Runner.Plan._constructStepGraph / Plan.init(steps:)), so a node's key path in the graph is that test's key-path representation. The value being recomputed is, by construction, already in hand.

Every path that applies the .precomputed operation does so against that same graph — or, for tag filters, a key-path preserving mapValues of it into FilterItem — so the invariant holds for ID, tag, and combined filters alike; intermediate nodes with no test value are skipped by the existing guard let item.

Modifications

  • In Configuration.TestFilter.Operation.apply(to:), the .precomputed including/excluding branches now use the keyPath provided by mapValues (selection.contains(keyPath)) instead of selection.contains(item.test).
  • Added a comment explaining why the node's key path equals test.id.keyPathRepresentation, mirroring the existing explanatory comment on the sibling .function case.

The result is identical by construction, so behavior is unchanged. This is a small simplification that also drops a per-node ID reconstruction, [String] allocation, and source-location string format from the filtering path (which runs on every swift test --filter and tools-driven ID selection). No public API changes.

Measurements

Isolated micro-benchmark of the per-node membership check (faithful replica of keyPathRepresentation + the trie walk, built with -O, results stable across runs):

Tests OLD (ms) NEW (ms) Speedup
50 0.153 0.036 4.3×
250 0.787 0.202 3.9×
1,000 3.219 0.798 4.0×
5,000 17.502 4.439 3.9×
10,000 35.141 8.778 4.0×

Unlike #1730, this is a constant-factor win (the speedup stays ~4× rather than growing), since it removes a fixed per-node cost rather than reducing complexity — so absolute savings scale linearly with the number of tests filtered (sub-millisecond for typical suites, ~26 ms once per run at 10k tests). The benchmark is a conservative lower bound: the old path additionally rebuilds test.id, which the replica omits.

Existing PlanTests and TestFilter tests pass — selection/exclusion by ID, by tag (via the .function.precomputed path), and combined filters.

Checklist

  • Code and documentation should follow the style of the Style Guide.
  • If public symbols are renamed or modified, DocC references should be updated. (N/A — no public API changes.)

Comment thread Sources/Testing/Running/Configuration.TestFilter.swift Outdated
TestFilter's precomputed filter receives each node's key path from the graph traversal but discards it and reconstructs the same value via `item.test.id.keyPathRepresentation` — re-allocating a [String] and re-formatting the source location to a string per node. Since the graph is built by inserting each test at `test.id.keyPathRepresentation`, the node's key path *is* that value, so use it directly.

Simpler, and removes a per-node allocation + string format from the filtering path. (~3.7x on the isolated per-node membership check; absolute per-run cost is sub-ms — this is a readability cleanup first.)
@inju2403 inju2403 force-pushed the inju2403/reuse-traversal-keypath-in-test-filter branch from 7fa17ac to c7cf6d2 Compare June 5, 2026 12:59
@harlanhaskins
Copy link
Copy Markdown
Contributor

harlanhaskins commented Jun 5, 2026

Do you have a rough idea of what percentage of time this represents during a test plan execution? I would imagine 35ms over 10,000 test runs to be a rounding error compared to the tests themselves. Not saying we shouldn't take this change (it's a good change) but I'm wondering what the real-world, non-micro-benchmark impact of it is.

@inju2403
Copy link
Copy Markdown
Contributor Author

inju2403 commented Jun 5, 2026

@harlanhaskins
Agreed — speed-wise it's a rounding error (it runs once during plan construction, before any test executes), so your intuition is right. Speed wasn't really the main goal, though: the change is mostly about not rebuilding a key path the traversal already hands us, so it's a cleanup that also drops a per-node Test.ID reconstruction + allocation. The ~4× is just supporting evidence, not something a user would feel — happy to trim the benchmark framing if you'd rather it land purely as a simplification.

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.

2 participants