|
Bogdan Timofte
authored
a day ago
|
1
|
import HealthKit
|
|
|
2
|
import SQLite3
|
|
|
3
|
import XCTest
|
|
|
4
|
@testable import HealthProbe
|
|
|
5
|
|
|
|
6
|
final class SQLiteLargeImportPerformanceTests: XCTestCase {
|
|
|
7
|
private final class AsyncResultBox<T>: @unchecked Sendable {
|
|
|
8
|
var result: Result<T, Error>?
|
|
|
9
|
}
|
|
|
10
|
|
|
|
11
|
private var temporaryDirectory: URL!
|
|
|
12
|
|
|
|
13
|
override func setUpWithError() throws {
|
|
|
14
|
temporaryDirectory = FileManager.default.temporaryDirectory
|
|
|
15
|
.appending(path: "HealthProbeLargeImportTests-\(UUID().uuidString)", directoryHint: .isDirectory)
|
|
|
16
|
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true)
|
|
|
17
|
}
|
|
|
18
|
|
|
|
19
|
override func tearDownWithError() throws {
|
|
|
20
|
if let temporaryDirectory {
|
|
|
21
|
try? FileManager.default.removeItem(at: temporaryDirectory)
|
|
|
22
|
}
|
|
|
23
|
temporaryDirectory = nil
|
|
|
24
|
}
|
|
|
25
|
|
|
|
26
|
func testLargeSyntheticFullImportSmoke() throws {
|
|
|
27
|
let definitions = syntheticImportDefinitions()
|
|
|
28
|
let samples = makeSyntheticImportSamples(totalCount: 5_000, definitions: definitions)
|
|
|
29
|
let url = databaseURL(named: "LargeImportSmoke-\(UUID().uuidString).sqlite")
|
|
|
30
|
|
|
|
31
|
try runSyntheticImport(
|
|
|
32
|
samples: samples,
|
|
|
33
|
definitions: definitions,
|
|
|
34
|
databaseURL: url
|
|
|
35
|
)
|
|
|
36
|
}
|
|
|
37
|
|
|
|
38
|
func testLargeSyntheticFullImportBenchmark() throws {
|
|
|
39
|
let environment = ProcessInfo.processInfo.environment
|
|
|
40
|
let defaults = UserDefaults.standard
|
|
|
41
|
let isEnabled = environment["HP_ENABLE_LARGE_IMPORT_BENCHMARK"] == "1"
|
|
|
42
|
|| defaults.bool(forKey: "HP_ENABLE_LARGE_IMPORT_BENCHMARK")
|
|
|
43
|
|
|
|
44
|
guard isEnabled else {
|
|
|
45
|
throw XCTSkip("""
|
|
|
46
|
Large import benchmark is opt-in. Run with environment variable \
|
|
|
47
|
HP_ENABLE_LARGE_IMPORT_BENCHMARK=1 or launch argument \
|
|
|
48
|
-HP_ENABLE_LARGE_IMPORT_BENCHMARK YES, plus optional \
|
|
|
49
|
HP_LARGE_IMPORT_SAMPLE_COUNT / HP_LARGE_IMPORT_ITERATIONS overrides.
|
|
|
50
|
""")
|
|
|
51
|
}
|
|
|
52
|
|
|
|
53
|
let sampleCount = max(
|
|
|
54
|
1_000,
|
|
|
55
|
Int(environment["HP_LARGE_IMPORT_SAMPLE_COUNT"] ?? "")
|
|
|
56
|
?? defaults.integer(forKey: "HP_LARGE_IMPORT_SAMPLE_COUNT").nonZero
|
|
|
57
|
?? 250_000
|
|
|
58
|
)
|
|
|
59
|
let iterationCount = max(
|
|
|
60
|
1,
|
|
|
61
|
Int(environment["HP_LARGE_IMPORT_ITERATIONS"] ?? "")
|
|
|
62
|
?? defaults.integer(forKey: "HP_LARGE_IMPORT_ITERATIONS").nonZero
|
|
|
63
|
?? 1
|
|
|
64
|
)
|
|
|
65
|
let definitions = syntheticImportDefinitions()
|
|
|
66
|
let samples = makeSyntheticImportSamples(totalCount: sampleCount, definitions: definitions)
|
|
|
67
|
let options = XCTMeasureOptions()
|
|
|
68
|
options.iterationCount = iterationCount
|
|
|
69
|
|
|
|
70
|
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()], options: options) {
|
|
|
71
|
let url = databaseURL(named: "LargeImport-\(UUID().uuidString).sqlite")
|
|
|
72
|
do {
|
|
|
73
|
try runSyntheticImport(
|
|
|
74
|
samples: samples,
|
|
|
75
|
definitions: definitions,
|
|
|
76
|
databaseURL: url
|
|
|
77
|
)
|
|
|
78
|
} catch {
|
|
|
79
|
XCTFail("Large synthetic import benchmark failed: \(error)")
|
|
|
80
|
}
|
|
|
81
|
}
|
|
|
82
|
}
|
|
|
83
|
|
|
|
84
|
private func databaseURL(named name: String) -> URL {
|
|
|
85
|
temporaryDirectory.appending(path: name)
|
|
|
86
|
}
|
|
|
87
|
|
|
|
88
|
private func syntheticImportDefinitions() -> [(sampleType: HKQuantityType, unit: HKUnit, baseline: Double)] {
|
|
|
89
|
[
|
|
|
90
|
(HKQuantityType.quantityType(forIdentifier: .heartRate)!, HKUnit.count().unitDivided(by: .minute()), 55),
|
|
|
91
|
(HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!, .kilocalorie(), 1.5),
|
|
|
92
|
(HKQuantityType.quantityType(forIdentifier: .basalEnergyBurned)!, .kilocalorie(), 1.1),
|
|
|
93
|
(HKQuantityType.quantityType(forIdentifier: .stepCount)!, .count(), 12),
|
|
|
94
|
(HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!, .meter(), 8)
|
|
|
95
|
]
|
|
|
96
|
}
|
|
|
97
|
|
|
|
98
|
private func makeSyntheticImportSamples(
|
|
|
99
|
totalCount: Int,
|
|
|
100
|
definitions: [(sampleType: HKQuantityType, unit: HKUnit, baseline: Double)]
|
|
|
101
|
) -> [HKQuantitySample] {
|
|
|
102
|
return (0..<totalCount).map { index in
|
|
|
103
|
let definition = definitions[index % definitions.count]
|
|
|
104
|
let value = definition.baseline + Double(index % 17)
|
|
|
105
|
let start = Date(timeIntervalSince1970: 10_000 + Double(index * 60))
|
|
|
106
|
let end = start.addingTimeInterval(definition.sampleType.identifier == HKQuantityTypeIdentifier.heartRate.rawValue ? 5 : 60)
|
|
|
107
|
let quantity = HKQuantity(unit: definition.unit, doubleValue: value)
|
|
|
108
|
return HKQuantitySample(type: definition.sampleType, quantity: quantity, start: start, end: end)
|
|
|
109
|
}
|
|
|
110
|
}
|
|
|
111
|
|
|
|
112
|
private func runSyntheticImport(
|
|
|
113
|
samples: [HKQuantitySample],
|
|
|
114
|
definitions: [(sampleType: HKQuantityType, unit: HKUnit, baseline: Double)],
|
|
|
115
|
databaseURL url: URL
|
|
|
116
|
) throws {
|
|
|
117
|
let store = SQLiteHealthArchiveStore(databaseURL: url)
|
|
|
118
|
let observedAt = Date(timeIntervalSince1970: 5_000_000)
|
|
|
119
|
|
|
|
120
|
defer {
|
|
|
121
|
try? FileManager.default.removeItem(at: url)
|
|
|
122
|
try? FileManager.default.removeItem(at: URL(fileURLWithPath: url.path + "-shm"))
|
|
|
123
|
try? FileManager.default.removeItem(at: URL(fileURLWithPath: url.path + "-wal"))
|
|
|
124
|
}
|
|
|
125
|
|
|
|
126
|
try waitForArchiveOperation {
|
|
|
127
|
let observationID = try await store.beginObservation(
|
|
|
128
|
observedAt: observedAt,
|
|
|
129
|
triggerReason: "benchmarkLargeSyntheticImport",
|
|
|
130
|
selectedTypeSetHash: "synthetic-large-import"
|
|
|
131
|
)
|
|
|
132
|
_ = try await store.upsertSamples(
|
|
|
133
|
samples,
|
|
|
134
|
observedAt: observedAt,
|
|
|
135
|
observationID: observationID
|
|
|
136
|
)
|
|
|
137
|
for definition in definitions {
|
|
|
138
|
_ = try await store.markVerification(
|
|
|
139
|
sampleType: definition.sampleType,
|
|
|
140
|
verifiedAt: observedAt,
|
|
|
141
|
observationID: observationID
|
|
|
142
|
)
|
|
|
143
|
}
|
|
|
144
|
try await store.finishObservation(
|
|
|
145
|
observationID: observationID,
|
|
|
146
|
status: "completed",
|
|
|
147
|
endedAt: observedAt.addingTimeInterval(1)
|
|
|
148
|
)
|
|
|
149
|
return ()
|
|
|
150
|
}
|
|
|
151
|
|
|
|
152
|
XCTAssertEqual(try countRows(in: "samples", at: url), samples.count)
|
|
|
153
|
XCTAssertEqual(try countRows(in: "sample_versions", at: url), samples.count)
|
|
|
154
|
XCTAssertEqual(try countRows(in: "observation_type_runs", at: url), definitions.count)
|
|
|
155
|
}
|
|
|
156
|
|
|
|
157
|
private func waitForArchiveOperation<T>(_ operation: @escaping () async throws -> T) throws -> T {
|
|
|
158
|
let expectation = expectation(description: "archive operation")
|
|
|
159
|
let box = AsyncResultBox<T>()
|
|
|
160
|
|
|
|
161
|
Task {
|
|
|
162
|
do {
|
|
|
163
|
box.result = .success(try await operation())
|
|
|
164
|
} catch {
|
|
|
165
|
box.result = .failure(error)
|
|
|
166
|
}
|
|
|
167
|
expectation.fulfill()
|
|
|
168
|
}
|
|
|
169
|
|
|
|
170
|
wait(for: [expectation], timeout: 120)
|
|
|
171
|
return try XCTUnwrap(box.result).get()
|
|
|
172
|
}
|
|
|
173
|
|
|
|
174
|
private func countRows(in tableName: String, at url: URL) throws -> Int {
|
|
|
175
|
var db: OpaquePointer?
|
|
|
176
|
guard sqlite3_open_v2(url.path, &db, SQLITE_OPEN_READONLY | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK else {
|
|
|
177
|
sqlite3_close(db)
|
|
|
178
|
XCTFail("Could not open benchmark database")
|
|
|
179
|
return 0
|
|
|
180
|
}
|
|
|
181
|
defer { sqlite3_close(db) }
|
|
|
182
|
|
|
|
183
|
var statement: OpaquePointer?
|
|
|
184
|
guard sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM \(tableName)", -1, &statement, nil) == SQLITE_OK else {
|
|
|
185
|
sqlite3_finalize(statement)
|
|
|
186
|
XCTFail("Could not prepare benchmark count query")
|
|
|
187
|
return 0
|
|
|
188
|
}
|
|
|
189
|
defer { sqlite3_finalize(statement) }
|
|
|
190
|
|
|
|
191
|
guard sqlite3_step(statement) == SQLITE_ROW else {
|
|
|
192
|
return 0
|
|
|
193
|
}
|
|
|
194
|
return Int(sqlite3_column_int(statement, 0))
|
|
|
195
|
}
|
|
|
196
|
}
|
|
|
197
|
|
|
|
198
|
private extension Int {
|
|
|
199
|
var nonZero: Int? {
|
|
|
200
|
self == 0 ? nil : self
|
|
|
201
|
}
|
|
|
202
|
}
|