Files
2026-07-13 13:33:03 +08:00

308 lines
11 KiB
Swift

//
// ResultsCard.swift
// MNNLLMiOS
//
// Created by 游薪渝(揽清) on 2025/7/21.
//
import SwiftUI
/// Reusable results display card component for benchmark interface.
/// Shows comprehensive benchmark results with performance metrics and statistics.
struct ResultsCard: View {
let results: BenchmarkResults
var body: some View {
VStack(alignment: .leading, spacing: 20) {
resultsHeader
infoHeader
performanceMetrics
detailedStats
}
.padding(20)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.benchmarkCardBg)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.benchmarkSuccess.opacity(0.3), lineWidth: 1)
)
)
}
// MARK: - Private Views
private var infoHeader: some View {
let statistics = BenchmarkResultsHelper.shared.processTestResults(results.testResults, totalTimeSeconds: results.totalTimeSeconds)
return VStack(alignment: .leading, spacing: 8) {
Text(results.modelDisplayName)
.font(.headline)
Text(BenchmarkResultsHelper.shared.getDeviceInfo())
.font(.subheadline)
.foregroundColor(.secondary)
Text(String(localized: "Benchmark Config"))
.font(.headline)
Text(statistics.configText)
.font(.subheadline)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(.secondary)
}
}
private var resultsHeader: some View {
HStack {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(
LinearGradient(
colors: [Color.benchmarkSuccess.opacity(0.2), Color.benchmarkSuccess.opacity(0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 40, height: 40)
Image(systemName: "chart.bar.fill")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.benchmarkSuccess)
}
VStack(alignment: .leading, spacing: 2) {
Text(String(localized: "Benchmark Results"))
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(.primary)
Text(String(localized: "Performance analysis complete"))
.font(.caption)
.foregroundColor(.benchmarkSecondary)
}
}
Spacer()
Button(action: {
shareResults()
}) {
VStack(alignment: .center, spacing: 2) {
Image(systemName: "square.and.arrow.up")
.font(.title2)
.foregroundColor(.benchmarkSuccess)
Text(String(localized: "Share"))
.font(.caption)
.foregroundColor(.benchmarkSecondary)
}
}
.buttonStyle(PlainButtonStyle())
}
}
private var performanceMetrics: some View {
let statistics = BenchmarkResultsHelper.shared.processTestResults(results.testResults, totalTimeSeconds: results.totalTimeSeconds)
return VStack(spacing: 16) {
HStack(spacing: 12) {
if let prefillStats = statistics.prefillStats {
PerformanceMetricView(
icon: "speedometer",
title: String(localized: "Prefill Speed"),
value: BenchmarkResultsHelper.shared.formatSpeedStatisticsLine(prefillStats),
subtitle: String(localized: "Tokens per second"),
color: .benchmarkGradientStart
)
} else {
PerformanceMetricView(
icon: "speedometer",
title: String(localized: "Prefill Speed"),
value: String(localized: "N/A"),
subtitle: String(localized: "Tokens per second"),
color: .benchmarkGradientStart
)
}
if let decodeStats = statistics.decodeStats {
PerformanceMetricView(
icon: "gauge",
title: String(localized: "Decode Speed"),
value: BenchmarkResultsHelper.shared.formatSpeedStatisticsLine(decodeStats),
subtitle: String(localized: "Generation rate"),
color: .benchmarkGradientEnd
)
} else {
PerformanceMetricView(
icon: "gauge",
title: String(localized: "Decode Speed"),
value: String(localized: "N/A"),
subtitle: String(localized: "Generation rate"),
color: .benchmarkGradientEnd
)
}
}
HStack(spacing: 12) {
let totalMemoryKb = BenchmarkResultsHelper.shared.getTotalSystemMemoryKb()
let memoryInfo = BenchmarkResultsHelper.shared.formatMemoryUsage(
maxMemoryKb: results.maxMemoryKb,
totalKb: totalMemoryKb
)
PerformanceMetricView(
icon: "memorychip",
title: String(localized: "Memory Usage"),
value: memoryInfo.valueText,
subtitle: String(localized: "Peak memory"),
color: .benchmarkWarning
)
PerformanceMetricView(
icon: "clock",
title: String(localized: "Total Time"),
value: String(format: "%.2f s", statistics.totalTimeSeconds),
subtitle: String(localized: "Complete duration"),
color: .benchmarkSuccess
)
}
}
}
private var detailedStats: some View {
return VStack(alignment: .leading, spacing: 12) {
VStack(spacing: 8) {
HStack {
Text(String(localized: "Completed"))
.font(.caption)
.foregroundColor(.benchmarkSecondary)
Spacer()
Text(results.timestamp)
.font(.caption)
.foregroundColor(.benchmarkSecondary)
}
HStack {
Text(String(localized: "Powered By MNN"))
.font(.caption)
.foregroundColor(.benchmarkSecondary)
Spacer()
Text(verbatim: "https://github.com/alibaba/MNN")
.font(.caption)
.foregroundColor(.benchmarkSecondary)
}
}
.padding(.vertical, 8)
}
}
// MARK: - Helper Functions
/// Formats byte count into human-readable string
private func formatBytes(_ bytes: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useKB, .useMB, .useGB]
formatter.countStyle = .file
return formatter.string(fromByteCount: bytes)
}
/// Initiates sharing of benchmark results through system share sheet
private func shareResults() {
let viewToRender = body.frame(width: 390) // Adjust width as needed
if let image = viewToRender.snapshot() {
presentShareSheet(activityItems: [image, formatResultsForSharing()])
} else {
presentShareSheet(activityItems: [formatResultsForSharing()])
}
}
private func presentShareSheet(activityItems: [Any]) {
let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first,
let rootViewController = window.rootViewController
{
if let popover = activityViewController.popoverPresentationController {
popover.sourceView = window
popover.sourceRect = CGRect(x: window.bounds.midX, y: window.bounds.midY, width: 0, height: 0)
popover.permittedArrowDirections = []
}
rootViewController.present(activityViewController, animated: true)
}
}
/// Formats benchmark results into shareable text format with performance metrics and hashtags
private func formatResultsForSharing() -> String {
let statistics = BenchmarkResultsHelper.shared.processTestResults(results.testResults, totalTimeSeconds: results.totalTimeSeconds)
let deviceInfo = BenchmarkResultsHelper.shared.getDeviceInfo()
var shareText = """
📱 MNN LLM Benchmark Results
🤖 Model: \(results.modelDisplayName)
📱 \(deviceInfo)
📅 Completed: \(results.timestamp)
📊 Configuration:
\(statistics.configText)
⚡️ Performance Results:
"""
if let prefillStats = statistics.prefillStats {
shareText += "\n🔄 Prompt Processing: \(BenchmarkResultsHelper.shared.formatSpeedStatisticsLine(prefillStats))"
}
if let decodeStats = statistics.decodeStats {
shareText += "\n⚡️ Token Generation: \(BenchmarkResultsHelper.shared.formatSpeedStatisticsLine(decodeStats))"
}
let totalMemoryKb = BenchmarkResultsHelper.shared.getTotalSystemMemoryKb()
let memoryInfo = BenchmarkResultsHelper.shared.formatMemoryUsage(
maxMemoryKb: results.maxMemoryKb,
totalKb: totalMemoryKb
)
shareText += "\n💾 Peak Memory: \(memoryInfo.valueText) (\(memoryInfo.labelText))"
shareText += "\n\n📈 Summary:"
shareText += "\n• Total Tokens Processed: \(statistics.totalTokensProcessed)"
shareText += "\n• Number of Tests: \(statistics.totalTests)"
shareText += "\n\n#MNNLLMBenchmark #AIPerformance #MobileAI"
return shareText
}
}
extension View {
func snapshot() -> UIImage? {
let controller = UIHostingController(rootView: self)
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
}
#Preview {
ResultsCard(
results: BenchmarkResults(
modelDisplayName: "Qwen2.5-1.5B-Instruct",
maxMemoryKb: 1_200_000, // 1.2 GB in KB
testResults: [],
timestamp: "2025-01-21 14:30:25"
)
)
.padding()
}