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