From 37360975fc8df73a447627beb04a19fe7eedd317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 03:41:30 +0000 Subject: [PATCH 1/6] perf(chat-store): avoid reprepare for fixed point lookup limits --- crates/chat-store/src/lib.rs | 1 + crates/chat-store/src/lid.rs | 77 +++++-- crates/chat-store/src/point_lookup.rs | 60 ++++++ crates/chat-store/src/queries.rs | 26 +-- crates/chat-store/tests/point_lookup.rs | 49 +++++ .../chat-store/tests/point_lookup_profile.rs | 194 ++++++++++++++++++ docs/measurements/sqlite-point-lookup.csv | 85 ++++++++ .../sqlite-point-reprepare-memory.txt | 97 +++++++++ .../sqlite-point-reprepare-time.txt | 97 +++++++++ docs/sqlite-point-lookup-performance.md | 134 ++++++++++++ scripts/measure-sqlite-point-lookup.py | 85 ++++++++ scripts/sqlite-point-reprepare.c | 139 +++++++++++++ 12 files changed, 1017 insertions(+), 27 deletions(-) create mode 100644 crates/chat-store/src/point_lookup.rs create mode 100644 crates/chat-store/tests/point_lookup.rs create mode 100644 crates/chat-store/tests/point_lookup_profile.rs create mode 100644 docs/measurements/sqlite-point-lookup.csv create mode 100644 docs/measurements/sqlite-point-reprepare-memory.txt create mode 100644 docs/measurements/sqlite-point-reprepare-time.txt create mode 100644 docs/sqlite-point-lookup-performance.md create mode 100644 scripts/measure-sqlite-point-lookup.py create mode 100644 scripts/sqlite-point-reprepare.c diff --git a/crates/chat-store/src/lib.rs b/crates/chat-store/src/lib.rs index cc455916..b3ecff05 100644 --- a/crates/chat-store/src/lib.rs +++ b/crates/chat-store/src/lib.rs @@ -38,6 +38,7 @@ mod error; mod fts; mod lid; mod materialize; +mod point_lookup; mod queries; mod schema; mod spawn; diff --git a/crates/chat-store/src/lid.rs b/crates/chat-store/src/lid.rs index 6ec7e883..1dee8a56 100644 --- a/crates/chat-store/src/lid.rs +++ b/crates/chat-store/src/lid.rs @@ -80,22 +80,26 @@ fn counterpart_of( use schema::lid_pn_mapping::dsl; let user = jid.user.as_str(); if jid.is_lid() { - return Ok(dsl::lid_pn_mapping - .filter(dsl::device_id.eq(device_id).and(dsl::lid.eq(user))) - .select(dsl::phone_number) - .first::(conn) - .optional()? - .map(|pn| Jid::new(pn, Server::Pn).to_string())); - } - Ok(dsl::lid_pn_mapping - .filter(dsl::device_id.eq(device_id).and(dsl::phone_number.eq(user))) - // The lid tiebreak keeps routing stable when updated_at ties — - // flapping between counterpart keys would re-split the thread. - .order((dsl::updated_at.desc(), dsl::lid.desc())) - .select(dsl::lid) - .first::(conn) + return Ok(crate::point_lookup::First( + dsl::lid_pn_mapping + .filter(dsl::device_id.eq(device_id).and(dsl::lid.eq(user))) + .select(dsl::phone_number), + ) + .get_result::(conn) .optional()? - .map(|lid| Jid::new(lid, Server::Lid).to_string())) + .map(|pn| Jid::new(pn, Server::Pn).to_string())); + } + Ok(crate::point_lookup::First( + dsl::lid_pn_mapping + .filter(dsl::device_id.eq(device_id).and(dsl::phone_number.eq(user))) + // The lid tiebreak keeps routing stable when updated_at ties — + // flapping between counterpart keys would re-split the thread. + .order((dsl::updated_at.desc(), dsl::lid.desc())) + .select(dsl::lid), + ) + .get_result::(conn) + .optional()? + .map(|lid| Jid::new(lid, Server::Lid).to_string())) } /// Every key the peer's rows may live under: the given key, its PN, and all @@ -710,3 +714,46 @@ pub(crate) fn merge_split_chat( cs.message_chats.insert(dest.to_string()); Ok(dest.to_string()) } + +#[cfg(test)] +mod point_lookup_tests { + use super::*; + use diesel::connection::SimpleConnection; + + #[test] + fn counterpart_preserves_accounts_absence_and_latest_lid_ties() { + let mut conn = SqliteConnection::establish(":memory:").unwrap(); + conn.batch_execute( + "CREATE TABLE lid_pn_mapping(device_id INTEGER NOT NULL,lid TEXT NOT NULL,phone_number TEXT NOT NULL,updated_at BIGINT NOT NULL,PRIMARY KEY(lid,device_id));\ + INSERT INTO lid_pn_mapping VALUES(1,'100','500',1),(1,'200','500',2),(1,'300','500',2),(2,'100','900',9),(2,'400','500',9)", + ).unwrap(); + // Repeated reads also exercise cached statements with different binds. + for _ in 0..3 { + for (account, input, expected) in [ + (1, "100@lid", Some("500@s.whatsapp.net")), + (2, "100@lid", Some("900@s.whatsapp.net")), + (1, "500@s.whatsapp.net", Some("300@lid")), + (2, "500@s.whatsapp.net", Some("400@lid")), + (1, "400@lid", None), + (1, "999@s.whatsapp.net", None), + (3, "100@lid", None), + ] { + let jid = input.parse::().unwrap(); + assert_eq!( + counterpart_of(&mut conn, account, &jid).unwrap().as_deref(), + expected + ); + } + } + assert_eq!( + counterpart_chat_key(&mut conn, 1, "100:5@lid") + .unwrap() + .as_deref(), + Some("500@s.whatsapp.net") + ); + assert_eq!( + counterpart_chat_key(&mut conn, 1, "123@g.us").unwrap(), + None + ); + } +} diff --git a/crates/chat-store/src/point_lookup.rs b/crates/chat-store/src/point_lookup.rs new file mode 100644 index 00000000..bd229495 --- /dev/null +++ b/crates/chat-store/src/point_lookup.rs @@ -0,0 +1,60 @@ +//! Fixed first-row limit for the measured contact and identity lookups. +//! +//! SQLite 3.53 specializes a bound LIMIT on every step after rebinding. +//! These queries always need one row. A literal keeps limit-aware planning +//! without a runtime limit to specialize. See docs/sqlite-point-lookup-performance.md. +use diesel::prelude::*; +use diesel::query_builder::{AstPass, Query, QueryFragment, QueryId}; +use diesel::sqlite::Sqlite; + +#[derive(QueryId)] +pub(crate) struct First(pub(crate) Q); + +impl Query for First { + type SqlType = Q::SqlType; +} + +impl> QueryFragment for First { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Sqlite>) -> QueryResult<()> { + self.0.walk_ast(out.reborrow())?; + out.push_sql(" LIMIT 1"); + Ok(()) + } +} + +impl RunQueryDsl for First {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::schema::contacts::dsl; + use diesel::connection::SimpleConnection; + + #[test] + fn fixed_limit_keeps_filters_binds_and_optional_first_semantics() { + let mut conn = SqliteConnection::establish(":memory:").unwrap(); + conn.batch_execute("CREATE TABLE contacts(device_id INTEGER NOT NULL,jid TEXT NOT NULL,push_name TEXT,full_name TEXT,first_name TEXT,business_name TEXT,PRIMARY KEY(device_id,jid)); INSERT INTO contacts(device_id,jid) VALUES(1,'one'),(1,'two'),(2,'one')").unwrap(); + for (account, key) in [ + (1, "one"), + (1, "two"), + (2, "one"), + (1, "absent"), + (3, "one"), + ] { + let query = || { + dsl::contacts + .filter(dsl::device_id.eq(account).and(dsl::jid.eq(key))) + .select(dsl::jid) + }; + let expected = query().first::(&mut conn).optional().unwrap(); + let fixed = First(query()); + let sql = diesel::debug_query::(&fixed).to_string(); + assert!(sql.contains(" LIMIT 1 -- binds:"), "{sql}"); + assert!(!sql.contains("LIMIT ?")); + assert_eq!( + fixed.get_result::(&mut conn).optional().unwrap(), + expected + ); + } + } +} diff --git a/crates/chat-store/src/queries.rs b/crates/chat-store/src/queries.rs index c3a285a9..5d63ff81 100644 --- a/crates/chat-store/src/queries.rs +++ b/crates/chat-store/src/queries.rs @@ -2166,18 +2166,20 @@ impl ChatStore { let row: Option<(ContactRow, Option)> = self .db() .read(move |conn| { - let row: Option = dsl::contacts - .filter(dsl::device_id.eq(device_id).and(dsl::jid.eq(&jid_str))) - .select(( - dsl::jid, - dsl::push_name, - dsl::full_name, - dsl::first_name, - dsl::business_name, - )) - .first(conn) - .optional() - .map_err(db_err)?; + let row: Option = crate::point_lookup::First( + dsl::contacts + .filter(dsl::device_id.eq(device_id).and(dsl::jid.eq(&jid_str))) + .select(( + dsl::jid, + dsl::push_name, + dsl::full_name, + dsl::first_name, + dsl::business_name, + )), + ) + .get_result(conn) + .optional() + .map_err(db_err)?; let labels = match &row { Some((jid, _, _, _, _)) => { labels_for(conn, device_id, std::slice::from_ref(jid))?.remove(jid) diff --git a/crates/chat-store/tests/point_lookup.rs b/crates/chat-store/tests/point_lookup.rs new file mode 100644 index 00000000..4e830ced --- /dev/null +++ b/crates/chat-store/tests/point_lookup.rs @@ -0,0 +1,49 @@ +mod common; +use common::*; + +#[tokio::test] +async fn contact_lookup_keeps_account_labels_optional_fields_and_missing_rows() { + let (db, store) = test_store().await; + db.database().provision_device(2).await.unwrap(); + db.shared().run(|conn| { + diesel::sql_query("INSERT INTO contacts(device_id,jid,push_name,full_name,first_name,business_name) VALUES(1,'559900000001@s.whatsapp.net','push','full','first','business'),(2,'559900000001@s.whatsapp.net','other','other','other','other'),(1,'559900000002@s.whatsapp.net',NULL,NULL,NULL,NULL)") + .execute(conn).map_err(db_err)?; + diesel::sql_query("INSERT INTO contact_labels(device_id,jid,alias,tags) VALUES(1,'559900000001@s.whatsapp.net','alias','[\"tag\"]'),(2,'559900000001@s.whatsapp.net','other','[]')") + .execute(conn).map_err(db_err)?; + Ok(()) + }).await.unwrap(); + for _ in 0..3 { + let contact = store + .contact(&jid("559900000001:7@s.whatsapp.net")) + .await + .unwrap() + .unwrap(); + assert_eq!(contact.jid, jid(PEER)); + assert_eq!(contact.push_name.as_deref(), Some("push")); + assert_eq!(contact.full_name.as_deref(), Some("full")); + assert_eq!(contact.first_name.as_deref(), Some("first")); + assert_eq!(contact.business_name.as_deref(), Some("business")); + assert_eq!(contact.alias.as_deref(), Some("alias")); + assert_eq!(contact.tags, vec!["tag"]); + let blank = store + .contact(&jid("559900000002@s.whatsapp.net")) + .await + .unwrap() + .unwrap(); + assert!( + blank.full_name.is_none() + && blank.push_name.is_none() + && blank.first_name.is_none() + && blank.business_name.is_none() + && blank.alias.is_none() + ); + assert!(blank.tags.is_empty()); + assert!( + store + .contact(&jid("559900000003@s.whatsapp.net")) + .await + .unwrap() + .is_none() + ); + } +} diff --git a/crates/chat-store/tests/point_lookup_profile.rs b/crates/chat-store/tests/point_lookup_profile.rs new file mode 100644 index 00000000..9605e47e --- /dev/null +++ b/crates/chat-store/tests/point_lookup_profile.rs @@ -0,0 +1,194 @@ +//! Linux release workload for contact reads and nested identity resolution. +//! Synthetic setup and warmup are excluded. Rust allocation traffic is measured +//! separately from timing; SQLite's C allocator is outside GlobalAlloc. +#![cfg(target_os = "linux")] +#![allow(clippy::disallowed_methods)] +// The profiling allocator and Linux CPU clock wrap their documented FFI only. +#![allow(unsafe_code)] + +mod common; +use common::*; +use portable_atomic::AtomicU64; +use std::alloc::{GlobalAlloc, Layout, System}; +use std::sync::atomic::{AtomicBool, Ordering}; + +static COUNT: AtomicBool = AtomicBool::new(false); +static CALLS: AtomicU64 = AtomicU64::new(0); +static BYTES: AtomicU64 = AtomicU64::new(0); +struct Allocator; + +static LIVE: AtomicU64 = AtomicU64::new(0); +static PEAK: AtomicU64 = AtomicU64::new(0); +fn allocated(size: usize) { + let live = LIVE.fetch_add(size as u64, Ordering::Relaxed) + size as u64; + if COUNT.load(Ordering::Relaxed) { + CALLS.fetch_add(1, Ordering::Relaxed); + BYTES.fetch_add(size as u64, Ordering::Relaxed); + PEAK.fetch_max(live, Ordering::Relaxed); + } +} +unsafe impl GlobalAlloc for Allocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let p = unsafe { System.alloc(layout) }; + if !p.is_null() { + allocated(layout.size()); + } + p + } + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + let p = unsafe { System.alloc_zeroed(layout) }; + if !p.is_null() { + allocated(layout.size()); + } + p + } + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 { + let p = unsafe { System.realloc(ptr, layout, size) }; + if !p.is_null() { + LIVE.fetch_sub(layout.size() as u64, Ordering::Relaxed); + allocated(size); + } + p + } + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + LIVE.fetch_sub(layout.size() as u64, Ordering::Relaxed); + unsafe { System.dealloc(ptr, layout) } + } +} + +#[global_allocator] +static ALLOCATOR: Allocator = Allocator; + +fn cpu_ns() -> u64 { + #[repr(C)] + struct Timespec { + seconds: std::ffi::c_long, + nanos: std::ffi::c_long, + } + unsafe extern "C" { + fn clock_gettime(clock: std::ffi::c_int, time: *mut Timespec) -> std::ffi::c_int; + } + let mut time = Timespec { + seconds: 0, + nanos: 0, + }; + // Linux CLOCK_PROCESS_CPUTIME_ID includes all pool/runtime threads. + assert_eq!(unsafe { clock_gettime(2, &mut time) }, 0); + time.seconds as u64 * 1_000_000_000 + time.nanos as u64 +} + +fn rss_kib() -> u64 { + std::fs::read_to_string("/proc/self/status") + .unwrap() + .lines() + .find_map(|line| line.strip_prefix("VmRSS:")) + .unwrap() + .split_whitespace() + .next() + .unwrap() + .parse() + .unwrap() +} + +const CHATS: usize = 48; + +#[tokio::test] +#[ignore = "Linux release synthetic point lookup measurement"] +async fn point_lookup_workload() { + let case = std::env::var("POINT_CASE").unwrap_or_else(|_| "contact".into()); + let iterations: usize = std::env::var("POINT_ITERATIONS") + .unwrap_or_else(|_| "200".into()) + .parse() + .unwrap(); + let allocations = std::env::var("POINT_ALLOC").is_ok_and(|v| v == "1"); + let (db, store) = test_store().await; + db.database().provision_device(2).await.unwrap(); + db.shared().run(|conn| { + diesel::sql_query("WITH RECURSIVE n(i) AS (VALUES(0) UNION ALL SELECT i+1 FROM n WHERE i<47) INSERT INTO contacts(device_id,jid,full_name) SELECT 1,printf('55990000%04d@s.whatsapp.net',i),'synthetic' FROM n").execute(conn).map_err(db_err)?; + diesel::sql_query("INSERT INTO contacts SELECT 2,jid,'other', 'other',NULL,NULL FROM contacts WHERE device_id=1").execute(conn).map_err(db_err)?; + diesel::sql_query("WITH RECURSIVE n(i) AS (VALUES(0) UNION ALL SELECT i+1 FROM n WHERE i<47) INSERT INTO lid_pn_mapping(device_id,lid,phone_number,created_at,updated_at,learning_source) SELECT 1,printf('77000000%04d',i),printf('55990000%04d',i),1,1,'synthetic' FROM n").execute(conn).map_err(db_err)?; + diesel::sql_query("INSERT INTO lid_pn_mapping(device_id,lid,phone_number,created_at,updated_at,learning_source) SELECT 2,lid,'999',1,1,'synthetic' FROM lid_pn_mapping WHERE device_id=1").execute(conn).map_err(db_err)?; + Ok(()) + }).await.unwrap(); + let chats: Vec = (0..CHATS) + .map(|i| { + jid(&if case == "lid" { + format!("77000000{i:04}@lid") + } else { + format!("55990000{i:04}@s.whatsapp.net") + }) + }) + .collect(); + let missing = jid("559999999999@s.whatsapp.net"); + // Include present/absent contact reads. LID reaction reads exercise the + // nested identity lookup and its public pool/decoding path with no rows. + for _ in 0..3 { + for chat in &chats { + if case == "contact" { + assert_eq!( + store + .contact(chat) + .await + .unwrap() + .unwrap() + .full_name + .as_deref(), + Some("synthetic") + ); + assert!(store.contact(&missing).await.unwrap().is_none()); + } else { + assert!( + store + .reactions_for(chat, vec!["synthetic-id".into()]) + .await + .unwrap() + .is_empty() + ); + } + } + } + let rss_before = rss_kib(); + CALLS.store(0, Ordering::Relaxed); + BYTES.store(0, Ordering::Relaxed); + let live_before = LIVE.load(Ordering::Relaxed); + PEAK.store(live_before, Ordering::Relaxed); + let cpu_before = cpu_ns(); + COUNT.store(allocations, Ordering::Relaxed); + let start = wacore::time::Instant::now(); + for _ in 0..iterations { + for chat in &chats { + if case == "contact" { + assert_eq!( + store + .contact(chat) + .await + .unwrap() + .unwrap() + .full_name + .as_deref(), + Some("synthetic") + ); + assert!(store.contact(&missing).await.unwrap().is_none()); + } else { + assert!( + store + .reactions_for(chat, vec!["synthetic-id".into()]) + .await + .unwrap() + .is_empty() + ); + } + } + } + let elapsed_ns = start.elapsed().as_nanos(); + COUNT.store(false, Ordering::Relaxed); + let cpu_ns = cpu_ns() - cpu_before; + let calls = CALLS.load(Ordering::Relaxed); + let bytes = BYTES.load(Ordering::Relaxed); + let live_after = LIVE.load(Ordering::Relaxed); + let peak = PEAK.load(Ordering::Relaxed); + let rss_after = rss_kib(); + println!( + "POINT rust_live_before={live_before} rust_live_after={live_after} rust_peak={peak} elapsed_ns={elapsed_ns} cpu_ns={cpu_ns} alloc_calls={calls} alloc_bytes={bytes} rss_before_kib={rss_before} rss_after_kib={rss_after} rows=48 checksum=0 quotes=0 reactions=0" + ); +} diff --git a/docs/measurements/sqlite-point-lookup.csv b/docs/measurements/sqlite-point-lookup.csv new file mode 100644 index 00000000..92ba2d68 --- /dev/null +++ b/docs/measurements/sqlite-point-lookup.csv @@ -0,0 +1,85 @@ +label,case,allocations,pair,iterations,rust_live_before,rust_live_after,rust_peak,elapsed_ns,cpu_ns,alloc_calls,alloc_bytes,rss_before_kib,rss_after_kib,rows,checksum,quotes,reactions +head,contact,0,1,200,52807,52974,52807,215169740,215145381,0,0,7548,7648,48,0,0,0 +base,contact,0,1,200,57866,63626,57866,440932474,440910854,0,0,8352,9576,48,0,0,0 +base,contact,0,2,200,44752,48949,44752,362011994,361978772,0,0,6908,7552,48,0,0,0 +head,contact,0,2,200,57610,58010,57610,215556467,215555313,0,0,8016,8196,48,0,0,0 +head,contact,0,3,200,53562,53962,53562,194592107,194573155,0,0,7640,7724,48,0,0,0 +base,contact,0,3,200,66455,65831,66455,374550736,374544826,0,0,9000,10108,48,0,0,0 +base,contact,0,4,200,44752,48949,44752,351471562,351424513,0,0,6840,7492,48,0,0,0 +head,contact,0,4,200,44903,47506,44903,184263288,184218830,0,0,7084,7232,48,0,0,0 +head,contact,0,5,200,57742,62158,57742,192481448,192480791,0,0,7776,8348,48,0,0,0 +base,contact,0,5,200,44752,47424,44752,348673004,348629981,0,0,7004,7472,48,0,0,0 +base,contact,0,6,200,69798,69174,69798,438543642,438455998,0,0,9128,10320,48,0,0,0 +head,contact,0,6,200,44903,46971,44903,206695407,206693890,0,0,6848,6972,48,0,0,0 +head,contact,0,7,200,54307,57241,54307,191959062,191926682,0,0,7668,8024,48,0,0,0 +base,contact,0,7,200,44752,61723,44752,430210869,430162157,0,0,7068,9504,48,0,0,0 +head,contact,1,1,200,55350,55110,60369,248139933,248110336,374401,26265616,7744,7920,48,0,0,0 +base,contact,1,1,200,54861,55261,60858,437165810,437118951,374401,26419216,8284,8936,48,0,0,0 +base,contact,1,2,200,55314,56025,64694,431123476,431044518,374408,26419950,8260,8964,48,0,0,0 +head,contact,1,2,200,44752,47191,50484,193445028,193442563,374458,26269054,6964,7224,48,0,0,0 +head,contact,1,3,200,44752,48716,52055,196517298,196515685,374478,26271240,6828,7172,48,0,0,0 +base,contact,1,3,200,54408,56482,63285,421279239,421253094,374437,26420356,8116,8916,48,0,0,0 +base,contact,1,4,200,64017,63841,69108,392146031,392117731,374402,26419280,8804,9768,48,0,0,0 +head,contact,1,4,200,55419,55563,60776,204210801,204177300,374401,26265616,7772,7920,48,0,0,0 +head,contact,1,5,200,58401,57841,63054,209340326,209240273,374402,26265680,7988,8220,48,0,0,0 +base,contact,1,5,200,44752,49018,52749,392492979,392463556,374490,26425220,6936,7708,48,0,0,0 +base,contact,1,6,200,44519,47122,49550,345160871,345126214,374434,26421894,6956,7284,48,0,0,0 +head,contact,1,6,200,45205,47488,50095,189160893,189147255,374435,26268358,7004,7148,48,0,0,0 +head,contact,1,7,200,58168,58312,62373,193102887,193073967,374401,26265616,8092,8304,48,0,0,0 +base,contact,1,7,200,44752,47122,49647,357844248,357810111,374440,26422084,6980,7372,48,0,0,0 +head,lid,0,1,200,44666,47292,44666,221306605,221257345,0,0,7236,7564,48,0,0,0 +base,lid,0,1,200,44081,48712,44081,287130709,287090733,0,0,7112,7940,48,0,0,0 +base,lid,0,2,200,65634,58633,65634,346882960,346839095,0,0,9192,9748,48,0,0,0 +head,lid,0,2,200,53315,55646,53315,275259667,275216646,0,0,8176,8792,48,0,0,0 +head,lid,0,3,200,45585,49270,45585,265112398,265002153,0,0,7316,7876,48,0,0,0 +base,lid,0,3,200,56288,55024,56288,296480486,296469014,0,0,8852,9196,48,0,0,0 +base,lid,0,4,200,44364,47338,44364,325950648,325890951,0,0,7120,7748,48,0,0,0 +head,lid,0,4,200,44771,48817,44771,214343509,214316146,0,0,7160,7684,48,0,0,0 +head,lid,0,5,200,44593,58482,44593,284975697,284931355,0,0,7068,9256,48,0,0,0 +base,lid,0,5,200,44081,47594,44081,285033480,284359546,0,0,7044,7748,48,0,0,0 +base,lid,0,6,200,74379,74746,74379,373397334,373369754,0,0,9396,11448,48,0,0,0 +head,lid,0,6,200,56498,55431,56498,331516477,331469613,0,0,8612,8880,48,0,0,0 +head,lid,0,7,200,57142,55431,57142,391790583,390169743,0,0,8400,8848,48,0,0,0 +base,lid,0,7,200,63225,63389,63225,510373818,510302237,0,0,9144,10136,48,0,0,0 +head,lid,1,1,200,44817,54086,59166,304899241,304010297,556971,34283706,7324,8688,48,0,0,0 +base,lid,1,1,200,58525,58139,63763,319012144,318981750,556814,34349260,9136,9656,48,0,0,0 +base,lid,1,2,200,56479,55431,60909,363361766,363266927,556850,34350880,8640,9228,48,0,0,0 +head,lid,1,2,200,58552,59080,61486,314393776,314344345,556801,34272016,8636,8968,48,0,0,0 +head,lid,1,3,200,45178,48561,51991,246313799,246193402,556860,34277070,7252,7724,48,0,0,0 +base,lid,1,3,200,55835,55129,59839,436397266,372322076,556813,34349196,8624,9052,48,0,0,0 +base,lid,1,4,200,58525,82003,98566,437521385,333921054,557259,34382300,9196,11996,48,0,0,0 +head,lid,1,4,200,46869,48561,51991,301380637,245312736,556827,34274392,7308,7532,48,0,0,0 +head,lid,1,5,200,53709,53725,58179,320550378,248821888,556801,34272016,8556,8656,48,0,0,0 +base,lid,1,5,200,75705,80676,96460,402675964,340719251,556981,34365030,9720,11760,48,0,0,0 +base,lid,1,6,200,44876,49014,52700,363702490,291637463,556890,34354820,7092,7976,48,0,0,0 +head,lid,1,6,200,44620,48561,51735,289188457,228041021,556872,34277450,7016,7580,48,0,0,0 +head,lid,1,7,200,59104,56927,63941,301700091,227564842,556856,34274270,8504,9024,48,0,0,0 +base,lid,1,7,200,45178,65095,73901,505857410,389452750,557218,34375610,7188,10328,48,0,0,0 +head,pn,0,1,200,44619,69425,44619,349546946,259056254,0,0,7124,10184,48,0,0,0 +base,pn,0,1,200,68039,63946,68039,377484785,282657475,0,0,8740,9484,48,0,0,0 +base,pn,0,2,200,56182,74228,56182,358695583,278462256,0,0,8672,10640,48,0,0,0 +head,pn,0,2,200,71694,72947,71694,422972536,314845099,0,0,9068,10420,48,0,0,0 +head,pn,0,3,200,53255,53829,53255,324725622,253897602,0,0,8392,8564,48,0,0,0 +base,pn,0,3,200,60146,56578,60146,284490456,251788074,0,0,8624,9000,48,0,0,0 +base,pn,0,4,200,44921,47140,44921,201979004,201946543,0,0,7168,7440,48,0,0,0 +head,pn,0,4,200,44770,46989,44770,197821312,197798342,0,0,7224,7504,48,0,0,0 +head,pn,0,5,200,44770,46733,44770,200186119,200183437,0,0,7264,7532,48,0,0,0 +base,pn,0,5,200,60914,56578,60914,205525349,205517069,0,0,8588,8972,48,0,0,0 +base,pn,0,6,200,44875,47442,44875,203443023,203431677,0,0,7008,7476,48,0,0,0 +head,pn,0,6,200,56346,54826,56346,212132780,212130655,0,0,8436,8636,48,0,0,0 +head,pn,0,7,200,44665,49722,44665,211868311,211865413,0,0,7108,7912,48,0,0,0 +base,pn,0,7,200,54220,53724,54220,204392773,204379379,0,0,8352,8528,48,0,0,0 +head,pn,1,1,200,45584,48816,51489,200802498,200793889,499254,30504080,7368,7788,48,0,0,0 +base,pn,1,1,200,44514,48816,51745,210852357,210844590,499266,30504460,7144,7672,48,0,0,0 +base,pn,1,2,200,44336,48711,52152,219890582,219784199,499285,30505310,7036,7716,48,0,0,0 +head,pn,1,2,200,57009,55041,58994,220761664,220608518,499202,30499280,8612,8808,48,0,0,0 +head,pn,1,3,200,44921,48711,52152,211287436,211284815,499266,30504460,7204,7716,48,0,0,0 +base,pn,1,3,200,50487,53934,58920,218204233,217246666,499306,30505662,7480,8528,48,0,0,0 +base,pn,1,4,200,54732,54419,59908,216622591,216604599,499208,30499950,8456,8668,48,0,0,0 +head,pn,1,4,200,60251,63905,73947,231532856,231522667,499362,30510092,8732,9676,48,0,0,0 +head,pn,1,5,200,58545,57621,64111,235105556,235103702,499275,30502584,8364,8972,48,0,0,0 +base,pn,1,5,200,64261,63191,71240,256133628,256117377,499288,30507204,8776,9468,48,0,0,0 +base,pn,1,6,200,71222,74397,86734,240799630,240783466,499407,30509550,8968,10572,48,0,0,0 +head,pn,1,6,200,44816,48967,52152,216399896,216390611,499260,30504270,7220,7680,48,0,0,0 +head,pn,1,7,200,58465,58993,63714,226744233,226742804,499201,30499216,8804,9192,48,0,0,0 +base,pn,1,7,200,69753,62409,70906,230680552,228704132,499202,30499280,8844,9308,48,0,0,0 diff --git a/docs/measurements/sqlite-point-reprepare-memory.txt b/docs/measurements/sqlite-point-reprepare-memory.txt new file mode 100644 index 00000000..c1388364 --- /dev/null +++ b/docs/measurements/sqlite-point-reprepare-memory.txt @@ -0,0 +1,97 @@ +SQLite 3.53.2 source 2026-06-03 19:12:13 d6e03d8c777cfa2d35e3b60d8ec3e0187f3e9f99d8e2ee9cac695fd6fcdf1a24 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=0 prepare_ns=9424 wall_ns=174255992 cpu_ns=174240746 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=1240000 alloc_bytes=144480000 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=0 prepare_ns=7491 wall_ns=7863529 cpu_ns=7865226 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=1 prepare_ns=9545 wall_ns=7895638 cpu_ns=7897513 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=1 prepare_ns=8873 wall_ns=171569376 cpu_ns=171569132 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=1240000 alloc_bytes=144480000 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=2 prepare_ns=7542 wall_ns=172181987 cpu_ns=172182079 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=1240000 alloc_bytes=144480000 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=2 prepare_ns=7391 wall_ns=8238333 cpu_ns=8239749 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=3 prepare_ns=10446 wall_ns=7590668 cpu_ns=7591100 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=3 prepare_ns=8142 wall_ns=171644489 cpu_ns=171606147 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=1240000 alloc_bytes=144480000 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=4 prepare_ns=7531 wall_ns=170779437 cpu_ns=170779555 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=1240000 alloc_bytes=144480000 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=4 prepare_ns=7441 wall_ns=7832974 cpu_ns=7833628 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=5 prepare_ns=7421 wall_ns=7768326 cpu_ns=7770476 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=5 prepare_ns=8243 wall_ns=171009703 cpu_ns=171009215 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=1240000 alloc_bytes=144480000 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=6 prepare_ns=7542 wall_ns=172381267 cpu_ns=172381182 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=1240000 alloc_bytes=144480000 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=6 prepare_ns=8333 wall_ns=7913144 cpu_ns=7914799 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=7 prepare_ns=9875 wall_ns=7723829 cpu_ns=7725158 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=7 prepare_ns=8593 wall_ns=173611808 cpu_ns=173611110 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=1240000 alloc_bytes=144480000 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=0 prepare_ns=5268 wall_ns=103080526 cpu_ns=103082404 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=640000 alloc_bytes=85440000 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=0 prepare_ns=4968 wall_ns=7381504 cpu_ns=7382122 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=1 prepare_ns=5478 wall_ns=7445591 cpu_ns=7446656 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=1 prepare_ns=5849 wall_ns=104920494 cpu_ns=104907672 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=640000 alloc_bytes=85440000 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=2 prepare_ns=4606 wall_ns=104958792 cpu_ns=104948282 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=640000 alloc_bytes=85440000 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=2 prepare_ns=4527 wall_ns=8713868 cpu_ns=8716644 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=3 prepare_ns=7842 wall_ns=7693082 cpu_ns=7695645 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=3 prepare_ns=6950 wall_ns=110876644 cpu_ns=110878507 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=640000 alloc_bytes=85440000 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=4 prepare_ns=4687 wall_ns=116305201 cpu_ns=116306620 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=640000 alloc_bytes=85440000 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=4 prepare_ns=4898 wall_ns=7324789 cpu_ns=7325918 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=5 prepare_ns=5828 wall_ns=7773694 cpu_ns=7775643 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=5 prepare_ns=6970 wall_ns=109069550 cpu_ns=109071041 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=640000 alloc_bytes=85440000 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=6 prepare_ns=4697 wall_ns=107130329 cpu_ns=107131318 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=640000 alloc_bytes=85440000 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=6 prepare_ns=4667 wall_ns=7677399 cpu_ns=7678049 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=7 prepare_ns=6580 wall_ns=7998943 cpu_ns=7999754 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=7 prepare_ns=6620 wall_ns=103318705 cpu_ns=103319895 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=640000 alloc_bytes=85440000 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=0 prepare_ns=8282 wall_ns=200373407 cpu_ns=200362782 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=1060000 alloc_bytes=518560000 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=0 prepare_ns=7090 wall_ns=48670917 cpu_ns=48661565 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=180000 alloc_bytes=401600000 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=1 prepare_ns=10676 wall_ns=48949716 cpu_ns=48951945 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=180000 alloc_bytes=401600000 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=1 prepare_ns=8442 wall_ns=195971304 cpu_ns=195960093 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=1060000 alloc_bytes=518560000 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=2 prepare_ns=6790 wall_ns=202200585 cpu_ns=202198615 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=1060000 alloc_bytes=518560000 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=2 prepare_ns=6901 wall_ns=51036534 cpu_ns=51038128 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=180000 alloc_bytes=401600000 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=3 prepare_ns=61322 wall_ns=50662953 cpu_ns=50665348 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=180000 alloc_bytes=401600000 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=3 prepare_ns=9274 wall_ns=204389688 cpu_ns=204388892 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=1060000 alloc_bytes=518560000 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=4 prepare_ns=6890 wall_ns=206839594 cpu_ns=206812164 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=1060000 alloc_bytes=518560000 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=4 prepare_ns=7030 wall_ns=52059544 cpu_ns=52038357 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=180000 alloc_bytes=401600000 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=5 prepare_ns=11287 wall_ns=51703348 cpu_ns=51705595 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=180000 alloc_bytes=401600000 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=5 prepare_ns=9103 wall_ns=208055393 cpu_ns=208005758 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=1060000 alloc_bytes=518560000 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=6 prepare_ns=7241 wall_ns=206694626 cpu_ns=206693359 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=1060000 alloc_bytes=518560000 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=6 prepare_ns=6950 wall_ns=51579051 cpu_ns=51581011 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=180000 alloc_bytes=401600000 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=7 prepare_ns=10686 wall_ns=51351940 cpu_ns=51354826 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=180000 alloc_bytes=401600000 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=7 prepare_ns=10035 wall_ns=197058990 cpu_ns=197058317 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=1060000 alloc_bytes=518560000 diff --git a/docs/measurements/sqlite-point-reprepare-time.txt b/docs/measurements/sqlite-point-reprepare-time.txt new file mode 100644 index 00000000..6271dccc --- /dev/null +++ b/docs/measurements/sqlite-point-reprepare-time.txt @@ -0,0 +1,97 @@ +SQLite 3.53.2 source 2026-06-03 19:12:13 d6e03d8c777cfa2d35e3b60d8ec3e0187f3e9f99d8e2ee9cac695fd6fcdf1a24 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=0 prepare_ns=7461 wall_ns=133806079 cpu_ns=133627206 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=0 prepare_ns=6390 wall_ns=7887829 cpu_ns=7889601 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=1 prepare_ns=7952 wall_ns=8208662 cpu_ns=8209935 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=1 prepare_ns=7802 wall_ns=125314937 cpu_ns=125315666 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=2 prepare_ns=5839 wall_ns=128463970 cpu_ns=128463860 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=2 prepare_ns=5919 wall_ns=8730788 cpu_ns=8732077 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=3 prepare_ns=7572 wall_ns=8311477 cpu_ns=8312998 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=3 prepare_ns=7141 wall_ns=124694623 cpu_ns=124695639 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=4 prepare_ns=6059 wall_ns=128462858 cpu_ns=128463449 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=4 prepare_ns=5758 wall_ns=8484667 cpu_ns=8487124 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=5 prepare_ns=8192 wall_ns=7993398 cpu_ns=7994927 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=5 prepare_ns=7621 wall_ns=126428304 cpu_ns=126429592 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=6 prepare_ns=6260 wall_ns=125285362 cpu_ns=125286073 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=6 prepare_ns=6260 wall_ns=7959386 cpu_ns=7960426 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=0 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=0 sample=7 prepare_ns=8282 wall_ns=8301662 cpu_ns=8303042 reprepare=0 heap_before=96000 heap_after=96000 heap_peak=96000 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=0 bound=1 | SEARCH contacts USING INDEX sqlite_autoindex_contacts_1 (device_id=? AND jid=?) +q=0 bound=1 sample=7 prepare_ns=7011 wall_ns=128586694 cpu_ns=128562125 reprepare=20000 heap_before=96000 heap_after=96000 heap_peak=102336 stmt_bytes=4192 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=0 prepare_ns=4347 wall_ns=79669788 cpu_ns=79672102 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=0 prepare_ns=4116 wall_ns=7757553 cpu_ns=7759068 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=1 prepare_ns=4497 wall_ns=7421646 cpu_ns=7422260 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=1 prepare_ns=5468 wall_ns=78994792 cpu_ns=78996984 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=2 prepare_ns=4066 wall_ns=78208768 cpu_ns=78210972 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=2 prepare_ns=5117 wall_ns=8109343 cpu_ns=8110588 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=3 prepare_ns=5909 wall_ns=7767538 cpu_ns=7769934 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=3 prepare_ns=5639 wall_ns=81084619 cpu_ns=81086432 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=4 prepare_ns=3665 wall_ns=82181831 cpu_ns=82184826 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=4 prepare_ns=3696 wall_ns=7262677 cpu_ns=7264107 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=5 prepare_ns=4256 wall_ns=7259373 cpu_ns=7259920 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=5 prepare_ns=3575 wall_ns=83253865 cpu_ns=83197282 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=6 prepare_ns=3805 wall_ns=80368140 cpu_ns=80368961 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=6 prepare_ns=3806 wall_ns=7381716 cpu_ns=7382412 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=0 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=0 sample=7 prepare_ns=5588 wall_ns=7712445 cpu_ns=7713211 reprepare=0 heap_before=93656 heap_after=93656 heap_peak=93656 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=1 bound=1 | SEARCH lid_pn_mapping USING INDEX sqlite_autoindex_lid_pn_mapping_1 (lid=? AND device_id=?) +q=1 bound=1 sample=7 prepare_ns=4587 wall_ns=81359273 cpu_ns=81361588 reprepare=20000 heap_before=93656 heap_after=93656 heap_peak=97736 stmt_bytes=1840 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=0 prepare_ns=7271 wall_ns=155218690 cpu_ns=155209312 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=0 prepare_ns=5819 wall_ns=40777970 cpu_ns=40781168 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=1 prepare_ns=9664 wall_ns=42645292 cpu_ns=42648438 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=1 prepare_ns=8884 wall_ns=167807447 cpu_ns=167708465 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=2 prepare_ns=5368 wall_ns=166421922 cpu_ns=166379982 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=2 prepare_ns=5618 wall_ns=40225927 cpu_ns=40227980 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=3 prepare_ns=10156 wall_ns=39484457 cpu_ns=39487125 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=3 prepare_ns=69354 wall_ns=159469863 cpu_ns=159470490 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=4 prepare_ns=5619 wall_ns=158452832 cpu_ns=158452965 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=4 prepare_ns=5488 wall_ns=38673477 cpu_ns=38676867 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=5 prepare_ns=10356 wall_ns=39889987 cpu_ns=39892804 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=5 prepare_ns=8152 wall_ns=158051410 cpu_ns=158051733 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=6 prepare_ns=5679 wall_ns=169410457 cpu_ns=169387428 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=6 prepare_ns=5538 wall_ns=45475670 cpu_ns=45478581 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=0 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=0 sample=7 prepare_ns=10185 wall_ns=45897995 cpu_ns=45900444 reprepare=0 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 +plan q=2 bound=1 | SEARCH lid_pn_mapping USING INDEX idx_lid_pn_mapping_phone (phone_number=? AND device_id=?) | USE TEMP B-TREE FOR ORDER BY +q=2 bound=1 sample=7 prepare_ns=8282 wall_ns=170596231 cpu_ns=170596275 reprepare=20000 heap_before=94384 heap_after=94384 heap_peak=114464 stmt_bytes=2488 alloc_calls=0 alloc_bytes=0 diff --git a/docs/sqlite-point-lookup-performance.md b/docs/sqlite-point-lookup-performance.md new file mode 100644 index 00000000..fbe3ee34 --- /dev/null +++ b/docs/sqlite-point-lookup-performance.md @@ -0,0 +1,134 @@ +# Point lookups and SQLite LIMIT specialization + +The remaining `sqlite3Reprepare` samples under `ChatStore::contact` and +`reactions_for → chat_key_candidates → counterpart_of` came from Diesel's +`.first()`, which emits a bound `LIMIT ?`. On bundled SQLite 3.53.2, rebinding +that limit to 1 specializes the statement again during `sqlite3_step`. +Prepared-statement caching does not avoid that parser work. + +`point_lookup::First` appends the literal `LIMIT 1` to the existing typed +query and calls `get_result().optional()`. It delegates bind collection and +cache-safety checks, and its derived query ID includes the inner query's ID. +Only `contact` and the two directions of `counterpart_of` use it. The fixed +limit preserves limit-aware planning; no CAST or global change is involved. +Message page readers retain the separate #283 implementation. + +## Reproduction + +Base is main `33108ce`, core `af2e018c`, plus the identical synthetic harness +from this change. Head adds the three lookup changes. Both release test +binaries were built sequentially on the same Linux x86_64 cloud machine, +AMD EPYC 9V74, Rust 1.99.1, using the repository's release settings, including +fat LTO and one codegen unit. No production dependencies or SQLite flags changed. + +Build each version, copying the executable Cargo reports aside before the +next build: + +```sh +cargo test -p oxidezap-chat-store --release --test point_lookup_profile --no-run +python3 scripts/measure-sqlite-point-lookup.py /tmp/point-base /tmp/point-head \ + --iterations 200 --samples 7 --cpu 0 \ + --output docs/measurements/sqlite-point-lookup.csv +``` + +Each process seeds 48 synthetic contacts and PN/LID mappings, with competing +rows in account 2. Contact batches read 48 present and 48 absent contacts. +LID batches perform 48 empty `reactions_for` reads that exercise the nested +LID→PN lookup. PN batches are an unchanged control: that path obtains all +LIDs without calling `counterpart_of`. Assertions check the result of every +read. Setup and three warmup batches are outside the measured interval. +The runner alternates base/head order and excludes an extra warmup pair. +All children are pinned to CPU 0. Timing and allocation passes are separate. + +## Pooled read results + +Seven retained paired samples, each containing 200 batches. Ratios compare +head to base within each pair; ranges describe observed variability, not +confidence intervals. The milliseconds columns are separate sample medians. + +| Workload | Wall ms base → head | Median paired wall ratio, range | CPU ms base → head | Median paired CPU ratio, range | +| --- | --- | --- | --- | --- | +| Contact, present + absent | 374.55 → 194.59 | 0.520, 0.446–0.595 | 374.54 → 194.57 | 0.519, 0.446–0.595 | +| LID reaction lookup | 325.95 → 275.26 | 0.794, 0.658–1.000 | 325.89 → 275.22 | 0.793, 0.658–1.002 | +| PN reaction control | 205.53 → 212.13 | 1.037, 0.926–1.179 | 205.52 → 212.13 | 1.008, 0.917–1.131 | + +Contact CPU fell about 48% and nested LID reaction CPU about 21% in these +synthetic batches. The unchanged control shows noise and no demonstrated +improvement. This includes pool scheduling, query binding, decoding, auxiliary +queries, assertions and result destruction. It does not isolate parser time. +No application startup or end-to-end percentage follows from these results, +and inclusive profiler sample shares cannot be converted into a guaranteed gain. + +## Engine mechanism, initial prepare and C memory + +Compile the probe against the **matching release archive** and header from +Cargo's resolved `libsqlite3-sys 0.38.2`, not a system SQLite: + +```sh +cc -Wall -Wextra -O2 -I \ + scripts/sqlite-point-reprepare.c -lm -lpthread \ + -o /tmp/point-probe-release +taskset -c 0 /tmp/point-probe-release > sqlite-point-reprepare-time.txt +POINT_C_ALLOC=1 taskset -c 0 /tmp/point-probe-release > sqlite-point-reprepare-memory.txt +``` + +The engine source ID is recorded in both raw files. The probe uses synthetic +contacts and mapping tables with their production key/index order. It compares +logically equivalent bound/fixed-limit queries, including absent keys and the +newest-LID timestamp tie. Its SQL omits Diesel's identifier quoting. It measures +one initial `sqlite3_prepare_v3` separately, then 20,000 bind/step/reset cycles, +half present and half absent. It alternates variant order, retains seven pairs +and excludes pair zero. No pool or Rust decoding is involved. + +Every bound variant recorded **20,000 reprepares** via +`SQLITE_STMTSTATUS_REPREPARE`; every literal variant recorded **zero**. +EXPLAIN QUERY PLAN matched for all pairs: contact/LID autoindex lookups; +PN reverse index plus a temporary B-tree for ordering. Matching plans do not +prove identical internal optimizer estimates. Initial prepare medians were +4–11 µs in either variant; no cold-prepare saving is claimed. + +The allocation pass wraps SQLite's own malloc/realloc methods before engine +initialization. Counts and requested bytes exclude initial prepare. Reallocation +bytes count the full requested target size, not a net or retained amount. + +| Query, per 20,000 cycles | C malloc/realloc calls base → head | Requested C bytes base → head | SQLite live bytes before = after, both | SQLite peak bytes base → head | +| --- | --- | --- | --- | --- | +| Contact | 1,240,000 → 0 | 144,480,000 → 0 | 96,000 | 102,336 → 96,000 | +| LID→PN | 640,000 → 0 | 85,440,000 → 0 | 93,656 | 97,736 → 93,656 | +| PN→LID | 1,060,000 → 180,000 | 518,560,000 → 401,600,000 | 94,384 | 114,464 → 114,464 | + +Those byte counters show transient allocation traffic, not a heap reduction of +hundreds of megabytes. PN ordering still allocates. Retained SQLite memory and +statement memory were unchanged. Direct timing-pass CPU medians for the same +20,000 cycles were 126.43→8.30 ms, 81.09→7.42 ms and 166.38→40.23 ms, +respectively. The pooled measurements above determine the integration benefit. + +## Rust allocation and process memory limits + +The pooled harness wraps `GlobalAlloc`. It records successful alloc/realloc +calls, full requested bytes, requested live Rust bytes and their peak. Live +accounting runs in both timing binaries, adding the same atomic bookkeeping; +allocation traffic/peak counting is enabled only in separate allocation passes. +This counter cannot see SQLite's C allocations, allocator metadata or physical +realloc overlap. Runtime and pool background allocations can also contribute. + +Median requested Rust bytes per 200 batches were 26,420,356→26,265,680 for +contacts and 34,354,820→34,274,392 for LID reactions. Counts were effectively +unchanged. The shorter SQL explains the small buffer-size difference. +Requested live/peak Rust bytes and process RSS varied substantially, including +in the unchanged PN control. They do not demonstrate a pooled retained-heap or +RSS reduction. RSS is a working-set snapshot, not a live heap profile. + +All raw pooled samples and both native probe runs are in `docs/measurements/`. +No account data, connected session, network traffic, GUI, file-backed I/O or +startup timing is included. Browser/other-engine performance must be remeasured; +the literal limit and typed queries remain portable, with no production FFI. + +## Correctness and validation + +The tests compare the wrapper against `.first().optional()` and exercise +repeated binds, present/absent keys, optional contact fields, contact labels, +device-suffixed JIDs, account isolation and PN/LID timestamp/LID tie ordering. +Existing alias/reaction/routing regressions remain covered by the full suite. +Locally: 460 chat-store tests passed, seven opt-in tests ignored; all-targets, +all-features chat-store Clippy passed with warnings denied, and formatting passed. diff --git a/scripts/measure-sqlite-point-lookup.py b/scripts/measure-sqlite-point-lookup.py new file mode 100644 index 00000000..236ebed1 --- /dev/null +++ b/scripts/measure-sqlite-point-lookup.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Paired pooled point lookup workloads on Linux release test binaries. + +Use binaries built from the same harness and release flags on one runner. +Each process seeds synthetic data, warms its pool, then measures only reads. Contact includes present and absent rows; lid reaches the +nested mapping lookup through reactions_for; pn is an unchanged control. +""" +import argparse +import csv +import json +import os +from pathlib import Path +import re +import statistics +import subprocess + + +def sample(binary, case, allocations, iterations): + env = dict(os.environ, POINT_CASE=case, + POINT_ALLOC=str(allocations), POINT_ITERATIONS=str(iterations)) + result = subprocess.run( + [str(binary), "--ignored", "--exact", "point_lookup_workload", "--nocapture"], + env=env, text=True, capture_output=True, check=True, + ) + # libtest can prefix this line with its progress text on a pinned CPU. + match = re.search(r"\bPOINT (.+)$", result.stdout, re.MULTILINE) + if not match: + raise RuntimeError(result.stdout + result.stderr) + return {key: int(value) for key, value in re.findall(r"(\w+)=(\d+)", match[1])} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("base", type=Path) + parser.add_argument("head", type=Path) + parser.add_argument("--iterations", type=int, default=64) + parser.add_argument("--samples", type=int, default=7) + parser.add_argument("--cases", nargs="+", choices=["contact", "lid", "pn"], + default=["contact", "lid", "pn"]) + parser.add_argument("--modes", nargs="+", type=int, choices=[0, 1], default=[0, 1]) + parser.add_argument("--cpu", type=int, help="pin this runner and its children to an allowed Linux CPU") + parser.add_argument("--output", type=Path, default=Path("sqlite-point-lookup.csv")) + args = parser.parse_args() + if min(args.iterations, args.samples) < 1: + parser.error("iterations and samples must be positive") + if args.cpu is not None: + if args.cpu not in os.sched_getaffinity(0): + parser.error("cpu is outside this process's allowed affinity") + os.sched_setaffinity(0, {args.cpu}) + binaries = [("base", args.base.resolve()), ("head", args.head.resolve())] + rows = [] + for case in args.cases: + for allocations in args.modes: + for pair in range(args.samples + 1): + order = binaries if pair % 2 == 0 else binaries[::-1] + observed = [] + for label, binary in order: + result = sample(binary, case, allocations, args.iterations) + observed.append(tuple(result[key] for key in ["rows", "checksum", "quotes", "reactions"])) + row = dict(label=label, case=case, allocations=allocations, + pair=pair, iterations=args.iterations, **result) + if pair: + rows.append(row) + if observed[0] != observed[1]: + raise RuntimeError(f"base/head result mismatch: {case}, {observed}") + print(json.dumps(dict(case=case, allocations=allocations, completed=True)), flush=True) + with args.output.open("w", newline="") as output: + writer = csv.DictWriter(output, fieldnames=list(rows[0]), lineterminator="\n") + writer.writeheader() + writer.writerows(rows) + for case in args.cases: + timed = [row for row in rows if row["case"] == case and row["allocations"] == 0] + if not timed: + continue + base = {row["pair"]: row for row in timed if row["label"] == "base"} + head = {row["pair"]: row for row in timed if row["label"] == "head"} + print(json.dumps(dict(case=case, **{ + metric + "_paired_ratio_median": statistics.median( + head[pair][metric] / base[pair][metric] for pair in base) + for metric in ["elapsed_ns", "cpu_ns"] + })), flush=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/sqlite-point-reprepare.c b/scripts/sqlite-point-reprepare.c new file mode 100644 index 00000000..2b57efb6 --- /dev/null +++ b/scripts/sqlite-point-reprepare.c @@ -0,0 +1,139 @@ +/* Link to the exact Cargo bundled archive. Synthetic data only. */ +#define _POSIX_C_SOURCE 200809L +#include "sqlite3.h" +#include +#include +#include +#include +static sqlite3_mem_methods original; +static int counting; +static long long allocations, bytes; +static void *allocate(int n) { + void *p = original.xMalloc(n); + if (p && counting) { + allocations++; + bytes += n; + } + return p; +} +static void *reallocate(void *p, int n) { + void *out = original.xRealloc(p, n); + if (out && counting) { + allocations++; + bytes += n; + } + return out; +} +static void ok(int rc) { + if (rc != SQLITE_OK) { + fprintf(stderr, "SQLite %d\n", rc); + exit(1); + } +} +static long long ns(clockid_t id) { + struct timespec t; + if (clock_gettime(id, &t)) + abort(); + return (long long)t.tv_sec * 1000000000 + t.tv_nsec; +} +static void bind(sqlite3_stmt *s, int bound, int absent) { + ok(sqlite3_bind_int(s, 1, 1)); + ok(sqlite3_bind_text(s, 2, absent ? "missing" : "synthetic", -1, + SQLITE_STATIC)); + if (bound) + ok(sqlite3_bind_int64(s, 3, 1)); +} +int main(void) { + ok(sqlite3_config(SQLITE_CONFIG_GETMALLOC, &original)); + sqlite3_mem_methods measured = original; + measured.xMalloc = allocate; + measured.xRealloc = reallocate; + ok(sqlite3_config(SQLITE_CONFIG_MALLOC, &measured)); + sqlite3 *db = 0; + ok(sqlite3_open(":memory:", &db)); + ok(sqlite3_exec(db, + "CREATE TABLE contacts(device_id INTEGER NOT NULL,jid TEXT " + "NOT NULL,push_name TEXT,full_name TEXT,first_name " + "TEXT,business_name TEXT,PRIMARY KEY(device_id,jid));" + "CREATE TABLE lid_pn_mapping(device_id INTEGER NOT NULL,lid " + "TEXT NOT NULL,phone_number TEXT NOT NULL,created_at BIGINT " + "NOT NULL,updated_at BIGINT NOT NULL,learning_source TEXT " + "NOT NULL,PRIMARY KEY(lid,device_id));" + "CREATE INDEX idx_lid_pn_mapping_phone ON " + "lid_pn_mapping(phone_number,device_id);" + "INSERT INTO contacts " + "VALUES(1,'synthetic',NULL,'name',NULL,NULL),(2,'synthetic'," + "NULL,'other account',NULL,NULL);" + "INSERT INTO lid_pn_mapping " + "VALUES(1,'synthetic','synthetic',1,2,'test'),(1,'older','" + "synthetic',1,1,'test'),(1,'tie','synthetic',1,2,'test'),(2,'" + "synthetic','other',1,9,'test');", + 0, 0, 0)); + const char *queries[] = { + "SELECT jid,push_name,full_name,first_name,business_name FROM contacts " + "WHERE device_id=? AND jid=?", + "SELECT phone_number FROM lid_pn_mapping WHERE device_id=? AND lid=?", + "SELECT lid FROM lid_pn_mapping WHERE device_id=? AND phone_number=? " + "ORDER BY updated_at DESC,lid DESC"}; + printf("SQLite %s source %s\n", sqlite3_libversion(), sqlite3_sourceid()); + for (int q = 0; q < 3; q++) + for (int sample = 0; sample < 8; sample++) + for (int position = 0; position < 2; position++) { + int bound = sample % 2 ? position : 1 - position; + char sql[1024]; + snprintf(sql, sizeof sql, "%s LIMIT %s", queries[q], bound ? "?" : "1"); + char explain[1100]; + snprintf(explain, sizeof explain, "EXPLAIN QUERY PLAN %s", sql); + sqlite3_stmt *s = 0; + ok(sqlite3_prepare_v3(db, explain, -1, 0, &s, 0)); + bind(s, bound, 0); + printf("plan q=%d bound=%d", q, bound); + while (sqlite3_step(s) == SQLITE_ROW) + printf(" | %s", sqlite3_column_text(s, 3)); + puts(""); + ok(sqlite3_finalize(s)); + + long long start = ns(CLOCK_MONOTONIC); + ok(sqlite3_prepare_v3(db, sql, -1, SQLITE_PREPARE_PERSISTENT, &s, 0)); + long long prepare = ns(CLOCK_MONOTONIC) - start; + sqlite3_int64 before = sqlite3_memory_used(); + sqlite3_memory_highwater(1); + allocations = bytes = 0; + counting = getenv("POINT_C_ALLOC") != NULL; + long long cpu = ns(CLOCK_PROCESS_CPUTIME_ID); + start = ns(CLOCK_MONOTONIC); + int reprep = 0; + for (int i = 0; i < 20000; i++) { + bind(s, bound, i % 2); + int rc = sqlite3_step(s); + if (rc != (i % 2 ? SQLITE_DONE : SQLITE_ROW)) + abort(); + if (rc == SQLITE_ROW) { + const char *v = + (const char *)sqlite3_column_text(s, q == 0 ? 2 : 0); + const char *want = q == 0 ? "name" : q == 1 ? "synthetic" : "tie"; + if (!v || strcmp(v, want)) + abort(); + } + reprep += sqlite3_stmt_status(s, SQLITE_STMTSTATUS_REPREPARE, 1); + ok(sqlite3_reset(s)); + ok(sqlite3_clear_bindings(s)); + } + counting = 0; + long long wall = ns(CLOCK_MONOTONIC) - start; + cpu = ns(CLOCK_PROCESS_CPUTIME_ID) - cpu; + printf( + "q=%d bound=%d sample=%d prepare_ns=%lld wall_ns=%lld cpu_ns=%lld " + "reprepare=%d heap_before=%lld heap_after=%lld heap_peak=%lld " + "stmt_bytes=%d alloc_calls=%lld alloc_bytes=%lld\n", + q, bound, sample, prepare, wall, cpu, reprep, (long long)before, + (long long)sqlite3_memory_used(), + (long long)sqlite3_memory_highwater(0), + sqlite3_stmt_status(s, SQLITE_STMTSTATUS_MEMUSED, 0), allocations, + bytes); + if (reprep != (bound ? 20000 : 0)) + abort(); + ok(sqlite3_finalize(s)); + } + ok(sqlite3_close(db)); +} From d21f0915ec254053815a69571670e4c80ef7de62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 06:52:50 +0000 Subject: [PATCH 2/6] fix(web): keep GUI avatar decoding out of shared storage tests --- crates/gui/src/session/avatar/mod.rs | 4 +++- crates/gui/src/session/avatar/web.rs | 10 ---------- crates/gui/src/session/avatar/web_platform.rs | 13 +++++++++++++ 3 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 crates/gui/src/session/avatar/web_platform.rs diff --git a/crates/gui/src/session/avatar/mod.rs b/crates/gui/src/session/avatar/mod.rs index ed7d0a69..63b1c27b 100644 --- a/crates/gui/src/session/avatar/mod.rs +++ b/crates/gui/src/session/avatar/mod.rs @@ -14,6 +14,8 @@ mod native; mod web; #[cfg(target_family = "wasm")] mod web_lock; +#[cfg(target_family = "wasm")] +mod web_platform; #[cfg(all(test, not(target_family = "wasm")))] mod probe; @@ -21,7 +23,7 @@ mod probe; #[cfg(not(target_family = "wasm"))] use native as imp; #[cfg(target_family = "wasm")] -use web as imp; +use web_platform as imp; use portable_atomic::{AtomicU64, Ordering}; use std::collections::{HashMap, HashSet}; diff --git a/crates/gui/src/session/avatar/web.rs b/crates/gui/src/session/avatar/web.rs index 759c9a7e..7cb5c55b 100644 --- a/crates/gui/src/session/avatar/web.rs +++ b/crates/gui/src/session/avatar/web.rs @@ -27,16 +27,6 @@ static STORAGE_GENERATION: portable_atomic::AtomicU64 = portable_atomic::AtomicU static ACTIVE_ACCOUNT: RwLock> = RwLock::new(None); -pub(super) async fn decode_avatar( - bytes: Vec, - _priority: super::DecodePriority, - current: impl FnOnce() -> bool + Send + 'static, -) -> Option<(std::sync::Arc, u64)> { - current() - .then(|| crate::session::media::decode_avatar(&bytes)) - .flatten() -} - pub fn set_active_account(account: Option) { if let Ok(mut lock) = ACTIVE_ACCOUNT.write() { if *lock != account { diff --git a/crates/gui/src/session/avatar/web_platform.rs b/crates/gui/src/session/avatar/web_platform.rs new file mode 100644 index 00000000..8a796be6 --- /dev/null +++ b/crates/gui/src/session/avatar/web_platform.rs @@ -0,0 +1,13 @@ +//! Web avatar decoding and the persistent storage implementation. + +pub use super::web::*; + +pub(super) async fn decode_avatar( + bytes: Vec, + _priority: super::DecodePriority, + current: impl FnOnce() -> bool + Send + 'static, +) -> Option<(std::sync::Arc, u64)> { + current() + .then(|| crate::session::media::decode_avatar(&bytes)) + .flatten() +} From 0da555ad0a04a5779780c7c84afab69c09acb149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 06:52:50 +0000 Subject: [PATCH 3/6] test(plugin-host): exhaust fuel without fifty million debug dispatches --- crates/plugin-host/src/tests.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/plugin-host/src/tests.rs b/crates/plugin-host/src/tests.rs index 79fc2111..43c6b6a3 100644 --- a/crates/plugin-host/src/tests.rs +++ b/crates/plugin-host/src/tests.rs @@ -285,7 +285,12 @@ const PONG_WITHOUT_PERMISSION: &str = r#"(module )"#; fn spins() -> String { - versioned(SPINS) + // Wasmi charges const operators before folding constant/drop pairs away. + // Burn the real per-call fuel budget with fewer interpreter dispatches: + // a bare branch needs 50 million debug dispatches and races the 10 s + // publication deadline on a busy CI runner. The loop, fuel budget, + // deadline and fuel-trap assertion remain unchanged. + versioned(&SPINS.replace("$FUEL_WORK", &"i32.const 0 drop\n".repeat(64))) } const SPINS: &str = r#"(module @@ -293,7 +298,7 @@ const SPINS: &str = r#"(module (memory (export "memory") 1) (func (export "oxi_abi_version") (result i32) (i32.const $ABI_VERSION)) (func (export "oxi_init") (result i32) (call $subscribe (i64.const 2)) (i32.const 0)) - (func (export "oxi_on_event") (param i32 i32) (result i32) (loop (br 0)) (i32.const 0)) + (func (export "oxi_on_event") (param i32 i32) (result i32) (loop $FUEL_WORK (br 0)) (i32.const 0)) )"#; /// Asks for one page and then for far more than the limit allows. From a1758d06675e2a4a978da5bcf34472da21c6d6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 06:55:01 +0000 Subject: [PATCH 4/6] docs: record baseline web failure and native fuel test reproduction --- docs/measurements/ci-288-289-triage.md | 105 +++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/measurements/ci-288-289-triage.md diff --git a/docs/measurements/ci-288-289-triage.md b/docs/measurements/ci-288-289-triage.md new file mode 100644 index 00000000..af95231e --- /dev/null +++ b/docs/measurements/ci-288-289-triage.md @@ -0,0 +1,105 @@ +# CI failures shared by the profiling PRs + +Checkpoint for #288 and #289, against main `33108ce`, core `af2e018c`. +These are baseline issues, independent of the SQL and phone-metadata patches. +No production dependencies, fuel limits, test deadlines or assertions change. + +## Web compilation failure + +The [#288 web job](https://github.com/oxidezap/client/actions/runs/40998832174/job/123878204949) +and [#289 web job](https://github.com/oxidezap/client/actions/runs/41000719222/job/123884222100) +fail before launching a browser. Both report the same errors: + +```text +error[E0433]: cannot find `session` in the crate root + avatar/web.rs:36: crate::session::media::decode_avatar(&bytes) +error[E0425]: cannot find type `DecodePriority` in module `super` + avatar/web.rs:32: _priority: super::DecodePriority +error[E0433]: cannot find module or crate `gpui` in this scope + avatar/web.rs:34: Arc +``` + +`daemon::media::avatar_web_tests` imports the actual GUI storage source by +path so it can exercise Cache Storage and Web Locks without linking GPUI. +#282 added a GUI decode function to that file. The daemon has neither those +GUI modules/types nor a GPUI dependency. Main already has both the import +and the GUI function; neither profiling PR changes them. A clean baseline +compilation reproduced all three errors with the same library-test target: + +```sh +RUSTFLAGS='--cfg web_sys_unstable_apis' \ + cargo test -p oxidezap-daemon --lib --target wasm32-unknown-unknown --no-run +``` + +The fix moves only the GUI decode function into `avatar/web_platform.rs`. +The GUI selects that adapter as `imp`; it reexports the existing `web` +storage implementation. The daemon still imports `web.rs`, unchanged at its +call site. Storage implementation bytes are identical after removing the +GUI function. No browser tests, data-isolation checks or storage logic are +removed or stubbed, and the daemon gains no GUI dependency. + +## Native fuel test flake + +The [#288 Check job](https://github.com/oxidezap/client/actions/runs/40998832174/job/123878205132) +passed workspace Clippy and then failed at: + +```text +tests::a_plugin_that_loops_forever_runs_out_of_fuel_and_is_stopped +crates/plugin-host/src/tests.rs:110:13: +timed out waiting for the plugin to be stopped +``` + +Nextest reported failure after 11.004 seconds. The adjacent self-waking +plugin test took 21.454 seconds. The failed fixture is a bare infinite +branch, draining the real 50,000,000-unit per-call fuel budget one branch +at a time in the debug interpreter. Its publication poll has a ten-second +wall-clock deadline. Both plugin-host sources and its dependency graph are +unchanged by #288; chat-store is not a plugin-host dependency. + +The unchanged fixture passed isolated on #288 in 8.44 seconds and on main +in 8.47 seconds. Pinning the main test and one synthetic busy Python process +to the same CPU reproduced the exact timeout; the process finished after +16.75 seconds. The competing process was terminated in a `finally` block. +This establishes a load-sensitive baseline flake, not a SQL regression. + +Reproduction uses the test executable reported by Cargo: + +```sh +cargo test -p oxidezap-plugin-host --all-features --lib \ + tests::a_plugin_that_loops_forever_runs_out_of_fuel_and_is_stopped -- --exact +# To reproduce contention, pin this executable and one CPU-busy process +# to the same allowed CPU; always terminate that process after the test. +``` + +The fixture now inserts 64 constant/drop pairs inside the same infinite +loop. Wasmi 2.0 meters each constant operator before folding the pair away; +`engine/translator/func/fuel.rs` and `engine/costs.rs` in the locked wasmi +source define this behavior. The loop consumes the same production fuel +budget with fewer interpreter dispatches. Its unconditional branch, +ten-second deadline, stopped-state assertion and fuel-error assertion all +remain. No runtime sandbox setting or budget changes. + +With the same CPU contention, the adjusted fixture passed five consecutive +runs in 0.60–0.68 seconds. Isolated, it passed in 0.25 seconds. The full +plugin-host suite passed, 135 tests and one existing ignored test. Clippy +for all plugin-host targets/features passed with warnings denied. + +## Browser validation + +The existing daemon and session library tests were built and run with the +CI flags, stable Rust 1.99.1 and wasm-bindgen-test-runner 0.2.129. A matching +Chromium/ChromeDriver 156.0.8078.73 pair was unpacked into `/tmp` for the +synthetic browser runs; no installed browser or user profile was modified. + +```sh +RUSTFLAGS='--cfg web_sys_unstable_apis' \ +CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ + cargo test -p oxidezap-daemon -p oxidezap-session --lib \ + --target wasm32-unknown-unknown +``` + +All 16 daemon tests and 32 session tests passed, including the real avatar +Cache Storage recovery/account-isolation tests. Existing unrelated WASM +warnings remain. The production GUI also needs its normal Pages build; +Windows, macOS and the remaining CI jobs remain the PR's checks. No merge +is performed as part of this investigation. From 0e3e4b8405daca9e99d730163968b666993ba9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 07:01:48 +0000 Subject: [PATCH 5/6] test(plugin-host): retain explicit bare-branch fuel coverage --- crates/plugin-host/src/tests.rs | 31 ++++++++++++++++++++++++++ docs/measurements/ci-288-289-triage.md | 6 +++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/plugin-host/src/tests.rs b/crates/plugin-host/src/tests.rs index 43c6b6a3..dffea915 100644 --- a/crates/plugin-host/src/tests.rs +++ b/crates/plugin-host/src/tests.rs @@ -613,6 +613,37 @@ fn a_command_outside_the_declared_capabilities_never_reaches_the_daemon() { assert!(surface.is_running(), "but it is not stopped for trying"); } +/// Keep the original empty-loop case covered independently of the cheaper +/// host fixture: adding billed work must not hide a free-branch regression. +#[test] +fn a_bare_branch_loop_consumes_fuel() { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + let worker = std::thread::spawn(move || { + let mut config = wasmi::Config::default(); + config.consume_fuel(true); + let engine = wasmi::Engine::new(&config); + let bytes = wat::parse_str("(module (func (export \"spin\") (loop (br 0))))") + .expect("valid bare-loop fixture"); + let module = wasmi::Module::new(&engine, &bytes[..]).expect("the module validates"); + let mut store = wasmi::Store::new(&engine, ()); + store.set_fuel(32).expect("fuel metering is enabled"); + let instance = wasmi::Linker::new(&engine) + .instantiate_and_start(&mut store, &module) + .expect("the module has no start function"); + let spin = instance.get_typed_func::<(), ()>(&store, "spin").unwrap(); + let error = spin + .call(&mut store, ()) + .expect_err("the loop exhausts fuel"); + tx.send(error.as_trap_code()).unwrap(); + }); + assert_eq!( + rx.recv_timeout(Duration::from_secs(10)) + .expect("bare branches must consume fuel"), + Some(wasmi::TrapCode::OutOfFuel) + ); + worker.join().expect("the fuel probe completed"); +} + /// The number that makes running a stranger's code in this process /// defensible. #[test] diff --git a/docs/measurements/ci-288-289-triage.md b/docs/measurements/ci-288-289-triage.md index af95231e..ff949658 100644 --- a/docs/measurements/ci-288-289-triage.md +++ b/docs/measurements/ci-288-289-triage.md @@ -77,11 +77,13 @@ loop. Wasmi 2.0 meters each constant operator before folding the pair away; source define this behavior. The loop consumes the same production fuel budget with fewer interpreter dispatches. Its unconditional branch, ten-second deadline, stopped-state assertion and fuel-error assertion all -remain. No runtime sandbox setting or budget changes. +remain. A separate bounded 32-unit probe retains the original bare-branch +case and asserts the precise OutOfFuel trap, so adding other billed work +cannot hide a free-branch regression. No runtime sandbox setting or budget changes. With the same CPU contention, the adjusted fixture passed five consecutive runs in 0.60–0.68 seconds. Isolated, it passed in 0.25 seconds. The full -plugin-host suite passed, 135 tests and one existing ignored test. Clippy +plugin-host suite passed, 136 tests and one existing ignored test. Clippy for all plugin-host targets/features passed with warnings denied. ## Browser validation From 71130e9f153a4b3dde3b121f466ec5caaf9a0be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas=20de=20Oliveira=20Lopes?= Date: Fri, 30 Oct 2026 07:09:23 +0000 Subject: [PATCH 6/6] fix(ci): avoid duplicating existing avatar probe instrumentation --- .github/workflows/avatar-restore.yml | 9 ++++++--- docs/measurements/ci-288-289-triage.md | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/avatar-restore.yml b/.github/workflows/avatar-restore.yml index 5ee33dbe..671476e9 100644 --- a/.github/workflows/avatar-restore.yml +++ b/.github/workflows/avatar-restore.yml @@ -56,13 +56,16 @@ jobs: text = mod.read_text() marker = 'mod native;' assert marker in text - mod.write_text(text.replace(marker, marker + '\n#[cfg(all(test, not(target_family = "wasm")))]\nmod probe;', 1)) + if 'mod probe;' not in text: + mod.write_text(text.replace(marker, marker + '\n#[cfg(all(test, not(target_family = "wasm")))]\nmod probe;', 1)) native = root / 'native.rs' text = native.read_text() marker = 'pub async fn read_persistent(key: &str) -> Option> {' assert marker in text - text = text.replace(marker, marker + '\n #[cfg(test)]\n PERSISTENT_READS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);', 1) - text += '\n#[cfg(test)]\npub(super) static PERSISTENT_READS: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(0);\n' + if 'PERSISTENT_READS.fetch_add' not in text: + text = text.replace(marker, marker + '\n #[cfg(test)]\n PERSISTENT_READS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);', 1) + if 'static PERSISTENT_READS:' not in text: + text += '\n#[cfg(test)]\npub(super) static PERSISTENT_READS: portable_atomic::AtomicU64 = portable_atomic::AtomicU64::new(0);\n' native.write_text(text) PY cargo test -p oxidezap-gui --bin oxidezap --locked native_restore_probe -- --ignored --nocapture diff --git a/docs/measurements/ci-288-289-triage.md b/docs/measurements/ci-288-289-triage.md index ff949658..ad22cfb6 100644 --- a/docs/measurements/ci-288-289-triage.md +++ b/docs/measurements/ci-288-289-triage.md @@ -105,3 +105,21 @@ Cache Storage recovery/account-isolation tests. Existing unrelated WASM warnings remain. The production GUI also needs its normal Pages build; Windows, macOS and the remaining CI jobs remain the PR's checks. No merge is performed as part of this investigation. + +## Avatar comparison workflow instrumentation + +Changing an avatar file activates the existing `Avatar restore` comparison +workflow. Its base step exposed another baseline harness problem on #290: +E0428, duplicate `probe` module and duplicate `PERSISTENT_READS` static. +The workflow unconditionally injected instrumentation that #282 had already +committed into main. It also would have double-counted reads by adding a +second increment. This failed while measuring the unchanged base, before +running the candidate. + +The injection now adds each declaration/increment only when absent. Both +legacy uninstrumented inputs and current main were exercised through the +actual Python block extracted from the workflow. Each produced one probe, +one counter and one increment; applying the block twice was byte-identical. +For current main the entire output was byte-identical to the input. Both +base/candidate measurements and PNG/JPEG probe runs remain enabled. This is +an idempotency fix, with no deletion of the performance or lifecycle checks.