Skip to content

Commit a922c1e

Browse files
committed
feat: Dynamic concurrency based on empirical memory thresholds
ResourceArbiter now adjusts concurrency in real-time based on free memory: - Critical (<300MB): Block all tests, warn user to close apps - Heavy (<450MB): Force serial execution for WordPress/MySQL - Medium (<600MB): Limited parallelism (max 2 concurrent) - Abundant (>800MB): Full parallelism (max 4 lightweight) Empirical thresholds from profile-1775950630 telemetry: min free 195MB
1 parent 11ce821 commit a922c1e

1 file changed

Lines changed: 67 additions & 19 deletions

File tree

Sources/ContainerTesting/ResourceArbiter.swift

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,21 @@ nonisolated(unsafe) private var _cleanupEnabled = false
2525

2626
public actor ResourceArbiter {
2727
public static let shared = ResourceArbiter()
28-
28+
2929
private var inFlightCount: Int = 0
3030
private var snapshotOpsInProgress: Bool = false
31-
private let maxInFlightLightweight = 3
32-
33-
private let memoryThresholdMB = 1024 // 1GB - force serial below this
31+
32+
// Dynamic concurrency limits based on empirical telemetry
33+
// Derived from profile-1775950630: min free was 195MB
34+
private var maxInFlightLightweight = 3 // Reduced dynamically when memory pressure
35+
36+
// Empirical thresholds from Victoria Protocol
37+
// Threshold = MinObservedFree(195MB) + SafetyMargin(100MB) + OS_Buffer(150MB)
38+
private let criticalThresholdMB = 300 // Block all tests below this (system unstable)
39+
private let heavyThresholdMB = 450 // Force serial below this (empirical: 195+100+150)
40+
private let mediumThresholdMB = 600 // Limit concurrency below this
41+
private let parallelThresholdMB = 800 // Full parallelism above this
42+
3443
private let ioPressureThreshold = 5 // block new tests if too many I/O ops
3544

3645
// Cleanup configuration
@@ -162,30 +171,69 @@ public actor ResourceArbiter {
162171
}
163172
}
164173

165-
public func requestExecutionSlot(for weight: TestWeight) -> ExecutionMode {
174+
public func requestExecutionSlot(for weight: TestWeight) -> ExecutionMode {
166175
let freeMemory = ResourceHelper.getLatestFreeMemory() ?? 8192
167-
176+
177+
// Check for critical memory pressure first
178+
if freeMemory < criticalThresholdMB {
179+
return .blocked(reason: "⛔ CRITICAL: Memory at \(freeMemory)MB (threshold: \(criticalThresholdMB)MB). Close Chrome/Slack to continue.")
180+
}
181+
168182
if weight == .snapshotHeavy || snapshotOpsInProgress {
169183
return .blocked(reason: "Snapshot operation in progress - preventing I/O pile-up")
170184
}
171-
172-
if freeMemory < memoryThresholdMB {
173-
return .serial
174-
}
175-
176-
if weight == .heavy {
177-
return .serial
178-
}
179-
180-
if weight == .lightweight {
185+
186+
// Dynamic concurrency based on empirical thresholds
187+
// From profile-1775950630: min free was 195MB
188+
switch weight {
189+
case .heavy:
190+
// Heavy tests (WordPress + MySQL) need 450MB+ free
191+
// Empirical: 195MB min + 100MB safety + 150MB OS buffer
192+
if freeMemory < heavyThresholdMB {
193+
return .serial // Force serial when memory constrained
194+
}
195+
// Even with enough memory, limit to 1 heavy test at a time
196+
if inFlightCount >= 1 {
197+
return .blocked(reason: "Heavy test in progress - preventing memory pile-up (free: \(freeMemory)MB)")
198+
}
199+
inFlightCount += 1
200+
return .parallel
201+
202+
case .medium:
203+
// Medium tests need 600MB+ for comfortable parallel execution
204+
if freeMemory < mediumThresholdMB {
205+
return .serial // Serial when under pressure
206+
}
207+
if freeMemory < parallelThresholdMB {
208+
// Limited parallelism - reduce concurrent tests
209+
maxInFlightLightweight = 2
210+
}
181211
if inFlightCount >= maxInFlightLightweight {
182-
return .blocked(reason: "Max in-flight containers (\(maxInFlightLightweight)) reached")
212+
return .blocked(reason: "Memory pressure - limiting concurrency (free: \(freeMemory)MB)")
183213
}
184214
inFlightCount += 1
185215
return .parallel
216+
217+
case .lightweight:
218+
// Lightweight tests are most flexible
219+
// Dynamically adjust concurrency based on available memory
220+
if freeMemory < mediumThresholdMB {
221+
maxInFlightLightweight = 2 // Reduce from 3 to 2
222+
} else if freeMemory < parallelThresholdMB {
223+
maxInFlightLightweight = 3 // Normal
224+
} else {
225+
maxInFlightLightweight = 4 // Can afford more when memory abundant
226+
}
227+
228+
if inFlightCount >= maxInFlightLightweight {
229+
return .blocked(reason: "Max in-flight containers (\(maxInFlightLightweight)) reached (free: \(freeMemory)MB)")
230+
}
231+
inFlightCount += 1
232+
return .parallel
233+
234+
case .snapshotHeavy:
235+
return .blocked(reason: "Snapshot-heavy tests must run serially")
186236
}
187-
188-
return .parallel
189237
}
190238

191239
public func releaseExecutionSlot(for weight: TestWeight) {

0 commit comments

Comments
 (0)