@@ -158,6 +158,84 @@ fn setup_store() -> SqliteAccountStorage {
158158 setup_store_with_group ( group ( ) )
159159}
160160
161+ // Compare the existing readiness API with migration 0066’s index on synthetic history.
162+ // Setup, index construction, and the connection/KDF are outside the timed region.
163+ // Run: cargo test -p storage-sqlite --release --lib chat_startup_benchmark -- --ignored --nocapture
164+ #[ test]
165+ #[ ignore = "file-backed chat startup performance investigation" ]
166+ fn chat_startup_benchmark ( ) {
167+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
168+ let key = SqlCipherKey :: new ( "synthetic chat benchmark" ) . unwrap ( ) ;
169+ let store =
170+ SqliteAccountStorage :: open_encrypted ( dir. path ( ) . join ( "account.sqlite3" ) , & key) . unwrap ( ) ;
171+ store
172+ . save_account_projection_state (
173+ & StoredAccountState {
174+ label : "benchmark" . to_owned ( ) ,
175+ groups : vec ! [ group( ) ] ,
176+ ..StoredAccountState :: default ( )
177+ } ,
178+ 256 ,
179+ MAX_FUTURE_SKEW_SECS ,
180+ )
181+ . unwrap ( ) ;
182+ store
183+ . lock ( )
184+ . unwrap ( )
185+ . execute_batch ( "DROP INDEX idx_message_timeline_group_received" )
186+ . unwrap ( ) ;
187+ let mut seeded = 0 ;
188+ let body = "x" . repeat ( 512 ) ;
189+ for count in [ 10_000 , 100_000 ] {
190+ cgka_traits:: StorageProvider :: with_transaction ( & store, |store| {
191+ for index in seeded..count {
192+ let mut event = chat ( & format ! ( "{index:064x}" ) , REMOTE , index, & body) ;
193+ event. source_epoch = Some ( 1 ) ;
194+ store. record_app_event ( & event) ?;
195+ }
196+ Ok :: < _ , cgka_traits:: StorageError > ( ( ) )
197+ } )
198+ . unwrap ( ) ;
199+ seeded = count;
200+ let id = format ! ( "{:064x}" , count - 1 ) ;
201+ store
202+ . mark_timeline_message_read ( LOCAL , GROUP , & id, & no_mentions)
203+ . unwrap ( ) ;
204+ store. ensure_chat_list_rows ( LOCAL , & no_mentions) . unwrap ( ) ;
205+ let expected = store. chat_list_rows ( ChatListQuery :: default ( ) ) . unwrap ( ) ;
206+ assert_eq ! ( expected. len( ) , 1 ) ;
207+ assert_eq ! (
208+ expected[ 0 ] . last_message. as_ref( ) . unwrap( ) . message_id_hex,
209+ id
210+ ) ;
211+ assert_eq ! ( expected[ 0 ] . unread_count, 0 ) ;
212+ // Repeat A/B after dropping the index to expose cache/order effects.
213+ for mode in [ "baseline" , "indexed" , "baseline" , "indexed" ] {
214+ if mode == "indexed" {
215+ store. lock ( ) . unwrap ( ) . execute_batch ( "CREATE INDEX idx_message_timeline_group_received ON message_timeline(group_id_hex, received_at)" ) . unwrap ( ) ;
216+ }
217+ store. ensure_chat_list_rows ( LOCAL , & no_mentions) . unwrap ( ) ;
218+ let mut samples = Vec :: new ( ) ;
219+ for _ in 0 ..7 {
220+ let start = std:: time:: Instant :: now ( ) ;
221+ store. ensure_chat_list_rows ( LOCAL , & no_mentions) . unwrap ( ) ;
222+ let rows = store. chat_list_rows ( ChatListQuery :: default ( ) ) . unwrap ( ) ;
223+ samples. push ( start. elapsed ( ) . as_micros ( ) ) ;
224+ assert_eq ! ( rows, expected) ;
225+ }
226+ samples. sort_unstable ( ) ;
227+ eprintln ! ( "count={count} mode={mode} median_us={}" , samples[ 3 ] ) ;
228+ if mode == "indexed" {
229+ store
230+ . lock ( )
231+ . unwrap ( )
232+ . execute_batch ( "DROP INDEX idx_message_timeline_group_received" )
233+ . unwrap ( ) ;
234+ }
235+ }
236+ }
237+ }
238+
161239/// Preview work stays bounded for accepted history and displaced pending sends.
162240#[ test]
163241fn preview_query_work ( ) {
0 commit comments