Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4589782
docs(cache): add Phase 1 plan and summary for cache rewrite
AnthonyMDev May 7, 2026
703563d
docs(cache): add Phase 1 AI execution plan
AnthonyMDev May 7, 2026
f9d486f
docs(cache): adopt long-lived plan branch as stack base
AnthonyMDev May 7, 2026
5c0a186
docs(cache): add Phase 1 performance measurement plan
AnthonyMDev May 7, 2026
b4f01e1
docs(cache): split merge cadence — ADR PRs incremental, code PRs batched
AnthonyMDev May 7, 2026
50e697e
docs(cache): ADR 0001 — major version bump rationale (#968)
AnthonyMDev May 7, 2026
6df7621
docs(cache): ADR 0002 — Record abstraction (field-aware via CachedFie…
AnthonyMDev May 7, 2026
6c8ac5f
docs(cache): ADR 0003 — TTL semantics (tri-state, selection-set scope…
AnthonyMDev May 7, 2026
c0d3cb6
docs(cache): update plan for stale-tolerance design (ADR 0005 forthco…
AnthonyMDev May 7, 2026
01cb536
docs(cache): ADR 0004 — Watcher × TTL (opt-in auto-refresh, permissiv…
AnthonyMDev May 11, 2026
663ac77
docs(cache): ADR 0005 — stale-tolerance API surface (#972)
AnthonyMDev May 11, 2026
da9a2ff
docs(cache): codify inline-documentation conventions (#988)
AnthonyMDev May 20, 2026
26185b9
docs(cache): add progress-tracker update to §4.7 on-merge checklist (…
AnthonyMDev May 26, 2026
7e4b411
docs(cache): note Phase 1A writtenAt default and Phase 1C migration p…
AnthonyMDev May 26, 2026
fb5efc6
feat(perf): macOS 2.x cache baseline benchmark harness + dataset (PR-…
AnthonyMDev May 26, 2026
abdba7f
feat(cache): introduce CachedField type (no consumers yet) (PR-005) (…
AnthonyMDev May 26, 2026
fa681fe
refactor(cache): change Record.fields type to [CacheKey: CachedField]…
AnthonyMDev May 26, 2026
64f6bc0
feat(sqlite): add schema_metadata table and version detection (PR-007…
AnthonyMDev May 26, 2026
ba390ab
feat(sqlite): new schema DDL + SchemaVersion + SQLiteSchema namespace…
AnthonyMDev May 27, 2026
2aaeba8
docs(cache): ADR 0006 — list storage strategy (#1002)
AnthonyMDev May 29, 2026
4dfb34e
docs(cache): rewrite §7.1, §7.2, §7.3 and amend §8 PR-009 per ADR 000…
AnthonyMDev May 29, 2026
9b2dbb3
docs(cache): ADR 0007 — selection-set-aware cache reads (PR-009a) (#1…
AnthonyMDev May 29, 2026
e06a2b8
feat(sqlite): replace records DDL with position-keyed v4 schema (PR-0…
AnthonyMDev May 29, 2026
ad05d43
docs(cache): amend ADR 0007 implementation sequence (d-iii, d-iv, 009…
AnthonyMDev Jun 2, 2026
510206c
Merge remote-tracking branch 'origin/main' into cache-rewrite/phase-1…
AnthonyMDev Jun 9, 2026
840d6ce
Merge remote-tracking branch 'origin/main' into cache-rewrite/phase-1…
AnthonyMDev Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation

/// Async measurement harness used by the cache-rewrite Phase 1 perf dataset.
///
/// Runs `body` for `warmupIterations + measuredIterations`, discards warmup,
/// and computes mean/std/P50/P95/P99 from the measured samples in milliseconds.
/// Emits a single `BENCHMARK_RESULT_JSONL: { ... }` line to stdout per call so
/// the dataset assembler script can grep results out of `xcodebuild` output.
///
/// One iteration runs `body` exactly once; the harness does not retry on throw —
/// a thrown error fails the test outright. Pass `setup` for per-iteration state
/// that should not be timed (cache warmup, fresh fixtures, etc.).
public struct BenchmarkHarness: Sendable {
public let scenario: String
public let tier: Int
public let warmupIterations: Int
public let measuredIterations: Int

public init(
scenario: String,
tier: Int,
warmupIterations: Int = 5,
measuredIterations: Int = 50
) {
self.scenario = scenario
self.tier = tier
self.warmupIterations = warmupIterations
self.measuredIterations = measuredIterations
}

/// Run `body` for the configured iteration count and emit a `BenchmarkResult`.
///
/// `setup` runs before each iteration *outside* the measurement window. Use it
/// to reset fixtures (clear cache, seed records) without polluting the sample.
/// Closures are non-`Sendable` because they routinely capture references to
/// non-`Sendable` caches/stores under test; the harness itself runs the loop
/// sequentially so there's no actual concurrency to worry about.
public func measure(
setup: ((Int) async throws -> Void)? = nil,
body: (Int) async throws -> Void
) async throws -> BenchmarkResult {
var iterationDurationsNs: [UInt64] = []
iterationDurationsNs.reserveCapacity(measuredIterations)

for i in 0..<(warmupIterations + measuredIterations) {
try await setup?(i)

let start = DispatchTime.now().uptimeNanoseconds
try await body(i)
let end = DispatchTime.now().uptimeNanoseconds

if i >= warmupIterations {
iterationDurationsNs.append(end &- start)
}
}

let result = BenchmarkResult(
scenario: scenario,
tier: tier,
iterationDurationsNs: iterationDurationsNs,
iterations: measuredIterations
)
BenchmarkOutput.emit(result)
return result
}
}

/// Emits `BenchmarkResult` records as JSONL lines that the run script picks up.
public enum BenchmarkOutput {
static let encoder: JSONEncoder = {
let e = JSONEncoder()
e.outputFormatting = [.sortedKeys]
return e
}()

public static func emit(_ result: BenchmarkResult) {
guard let data = try? encoder.encode(result),
let line = String(data: data, encoding: .utf8) else {
return
}
// Single-line marker so the post-processor can grep + jq.
print("BENCHMARK_RESULT_JSONL: \(line)")
}
}
46 changes: 46 additions & 0 deletions Tests/ApolloPerformanceTests/CacheBenchmarks/BenchmarkResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation

/// One measured scenario row in the published perf dataset.
///
/// Schema corresponds to `cache-rewrite-phase1-perf.md` §5.1. Latencies are
/// reported in milliseconds for human readability; the harness records in
/// nanoseconds internally to avoid integer truncation on sub-ms samples.
public struct BenchmarkResult: Codable, Sendable {
public let scenario: String
public let tier: Int
public let iterations: Int
public let mean_ms: Double
public let std_ms: Double
public let p50_ms: Double
public let p95_ms: Double
public let p99_ms: Double

public init(scenario: String, tier: Int, iterationDurationsNs: [UInt64], iterations: Int) {
self.scenario = scenario
self.tier = tier
self.iterations = iterations

let iterationDurationsMs = iterationDurationsNs.map { Double($0) / 1_000_000.0 }
let sorted = iterationDurationsMs.sorted()

let mean = iterationDurationsMs.reduce(0, +) / Double(max(iterationDurationsMs.count, 1))
let variance = iterationDurationsMs.reduce(0.0) { $0 + ($1 - mean) * ($1 - mean) }
/ Double(max(iterationDurationsMs.count - 1, 1))

self.mean_ms = mean
self.std_ms = variance.squareRoot()
self.p50_ms = Self.percentile(sorted, 0.50)
self.p95_ms = Self.percentile(sorted, 0.95)
self.p99_ms = Self.percentile(sorted, 0.99)
}

private static func percentile(_ sorted: [Double], _ p: Double) -> Double {
guard !sorted.isEmpty else { return 0 }
let rank = p * Double(sorted.count - 1)
let lo = Int(rank.rounded(.down))
let hi = Int(rank.rounded(.up))
if lo == hi { return sorted[lo] }
let frac = rank - Double(lo)
return sorted[lo] + (sorted[hi] - sorted[lo]) * frac
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@_spi(Execution) import Apollo
import ApolloAPI
import Foundation

/// Synthetic workload helpers shared across Tier 2 (`NormalizedCache`) benchmarks.
///
/// Mirrors the size buckets in `cache-rewrite-phase1-perf.md` §3.1. Records use
/// stable cache keys (`record_<index>`) and a deterministic field-name pattern
/// so independent runs produce comparable shapes.
public enum BenchmarkWorkloads {
public static let fieldsPerRecord = 10

/// Build `count` records of `fieldsPerRecord` scalar fields each. Fields
/// alternate between String and Int values so the cache exercises both
/// scalar paths during serialization.
public static func syntheticRecords(count: Int) -> [Record] {
(0..<count).map { i in
var values: [CacheKey: Record.Value] = [:]
for f in 0..<fieldsPerRecord {
let key: CacheKey = "field_\(f)"
if f % 2 == 0 {
values[key] = "value_\(i)_\(f)"
} else {
values[key] = i * 100 + f
}
}
return Record(key: "record_\(i)", values)
}
}

/// First `count` cache keys produced by `syntheticRecords(count:)`. Useful
/// for batched `loadRecords(forKeys:)` scenarios.
public static func syntheticKeys(count: Int) -> Set<CacheKey> {
Set((0..<count).map { CacheKey("record_\($0)") })
}
}
Loading
Loading