Files
Termind/TFMTests/TranscriptPerformanceTests.swift
T
Kaede AkinoandClaude Code c43e5bc311 fix(assistant): preserve transcript continuity across run transitions
Separate interaction refreshes from layout invalidation on macOS and iOS. Retain active editors across edit-eligibility changes and cover hosting identity, input composition, navigation controls, and model-driven streaming replay.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-18 14:15:04 +08:00

277 lines
11 KiB
Swift

import Darwin
import Foundation
import TermindApplication
import TermindPresentation
import Testing
@testable import Termind
@MainActor
@Suite(.serialized)
struct TranscriptPerformanceTests {
@Test
func correctRendererScrollAndStreamingBenchmark() {
let coordinator = TranscriptListView.Coordinator()
let listView = coordinator.makeListView()
listView.frame = CGRect(x: 0, y: 0, width: 420, height: 640)
var items = makeItems(count: 300)
let footprintBefore = physicalFootprint()
let initial = elapsed {
update(coordinator, listView, items: items)
listView.layoutSubtreeIfNeeded()
}
let maximum = max(0, listView.maximumContentOffset.y)
let offsets = stride(from: CGFloat.zero, through: maximum, by: 180).map { $0 }
let passes =
Int(
ProcessInfo.processInfo.environment["TRANSCRIPT_BENCHMARK_PASSES"] ?? "3"
) ?? 3
let scroll = elapsed {
for _ in 0..<passes {
for offset in offsets {
listView.setContentOffset(CGPoint(x: 0, y: offset), animated: false)
listView.layoutSubtreeIfNeeded()
}
for offset in offsets.reversed() {
listView.setContentOffset(CGPoint(x: 0, y: offset), animated: false)
listView.layoutSubtreeIfNeeded()
}
}
}
let lastID = items[items.count - 1].id
let stream = elapsed {
// Twenty synchronous presentation updates, not twenty displayed frames.
for revision in 0..<20 {
let message = ChatMessage(
id: lastID,
role: .assistant,
text: Self.markdown(index: 299)
+ String(repeating: " token", count: revision + 1),
isStreaming: true
)
items[items.count - 1] = TranscriptEntry(
id: lastID,
content: .assistant(message, canFork: false),
hasLeadingSpacing: true
)
update(coordinator, listView, items: items)
listView.layoutSubtreeIfNeeded()
}
}
let footprintAfter = physicalFootprint()
let footprintDelta =
footprintAfter >= footprintBefore ? footprintAfter - footprintBefore : 0
print(
String(
format:
"TRANSCRIPT_BENCHMARK renderer=correct initial_ms=%.1f scroll_ms=%.1f stream_ms=%.1f footprint_delta_mb=%.1f mounted_rows=%d",
initial * 1_000,
scroll * 1_000,
stream * 1_000,
Double(footprintDelta) / 1_048_576,
listView.visibleRowViews.count
))
#expect(listView.visibleRowViews.count < 40)
#expect(scroll < 30)
#expect(stream < 5)
}
@Test(arguments: [false, true])
func modelDrivenStreamingReplayReportsUpdateCosts(readingHistory: Bool) async throws {
let coordinator = TranscriptListView.Coordinator()
let listView = coordinator.makeListView()
listView.frame = CGRect(x: 0, y: 0, width: 420, height: 640)
let presentation = TranscriptPresentationState()
let messages = (0..<300).map { ChatMessage(role: .assistant, text: Self.markdown(index: $0)) }
let conversation = ConversationModel(messages: messages)
let lastID = try #require(messages.last?.id)
func applyConversation() -> (projection: TimeInterval, rendering: TimeInterval) {
var entries: [TranscriptEntry] = []
let projection = elapsed {
let availability = conversation.messageActionAvailability
entries = TranscriptEntry.items(
timeline: conversation.timeline,
isInterrupted: conversation.runState == .interrupted,
canEdit: availability.editable.contains,
canFork: availability.forkable.contains
)
}
let rendering = elapsed {
coordinator.update(
listView: listView, entries: entries, latestUserMessageID: nil,
configuration: .init(
isCompact: true, showsTerminalActions: false,
isConversationStreaming: conversation.isStreaming,
locale: Locale(identifier: "en"), reduceMotion: false, colorScheme: .light
),
actions: .init(), presentation: presentation
)
listView.layoutSubtreeIfNeeded()
}
return (projection, rendering)
}
_ = applyConversation()
if readingHistory {
listView.setContentOffset(CGPoint(x: 0, y: 180), animated: false)
listView.layoutSubtreeIfNeeded()
}
let anchorIndex = try #require(listView.indicesForVisibleRows.first)
let anchorID = listView.content[anchorIndex].id
let anchor = try #require(listView.rowView(for: anchorID))
let anchorOffset = listView.rectForRow(with: anchorID).minY - listView.contentOffset.y
var projectionSamples: [TimeInterval] = []
var renderSamples: [TimeInterval] = []
var eventSamples: [TimeInterval] = []
var maximumAnchorDrift: CGFloat = 0
var anchorReplacements = 0
var previousAnchor = anchor
conversation.handle(.runState(.running))
for revision in 0..<20 {
eventSamples.append(
elapsed {
conversation.handle(.messageDelta(messageID: lastID, text: "\n\nReplay paragraph \(revision)."))
})
let sample = applyConversation()
projectionSamples.append(sample.projection)
renderSamples.append(sample.rendering)
// Give deferred layout work a turn; this remains an offscreen replay,
// not a measurement of display refresh or animation hitch rate.
await withCheckedContinuation { continuation in
DispatchQueue.main.async { continuation.resume() }
}
listView.layoutSubtreeIfNeeded()
if readingHistory {
let current = try #require(listView.rowView(for: anchorID))
if current !== previousAnchor { anchorReplacements += 1 }
previousAnchor = current
maximumAnchorDrift = max(
maximumAnchorDrift,
abs(listView.rectForRow(with: anchorID).minY - listView.contentOffset.y - anchorOffset)
)
}
}
conversation.handle(.runState(.completed))
_ = applyConversation()
if readingHistory {
let current = try #require(listView.rowView(for: anchorID))
if current !== previousAnchor { anchorReplacements += 1 }
maximumAnchorDrift = max(
maximumAnchorDrift,
abs(listView.rectForRow(with: anchorID).minY - listView.contentOffset.y - anchorOffset)
)
#expect(current === anchor)
#expect(maximumAnchorDrift <= 1)
}
let last = try #require(listView.content.first(where: { $0.id == lastID }))
guard case .assistant(let message, _) = last.content else {
Issue.record("Expected the streamed assistant message")
return
}
#expect(message.text.hasSuffix("Replay paragraph 19."))
#expect(!message.isStreaming)
print(
String(
format:
"TRANSCRIPT_MODEL_REPLAY history=%@ event_p95_ms=%.2f projection_p95_ms=%.2f render_p50_ms=%.2f render_p95_ms=%.2f render_max_ms=%.2f anchor_drift=%.2f anchor_replacements=%d",
readingHistory.description,
percentile(eventSamples, fraction: 0.95) * 1_000,
percentile(projectionSamples, fraction: 0.95) * 1_000,
percentile(renderSamples, fraction: 0.5) * 1_000,
percentile(renderSamples, fraction: 0.95) * 1_000,
(renderSamples.max() ?? 0) * 1_000,
Double(maximumAnchorDrift), anchorReplacements
))
}
private func percentile(_ samples: [TimeInterval], fraction: Double) -> TimeInterval {
let sorted = samples.sorted()
guard !sorted.isEmpty else { return 0 }
return sorted[min(sorted.count - 1, max(0, Int(ceil(Double(sorted.count) * fraction)) - 1))]
}
private func makeItems(count: Int) -> [TranscriptEntry] {
(0..<count).map { index in
let id = UUID()
return TranscriptEntry(
id: id,
content: .assistant(
ChatMessage(
id: id,
role: .assistant,
text: Self.markdown(index: index)
),
canFork: false
),
hasLeadingSpacing: index > 0
)
}
}
private static func markdown(index: Int) -> String {
"""
## Result \(index)
This is a representative assistant response with **emphasis**, a [safe link](https://example.com), and enough prose to wrap across several lines in the compact transcript.
- first item
- second item
- third item
```swift
let value = \(index)
print(value)
```
"""
}
private func update(
_ coordinator: TranscriptListView.Coordinator,
_ listView: TranscriptListView.NativeListView,
items: [TranscriptEntry]
) {
coordinator.update(
listView: listView,
entries: items,
latestUserMessageID: nil,
configuration: .init(
isCompact: true,
showsTerminalActions: false,
isConversationStreaming: false,
locale: Locale(identifier: "en"),
reduceMotion: true,
colorScheme: .light
),
actions: .init(),
presentation: TranscriptPresentationState()
)
}
private func elapsed(_ operation: () -> Void) -> TimeInterval {
let start = ContinuousClock.now
operation()
let duration = start.duration(to: .now).components
return TimeInterval(duration.seconds) + TimeInterval(duration.attoseconds) / 1e18
}
private func physicalFootprint() -> UInt64 {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(
MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size
)
let result = withUnsafeMutablePointer(to: &info) { pointer in
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
return result == KERN_SUCCESS ? info.phys_footprint : 0
}
}