Skip to content
Merged
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
20 changes: 7 additions & 13 deletions Sources/FocusRelayCLI/BenchmarkListTasksCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct BenchmarkListTasks: AsyncParsableCommand {
var intervalMS: Int = 1500

@Option(name: .customLong("cooldown-ms"), help: "Cooldown delay after failed/timeout calls in milliseconds.")
var cooldownMS: Int = 3000
var cooldownMS: Int = 13_000

@Option(name: .customLong("completed-after"), help: "Fixed ISO8601 anchor for completed-date scenario.")
var completedAfter: String = "2020-01-01T00:00:00Z"
Expand Down Expand Up @@ -52,7 +52,7 @@ struct BenchmarkListTasks: AsyncParsableCommand {
if warmupCalls > 0 {
for index in 0..<warmupCalls {
let scenario = scenarios[benchmarkScenarioIndex(callIndex: index, scenarioCount: scenarios.count)]
let event = try await listTaskBenchCall(
_ = try await listTaskBenchCall(
scenario: scenario,
phase: "warmup",
service: service,
Expand All @@ -62,9 +62,6 @@ struct BenchmarkListTasks: AsyncParsableCommand {
callIndex: &callIndex,
rawURL: rawURL
)
if event.timeout {
await runBenchmarkTimeoutRecoveryGate()
}
}
}

Expand All @@ -89,9 +86,6 @@ struct BenchmarkListTasks: AsyncParsableCommand {
var stats = scenarioStats[event.scenario] ?? BenchmarkStats()
stats.ingest(ok: event.ok, timeout: event.timeout, latencyMs: event.latencyMs)
scenarioStats[event.scenario] = stats
if event.timeout {
await runBenchmarkTimeoutRecoveryGate()
}
}

let scenarioNames = scenarios.map(\.name)
Expand Down Expand Up @@ -203,12 +197,8 @@ private func listTaskBenchCall(
} catch {
let latencyMs = Date().timeIntervalSince(started) * 1000
let timeout = isBenchmarkTimeout(error)
try await enforceBenchmarkInterval(started: started, intervalMS: intervalMS)
if cooldownMS > 0 {
try? await Task.sleep(nanoseconds: UInt64(cooldownMS) * 1_000_000)
}
if timeout {
let diagnostic = await captureBenchmarkTimeoutDiagnostic(
let diagnostic = captureBenchmarkTimeoutDiagnostic(
scenario: scenario.name,
phase: phase,
callIndex: callIndex,
Expand All @@ -217,6 +207,10 @@ private func listTaskBenchCall(
)
try? appendBenchmarkJSONLine(diagnostic, to: timeoutDiagnosticsURL)
}
try await enforceBenchmarkInterval(started: started, intervalMS: intervalMS)
if cooldownMS > 0 {
try? await Task.sleep(nanoseconds: UInt64(cooldownMS) * 1_000_000)
}
let event = ListTaskEvent(
timestamp: benchmarkISO8601Now(),
phase: phase,
Expand Down
8 changes: 1 addition & 7 deletions Sources/FocusRelayCLI/BenchmarkProjectCountsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct BenchmarkProjectCounts: AsyncParsableCommand {
var intervalMS: Int = 1200

@Option(name: .customLong("cooldown-ms"), help: "Cooldown delay after failed/timeout calls in milliseconds.")
var cooldownMS: Int = 5000
var cooldownMS: Int = 13_000

@Option(name: .customLong("memory-interval-seconds"), help: "RSS sample interval in seconds.")
var memoryIntervalSeconds: Int = 30
Expand Down Expand Up @@ -78,9 +78,6 @@ struct BenchmarkProjectCounts: AsyncParsableCommand {
try await service.getProjectCounts(filter: scenario.filter)
}
try appendBenchmarkJSONLine(event, to: rawURL)
if event.timeout {
await runBenchmarkTimeoutRecoveryGate()
}
}
}

Expand Down Expand Up @@ -112,9 +109,6 @@ struct BenchmarkProjectCounts: AsyncParsableCommand {
overall: &overallStats,
scenarios: &scenarioStats
)
if event.timeout {
await runBenchmarkTimeoutRecoveryGate()
}
}

let summary = renderCountBenchmarkSummary(
Expand Down
76 changes: 23 additions & 53 deletions Sources/FocusRelayCLI/BenchmarkSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ struct BenchmarkTimeoutProcessSnapshot: Codable {
let rssKB: Int?
}

struct BenchmarkTimeoutBridgeHealthSnapshot: Codable {
let ok: Bool
let detail: String
}

struct BenchmarkTimeoutDiagnostic: Codable {
let timestamp: String
let transport: String
Expand All @@ -73,7 +68,6 @@ struct BenchmarkTimeoutDiagnostic: Codable {
let queue: BenchmarkTimeoutQueueSnapshot?
let omniFocus: BenchmarkTimeoutProcessSnapshot
let focusrelay: BenchmarkTimeoutProcessSnapshot
let bridgeHealth: BenchmarkTimeoutBridgeHealthSnapshot?
}

func validateBenchmarkArguments(
Expand Down Expand Up @@ -130,12 +124,8 @@ func runCountBenchmarkCall<Counts: Codable>(
} catch {
let latencyMs = Date().timeIntervalSince(started) * 1000
let timeout = isBenchmarkTimeout(error)
try await enforceBenchmarkInterval(started: started, intervalMS: intervalMS)
if cooldownMS > 0 {
try? await Task.sleep(nanoseconds: UInt64(cooldownMS) * 1_000_000)
}
if timeout {
let diagnostic = await captureBenchmarkTimeoutDiagnostic(
let diagnostic = captureBenchmarkTimeoutDiagnostic(
scenario: scenario,
phase: phase,
callIndex: callIndex,
Expand All @@ -144,6 +134,10 @@ func runCountBenchmarkCall<Counts: Codable>(
)
try? appendBenchmarkJSONLine(diagnostic, to: timeoutDiagnosticsURL)
}
try await enforceBenchmarkInterval(started: started, intervalMS: intervalMS)
if cooldownMS > 0 {
try? await Task.sleep(nanoseconds: UInt64(cooldownMS) * 1_000_000)
}
return CountBenchmarkEvent(
timestamp: benchmarkISO8601Now(),
phase: phase,
Expand Down Expand Up @@ -240,15 +234,6 @@ func startBenchmarkMemorySampling(memoryURL: URL, intervalSeconds: Int) -> Task<
}
}

func runBenchmarkTimeoutRecoveryGate() async {
let environment = ProcessInfo.processInfo.environment
let recoveryMS = max(0, environment["FOCUS_RELAY_TIMEOUT_RECOVERY_MS"].flatMap(Int.init) ?? 10_000)
if recoveryMS > 0 {
try? await Task.sleep(nanoseconds: UInt64(recoveryMS) * 1_000_000)
}
_ = try? await OmniFocusBridgeService().healthCheck()
}

func enforceBenchmarkInterval(started: Date, intervalMS: Int) async throws {
guard intervalMS > 0 else { return }
let elapsed = Date().timeIntervalSince(started)
Expand Down Expand Up @@ -379,11 +364,12 @@ func captureBenchmarkTimeoutDiagnostic(
callIndex: Int,
latencyMs: Double,
errorMessage: String
) async -> BenchmarkTimeoutDiagnostic {
) -> BenchmarkTimeoutDiagnostic {
let timestamp = benchmarkISO8601Now()
let requestID = extractBenchmarkRequestID(from: errorMessage)
guard let baseURL = try? IPCEnvironment.defaultBaseURL() else {
return BenchmarkTimeoutDiagnostic(
timestamp: benchmarkISO8601Now(),
timestamp: timestamp,
transport: benchmarkTransport,
scenario: scenario,
phase: phase,
Expand All @@ -397,52 +383,37 @@ func captureBenchmarkTimeoutDiagnostic(
process: "focusrelay",
pid: ProcessInfo.processInfo.processIdentifier,
rssKB: nil
),
bridgeHealth: BenchmarkTimeoutBridgeHealthSnapshot(
ok: false,
detail: "OmniFocus 4 container not found; IPC diagnostics unavailable"
)
)
}
let requestsURL = baseURL.appendingPathComponent("requests", isDirectory: true)
let locksURL = baseURL.appendingPathComponent("locks", isDirectory: true)
let responsesURL = baseURL.appendingPathComponent("responses", isDirectory: true)
let queue = BenchmarkTimeoutQueueSnapshot(
basePath: baseURL.path,
requestsCount: benchmarkDirectoryEntryCount(requestsURL),
locksCount: benchmarkDirectoryEntryCount(locksURL),
responsesCount: benchmarkDirectoryEntryCount(responsesURL),
requestExists: requestID.map { FileManager.default.fileExists(atPath: requestsURL.appendingPathComponent("\($0).json").path) },
lockExists: requestID.map { FileManager.default.fileExists(atPath: locksURL.appendingPathComponent("\($0).lock").path) },
responseExists: requestID.map { FileManager.default.fileExists(atPath: responsesURL.appendingPathComponent("\($0).json").path) },
sampleRequests: benchmarkDirectoryEntrySamples(requestsURL),
sampleLocks: benchmarkDirectoryEntrySamples(locksURL),
sampleResponses: benchmarkDirectoryEntrySamples(responsesURL)
)
let omniPID = currentOmniFocusPID()
let benchmarkPID = ProcessInfo.processInfo.processIdentifier
let bridgeHealth: BenchmarkTimeoutBridgeHealthSnapshot
if let result = try? await OmniFocusBridgeService().healthCheck() {
bridgeHealth = BenchmarkTimeoutBridgeHealthSnapshot(
ok: result.ok,
detail: "plugin=\(result.plugin ?? "unknown") version=\(result.version ?? "unknown")"
)
} else {
bridgeHealth = BenchmarkTimeoutBridgeHealthSnapshot(
ok: false,
detail: "bridge-health-check failed after timeout"
)
}

return BenchmarkTimeoutDiagnostic(
timestamp: benchmarkISO8601Now(),
timestamp: timestamp,
transport: benchmarkTransport,
scenario: scenario,
phase: phase,
callIndex: callIndex,
latencyMs: latencyMs,
error: errorMessage,
requestId: requestID,
queue: BenchmarkTimeoutQueueSnapshot(
basePath: baseURL.path,
requestsCount: benchmarkDirectoryEntryCount(requestsURL),
locksCount: benchmarkDirectoryEntryCount(locksURL),
responsesCount: benchmarkDirectoryEntryCount(responsesURL),
requestExists: requestID.map { FileManager.default.fileExists(atPath: requestsURL.appendingPathComponent("\($0).json").path) },
lockExists: requestID.map { FileManager.default.fileExists(atPath: locksURL.appendingPathComponent("\($0).lock").path) },
responseExists: requestID.map { FileManager.default.fileExists(atPath: responsesURL.appendingPathComponent("\($0).json").path) },
sampleRequests: benchmarkDirectoryEntrySamples(requestsURL),
sampleLocks: benchmarkDirectoryEntrySamples(locksURL),
sampleResponses: benchmarkDirectoryEntrySamples(responsesURL)
),
queue: queue,
omniFocus: BenchmarkTimeoutProcessSnapshot(
process: "OmniFocus",
pid: omniPID,
Expand All @@ -452,8 +423,7 @@ func captureBenchmarkTimeoutDiagnostic(
process: "focusrelay",
pid: benchmarkPID,
rssKB: benchmarkRSSKilobytes(pid: benchmarkPID)
),
bridgeHealth: bridgeHealth
)
)
}

Expand Down
8 changes: 1 addition & 7 deletions Sources/FocusRelayCLI/BenchmarkTaskCountsCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct BenchmarkTaskCounts: AsyncParsableCommand {
var intervalMS: Int = 1200

@Option(name: .customLong("cooldown-ms"), help: "Cooldown delay after failed/timeout calls in milliseconds.")
var cooldownMS: Int = 5000
var cooldownMS: Int = 13_000

@Option(name: .customLong("memory-interval-seconds"), help: "RSS sample interval in seconds.")
var memoryIntervalSeconds: Int = 30
Expand Down Expand Up @@ -78,9 +78,6 @@ struct BenchmarkTaskCounts: AsyncParsableCommand {
try await service.getTaskCounts(filter: scenario.filter)
}
try appendBenchmarkJSONLine(event, to: rawURL)
if event.timeout {
await runBenchmarkTimeoutRecoveryGate()
}
}
}

Expand Down Expand Up @@ -112,9 +109,6 @@ struct BenchmarkTaskCounts: AsyncParsableCommand {
overall: &overallStats,
scenarios: &scenarioStats
)
if event.timeout {
await runBenchmarkTimeoutRecoveryGate()
}
}

let summary = renderCountBenchmarkSummary(
Expand Down
37 changes: 37 additions & 0 deletions Tests/FocusRelayCLITests/FocusRelayCLITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,43 @@ func countBenchmarkArtifactKeepsHistoricalKeys() throws {
#expect(object["transport"] as? String == "plugin")
}

@Test
func timeoutDiagnosticArtifactContainsPassiveQueueAndProcessStateOnly() throws {
let diagnostic = BenchmarkTimeoutDiagnostic(
timestamp: "2026-08-18T00:00:00.000Z",
transport: benchmarkTransport,
scenario: "default",
phase: "measured",
callIndex: 7,
latencyMs: 45_000,
error: "requestId=ABC-123 timed out",
requestId: "ABC-123",
queue: BenchmarkTimeoutQueueSnapshot(
basePath: "/tmp/focusrelay",
requestsCount: 1,
locksCount: 1,
responsesCount: 0,
requestExists: true,
lockExists: true,
responseExists: false,
sampleRequests: ["ABC-123.json"],
sampleLocks: ["ABC-123.lock"],
sampleResponses: []
),
omniFocus: BenchmarkTimeoutProcessSnapshot(process: "OmniFocus", pid: 42, rssKB: 1_024),
focusrelay: BenchmarkTimeoutProcessSnapshot(process: "focusrelay", pid: 43, rssKB: 512)
)
let object = try #require(JSONSerialization.jsonObject(with: JSONEncoder().encode(diagnostic)) as? [String: Any])
let queue = try #require(object["queue"] as? [String: Any])

#expect(queue["requestExists"] as? Bool == true)
#expect(queue["lockExists"] as? Bool == true)
#expect(queue["responseExists"] as? Bool == false)
#expect(object["omniFocus"] != nil)
#expect(object["focusrelay"] != nil)
#expect(object["bridgeHealth"] == nil)
}

@Test
func listBenchmarkArtifactKeepsHistoricalKeys() throws {
let event = ListTaskEvent(
Expand Down
6 changes: 3 additions & 3 deletions scripts/benchmark-suite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,21 @@ case "$PROFILE" in
: "${TOTAL_HOURS:=0.5}"
: "${WARMUP_CALLS:=10}"
: "${INTERVAL_MS:=2000}"
: "${COOLDOWN_MS:=3000}"
: "${COOLDOWN_MS:=13000}"
: "${MEMORY_INTERVAL_SECONDS:=60}"
;;
release)
: "${TOTAL_HOURS:=1.5}"
: "${WARMUP_CALLS:=10}"
: "${INTERVAL_MS:=5000}"
: "${COOLDOWN_MS:=5000}"
: "${COOLDOWN_MS:=15000}"
: "${MEMORY_INTERVAL_SECONDS:=60}"
;;
stress)
: "${TOTAL_HOURS:=3}"
: "${WARMUP_CALLS:=20}"
: "${INTERVAL_MS:=1500}"
: "${COOLDOWN_MS:=3000}"
: "${COOLDOWN_MS:=13000}"
: "${MEMORY_INTERVAL_SECONDS:=30}"
;;
*)
Expand Down
Loading