Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions Sources/SwiftWarningControl/WarningControlRegions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,35 @@ public struct WarningControlRegionTree {
guard !controls.isEmpty else { return }
let newNode = WarningControlRegionNode(range: range)
for (diagnosticGroupIdentifier, control) in controls {
// Handle the control for the added diagnostic group
// and propagate it to all of its subgroups.
// Breadth-first walk over the inheritance tree of `diagnosticGroupIdentifier`,
// applying `control` to every transitive sub-group.
//
// We use an index-based queue (rather than `Array.removeFirst()`, which is O(N))
// and dedupe at dequeue time. Previously, a sub-group reachable from more than one
// already-queued group would be enqueued multiple times and have its control
// re-applied each time, which was both redundant and made the loop O(N^2) in the
// queue size. For a diamond-shaped inheritance graph this caused the queue to
// grow exponentially, so the function would not terminate in any reasonable time.
var groups: [DiagnosticGroupIdentifier] = [diagnosticGroupIdentifier]
var groupIndex = 0
var processedGroups: Set<DiagnosticGroupIdentifier> = []
while !groups.isEmpty {
let groupIdentifier = groups.removeFirst()
processedGroups.insert(groupIdentifier)
while groupIndex < groups.count {
let groupIdentifier = groups[groupIndex]
groupIndex += 1
// Skip groups we already reached via another path through the inheritance tree.
guard processedGroups.insert(groupIdentifier).inserted else { continue }

newNode.addWarningGroupControl(for: groupIdentifier, control: control)
if control != .ignored {
enabledGroups.insert(diagnosticGroupIdentifier)
}

let newSubGroups = groupInheritanceTree.subgroups(of: groupIdentifier).filter { !processedGroups.contains($0) }
// Ensure we add a corresponding control to each direct and
// transitive sub-group of the one specified on this control.
groups.append(contentsOf: newSubGroups)
// Enqueue not-yet-processed sub-groups so each direct and transitive sub-group
// also receives the control.
for subGroup in groupInheritanceTree.subgroups(of: groupIdentifier)
where !processedGroups.contains(subGroup) {
groups.append(subGroup)
}
}
}
insertIntoSubtree(newNode, parent: rootRegionNode)
Expand Down
124 changes: 124 additions & 0 deletions Tests/PerformanceTest/WarningControlRegionsPerformanceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftIfConfig
import SwiftParser
import SwiftSyntax
import SwiftWarningControl
import XCTest
import _InstructionCounter
import _SwiftSyntaxTestSupport

/// Linear inheritance chain: g0 -> g1 -> g2 -> ... -> gN.
/// Exercises the BFS but the queue never exceeds 1 element (each level has 1 sub-group).
private func makeChain(depth: Int) -> DiagnosticGroupInheritanceTree {
var subGroups: [DiagnosticGroupIdentifier: [DiagnosticGroupIdentifier]] = [:]
for i in 0..<depth {
subGroups[DiagnosticGroupIdentifier("g\(i)")] = [DiagnosticGroupIdentifier("g\(i + 1)")]
}
return try! DiagnosticGroupInheritanceTree(subGroups: subGroups)
}

/// Wide-and-shallow tree: one root with N direct sub-groups.
/// Exercises `Array.removeFirst()` shift cost on a large queue.
private func makeFan(width: Int) -> DiagnosticGroupInheritanceTree {
var children: [DiagnosticGroupIdentifier] = []
for i in 0..<width {
children.append(DiagnosticGroupIdentifier("g\(i)"))
}
return try! DiagnosticGroupInheritanceTree(
subGroups: [DiagnosticGroupIdentifier("root"): children]
)
}

/// Diamond DAG: each level has 2 nodes; both point to both nodes at the next level.
/// Exercises de-duplication. Without de-dup at dequeue, the queue grows as 2^depth,
/// so even small depths blow up.
private func makeDiamondDAG(depth: Int) -> DiagnosticGroupInheritanceTree {
var subGroups: [DiagnosticGroupIdentifier: [DiagnosticGroupIdentifier]] = [:]
func id(_ level: Int, _ side: Int) -> DiagnosticGroupIdentifier {
DiagnosticGroupIdentifier("l\(level)s\(side)")
}
for level in 0..<depth - 1 {
subGroups[id(level, 0)] = [id(level + 1, 0), id(level + 1, 1)]
subGroups[id(level, 1)] = [id(level + 1, 0), id(level + 1, 1)]
}
return try! DiagnosticGroupInheritanceTree(subGroups: subGroups)
}

final class WarningControlRegionsPerformanceTests: XCTestCase {

/// Calls the public entry point with `globalControls` so we exercise
/// `addWarningGroupControls` (which contains the BFS of interest).
private func buildTree(seed: String, inheritance: DiagnosticGroupInheritanceTree) {
let parsed = Parser.parse(source: "")
_ = parsed.warningGroupControlRegionTree(
configuredRegions: .empty,
globalControls: [(DiagnosticGroupIdentifier(seed), .error)],
groupInheritanceTree: inheritance
)
}

private func runAndReport(
label: String,
inheritance: DiagnosticGroupInheritanceTree,
seed: String,
iterations: Int
) {
for _ in 0..<3 {
buildTree(seed: seed, inheritance: inheritance)
}

let startInstructions = getInstructionsExecuted()
let t0 = Date()
for _ in 0..<iterations {
buildTree(seed: seed, inheritance: inheritance)
}
let wall = Date().timeIntervalSince(t0)
let endInstructions = getInstructionsExecuted()
let instructions = endInstructions - startInstructions

print(
"[BENCH] \(label) iters=\(iterations) wall=\(String(format: "%.3f", wall * 1000))ms "
+ "instructions=\(instructions) per-call=\(instructions / UInt64(iterations))"
)
}

func testWarningControlChainBenchmark() throws {
try XCTSkipIf(longTestsDisabled)
print("--- chain (linear inheritance; queue stays tiny so this is mostly a baseline) ---")
for depth in [50, 200, 800] {
let inh = makeChain(depth: depth)
runAndReport(label: "chain depth=\(depth)", inheritance: inh, seed: "g0", iterations: 20)
}
}

func testWarningControlFanBenchmark() throws {
try XCTSkipIf(longTestsDisabled)
print("--- fan (root has N direct sub-groups; exercises removeFirst() shift cost) ---")
for width in [50, 200, 800, 1600] {
let inh = makeFan(width: width)
runAndReport(label: "fan width=\(width)", inheritance: inh, seed: "root", iterations: 20)
}
}

func testWarningControlDiamondBenchmark() throws {
try XCTSkipIf(longTestsDisabled)
// Note: keep depths small. Without the de-dup fix the queue grows as 2^depth so
// even depth 10 takes too long for a unit-test run.
print("--- diamond DAG (each level has 2 nodes pointing to both of next level) ---")
for depth in [4, 6, 8] {
let inh = makeDiamondDAG(depth: depth)
runAndReport(label: "diamond depth=\(depth)", inheritance: inh, seed: "l0s0", iterations: 10)
}
}
}
25 changes: 25 additions & 0 deletions Tests/SwiftWarningControlTest/WarningControlTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,31 @@ public class WarningGroupControlTests: XCTestCase {
)
}

/// A sub-group that is reachable from multiple parents (a diamond-shaped
/// inheritance graph) must still have the control applied to it. Previously,
/// the BFS in `addWarningGroupControls` did not de-duplicate at dequeue, so
/// such a sub-group was enqueued (and re-processed) once per incoming path;
/// for deeper diamonds this caused the queue to grow exponentially.
func testDiamondSubGroupInheritance() throws {
try assertWarningGroupControl(
"""
@diagnose(SuperGroup, as: error)
func foo() {
1️⃣let x = 1
}
""",
groupInheritanceTree: DiagnosticGroupInheritanceTree(subGroups: [
"SuperGroup": ["ChildA", "ChildB"],
"ChildA": ["SharedGroup"],
"ChildB": ["SharedGroup"],
]),
diagnosticGroupID: "SharedGroup",
states: [
"1️⃣": .error
]
)
}

func testInheritanceTreeCycle() throws {
XCTAssertThrowsError(
try DiagnosticGroupInheritanceTree(subGroups: [
Expand Down