Files
OSGKeyboard/OSGKeyboardTests/VoicePipelinePerformanceTests.swift
Rocky 498f407585 feat(account): add managed credits and cloud gateway
Introduce optional Apple account-backed credits with scoped gateway access while preserving local and BYOK paths. Refresh assistant behavior, tests, privacy disclosures, docs, and the website for the 2.0 experience.
2026-08-20 11:43:21 +08:00

152 lines
6.0 KiB
Swift

// VoicePipelinePerformanceTests.swift
// OSGKeyboardTests
//
// Hermetic performance coverage for voice → ASR → guard → polish → bridge.
// Stub ASR/LLM + synthetic PCM (no mic / no live network).
// Run: ./Scripts/run-tests.sh perf
@testable import OSGKeyboardHostSupport
@testable import OSGKeyboardShared
import XCTest
final class VoicePipelinePerformanceTests: XCTestCase {
/// Generous CI ceilings for stubbed work (not device mic/ASR SLAs).
private enum SLA {
static let pcmFeed: TimeInterval = 0.050
static let chunkASRIdle: TimeInterval = 0.750
static let transcriptGuard: TimeInterval = 0.020
static let polishIdle: TimeInterval = 0.750
static let bridgeDeliver: TimeInterval = 0.100
static let totalE2EIdle: TimeInterval = 1.500
/// Injected 40ms ASR delay must land in chunk_asr (±25ms).
static let injectedDelayTolerance: TimeInterval = 0.025
}
func testEndToEndStageTimingsUnderStubSLA() async throws {
let timings = try await VoicePipelinePerfHarness.run()
attachReport(timings, name: "e2e-idle")
XCTAssertFalse(timings.asrText.isEmpty)
XCTAssertEqual(timings.polishedText, "今天部署已经全部完成。")
XCTAssertEqual(timings.deliveredText, timings.polishedText)
XCTAssertLessThan(timings.pcmFeedSeconds, SLA.pcmFeed, "pcm_feed")
XCTAssertLessThan(timings.chunkASRSeconds, SLA.chunkASRIdle, "chunk_asr")
XCTAssertLessThan(timings.transcriptGuardSeconds, SLA.transcriptGuard, "transcript_guard")
XCTAssertLessThan(timings.polishSeconds, SLA.polishIdle, "polish")
XCTAssertLessThan(timings.bridgeDeliverSeconds, SLA.bridgeDeliver, "bridge_deliver")
XCTAssertLessThan(timings.totalSeconds, SLA.totalE2EIdle, "total_e2e")
// Stage sum (excluding total) should not exceed total by much.
let staged =
timings.pcmFeedSeconds
+ timings.chunkASRSeconds
+ timings.transcriptGuardSeconds
+ timings.batchFallbackSeconds
+ timings.polishSeconds
+ timings.bridgeDeliverSeconds
XCTAssertLessThanOrEqual(staged, timings.totalSeconds + 0.005)
}
func testChunkASRStageReflectsInjectedASRDelay() async throws {
let delayNs: UInt64 = 40_000_000 // 40 ms
let timings = try await VoicePipelinePerfHarness.run(
config: .init(asrDelayNanoseconds: delayNs)
)
attachReport(timings, name: "asr-delay-40ms")
let expected = Double(delayNs) / 1e9
XCTAssertGreaterThan(
timings.chunkASRSeconds,
expected - SLA.injectedDelayTolerance,
"chunk_asr should include injected ASR delay"
)
XCTAssertGreaterThan(
timings.chunkASRSeconds,
timings.polishSeconds,
"with ASR delay, chunk_asr should dominate polish"
)
}
func testPolishStageReflectsInjectedLLMDelay() async throws {
let delayNs: UInt64 = 50_000_000 // 50 ms
let timings = try await VoicePipelinePerfHarness.run(
config: .init(polishDelayNanoseconds: delayNs)
)
attachReport(timings, name: "polish-delay-50ms")
let expected = Double(delayNs) / 1e9
XCTAssertGreaterThan(
timings.polishSeconds,
expected - SLA.injectedDelayTolerance,
"polish should include injected LLM delay"
)
}
func testBatchFallbackStageAppearsWhenPartialWins() async throws {
let longPartial = String(repeating: "部", count: 40)
let timings = try await VoicePipelinePerfHarness.run(
config: .init(
asrTranscript: "短",
polishedTranscript: longPartial,
partialSnapshotOverride: longPartial,
batchTranscript: longPartial + "批",
runBatchFallbackIfNeeded: true
)
)
attachReport(timings, name: "batch-fallback")
XCTAssertTrue(timings.didRunBatchFallback)
XCTAssertGreaterThan(timings.batchFallbackSeconds, 0)
XCTAssertFalse(timings.deliveredText.isEmpty)
}
func testIndividualStagesRemainMeasurableInIsolation() async throws {
// Chunker + stub ASR only (no polish/bridge) — smoke that pcm→text stays fast.
let samples = SyntheticPCM.tone(durationSeconds: 0.2, sampleRate: 1_000)
let stream = SyntheticPCM.stream(samples: samples, sampleRate: 1_000)
let asr = TimedStubChunkASR(transcript: "隔离ASR")
let pipeline = ChunkedUtterancePipeline(
asr: asr,
locale: Locale(identifier: "zh-Hans"),
config: FlowUtteranceChunkConfig(
maxChunkDurationSeconds: 0.05,
overlapDurationSeconds: 0,
pauseExtensionMaxSeconds: 0,
pauseRMSThreshold: 1.0,
minFinalChunkDurationSeconds: 0.05,
sampleRate: 1_000
)
)
let start = ContinuousClock.now
let outcome = await pipeline.transcribe(stream: stream) { _ in }
let elapsed = elapsedSeconds(since: start)
guard case .success(let success) = outcome else {
return XCTFail("expected ASR success")
}
XCTAssertTrue(success.text.contains("隔离ASR"))
XCTAssertLessThan(elapsed, SLA.chunkASRIdle)
}
// MARK: - Helpers
private func attachReport(_ timings: VoicePipelineStageTimings, name: String) {
let text = timings.reportText()
// Surfaces in the test report navigator / xcresult.
let attachment = XCTAttachment(string: text)
attachment.name = "voice-pipeline-\(name)"
attachment.lifetime = .keepAlways
add(attachment)
print(text)
}
private func elapsedSeconds(since start: ContinuousClock.Instant) -> TimeInterval {
let duration = start.duration(to: .now)
return Double(duration.components.seconds)
+ Double(duration.components.attoseconds) / 1e18
}
}