Skip to content

feat: Mark reactions as IMAP-seen in marknoticed_chat(), second try #6480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 22, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ harness = false
name = "get_chat_msgs"
harness = false

[[bench]]
name = "marknoticed_chat"
harness = false

[[bench]]
name = "get_chatlist"
harness = false
Expand Down
94 changes: 94 additions & 0 deletions benches/marknoticed_chat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#![recursion_limit = "256"]
use std::path::Path;

use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use futures_lite::future::block_on;
use tempfile::tempdir;

async fn marknoticed_chat_benchmark(context: &Context, chats: &[ChatId]) {
for c in chats.iter().take(20) {
chat::marknoticed_chat(context, *c).await.unwrap();
}
}

fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let rt = tokio::runtime::Runtime::new().unwrap();

let chats: Vec<_> = rt.block_on(async {
let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
.await
.unwrap();
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
let len = chatlist.len();
(1..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
});

// This mainly tests the performance of marknoticed_chat()
// when nothing has to be done
c.bench_function(
"chat::marknoticed_chat (mark 20 chats as noticed repeatedly)",
|b| {
let dir = tempdir().unwrap();
let dir = dir.path();
let new_db = dir.join("dc.db");
std::fs::copy(&path, &new_db).unwrap();

let context = block_on(async {
Context::new(Path::new(&new_db), 100, Events::new(), StockStrings::new())
.await
.unwrap()
});

b.to_async(&rt)
.iter(|| marknoticed_chat_benchmark(&context, black_box(&chats)))
},
);

// If the first 20 chats contain fresh messages or reactions,
// this tests the performance of marking them as noticed.
c.bench_function(
"chat::marknoticed_chat (mark 20 chats as noticed, resetting after every iteration)",
|b| {
b.to_async(&rt).iter_batched(
|| {
let dir = tempdir().unwrap();
let new_db = dir.path().join("dc.db");
std::fs::copy(&path, &new_db).unwrap();

let context = block_on(async {
Context::new(
Path::new(&new_db),
100,
Events::new(),
StockStrings::new(),
)
.await
.unwrap()
});
(dir, context)
},
|(_dir, context)| {
let chats = &chats;
async move {
marknoticed_chat_benchmark(black_box(&context), black_box(chats)).await
}
},
BatchSize::PerIteration,
);
},
);
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
}
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
38 changes: 38 additions & 0 deletions deltachat-rpc-client/tests/test_something.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,44 @@ def test_message(acfactory) -> None:
assert reactions == snapshot.reactions


