Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

329 lines
16 KiB
Swift

// Phase 3 — Performance.
//
// One tab for every "how the engine runs" knob. Three sections:
// • Scheduler — max_concurrent_requests (moved from ServerScreen for
// scheduler coherence), embedding_batch_size, and chunked_prefill.
// • Memory & Lifecycle — prefill memory guard tier, server-wide idle
// timeout, model fallback routing.
// • Cache — master enable toggle gates a hot-cache toggle + size, a
// cold-cache directory + size, and an advanced initial-blocks tuning
// knob (requires restart).
//
// All fields are server-side already (`omlx/admin/routes.py:198-235`)
// — Phase 3 is pure UI. Single Apply button at the bottom, Storage /
// Network pattern: disabled until at least one trimmed draft diverges
// from its loaded value, and only changed fields are sent in the PATCH
// so out-of-band edits to siblings stay intact.
import SwiftUI
struct PerformanceScreen: View {
@Environment(AppServices.self) private var services
@State private var vm = PerformanceScreenVM()
var body: some View {
VStack(alignment: .leading, spacing: 0) {
SchedulerSection(vm: vm)
MemoryLifecycleSection(vm: vm)
CacheSection(vm: vm)
HStack {
Spacer()
Button(String(localized: "performance.button.apply",
defaultValue: "Apply",
comment: "Apply button at the bottom of the Performance screen")) {
Task { await vm.save(client: services.client) }
}
.buttonStyle(.omlx(.primary))
.disabled(!vm.hasPendingChanges || vm.isSaving)
}
.padding(.horizontal, 18)
.padding(.top, 6)
if let error = vm.lastError {
Text(error)
.font(.omlxText(11))
.foregroundStyle(.red)
.padding(.horizontal, 18)
.padding(.top, 8)
}
}
.task { await vm.load(client: services.client) }
}
}
// MARK: - Scheduler
private struct SchedulerSection: View {
@Bindable var vm: PerformanceScreenVM
var body: some View {
SectionHeader(
String(localized: "performance.section.scheduler",
defaultValue: "Scheduler",
comment: "Section header for the scheduler rows"),
subtitle: String(localized: "performance.section.scheduler.sub",
defaultValue: "How many requests run at once and how the engine batches them.",
comment: "Subtitle for the Scheduler section")
)
ListGroup {
Row(
label: String(localized: "performance.scheduler.max_concurrent",
defaultValue: "Max Concurrent Requests",
comment: "Row label for max concurrent requests"),
sublabel: String(localized: "performance.scheduler.max_concurrent.sub",
defaultValue: "Cap on simultaneous /v1 requests.",
comment: "Sublabel for max concurrent requests")
) {
TextInput(text: $vm.maxConcurrentText, mono: true, width: 90)
}
Row(
label: String(localized: "performance.scheduler.embedding_batch_size",
defaultValue: "Embedding Batch Size",
comment: "Row label for embedding batch size"),
sublabel: String(localized: "performance.scheduler.embedding_batch_size.sub",
defaultValue: "Max input texts per embedding forward pass.",
comment: "Sublabel for embedding batch size")
) {
TextInput(text: $vm.embeddingBatchSizeText, mono: true, width: 90)
}
Row(
label: String(localized: "performance.scheduler.chunked_prefill",
defaultValue: "Chunked Prefill",
comment: "Row label for chunked prefill toggle"),
sublabel: String(localized: "performance.scheduler.chunked_prefill.sub",
defaultValue: "Split long prompts across scheduler ticks so other requests can interleave.",
comment: "Sublabel for chunked prefill toggle"),
isLast: true
) {
Toggle("", isOn: $vm.chunkedPrefill)
.labelsHidden().toggleStyle(.switch)
}
}
}
}
// MARK: - Memory & Lifecycle
private struct MemoryLifecycleSection: View {
@Bindable var vm: PerformanceScreenVM
var body: some View {
SectionHeader(
String(localized: "performance.section.memory",
defaultValue: "Memory & Lifecycle",
comment: "Section header for memory and lifecycle settings"),
subtitle: String(localized: "performance.section.memory.sub",
defaultValue: "Memory admission control and auto-unload behavior.",
comment: "Subtitle explaining memory and lifecycle settings")
)
ListGroup {
Row(
label: String(localized: "performance.memory.prefill_guard",
defaultValue: "Prefill Memory Guard",
comment: "Row label for prefill memory guard toggle"),
sublabel: String(localized: "performance.memory.prefill_guard.sub",
defaultValue: "Preflight prefill memory before kicking the engine and defer generation scheduling near the ceiling.",
comment: "Sublabel for prefill memory guard")
) {
Toggle("", isOn: $vm.prefillMemoryGuard)
.labelsHidden().toggleStyle(.switch)
}
Row(
label: String(localized: "performance.memory.guard_tier",
defaultValue: "Memory Guard Tier",
comment: "Row label for memory guard tier popup"),
sublabel: vm.memoryGuardTierDescription
) {
Popup(
selection: $vm.memoryGuardTier,
width: 150,
options: [
("safe",
String(localized: "performance.memory.guard_tier.safe",
defaultValue: "Safe",
comment: "Memory guard tier option: safe")),
("balanced",
String(localized: "performance.memory.guard_tier.balanced",
defaultValue: "Balanced",
comment: "Memory guard tier option: balanced")),
("aggressive",
String(localized: "performance.memory.guard_tier.aggressive",
defaultValue: "Aggressive",
comment: "Memory guard tier option: aggressive")),
("custom",
String(localized: "performance.memory.guard_tier.custom",
defaultValue: "Custom",
comment: "Memory guard tier option: custom")),
]
)
.disabled(!vm.prefillMemoryGuard)
}
if vm.prefillMemoryGuard && vm.memoryGuardTier == "custom" {
Row(
label: String(localized: "performance.memory.custom_ceiling",
defaultValue: "Custom Ceiling",
comment: "Row label for memory guard custom ceiling"),
sublabel: String(localized: "performance.memory.custom_ceiling.sub",
defaultValue: "Fixed process memory ceiling in GB. Used only with the Custom tier.",
comment: "Sublabel for memory guard custom ceiling")
) {
TextInput(
text: $vm.memoryGuardCustomCeilingText,
placeholder: String(localized: "performance.memory.custom_ceiling.placeholder",
defaultValue: "GB",
comment: "Placeholder for custom memory guard ceiling"),
mono: true,
suffix: "GB",
width: 110
)
}
}
Row(
label: String(localized: "performance.memory.idle_timeout",
defaultValue: "Idle Timeout",
comment: "Row label for idle timeout field"),
sublabel: String(localized: "performance.memory.idle_timeout.sub",
defaultValue: "Server-wide auto-unload after N seconds idle. Empty = disabled. Minimum 60.",
comment: "Sublabel for idle timeout")
) {
TextInput(
text: $vm.idleTimeoutText,
placeholder: String(localized: "performance.memory.idle_timeout.placeholder",
defaultValue: "off",
comment: "Placeholder text for the idle timeout field when disabled"),
mono: true,
suffix: "s",
width: 110
)
}
Row(
label: String(localized: "performance.memory.model_fallback",
defaultValue: "Model Fallback",
comment: "Row label for model fallback toggle"),
sublabel: String(localized: "performance.memory.model_fallback.sub",
defaultValue: "When the requested model isn't loaded, route to any loaded model instead of 404.",
comment: "Sublabel for model fallback toggle"),
isLast: true
) {
Toggle("", isOn: $vm.modelFallback)
.labelsHidden().toggleStyle(.switch)
}
}
}
}
// MARK: - Cache
private struct CacheSection: View {
@Bindable var vm: PerformanceScreenVM
var body: some View {
SectionHeader(
String(localized: "performance.section.cache",
defaultValue: "Cache",
comment: "Section header for KV cache settings"),
subtitle: String(localized: "performance.section.cache.sub",
defaultValue: "KV cache spillover. The master switch gates everything below.",
comment: "Subtitle for the Cache section")
)
ListGroup {
Row(
label: String(localized: "performance.cache.enabled",
defaultValue: "Cache Enabled",
comment: "Row label for the master cache enable toggle"),
sublabel: String(localized: "performance.cache.enabled.sub",
defaultValue: "Master switch for the engine's KV cache subsystem.",
comment: "Sublabel for the master cache enable toggle")
) {
Toggle("", isOn: $vm.cacheEnabled)
.labelsHidden().toggleStyle(.switch)
}
Row(
label: String(localized: "performance.cache.hot_only",
defaultValue: "Hot Cache Only",
comment: "Row label for the hot cache only toggle"),
sublabel: String(localized: "performance.cache.hot_only.sub",
defaultValue: "Skip SSD spillover. Useful on fast machines with abundant RAM.",
comment: "Sublabel for hot cache only toggle")
) {
Toggle("", isOn: $vm.hotCacheOnly)
.labelsHidden().toggleStyle(.switch)
.disabled(!vm.cacheEnabled)
}
Row(
label: String(localized: "performance.cache.hot_size",
defaultValue: "Hot Cache Size",
comment: "Row label for the hot cache size field"),
sublabel: String(localized: "performance.cache.hot_size.sub",
defaultValue: "RAM ceiling for hot cache. \"0\" disables, sizes like \"8GB\" are accepted.",
comment: "Sublabel describing accepted hot cache size values")
) {
TextInput(
text: $vm.hotCacheMaxSize,
placeholder: "0",
mono: true,
width: 140
)
.disabled(!vm.cacheEnabled)
}
Row(
label: String(localized: "performance.cache.ssd_dir",
defaultValue: "SSD Cache Directory",
comment: "Row label for the SSD cache directory field"),
sublabel: String(localized: "performance.cache.ssd_dir.sub",
defaultValue: "Where cold-spillover blocks live. Empty = base_path/cache.",
comment: "Sublabel for the SSD cache directory")
) {
TextInput(
text: $vm.ssdCacheDir,
placeholder: "<base_path>/cache",
mono: true,
width: 280
)
.disabled(!vm.cacheEnabled || vm.hotCacheOnly)
}
Row(
label: String(localized: "performance.cache.ssd_size",
defaultValue: "SSD Cache Size",
comment: "Row label for the SSD cache size field"),
sublabel: String(localized: "performance.cache.ssd_size.sub",
defaultValue: "Cold-spillover ceiling. \"auto\" = 10% of SSD capacity.",
comment: "Sublabel describing accepted SSD cache size values")
) {
TextInput(
text: $vm.ssdCacheMaxSize,
placeholder: String(localized: "performance.memory.placeholder_auto",
defaultValue: "auto",
comment: "Memory field placeholder meaning automatic"),
mono: true,
width: 140
)
.disabled(!vm.cacheEnabled || vm.hotCacheOnly)
}
Row(
label: String(localized: "performance.cache.initial_blocks",
defaultValue: "Initial Cache Blocks",
comment: "Row label for the initial cache blocks field"),
sublabel: String(localized: "performance.cache.initial_blocks.sub",
defaultValue: "Pre-allocated cache blocks at server start. Requires restart to apply.",
comment: "Sublabel for the initial cache blocks field"),
isLast: true
) {
TextInput(
text: $vm.initialCacheBlocksText,
placeholder: String(localized: "performance.memory.placeholder_auto",
defaultValue: "auto",
comment: "Memory field placeholder meaning automatic"),
mono: true,
width: 110
)
.disabled(!vm.cacheEnabled)
}
}
}
}