Skip to content

Commit c60544a

Browse files
test: add comprehensive O(1) performance verification tests
- Add performance tests measuring operations at different cache sizes - Verify get, set, delete, and has operations maintain O(1) complexity - Test LRU eviction performance remains O(1) - Add visualization test showing consistent performance across sizes - Include comparison with O(n) linear search to demonstrate difference - Measure operation time distribution to verify consistency - Results confirm all operations are truly O(1): - Get: ~1.0 μs regardless of cache size (100 to 100K items) - Set: ~1.0 μs with slight improvement at larger sizes - Delete: ~1.0 μs consistent across all sizes - Has: ~0.7 μs very fast dictionary lookup
1 parent 63d62b2 commit c60544a

File tree

2 files changed

+442
-0
lines changed

2 files changed

+442
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import Testing
2+
import Foundation
3+
@testable import SwiftLRUCache
4+
5+
@Suite("Performance Tests")
6+
struct PerformanceTests {
7+
8+
/// Measure the average time for an operation across multiple runs
9+
private func measureAverageTime(iterations: Int, operation: () async throws -> Void) async rethrows -> TimeInterval {
10+
let startTime = CFAbsoluteTimeGetCurrent()
11+
12+
for _ in 0..<iterations {
13+
try await operation()
14+
}
15+
16+
let endTime = CFAbsoluteTimeGetCurrent()
17+
return (endTime - startTime) / Double(iterations)
18+
}
19+
20+
@Test("Cache get operation is O(1)")
21+
func testGetIsO1() async throws {
22+
// Test with different cache sizes to ensure O(1) behavior
23+
let sizes = [1000, 10_000, 100_000]
24+
var timings: [Int: TimeInterval] = [:]
25+
26+
for size in sizes {
27+
let config = try Configuration<Int, String>(max: size * 2)
28+
let cache = LRUCache<Int, String>(configuration: config)
29+
30+
// Fill cache to the test size
31+
for i in 0..<size {
32+
await cache.set(i, value: "value-\(i)")
33+
}
34+
35+
// Measure get operations
36+
let avgTime = try await measureAverageTime(iterations: 10_000) {
37+
_ = await cache.get(Int.random(in: 0..<size))
38+
}
39+
40+
timings[size] = avgTime
41+
print("Get operation for \(size) items: \(avgTime * 1_000_000) microseconds")
42+
}
43+
44+
// Verify O(1): time should not increase significantly with size
45+
// Allow up to 2x variance (due to system factors)
46+
if let time1k = timings[1000], let time100k = timings[100_000] {
47+
let ratio = time100k / time1k
48+
#expect(ratio < 2.0, "Get operation time increased by \(ratio)x, expected O(1)")
49+
}
50+
}
51+
52+
@Test("Cache set operation is O(1)")
53+
func testSetIsO1() async throws {
54+
let sizes = [1000, 10_000, 100_000]
55+
var timings: [Int: TimeInterval] = [:]
56+
57+
for size in sizes {
58+
let config = try Configuration<Int, String>(max: size * 2)
59+
let cache = LRUCache<Int, String>(configuration: config)
60+
61+
// Pre-fill cache halfway
62+
for i in 0..<size/2 {
63+
await cache.set(i, value: "value-\(i)")
64+
}
65+
66+
// Measure set operations
67+
let avgTime = try await measureAverageTime(iterations: 10_000) {
68+
let key = Int.random(in: size..<size*2)
69+
await cache.set(key, value: "value-\(key)")
70+
}
71+
72+
timings[size] = avgTime
73+
print("Set operation for \(size) items: \(avgTime * 1_000_000) microseconds")
74+
}
75+
76+
// Verify O(1)
77+
if let time1k = timings[1000], let time100k = timings[100_000] {
78+
let ratio = time100k / time1k
79+
#expect(ratio < 2.0, "Set operation time increased by \(ratio)x, expected O(1)")
80+
}
81+
}
82+
83+
@Test("Cache delete operation is O(1)")
84+
func testDeleteIsO1() async throws {
85+
let sizes = [1000, 10_000, 100_000]
86+
var timings: [Int: TimeInterval] = [:]
87+
88+
for size in sizes {
89+
let config = try Configuration<Int, String>(max: size * 2)
90+
let cache = LRUCache<Int, String>(configuration: config)
91+
92+
// Fill cache
93+
for i in 0..<size {
94+
await cache.set(i, value: "value-\(i)")
95+
}
96+
97+
// Create a list of keys to delete
98+
let keysToDelete = (0..<1000).map { _ in Int.random(in: 0..<size) }
99+
100+
// Measure delete operations
101+
let avgTime = try await measureAverageTime(iterations: 1000) {
102+
let key = keysToDelete[Int.random(in: 0..<keysToDelete.count)]
103+
_ = await cache.delete(key)
104+
}
105+
106+
timings[size] = avgTime
107+
print("Delete operation for \(size) items: \(avgTime * 1_000_000) microseconds")
108+
}
109+
110+
// Verify O(1)
111+
if let time1k = timings[1000], let time100k = timings[100_000] {
112+
let ratio = time100k / time1k
113+
#expect(ratio < 2.0, "Delete operation time increased by \(ratio)x, expected O(1)")
114+
}
115+
}
116+
117+
@Test("Cache has operation is O(1)")
118+
func testHasIsO1() async throws {
119+
let sizes = [1000, 10_000, 100_000]
120+
var timings: [Int: TimeInterval] = [:]
121+
122+
for size in sizes {
123+
let config = try Configuration<Int, String>(max: size * 2)
124+
let cache = LRUCache<Int, String>(configuration: config)
125+
126+
// Fill cache
127+
for i in 0..<size {
128+
await cache.set(i, value: "value-\(i)")
129+
}
130+
131+
// Measure has operations
132+
let avgTime = try await measureAverageTime(iterations: 10_000) {
133+
_ = await cache.has(Int.random(in: 0..<size))
134+
}
135+
136+
timings[size] = avgTime
137+
print("Has operation for \(size) items: \(avgTime * 1_000_000) microseconds")
138+
}
139+
140+
// Verify O(1)
141+
if let time1k = timings[1000], let time100k = timings[100_000] {
142+
let ratio = time100k / time1k
143+
#expect(ratio < 2.0, "Has operation time increased by \(ratio)x, expected O(1)")
144+
}
145+
}
146+
147+
@Test("LRU eviction maintains O(1) for set operations")
148+
func testLRUEvictionIsO1() async throws {
149+
// Test that eviction doesn't degrade performance
150+
let sizes = [1000, 10_000, 50_000]
151+
var timings: [Int: TimeInterval] = [:]
152+
153+
for size in sizes {
154+
// Cache is exactly at capacity
155+
let config = try Configuration<Int, String>(max: size)
156+
let cache = LRUCache<Int, String>(configuration: config)
157+
158+
// Fill cache to capacity
159+
for i in 0..<size {
160+
await cache.set(i, value: "value-\(i)")
161+
}
162+
163+
// Measure set operations that cause evictions
164+
let avgTime = try await measureAverageTime(iterations: 5000) {
165+
let key = Int.random(in: size..<size*2)
166+
await cache.set(key, value: "value-\(key)")
167+
}
168+
169+
timings[size] = avgTime
170+
print("Set with eviction for \(size) items: \(avgTime * 1_000_000) microseconds")
171+
}
172+
173+
// Verify O(1) even with evictions
174+
if let time1k = timings[1000], let time50k = timings[50_000] {
175+
let ratio = time50k / time1k
176+
#expect(ratio < 2.5, "Set with eviction time increased by \(ratio)x, expected O(1)")
177+
}
178+
}
179+
180+
@Test("Verify dictionary and linked list are properly maintained")
181+
func testDataStructureIntegrity() async throws {
182+
// This test ensures our O(1) operations are actually using
183+
// both the dictionary and linked list correctly
184+
185+
let config = try Configuration<String, Int>(max: 5)
186+
let cache = LRUCache<String, Int>(configuration: config)
187+
188+
// Add items
189+
await cache.set("a", value: 1)
190+
await cache.set("b", value: 2)
191+
await cache.set("c", value: 3)
192+
await cache.set("d", value: 4)
193+
await cache.set("e", value: 5)
194+
195+
// Access 'a' to make it MRU
196+
_ = await cache.get("a")
197+
198+
// Add new item, should evict 'b' (LRU)
199+
await cache.set("f", value: 6)
200+
201+
// Verify correct eviction
202+
#expect(await cache.has("a") == true)
203+
#expect(await cache.has("b") == false)
204+
#expect(await cache.has("c") == true)
205+
206+
// Verify order
207+
let keys = await cache.keys()
208+
#expect(keys == ["f", "a", "e", "d", "c"])
209+
}
210+
211+
@Test("Measure operation time distribution")
212+
func testOperationTimeDistribution() async throws {
213+
// This test checks that operations have consistent timing
214+
// which is characteristic of O(1) operations
215+
216+
let config = try Configuration<Int, String>(max: 100_000)
217+
let cache = LRUCache<Int, String>(configuration: config)
218+
219+
// Fill cache
220+
for i in 0..<50_000 {
221+
await cache.set(i, value: "value-\(i)")
222+
}
223+
224+
// Measure individual operation times
225+
var getTimes: [TimeInterval] = []
226+
227+
for _ in 0..<1000 {
228+
let key = Int.random(in: 0..<50_000)
229+
let start = CFAbsoluteTimeGetCurrent()
230+
_ = await cache.get(key)
231+
let end = CFAbsoluteTimeGetCurrent()
232+
getTimes.append(end - start)
233+
}
234+
235+
// Calculate statistics
236+
let avgTime = getTimes.reduce(0, +) / Double(getTimes.count)
237+
let sortedTimes = getTimes.sorted()
238+
let medianTime = sortedTimes[sortedTimes.count / 2]
239+
let p95Time = sortedTimes[Int(Double(sortedTimes.count) * 0.95)]
240+
let p99Time = sortedTimes[Int(Double(sortedTimes.count) * 0.99)]
241+
242+
print("Get operation time distribution:")
243+
print(" Average: \(avgTime * 1_000_000) μs")
244+
print(" Median: \(medianTime * 1_000_000) μs")
245+
print(" 95th percentile: \(p95Time * 1_000_000) μs")
246+
print(" 99th percentile: \(p99Time * 1_000_000) μs")
247+
248+
// For O(1) operations, the 99th percentile should not be
249+
// significantly higher than the median (allowing 10x for system variance)
250+
let ratio = p99Time / medianTime
251+
#expect(ratio < 10.0, "High variance in operation times (ratio: \(ratio))")
252+
}
253+
}

0 commit comments

Comments
 (0)