def test_reaction_seen_on_another_dev(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
alice2.start_io()

bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")

event = bob.wait_for_incoming_msg_event()
msg_id = event.msg_id

message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot.chat.accept()
message.send_reaction("😎")
for a in [alice, alice2]:
while True:
event = a.wait_for_event()
if event.kind == EventType.INCOMING_REACTION:
break

alice2.clear_all_events()
alice_chat_bob.mark_noticed()
while True:
event = alice2.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
chat_id = event.chat_id
break
alice2_contact_bob = alice2.get_contact_by_addr(bob_addr)
alice2_chat_bob = alice2_contact_bob.create_chat()
assert chat_id == alice2_chat_bob.id


def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2)
Expand Down
37 changes: 32 additions & 5 deletions src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ use tokio::task;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
Expand Down Expand Up @@ -51,6 +50,7 @@ use crate::tools::{
truncate_msg_text, IsNoneOrEmpty, SystemTime,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{chatlist_events, imap};

/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -3328,7 +3328,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
} else {
start_chat_ephemeral_timers(context, chat_id).await?;

if context
let noticed_msgs_count = context
.sql
.execute(
"UPDATE msgs
Expand All @@ -3338,9 +3338,36 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
AND chat_id=?;",
(MessageState::InNoticed, MessageState::InFresh, chat_id),
)
.await?
== 0
{
.await?;

// This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed
// locally (i.e. when the chat was opened locally).
let hidden_messages = context
.sql
.query_map(
"SELECT id, rfc724_mid FROM msgs
WHERE state=?
AND hidden=1
AND chat_id=?
ORDER BY id DESC LIMIT 100", // LIMIT to 100 in order to avoid blocking the UI too long, usually there will be less than 100 messages anyway
(MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
|row| {
let msg_id: MsgId = row.get(0)?;
let rfc724_mid: String = row.get(1)?;
Ok((msg_id, rfc724_mid))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for (msg_id, rfc724_mid) in &hidden_messages {
message::update_msg_state(context, *msg_id, MessageState::InSeen).await?;
imap::markseen_on_imap_table(context, rfc724_mid).await?;
}
Copy link
Collaborator Author

@Hocuri Hocuri Jan 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance matters here because this is done in the "hot path" of the user opening a chat. This could be replaced by:

        context
            .sql
            .transaction(|trans| {
                trans.execute(
                    "UPDATE msgs SET state=? WHERE state=? AND hidden=1 AND chat_id=?",
                    (MessageState::InSeen, MessageState::InNoticed, chat_id),
                )?;
                trans.execute(
                    "INSERT OR IGNORE INTO imap_markseen (id)
                    SELECT id FROM imap WHERE rfc724_mid IN
                    (SELECT rfc724_mid FROM msgs WHERE state=? AND hidden=1 AND chat_id=?)",
                    (MessageState::InNoticed, chat_id),
                )?;
                Ok(())
            })
            .await?;
        context.scheduler.interrupt_inbox().await;

However, in my benchmark, this actually has worse performance:

image

Probably because the messages have to be loaded twice now. Also, this always opens a write connection, not only when messages actually have to be marked as noticed.

If you know of a possibly more performant version of this, tell me & I'll benchmark it. But the most important part is the SELECT statement anyway, since it's the only thing that will be executed even if no messages are fresh.

Maybe I'll make update_msg_state() and markseen_on_imap_table() take a transaction in a subsequent PR, maybe this improves the (benchmark) performance.


if noticed_msgs_count == 0 {
return Ok(());
}
}
Expand Down
31 changes: 24 additions & 7 deletions src/reaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ mod tests {
use deltachat_contact_tools::ContactAddress;

use super::*;
use crate::chat::{forward_msgs, get_chat_msgs, send_text_msg};
use crate::chat::{forward_msgs, get_chat_msgs, marknoticed_chat, send_text_msg};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::contact::{Contact, Origin};
Expand Down Expand Up @@ -623,7 +623,9 @@ Here's my footer -- bob@example.net"
.get_matching_opt(t, |evt| {
matches!(
evt,
EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
EventType::IncomingReaction { .. }
| EventType::IncomingMsg { .. }
| EventType::MsgsChanged { .. }
)
})
.await;
Expand Down Expand Up @@ -667,7 +669,8 @@ Here's my footer -- bob@example.net"
assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);

let bob_reaction_msg = bob.pop_sent_msg().await;
alice.recv_msg_trash(&bob_reaction_msg).await;
let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);

let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
Expand All @@ -691,6 +694,20 @@ Here's my footer -- bob@example.net"
.await?;
expect_no_unwanted_events(&alice).await;

marknoticed_chat(&alice, chat_alice.id).await?;
assert_eq!(
alice_reaction_msg.id.get_state(&alice).await?,
MessageState::InSeen
);
// Reactions don't request MDNs.
assert_eq!(
alice
.sql
.count("SELECT COUNT(*) FROM smtp_mdns", ())
.await?,
0
);

// Alice reacts to own message.
send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀")
.await
Expand Down Expand Up @@ -730,7 +747,7 @@ Here's my footer -- bob@example.net"
bob_msg1.chat_id.accept(&bob).await?;
send_reaction(&bob, bob_msg1.id, "👍").await?;
let bob_send_reaction = bob.pop_sent_msg().await;
alice.recv_msg_trash(&bob_send_reaction).await;
alice.recv_msg_hidden(&bob_send_reaction).await;
expect_incoming_reactions_event(
&alice,
alice_chat.id,
Expand Down Expand Up @@ -899,7 +916,7 @@ Here's my footer -- bob@example.net"
let bob_reaction_msg = bob.pop_sent_msg().await;

// Alice receives a reaction.
alice.recv_msg_trash(&bob_reaction_msg).await;
alice.recv_msg_hidden(&bob_reaction_msg).await;

let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
assert_eq!(reactions.to_string(), "👍1");
Expand Down Expand Up @@ -951,7 +968,7 @@ Here's my footer -- bob@example.net"
{
send_reaction(&alice2, alice2_msg.id, "👍").await?;
let msg = alice2.pop_sent_msg().await;
alice1.recv_msg_trash(&msg).await;
alice1.recv_msg_hidden(&msg).await;
}

// Check that the status is still the same.
Expand All @@ -973,7 +990,7 @@ Here's my footer -- bob@example.net"
let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;

send_reaction(&alice0, alice0_msg_id, "👀").await?;
alice1.recv_msg_trash(&alice0.pop_sent_msg().await).await;
alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await;

expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
Expand Down
Loading
Loading