Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
00405f1
other(benchmarks): scaffold SDKConfigBenchmark tuist project
facumenzella Jul 8, 2026
7ca95b8
other(benchmarks): add seeded rng and rc-container encoder
facumenzella Jul 8, 2026
592d6d6
other(benchmarks): add payload factory producing real sdk wire shapes
facumenzella Jul 8, 2026
bb316f6
other(benchmarks): add simulated network transport and fixture server
facumenzella Jul 8, 2026
58b2313
other(benchmarks): wire real manager-level sdk stacks per benchmark mode
facumenzella Jul 8, 2026
7dda708
other(benchmarks): add benchmark runner with phase metrics and jsonl …
facumenzella Jul 8, 2026
fb61655
other(benchmarks): add matrix runner, compare script and benchmark docs
facumenzella Jul 8, 2026
c20edc1
feat(benchmarks): add live transport pinned to the stress-test project
facumenzella Jul 8, 2026
8702995
other(benchmarks): apply review-loop fixes for measurement validity
facumenzella Jul 8, 2026
cfc5fb8
other(benchmarks): simplify pass: shared request classifier and sdk h…
facumenzella Jul 8, 2026
29a083b
other(benchmarks): apply second review pass fixes
facumenzella Jul 8, 2026
1071e15
other(benchmarks): apply third review pass fixes
facumenzella Jul 8, 2026
a4d931b
other(benchmarks): apply fourth review pass fixes
facumenzella Jul 8, 2026
5770b32
other(benchmarks): apply fifth review pass fixes
facumenzella Jul 8, 2026
7e828ba
other(benchmarks): wait for the iteration's config event and refresh …
facumenzella Jul 8, 2026
d0e8605
other(benchmarks): apply final review pass fixes
facumenzella Jul 9, 2026
2fdc3c7
other(benchmarks): document live inline blob delivery
facumenzella Jul 9, 2026
5d0ed6d
feat(benchmarks): swappable live target project
facumenzella Jul 9, 2026
9597957
other(benchmarks): resolve live API keys at run time instead of in so…
facumenzella Jul 9, 2026
49d9ffb
feat(benchmarks): add app-host launch measurement app
facumenzella Jul 9, 2026
9d22ded
feat(benchmarks): add app-launch xcuitest runner
facumenzella Jul 9, 2026
3bd6c21
feat(benchmarks): add app-launch variant runner and docs
facumenzella Jul 9, 2026
ee8c748
feat(benchmarks): inject TUIST_BENCH_API_KEY into the app scheme run …
facumenzella Jul 9, 2026
42762f4
feat(benchmarks): register blobs and config-path phases in both tiers
facumenzella Jul 9, 2026
fefc034
other(benchmarks): exclude Tuist-only benchmark targets from the xcod…
facumenzella Jul 9, 2026
a2edcb0
fix(benchmarks): make the app-launch variant switch survive cached de…
facumenzella Jul 9, 2026
b512e7a
fix(benchmarks): require a successful config outcome and quarantine f…
facumenzella Jul 9, 2026
0efad91
other(benchmarks): apply simplify-pass cleanups to the app-launch tier
facumenzella Jul 9, 2026
2ed2191
fix(benchmarks): validate cold config exchanges and warm priming
facumenzella Jul 9, 2026
1478c09
fix(benchmarks): require an explicit project label with externally su…
facumenzella Jul 9, 2026
fe0e434
feat(benchmarks): make configure + getOfferings the app-launch headline
facumenzella Jul 9, 2026
9aa2169
other(benchmarks): ignore python bytecode from compare.py runs
facumenzella Jul 9, 2026
b66b562
feat(benchmarks): attribute stored blobs to their config topics
facumenzella Jul 9, 2026
fff4dcc
fix(benchmarks): retry launches that produce nothing, once, with a vi…
facumenzella Jul 9, 2026
c57a9ea
Merge branch 'main' into facu/sdk-config-benchmark-v2
facumenzella Jul 10, 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ Tests/TestingApps/PaywallsTester/PaywallsTester/PaywallsTester.storekit

# Claude Code
.claude/

# Python bytecode (e.g. from running the benchmark compare.py)
__pycache__/
*.pyc
6 changes: 5 additions & 1 deletion Dangerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ require 'pathname'
require 'set'

EXCLUDED_SWIFT_PATH_PREFIXES = [
'Tests/APITesters/'
'Tests/APITesters/',
# Tuist-only benchmark targets (Projects/SDKConfigBenchmark and
# Projects/SDKConfigBenchmarkApp); intentionally absent from RevenueCat.xcodeproj.
'Tests/Benchmarks/',
'Tests/TestingApps/SDKConfigBenchmarkApp/'
].freeze

COMMENT_MARKER = "<!-- purchases-ios-danger -->"
Expand Down
104 changes: 104 additions & 0 deletions Projects/SDKConfigBenchmark/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import ProjectDescription
import ProjectDescriptionHelpers

