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/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/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() +} diff --git a/crates/plugin-host/src/tests.rs b/crates/plugin-host/src/tests.rs index 79fc2111..dffea915 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. @@ -608,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 new file mode 100644 index 00000000..ad22cfb6 --- /dev/null +++ b/docs/measurements/ci-288-289-triage.md @@ -0,0 +1,125 @@ +# 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. 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, 136 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. + +## 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. 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)); +}