mirror of https://github.com/alibaba/MNN.git
445 lines
14 KiB
Swift
445 lines
14 KiB
Swift
//
|
|
// BenchmarkViewModel.swift
|
|
// MNNLLMiOS
|
|
//
|
|
// Created by 游薪渝(揽清) on 2025/7/10.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftUI
|
|
import Combine
|
|
|
|
/**
|
|
* ViewModel for managing benchmark operations including model selection, test execution,
|
|
* progress tracking, and result management. Handles communication with BenchmarkService
|
|
* and provides UI state management for the benchmark interface.
|
|
*/
|
|
@MainActor
|
|
class BenchmarkViewModel: ObservableObject {
|
|
|
|
// MARK: - Published Properties
|
|
|
|
@Published var isLoading = false
|
|
@Published var isRunning = false
|
|
@Published var showProgressBar = false
|
|
@Published var showResults = false
|
|
@Published var showError = false
|
|
|
|
@Published var selectedModel: ModelInfo?
|
|
@Published var availableModels: [ModelInfo] = []
|
|
@Published var currentProgress: BenchmarkProgress?
|
|
@Published var benchmarkResults: BenchmarkResults?
|
|
@Published var errorMessage: String = ""
|
|
@Published var statusMessage: String = ""
|
|
|
|
@Published var startButtonText = String(localized: "Start Test")
|
|
@Published var isStartButtonEnabled = true
|
|
|
|
// MARK: - Private Properties
|
|
|
|
private let benchmarkService = BenchmarkService.shared
|
|
private let resultsHelper = BenchmarkResultsHelper.shared
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
// Model list manager for getting local models
|
|
private let modelListManager = ModelListManager.shared
|
|
|
|
// MARK: - Initialization & Setup
|
|
|
|
init() {
|
|
setupBindings()
|
|
loadAvailableModels()
|
|
}
|
|
|
|
/// Sets up reactive bindings between service and view model
|
|
private func setupBindings() {
|
|
benchmarkService.$isRunning
|
|
.receive(on: DispatchQueue.main)
|
|
.assign(to: \.isRunning, on: self)
|
|
.store(in: &cancellables)
|
|
|
|
// Update button text based on running state
|
|
benchmarkService.$isRunning
|
|
.receive(on: DispatchQueue.main)
|
|
.map { isRunning in
|
|
isRunning ? String(localized: "Stop Test") : String(localized: "Start Test")
|
|
}
|
|
.assign(to: \.startButtonText, on: self)
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
/// Loads available models from ModelListManager, filtering for downloaded models only
|
|
private func loadAvailableModels() {
|
|
Task {
|
|
isLoading = true
|
|
|
|
do {
|
|
// Get all models from ModelListManager
|
|
let allModels = try await modelListManager.loadModels()
|
|
|
|
// Filter only downloaded models that are available locally
|
|
availableModels = allModels.filter { model in
|
|
model.isDownloaded && model.localPath != nil
|
|
}
|
|
|
|
print("BenchmarkViewModel: Loaded \(availableModels.count) available local models")
|
|
|
|
} catch {
|
|
showErrorMessage("Failed to load models: \(error.localizedDescription)")
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
}
|
|
|
|
// MARK: - Public Action Handlers
|
|
|
|
/// Handles start/stop benchmark button taps
|
|
func onStartBenchmarkTapped() {
|
|
if !isRunning {
|
|
startBenchmark()
|
|
} else {
|
|
showStopConfirmationAlert()
|
|
}
|
|
}
|
|
|
|
/// Handles benchmark stop confirmation
|
|
func onStopBenchmarkTapped() {
|
|
stopBenchmark()
|
|
}
|
|
|
|
/// Handles model selection from dropdown
|
|
func onModelSelected(_ model: ModelInfo) {
|
|
selectedModel = model
|
|
}
|
|
|
|
/// Handles result deletion and cleanup
|
|
func onDeleteResultTapped() {
|
|
benchmarkResults = nil
|
|
showResults = false
|
|
hideStatus()
|
|
|
|
// Release model to free memory
|
|
benchmarkService.releaseModel()
|
|
}
|
|
|
|
/// Placeholder for future result submission functionality
|
|
func onSubmitResultTapped() {
|
|
// Implementation for submitting results (if needed)
|
|
// This could involve sharing or uploading results
|
|
}
|
|
|
|
// MARK: - Benchmark Execution
|
|
|
|
/// Initiates benchmark test with selected model and configured parameters
|
|
private func startBenchmark() {
|
|
guard let model = selectedModel else {
|
|
showErrorMessage("Please select a model first")
|
|
return
|
|
}
|
|
|
|
guard model.isDownloaded else {
|
|
showErrorMessage("Selected model is not downloaded or path is invalid")
|
|
return
|
|
}
|
|
|
|
onBenchmarkStarted()
|
|
|
|
Task {
|
|
// Initialize model if needed
|
|
let initialized = await benchmarkService.initializeModel(
|
|
modelId: model.id,
|
|
modelPath: model.localPath
|
|
)
|
|
|
|
guard initialized else {
|
|
showErrorMessage("Failed to initialize model")
|
|
resetUIState()
|
|
return
|
|
}
|
|
|
|
// Start memory monitoring
|
|
MemoryMonitor.shared.start()
|
|
|
|
// Start benchmark with optimized parameters for mobile devices
|
|
benchmarkService.runBenchmark(
|
|
modelId: model.id,
|
|
callback: self,
|
|
runtimeParams: createRuntimeParameters(),
|
|
testParams: createTestParameters()
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Creates runtime parameters optimized for iOS devices
|
|
private func createRuntimeParameters() -> RuntimeParameters {
|
|
return RuntimeParameters(
|
|
backends: [0], // CPU backend
|
|
threads: [4], // 4 threads for most iOS devices
|
|
useMmap: false, // Memory mapping disabled for iOS
|
|
power: [0], // Normal power mode
|
|
precision: [2], // Low precision for better performance
|
|
memory: [2], // Low memory usage
|
|
dynamicOption: [0] // No dynamic optimization
|
|
)
|
|
}
|
|
|
|
/// Creates test parameters suitable for mobile benchmarking
|
|
private func createTestParameters() -> TestParameters {
|
|
return TestParameters(
|
|
nPrompt: [256, 512], // Smaller prompt sizes for mobile
|
|
nGenerate: [64, 128], // Smaller generation sizes
|
|
nPrompGen: [(256, 64), (512, 128)], // Combined test cases
|
|
nRepeat: [3], // Fewer repetitions for faster testing
|
|
kvCache: "false", // Disable KV cache by default
|
|
loadTime: "false"
|
|
)
|
|
}
|
|
|
|
/// Stops the currently running benchmark
|
|
private func stopBenchmark() {
|
|
updateStatus("Stopping benchmark...")
|
|
benchmarkService.stopBenchmark()
|
|
MemoryMonitor.shared.stop()
|
|
}
|
|
|
|
// MARK: - UI State Management
|
|
|
|
/// Updates UI state when benchmark starts
|
|
private func onBenchmarkStarted() {
|
|
isStartButtonEnabled = true
|
|
showProgressBar = true
|
|
showResults = false
|
|
updateStatus("Initializing benchmark...")
|
|
}
|
|
|
|
/// Resets UI to initial state
|
|
private func resetUIState() {
|
|
isStartButtonEnabled = true
|
|
showProgressBar = false
|
|
hideStatus()
|
|
showResults = false
|
|
MemoryMonitor.shared.stop()
|
|
}
|
|
|
|
/// Updates status message display
|
|
private func updateStatus(_ message: String) {
|
|
statusMessage = message
|
|
}
|
|
|
|
/// Hides status message
|
|
private func hideStatus() {
|
|
statusMessage = ""
|
|
}
|
|
|
|
/// Shows error message alert
|
|
private func showErrorMessage(_ message: String) {
|
|
errorMessage = message
|
|
showError = true
|
|
}
|
|
|
|
/// Placeholder for stop confirmation alert (handled in View)
|
|
private func showStopConfirmationAlert() {
|
|
// This will be handled in the View with an alert
|
|
}
|
|
|
|
/// Formats progress messages with appropriate status text based on progress type
|
|
private func formatProgressMessage(_ progress: BenchmarkProgress) -> BenchmarkProgress {
|
|
let formattedMessage: String
|
|
|
|
switch progress.progressType {
|
|
case .initializing:
|
|
formattedMessage = "Initializing benchmark..."
|
|
case .warmingUp:
|
|
formattedMessage = "Warming up..."
|
|
case .runningTest:
|
|
formattedMessage = "Running test \(progress.currentIteration)/\(progress.totalIterations)"
|
|
case .processingResults:
|
|
formattedMessage = "Processing results..."
|
|
case .completed:
|
|
formattedMessage = "All tests completed"
|
|
case .stopping:
|
|
formattedMessage = "Stopping benchmark..."
|
|
default:
|
|
formattedMessage = progress.statusMessage
|
|
}
|
|
|
|
return BenchmarkProgress(
|
|
progress: progress.progress,
|
|
statusMessage: formattedMessage,
|
|
progressType: progress.progressType,
|
|
currentIteration: progress.currentIteration,
|
|
totalIterations: progress.totalIterations,
|
|
nPrompt: progress.nPrompt,
|
|
nGenerate: progress.nGenerate,
|
|
runTimeSeconds: progress.runTimeSeconds,
|
|
prefillTimeSeconds: progress.prefillTimeSeconds,
|
|
decodeTimeSeconds: progress.decodeTimeSeconds,
|
|
prefillSpeed: progress.prefillSpeed,
|
|
decodeSpeed: progress.decodeSpeed
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - BenchmarkCallback Implementation
|
|
|
|
extension BenchmarkViewModel: BenchmarkCallback {
|
|
|
|
/// Handles progress updates from benchmark service
|
|
func onProgress(_ progress: BenchmarkProgress) {
|
|
let formattedProgress = formatProgressMessage(progress)
|
|
currentProgress = formattedProgress
|
|
updateStatus(formattedProgress.statusMessage)
|
|
}
|
|
|
|
/// Handles benchmark completion with results processing
|
|
func onComplete(_ result: BenchmarkResult) {
|
|
guard let model = selectedModel else { return }
|
|
|
|
updateStatus("Processing results...")
|
|
|
|
// Create comprehensive benchmark results
|
|
let results = BenchmarkResults(
|
|
modelDisplayName: model.modelName,
|
|
maxMemoryKb: MemoryMonitor.shared.getMaxMemoryKb(),
|
|
testResults: [result.testInstance],
|
|
timestamp: DateFormatter.benchmarkTimestamp.string(from: Date())
|
|
)
|
|
|
|
benchmarkResults = results
|
|
showResults = true
|
|
|
|
// Only stop memory monitoring if benchmark is no longer running (all tests completed)
|
|
if !isRunning {
|
|
// Stop memory monitoring
|
|
MemoryMonitor.shared.stop()
|
|
}
|
|
|
|
// Always hide status after processing results
|
|
hideStatus()
|
|
|
|
print("BenchmarkViewModel: Benchmark completed successfully for model: \(model.modelName)")
|
|
}
|
|
|
|
/// Handles benchmark errors with user-friendly error messages
|
|
func onBenchmarkError(_ errorCode: Int, _ message: String) {
|
|
let errorCodeName = BenchmarkErrorCode(rawValue: errorCode)?.description ?? "Unknown"
|
|
showErrorMessage("Benchmark failed (\(errorCodeName)): \(message)")
|
|
resetUIState()
|
|
print("BenchmarkViewModel: Benchmark error (\(errorCode)): \(message)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Memory Monitoring
|
|
|
|
/**
|
|
* Singleton class for monitoring memory usage during benchmark execution.
|
|
* Tracks current and peak memory consumption using system APIs.
|
|
*/
|
|
class MemoryMonitor: ObservableObject {
|
|
|
|
static let shared = MemoryMonitor()
|
|
|
|
@Published private(set) var currentMemoryKb: Int64 = 0
|
|
private var maxMemoryKb: Int64 = 0
|
|
private var isMonitoring = false
|
|
private var monitoringTask: Task<Void, Never>?
|
|
|
|
private init() {}
|
|
|
|
/// Starts continuous memory monitoring
|
|
func start() {
|
|
guard !isMonitoring else { return }
|
|
|
|
isMonitoring = true
|
|
maxMemoryKb = 0
|
|
|
|
monitoringTask = Task {
|
|
while isMonitoring && !Task.isCancelled {
|
|
await updateMemoryUsage()
|
|
try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stops memory monitoring
|
|
func stop() {
|
|
isMonitoring = false
|
|
monitoringTask?.cancel()
|
|
monitoringTask = nil
|
|
}
|
|
|
|
/// Resets memory tracking counters
|
|
func reset() {
|
|
maxMemoryKb = 0
|
|
currentMemoryKb = 0
|
|
}
|
|
|
|
/// Returns the maximum memory usage recorded during monitoring
|
|
func getMaxMemoryKb() -> Int64 {
|
|
return maxMemoryKb
|
|
}
|
|
|
|
/// Updates current memory usage and tracks maximum
|
|
@MainActor
|
|
private func updateMemoryUsage() {
|
|
let memoryUsage = getCurrentMemoryUsage()
|
|
currentMemoryKb = memoryUsage
|
|
maxMemoryKb = max(maxMemoryKb, memoryUsage)
|
|
}
|
|
|
|
/// Gets current memory usage from system using mach task info
|
|
private func getCurrentMemoryUsage() -> Int64 {
|
|
var info = mach_task_basic_info()
|
|
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
|
|
|
|
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
|
|
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
|
|
task_info(mach_task_self_,
|
|
task_flavor_t(MACH_TASK_BASIC_INFO),
|
|
$0,
|
|
&count)
|
|
}
|
|
}
|
|
|
|
if kerr == KERN_SUCCESS {
|
|
return Int64(info.resident_size) / 1024 // Convert to KB
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Extensions
|
|
|
|
/// Extension providing user-friendly descriptions for benchmark error codes
|
|
extension BenchmarkErrorCode {
|
|
var description: String {
|
|
switch self {
|
|
case .benchmarkFailedUnknown:
|
|
return "Unknown Error"
|
|
case .testInstanceFailed:
|
|
return "Test Failed"
|
|
case .modelNotInitialized:
|
|
return "Model Not Ready"
|
|
case .benchmarkRunning:
|
|
return "Already Running"
|
|
case .benchmarkStopped:
|
|
return "Stopped"
|
|
case .nativeError:
|
|
return "Native Error"
|
|
case .modelError:
|
|
return "Model Error"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Extension providing formatted timestamp for benchmark results
|
|
extension DateFormatter {
|
|
static let benchmarkTimestamp: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "yyyy/M/dd HH:mm:ss"
|
|
return formatter
|
|
}()
|
|
}
|