// Benchmark harness for comparing the legacy offerings flow against the remote config
// endpoint flow. Compiles the SDK sources directly (like BinarySizeTest does for its
// local-source mode) so the benchmark can drive internal manager-level APIs without
// exposing new public SDK API. `SDK_CONFIG_BENCHMARK` installs a simulated transport
// in `HTTPClient`; `ENABLE_REMOTE_CONFIG` turns on the remote config gate.
//
// TUIST_SWIFT_CONDITIONS is folded in by hand instead of via
// `appendingTuistSwiftConditions()`, because that helper replaces the whole
// SWIFT_ACTIVE_COMPILATION_CONDITIONS value and would silently drop the benchmark flags.
let benchmarkSwiftConditions: SettingsDictionary = [
"SWIFT_ACTIVE_COMPILATION_CONDITIONS": SettingValue(stringLiteral: (
["$(inherited)", "SDK_CONFIG_BENCHMARK", "ENABLE_REMOTE_CONFIG"] + Environment.extraSwiftConditions
).joined(separator: " "))
]

let project = Project(
name: "SDKConfigBenchmark",
organizationName: .revenueCatOrgName,
settings: .framework,
targets: [
.target(
name: "SDKConfigBenchmarkCore",
destinations: [.mac],
product: .staticLibrary,
bundleId: "com.revenuecat.SDKConfigBenchmarkCore",
deploymentTargets: .macOS("13.0"),
infoPlist: .default,
sources: [
"../../Tests/Benchmarks/SDKConfigBenchmark/Sources/**/*.swift",
.glob(
"../../Sources/**/*.swift",
excluding: [
"../../Sources/LocalReceiptParsing/ReceiptParser-only-files/**/*.swift"
]
)
],
dependencies: [
.storeKit
],
settings: .settings(
base: benchmarkSwiftConditions
)
),
.target(
name: "SDKConfigBenchmark",
destinations: [.mac],
product: .commandLineTool,
bundleId: "com.revenuecat.SDKConfigBenchmark",
deploymentTargets: .macOS("13.0"),
infoPlist: .default,
sources: [
"../../Tests/Benchmarks/SDKConfigBenchmark/Main/**/*.swift"
],
dependencies: [
.target(name: "SDKConfigBenchmarkCore")
],
settings: .settings(
// The SDK's disk caches derive their directory from Bundle.main.bundleIdentifier,
// which a bare command-line binary does not have. Embedding the Info.plist into
// the binary gives it one, so etags/offerings/remote-config persistence works.
base: benchmarkSwiftConditions
.merging(["CREATE_INFOPLIST_SECTION_IN_BINARY": "YES"])
)
),
.target(
name: "SDKConfigBenchmarkTests",
destinations: [.mac],
product: .unitTests,
bundleId: "com.revenuecat.SDKConfigBenchmarkTests",
deploymentTargets: .macOS("13.0"),
infoPlist: .default,
sources: [
"../../Tests/Benchmarks/SDKConfigBenchmark/Tests/**/*.swift",
// App-host tier helpers, shared into this target so their aggregation
// logic is unit-testable without booting a simulator.
"../../Tests/TestingApps/SDKConfigBenchmarkApp/App/LaunchMeasurement.swift",
"../../Tests/TestingApps/SDKConfigBenchmarkApp/UITests/AppLaunchMetrics.swift"
],
dependencies: [
.target(name: "SDKConfigBenchmarkCore")
],
settings: .settings(
base: benchmarkSwiftConditions
)
)
],
schemes: [
.scheme(
name: "SDKConfigBenchmark",
shared: true,
buildAction: .buildAction(targets: ["SDKConfigBenchmark"]),
runAction: .runAction(configuration: "Release")
),
.scheme(
name: "SDKConfigBenchmarkTests",
shared: true,
buildAction: .buildAction(targets: ["SDKConfigBenchmarkTests"]),
testAction: .targets(["SDKConfigBenchmarkTests"])
)
]
)
85 changes: 85 additions & 0 deletions Projects/SDKConfigBenchmarkApp/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import ProjectDescription
import ProjectDescriptionHelpers

// App-host tier of the SDK config benchmark: a minimal iOS app that links the stock
// RevenueCat/RevenueCatUI products (unlike Projects/SDKConfigBenchmark, which compiles the
// SDK sources directly) and measures a real `Purchases.configure` launch end to end.
// Local/CI only; driven by Tests/TestingApps/SDKConfigBenchmarkApp/run-app-launch.sh.
//
// The legacy-vs-config variant switch is NOT set here: the SPM-built RevenueCat reads
// `SWIFT_ACTIVE_COMPILATION_CONDITIONS` from Local.xcconfig (see Package.swift), which the
// runner script rewrites per variant.

// Injected into the scheme's run environment so the app can be launched straight from Xcode:
// TUIST_BENCH_API_KEY=<key> tuist generate SDKConfigBenchmarkApp
// Without it, a direct run reports "BENCH_API_KEY missing" by design (keys never live in
// source or in committed scheme files).
let runEnvironment: [String: EnvironmentVariable] = {
var environment: [String: EnvironmentVariable] = [:]
if let apiKey = Environment.benchApiKey {
environment["BENCH_API_KEY"] = .environmentVariable(value: apiKey, isEnabled: true)
environment["BENCH_APP_USER_ID"] = .environmentVariable(value: "bench-xcode-run", isEnabled: true)
}
return environment
}()

