atagen

macltok

public
Star0
Watch0

Commit a5865326c81b

tarball

atagen <boss@atagen.co>2026-10-26 06:49 UTC

5 files changed+62-22
account: open the search and mentions stores side by side

Each store cipher runs PBKDF2 at 200k rounds, about 1.25s in a debug build, and every restore or login ran seven of them one after another on the async thread. Natively the two macltok stores now derive on blocking threads at once, saving one derivation's wait per restore and login. The session record and the sdk's four stores still derive serially.
Modified file: Cargo.toml+3 additions
@@ -69,6 +69,9 @@ web-sys = { version = "0.3.105", optional = true, features = [
6969 "WorkerGlobalScope",
7070 ] }
7171
72+[target.'cfg(not(target_family = "wasm"))'.dependencies]
73+tokio = { version = "1.47.1", features = ["rt"] }
74+
7275 [target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
7376 matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "0f2f982989765b79c39efdc30e86eaafccf69e4a", default-features = false, features = ["testing"] }
7477 matrix-sdk-test = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "0f2f982989765b79c39efdc30e86eaafccf69e4a" }
Modified file: src/account.rs+6 additions−5 deletions
@@ -865,15 +865,16 @@ impl Session {
865865 .ok_or(AccountError::MissingSession)?
866866 .to_owned(),
867867 };
868- let search =
869- crate::search::store::SearchStore::open(self.tracking.store.store(), account.clone())
870- .await;
871868 let opener = crate::mentions::store::Opener::new(
872869 self.tracking.store.store(),
873- account,
870+ account.clone(),
874871 config.max_mentions_bytes,
875872 );
876- let mentions = match opener.open().await {
873+ let (search, mentions) = futures_util::join!(
874+ crate::search::store::SearchStore::open(self.tracking.store.store(), account),
875+ opener.open(),
876+ );
877+ let mentions = match mentions {
877878 Ok(store) => Ok(store),
878879 Err(crate::mentions::store::OpenError::Unavailable(error)) => {
879880 Err(crate::mentions::service::Unopened {
Modified file: src/engine/executor.rs+14 additions
@@ -80,3 +80,17 @@ impl<T: 'static> Drop for TaskSet<T> {
8080 self.abort_all();
8181 }
8282 }
83+
84+/// a store cipher's pbkdf2 holds its thread for over a second in a debug build, so natively it
85+/// runs beside the runtime and two stores can derive at once
86+#[cfg(not(target_family = "wasm"))]
87+pub(crate) async fn derive<T: Send + 'static>(work: impl FnOnce() -> T + Send + 'static) -> T {
88+ tokio::task::spawn_blocking(work)
89+ .await
90+ .unwrap_or_else(|error| std::panic::resume_unwind(error.into_panic()))
91+}
92+
93+#[cfg(target_family = "wasm")]
94+pub(crate) async fn derive<T>(work: impl FnOnce() -> T) -> T {
95+ work()
96+}
Modified file: src/mentions/store/mod.rs+20 additions−9 deletions
@@ -6,7 +6,7 @@ use ruma::OwnedUserId;
66 use serde::{Deserialize, Serialize, de::DeserializeOwned};
77 use serde_json::{Value, value::RawValue};
88
9-use crate::{Error, ErrorKind, StoreConfig};
9+use crate::{Error, ErrorKind, StoreConfig, engine::executor::derive};
1010
1111 #[cfg(feature = "sqlite")]
1212 mod sqlite;
@@ -155,7 +155,7 @@ impl MentionsStore {
155155 .await
156156 .map_err(OpenError::Unavailable)?;
157157 let restored = match backend.load().await {
158- Ok(Some(snapshot)) => match restore(opener, snapshot) {
158+ Ok(Some(snapshot)) => match restore(opener, snapshot).await {
159159 Ok(restored) => Some(restored),
160160 Err(Rejected::Newer(error)) => return Err(OpenError::Newer(error)),
161161 Err(Rejected::Content(error)) => {
@@ -363,8 +363,14 @@ struct Schema {
363363 schema: u32,
364364 }
365365
366-fn restore(opener: &Opener, snapshot: Snapshot) -> Result<(StoreCipher, Snapshot, u64), Rejected> {
367- let cipher = StoreCipher::import(&opener.passphrase, &snapshot.exported)
366+async fn restore(
367+ opener: &Opener,
368+ snapshot: Snapshot,
369+) -> Result<(StoreCipher, Snapshot, u64), Rejected> {
370+ let passphrase = opener.passphrase.clone();
371+ let exported = snapshot.exported.clone();
372+ let cipher = derive(move || StoreCipher::import(&passphrase, &exported))
373+ .await
368374 .map_err(|error| storage_error("opening the mentions cipher", error))?;
369375 let schema: Schema = cipher
370376 .decrypt_value(&snapshot.head)
@@ -395,11 +401,16 @@ fn restore(opener: &Opener, snapshot: Snapshot) -> Result<(StoreCipher, Snapshot
395401 }
396402
397403 async fn create(backend: &Backend, opener: &Opener) -> Result<(StoreCipher, Snapshot, u64), Error> {
398- let cipher =
399- StoreCipher::new().map_err(|error| storage_error("creating the mentions cipher", error))?;
400- let exported = cipher
401- .export(&opener.passphrase)
402- .map_err(|error| storage_error("sealing the mentions cipher", error))?;
404+ let passphrase = opener.passphrase.clone();
405+ let (cipher, exported) = derive(move || {
406+ let cipher = StoreCipher::new()
407+ .map_err(|error| storage_error("creating the mentions cipher", error))?;
408+ let exported = cipher
409+ .export(&passphrase)
410+ .map_err(|error| storage_error("sealing the mentions cipher", error))?;
411+ Ok::<_, Error>((cipher, exported))
412+ })
413+ .await?;
403414 let head = seal_head(&cipher, &opener.account, 0, 0, 0)?;
404415 let snapshot = Snapshot {
405416 exported,
Modified file: src/search/store/mod.rs+19 additions−8 deletions
@@ -4,7 +4,7 @@ use matrix_sdk::{SendOutsideWasm, SyncOutsideWasm};
44 use matrix_sdk_store_encryption::{EncryptedValueBase64, StoreCipher};
55 use serde::{Deserialize, Serialize};
66
7-use crate::{Error, ErrorKind, StoreConfig, mentions::store::Account};
7+use crate::{Error, ErrorKind, StoreConfig, engine::executor::derive, mentions::store::Account};
88
99 #[cfg(feature = "indexeddb")]
1010 mod indexeddb;
@@ -65,8 +65,14 @@ impl SearchStore {
6565 user_id: account.user_id.to_string(),
6666 };
6767 let mut blobs = backend.load(&[CIPHER, ACCOUNT]).await?;
68- let opened = blobs.remove(CIPHER).and_then(|exported| {
69- let cipher = StoreCipher::import(config.passphrase(), &exported).ok()?;
68+ let imported = match blobs.remove(CIPHER) {
69+ Some(exported) => {
70+ let passphrase = config.passphrase().to_owned();
71+ derive(move || StoreCipher::import(&passphrase, &exported).ok()).await
72+ }
73+ None => None,
74+ };
75+ let opened = imported.and_then(|cipher| {
7076 let stored: Owner =
7177 serde_json::from_slice(&unseal(&cipher, ACCOUNT, blobs.remove(ACCOUNT)?)?).ok()?;
7278 (stored.homeserver == owner.homeserver && stored.user_id == owner.user_id)
@@ -75,11 +81,16 @@ impl SearchStore {
7581 if let Some(cipher) = opened {
7682 return Ok(Self { backend, cipher });
7783 }
78- let cipher = StoreCipher::new()
79- .map_err(|error| storage_error("creating the search cipher", error))?;
80- let exported = cipher
81- .export(config.passphrase())
82- .map_err(|error| storage_error("sealing the search cipher", error))?;
84+ let passphrase = config.passphrase().to_owned();
85+ let (cipher, exported) = derive(move || {
86+ let cipher = StoreCipher::new()
87+ .map_err(|error| storage_error("creating the search cipher", error))?;
88+ let exported = cipher
89+ .export(&passphrase)
90+ .map_err(|error| storage_error("sealing the search cipher", error))?;
91+ Ok::<_, Error>((cipher, exported))
92+ })
93+ .await?;
8394 let owner = serde_json::to_vec(&owner)
8495 .map_err(|error| storage_error("encoding the search owner", error))?;
8596 let sealed = seal(&cipher, ACCOUNT, owner)?;