import SwiftUI /// 性能自检:跑固定 prompt,展示当前后端(MNN·SME2 / MNN·NEON / MLX·GPU)的 /// prefill / decode 实测速度,并按后端存档对比 —— 挑战赛考核点的可见证据(§12 卖点 2/6)。 struct ModelSelfTestView: View { @State private var output = "" @State private var phase: Phase = .idle @State private var rate: Double = 0 @State private var lastResult: BenchmarkResult? @State private var history: [String: BenchmarkResult] = [:] private enum Phase: Equatable { case idle, loading, running, done, failed(String) var label: String { switch self { case .idle: return String(appLoc: "未开始") case .loading: return String(appLoc: "加载模型…") case .running: return String(appLoc: "推理中…") case .done: return String(appLoc: "完成 ✓") case .failed(let m): return String(appLoc: "失败:\(m)") } } } private var isBusy: Bool { phase == .loading || phase == .running } private var statusColor: Color { switch phase { case .failed: return Tj.Palette.brick case .done: return Tj.Palette.leaf default: return Tj.Palette.text2 } } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { promptCard HStack { Text(phase.label) .font(.tjScaled( 13, weight: .medium)) .foregroundStyle(statusColor) .lineLimit(1) Spacer() if rate > 0 { Text(String(format: "%.1f tok/s", rate)) .font(.tjScaled( 12, design: .monospaced)) .foregroundStyle(Tj.Palette.text3) } } Button { Task { await run() } } label: { Text(isBusy ? "运行中…" : "运行性能自检").frame(maxWidth: .infinity) } .buttonStyle(TjPrimaryButton()) .disabled(isBusy) if isBusy { AIFlowBar() } if let r = lastResult { statsCard(r) } outputCard if !history.isEmpty { historyCard } } .padding(16) } .background(Tj.Palette.sand.ignoresSafeArea()) .navigationTitle("性能自检") .navigationBarTitleDisplayMode(.inline) .onAppear { history = BenchmarkService.load() } } private var promptCard: some View { VStack(alignment: .leading, spacing: 6) { Text("测试 PROMPT") .font(.tjScaled( 11, weight: .semibold)) .tracking(0.5) .foregroundStyle(Tj.Palette.text3) Text(BenchmarkService.fixedPrompt) .font(.tjScaled( 14)) .foregroundStyle(Tj.Palette.text) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .tjCard() } private func statsCard(_ r: BenchmarkResult) -> some View { VStack(alignment: .leading, spacing: 10) { HStack { Text("本次结果") .font(.tjScaled( 12, weight: .semibold)) .foregroundStyle(Tj.Palette.text2) Spacer() TjBadge(text: r.backendLabel, style: .leaf) } HStack(spacing: 0) { metric(String(appLoc: "读入"), r.prefillTokensPerSecond > 0 ? String(format: "%.0f tok/s", r.prefillTokensPerSecond) : "—") metric(String(appLoc: "生成"), String(format: "%.1f tok/s", r.decodeTokensPerSecond)) metric(String(appLoc: "总耗时"), String(format: "%.1fs", r.totalSeconds)) } Text(String(appLoc: "prompt \(r.promptTokens) tok · 生成 \(r.genTokens) tok · 100% 本地")) .font(.tjScaled( 10, design: .monospaced)) .foregroundStyle(Tj.Palette.text3) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .tjCard() } private func metric(_ label: String, _ value: String) -> some View { VStack(spacing: 3) { Text(value) .font(.tjScaled( 15, weight: .semibold, design: .monospaced)) .foregroundStyle(Tj.Palette.text) Text(label) .font(.tjScaled( 10)) .foregroundStyle(Tj.Palette.text3) } .frame(maxWidth: .infinity) } private var outputCard: some View { ScrollView { Text(output.isEmpty ? "(暂无输出)" : output) .font(.system(.footnote, design: .monospaced)) .foregroundStyle(Tj.Palette.text) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) .padding(12) } .frame(maxHeight: 220) .background( RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous) .fill(Tj.Palette.paper) ) .overlay( RoundedRectangle(cornerRadius: Tj.Radius.md, style: .continuous) .strokeBorder(Tj.Palette.lineSoft, lineWidth: 1) ) } private var historyCard: some View { VStack(alignment: .leading, spacing: 10) { Text("各引擎实测对比") .font(.tjScaled( 12, weight: .semibold)) .foregroundStyle(Tj.Palette.text2) ForEach(history.keys.sorted(), id: \.self) { key in if let r = history[key] { HStack { Text(key) .font(.tjScaled( 12, weight: .medium)) .foregroundStyle(Tj.Palette.text) Spacer() Text(String(format: String(appLoc: "生成 %.1f tok/s"), r.decodeTokensPerSecond)) .font(.tjScaled( 12, design: .monospaced)) .foregroundStyle(Tj.Palette.leaf) Text(r.date.formatted(.dateTime.month().day())) .font(.tjScaled( 10)) .foregroundStyle(Tj.Palette.text3) } } } Text("在「我的 · 推理引擎」切换引擎后再跑一次,即可对比 SME2 与 GPU。") .font(.tjScaled( 10)) .foregroundStyle(Tj.Palette.text3) } .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .tjCard() } @MainActor private func run() async { output = "" rate = 0 lastResult = nil phase = .loading do { let result = try await BenchmarkService.shared.run { piece, r in output += piece if r > 0 { rate = r } if phase == .loading { phase = .running } } lastResult = result history = BenchmarkService.load() phase = .done } catch { phase = .failed(error.localizedDescription) } } } #Preview { NavigationStack { ModelSelfTestView() } }