import Foundation
private let defaultFunction = "{{DEFAULT_FUNCTION}}"
private let defaultIterations: UInt32 = 20
private let defaultWarmup: UInt32 = 3
private let reportSchemaV2 = "mobench.run/v2"
struct ReportIdentity {
let runId: String
let nonce: String
let logicalSessionId: String
let functionId: String
let producer: String
}
struct BenchParams {
let function: String
let iterations: UInt32
let warmup: UInt32
let identity: ReportIdentity?
fileprivate struct EncodedBenchSpec: Decodable {
let function: String
let iterations: UInt32
let warmup: UInt32
let schema_version: String?
let run_id: String?
let nonce: String?
let logical_session_id: String?
let function_id: String?
let producer: String?
}
static func fromBundle() -> BenchParams? {
guard let url = Bundle.main.url(forResource: "bench_spec", withExtension: "json") else {
return nil
}
do {
let data = try Data(contentsOf: url)
let decoded = try JSONDecoder().decode(EncodedBenchSpec.self, from: data)
return BenchParams(
function: decoded.function,
iterations: decoded.iterations,
warmup: decoded.warmup,
identity: decoded.schema_version == reportSchemaV2 ? decoded.reportIdentity() : nil
)
} catch {
print("[BenchRunner] ERROR: Failed to parse bench_spec.json: \(error)")
return nil
}
}
static func fromProcessInfo() -> BenchParams {
let info = ProcessInfo.processInfo
var function = defaultFunction
var iterations = defaultIterations
var warmup = defaultWarmup
if let value = info.environment["BENCH_FUNCTION"], !value.isEmpty {
function = value
}
if let value = info.environment["BENCH_ITERATIONS"], let parsed = UInt32(value) {
iterations = parsed
}
if let value = info.environment["BENCH_WARMUP"], let parsed = UInt32(value) {
warmup = parsed
}
for arg in info.arguments {
if arg.hasPrefix("--bench-function="), let value = arg.split(separator: "=", maxSplits: 1).last {
function = String(value)
} else if arg.hasPrefix("--bench-iterations="),
let value = arg.split(separator: "=", maxSplits: 1).last,
let parsed = UInt32(value) {
iterations = parsed
} else if arg.hasPrefix("--bench-warmup="),
let value = arg.split(separator: "=", maxSplits: 1).last,
let parsed = UInt32(value) {
warmup = parsed
}
}
return BenchParams(function: function, iterations: iterations, warmup: warmup, identity: nil)
}
static func resolved() -> BenchParams {
fromBundle() ?? fromProcessInfo()
}
}
private extension BenchParams.EncodedBenchSpec {
func reportIdentity() -> ReportIdentity? {
guard let run_id, let nonce, let logical_session_id, let function_id, let producer else {
return nil
}
return ReportIdentity(
runId: run_id,
nonce: nonce,
logicalSessionId: logical_session_id,
functionId: function_id,
producer: producer
)
}
}
struct BenchmarkResult {
let displayText: String
let jsonReport: String
}
enum {{PROJECT_NAME_PASCAL}}FFI {
static func runCurrentBenchmark() async -> BenchmarkResult {
run(params: BenchParams.resolved())
}
static func run(params: BenchParams) -> BenchmarkResult {
do {
let rawReport = try runBenchmarkJson(specJson: encodeSpec(params))
let report = try enrichBenchReport(rawReport, params: params)
return BenchmarkResult(
displayText: formatBenchReport(report),
jsonReport: serializeJSON(report)
)
} catch {
print("[BenchRunner] ERROR: Benchmark failed: \(error)")
let message = error.localizedDescription
return BenchmarkResult(
displayText: "Benchmark error: \(message)",
jsonReport: serializeFailureJSON(message, params: params)
)
}
}
private static func encodeSpec(_ params: BenchParams) throws -> String {
let spec: [String: Any] = [
"name": params.function,
"iterations": params.iterations,
"warmup": params.warmup
]
let data = try JSONSerialization.data(withJSONObject: spec, options: [])
return String(data: data, encoding: .utf8) ?? "{}"
}
private static func enrichBenchReport(_ rawReport: String, params: BenchParams) throws -> [String: Any] {
let data = Data(rawReport.utf8)
guard var json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
throw BoltFfiBenchError.execution("BoltFFI benchmark returned non-object JSON")
}
let spec = json["spec"] as? [String: Any] ?? [:]
json["function"] = spec["name"] as? String ?? defaultFunction
let samples = json["samples"] as? [[String: Any]] ?? []
let durations = samples.compactMap { coerceToUInt64($0["duration_ns"] ?? 0) }
json["samples_ns"] = durations
applyV2Envelope(to: &json, params: params, samplesNs: durations, outcome: ["status": "success"])
if !durations.isEmpty {
let sum = durations.reduce(0, +)
json["stats"] = [
"min_ns": durations.min() ?? 0,
"max_ns": durations.max() ?? 0,
"avg_ns": Double(sum) / Double(durations.count),
"mean_ns": sum / UInt64(durations.count),
"median_ns": median(durations)
] as [String: Any]
}
json["resources"] = [
"platform": "ios",
"timestamp_ms": Int64(Date().timeIntervalSince1970 * 1000)
] as [String: Any]
return json
}
private static func serializeFailureJSON(_ message: String, params: BenchParams) -> String {
var json: [String: Any] = ["error": true, "message": message, "samples_ns": []]
applyV2Envelope(
to: &json,
params: params,
samplesNs: [],
outcome: [
"status": "failure",
"error": ["code": "benchmark_error", "message": message]
]
)
return serializeJSON(json)
}
private static func applyV2Envelope(
to json: inout [String: Any],
params: BenchParams,
samplesNs: [UInt64],
outcome: [String: Any]
) {
guard let identity = params.identity else { return }
json["schema_version"] = reportSchemaV2
json["run_id"] = identity.runId
json["nonce"] = identity.nonce
json["logical_session_id"] = identity.logicalSessionId
json["function_id"] = identity.functionId
json["producer"] = identity.producer
json["requested"] = ["iterations": params.iterations, "warmup": params.warmup]
json["observed"] = [
"iterations": samplesNs.count,
"warmup": outcome["status"] as? String == "success" ? params.warmup : 0
]
json["samples_ns"] = samplesNs
json["outcome"] = outcome
}
private static func formatBenchReport(_ report: [String: Any]) -> String {
let spec = report["spec"] as? [String: Any] ?? [:]
let samples = report["samples_ns"] as? [UInt64] ?? []
var output = "=== Benchmark Results ===\n\n"
output += "Function: \(spec["name"] as? String ?? defaultFunction)\n"
output += "Iterations: \(coerceToUInt64(spec["iterations"] ?? defaultIterations) ?? UInt64(defaultIterations))\n"
output += "Warmup: \(coerceToUInt64(spec["warmup"] ?? defaultWarmup) ?? UInt64(defaultWarmup))\n\n"
output += "Samples (\(samples.count)):\n"
for (index, sample) in samples.enumerated() {
output += " \(index + 1). \(formatDuration(sample))\n"
}
if let stats = report["stats"] as? [String: Any] {
output += "\nStatistics:\n"
output += " Min: \(formatDuration(coerceToUInt64(stats["min_ns"] ?? 0) ?? 0))\n"
output += " Max: \(formatDuration(coerceToUInt64(stats["max_ns"] ?? 0) ?? 0))\n"
output += " Avg: \(formatDuration(coerceToUInt64(stats["mean_ns"] ?? 0) ?? 0))\n"
}
return output
}
private static func serializeJSON(_ value: [String: Any]) -> String {
do {
let data = try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys])
return String(data: data, encoding: .utf8) ?? "{}"
} catch {
return "{}"
}
}
private static func formatDuration(_ ns: UInt64) -> String {
let ms = Double(ns) / 1_000_000.0
if ms >= 1000.0 {
return String(format: "%.3fs", ms / 1000.0)
}
return String(format: "%.3fms", ms)
}
}
private enum BoltFfiBenchError: LocalizedError {
case execution(String)
var errorDescription: String? {
switch self {
case .execution(let message):
return message
}
}
}
private func coerceToUInt64(_ value: Any) -> UInt64? {
if let value = value as? UInt64 {
return value
}
if let value = value as? UInt32 {
return UInt64(value)
}
if let value = value as? Int {
return value >= 0 ? UInt64(value) : nil
}
if let value = value as? NSNumber {
return value.uint64Value
}
return nil
}
private func median(_ values: [UInt64]) -> UInt64 {
guard !values.isEmpty else {
return 0
}
let sorted = values.sorted()
let midpoint = sorted.count / 2
if sorted.count % 2 == 0 {
return (sorted[midpoint - 1] + sorted[midpoint]) / 2
}
return sorted[midpoint]
}
private func escapeJSON(_ value: String) -> String {
value
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "\"", with: "\\\"")
.replacingOccurrences(of: "\n", with: "\\n")
}