let project = Project(
name: "SDKConfigBenchmarkApp",
organizationName: .revenueCatOrgName,
packages: .projectPackages,
settings: .appProject,
targets: [
.target(
name: "SDKConfigBenchmarkApp",
destinations: [.iPhone],
product: .app,
bundleId: "com.revenuecat.SDKConfigBenchmarkApp",
deploymentTargets: .iOS("16.0"),
infoPlist: "../../Tests/TestingApps/SDKConfigBenchmarkApp/App/Info.plist",
sources: [
"../../Tests/TestingApps/SDKConfigBenchmarkApp/App/**/*.swift"
],
dependencies: [
.revenueCat,
.revenueCatUI
],
settings: .appTarget(including: ([:] as SettingsDictionary).appendingTuistSwiftConditions())
),
.target(
name: "SDKConfigBenchmarkAppUITests",
destinations: [.iPhone],
product: .uiTests,
bundleId: "com.revenuecat.SDKConfigBenchmarkAppUITests",
deploymentTargets: .iOS("16.0"),
infoPlist: .default,
sources: [
"../../Tests/TestingApps/SDKConfigBenchmarkApp/UITests/**/*.swift",
// Shared with the app target: the runner decodes the same LaunchSample
// the app encodes.
"../../Tests/TestingApps/SDKConfigBenchmarkApp/App/LaunchMeasurement.swift"
],
dependencies: [
.target(name: "SDKConfigBenchmarkApp")
],
settings: .appTarget
)
],
schemes: [
.scheme(
name: "SDKConfigBenchmarkApp",
shared: true,
buildAction: .buildAction(targets: ["SDKConfigBenchmarkApp"]),
// Release: the rows claim to measure the shipping SDK, so Debug-built
// (unoptimized, assertion-enabled) frameworks would misrepresent it.
testAction: .targets(
["SDKConfigBenchmarkAppUITests"],
configuration: "Release"
),
runAction: .runAction(
configuration: "Debug",
executable: "SDKConfigBenchmarkApp",
arguments: .arguments(environmentVariables: runEnvironment)
)
)
]
)
3 changes: 3 additions & 0 deletions Sources/Caching/DeviceCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,9 @@ private extension DeviceCache {

// swiftlint:disable avoid_using_directory_apis_directly
private func oldDocumentsDirectoryURL() -> URL? {
#if SDK_CONFIG_BENCHMARK
guard !DirectoryHelper.skipsLegacyDocumentsMigrations else { return nil }
#endif
let documentsDirectoryURL: URL?
if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) {
documentsDirectoryURL = URL.documentsDirectory
Expand Down
28 changes: 28 additions & 0 deletions Sources/Caching/DirectoryHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,35 @@ enum DirectoryHelper {
return Self.baseUrl(for: directoryType, inAppSpecificDirectory: false)
}

#if SDK_CONFIG_BENCHMARK
/// Benchmark-only: when set, every SDK disk cache resolves under this root. The standard
/// container directories ignore $HOME, so without this override concurrent benchmark
/// processes would share (and corrupt) one another's caches in the real user Library.
/// Set once at process startup, before any disk access.
static var benchmarkBaseDirectoryOverride: URL?

/// While the override is active, legacy Documents-directory migrations must not run:
/// they read, move, and delete files under the real user Documents folder, outside the
/// benchmark's sandbox.
static var skipsLegacyDocumentsMigrations: Bool {
return self.benchmarkBaseDirectoryOverride != nil
}
#endif

static func baseUrl(for type: DirectoryType, inAppSpecificDirectory: Bool = true) -> URL? {
#if SDK_CONFIG_BENCHMARK
if let overrideRoot = Self.benchmarkBaseDirectoryOverride {
switch type {
case .cache:
return overrideRoot.appendingPathComponent("caches")
#if !os(tvOS)
case .applicationSupport:
return overrideRoot.appendingPathComponent("application-support")
#endif
}
}
#endif

guard let baseDirectory = type.url else {
return nil
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/Networking/HTTPClient/ETagManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ private extension ETagManager {
}

private func oldETagDirectoryURL() -> URL? {
#if SDK_CONFIG_BENCHMARK
guard !DirectoryHelper.skipsLegacyDocumentsMigrations else { return nil }
#endif
// swiftlint:disable:next avoid_using_directory_apis_directly
guard let documentsURL = Self.fileManager.urls(
for: .documentDirectory,
Expand Down
3 changes: 3 additions & 0 deletions Sources/Networking/HTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class HTTPClient {
config.timeoutIntervalForRequest = requestTimeout
config.timeoutIntervalForResource = requestTimeout
config.urlCache = nil // We implement our own caching with `ETagManager`.
#if SDK_CONFIG_BENCHMARK
config.protocolClasses = [SimulatedTransportURLProtocol.self]
#endif
self.session = URLSession(configuration: config,
delegate: RedirectLoggerSessionDelegate(),
delegateQueue: nil)
Expand Down
Loading