Laggy toggle buttin in Tauri app

I have a Rust Tauri GUI app with two elements: A play-button and a text field displaying an amount of seconds passed in a timer. The goal is simple: Clicking the button should start a timer, clicking the button again should pause it. This should be toggled.

The code I currently have kind of works but feels really buggy. Clicking the button starts the timer. But when I click to pause it, it takes sometimes like 10 seconds before the app detects the pause-click. Sometimes the app also freezes.

I think this has something to do with my thread- or arc/mutex/lock-handling.

Rust code:

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::sync::{mpsc, Arc, Mutex, Condvar};
use tauri::{Manager, State};

struct IsPlayingState(Arc<(Mutex<bool>, Condvar)>);
struct FirstClickState(Arc<Mutex<bool>>);

fn counter(tx: mpsc::Sender<u32>, is_playing: Arc<(Mutex<bool>, Condvar)>) {
    let mut seconds: u32 = 0;
    
    
    loop {
        let (lock, cvar) = &*is_playing;
        let mut playing = lock.lock().unwrap();
        
        println!("Playing {:?}", playing);
        while !*playing {
            playing = cvar.wait(playing).unwrap();
        }

        if *playing {
            println!("Time {}", seconds);
            tx.send(seconds).expect("Failed to transmit seconds");
            seconds += 1;
            std::thread::sleep(std::time::Duration::from_secs(1));
        }
        
    }

}

#[tauri::command]
fn play(app_handle: tauri::AppHandle, is_playing: State<IsPlayingState>, first_click: State<FirstClickState>) {
    let (tx, rx) = mpsc::channel();
    let is_playing_clone = Arc::clone(&is_playing.0);
    let mut first_click_lock = first_click.0.lock().unwrap();

    let (lock, cvar) = &*is_playing.0;
    let mut playing = lock.lock().unwrap();
    *playing = !*playing;
    println!("Playing 2 {:?}", playing);

    if *playing {
        cvar.notify_one();
    }
    
    if *first_click_lock {
        println!("rofl");
        std::thread::spawn(move || {
            counter(tx, is_playing_clone);
        });

        std::thread::spawn(move || {
            while let Ok(seconds) = rx.recv() {
                app_handle.emit_all("update_time", seconds).expect("Failed to emit seconds");
            }
        });

        *first_click_lock = false;
    }
    

}

fn main() {

    let is_playing = Arc::new((Mutex::new(false), Condvar::new()));
    let first_click = Arc::new(Mutex::new(true));

    tauri::Builder::default()
        .manage(IsPlayingState(is_playing))
        .manage(FirstClickState(first_click))
        .invoke_handler(tauri::generate_handler![play])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Javascript (Tauri):

const { invoke } = window.__TAURI__.tauri;
const { listen } = window.__TAURI__.event;

async function play() {
    await invoke("play");
}

window.addEventListener("DOMContentLoaded", () => {
    document.getElementById("play-button").addEventListener("click", (e) => {
        e.preventDefault();
        play();
    });
    
    listen("update_time", (event) => {
        const seconds = event.payload;
        const time = new Date(seconds * 1000).toISOString().slice(11, 19);
        document.getElementById("time").textContent = `${time}`;
    });
});

HTML (Tauri):

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="styles.css" />
    <link rel="stylesheet" href="app.css" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Time Logger</title>
    <script type="module" src="/main.js" defer></script>
  </head>

  <body>
    <div class="container">
        <img id="play-button" alt=">" title="Start time" src="assets/play-button.png">
        <span id="time">00:00:00</span>
    </div>
  </body>
</html>

In this video you can see the problem. Clicking to play the timer works fine, but when clicking it to pause the timer keeps running for a few seconds before actually pausing the counter.

Edit: When I change the thread to sleep 100ms instead of 1s. And calculate the amount of time passed based on ms. The code runs a lot better. I think because the threads sleeps for 1s it is sometimes blocked from reading the playing variable state.

Yes, exactly. You shouldn't sleep the thread, this will block any subsequent operation. If you still want a timer, you can use tokio's sleep, which is asynchronous and won't block the thread.

Won't I have to make my whole code asynchronous then?

Then use something like this.

1 Like

Thanks! I accepted your answer because it is a good solution for people with a similar issue.

However, for my current use case, this implementation works very snappy, accurate and stable:

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::sync::{mpsc, Arc, Mutex, Condvar};
use tauri::{Manager, State};

struct IsPlayingState(Arc<(Mutex<bool>, Condvar)>);
struct FirstClickState(Arc<Mutex<bool>>);

fn counter(tx: mpsc::Sender<u64>, is_playing: Arc<(Mutex<bool>, Condvar)>) {
    let start_time: std::time::Instant = std::time::Instant::now();
    let mut total_pause_seconds: u64 = 0;
    
    loop {
        let (lock, cvar) = &*is_playing;
        let mut playing = lock.lock().unwrap();
        
        while !*playing {
            let pause_time: std::time::Instant = std::time::Instant::now();
            playing = cvar.wait(playing).unwrap();
            total_pause_seconds += (std::time::Instant::now()).duration_since(pause_time).as_secs();
        }

        if *playing {
            let seconds: u64 = (std::time::Instant::now()).duration_since(start_time).as_secs() - total_pause_seconds;
            tx.send(seconds).expect("Failed to transmit seconds");
            std::thread::sleep(std::time::Duration::from_millis(35));
        }
    }
}

#[tauri::command]
fn play(app_handle: tauri::AppHandle, is_playing: State<IsPlayingState>, first_click: State<FirstClickState>) {
    let (tx, rx) = mpsc::channel();
    let is_playing_clone = Arc::clone(&is_playing.0);
    let mut first_click_lock = first_click.0.lock().unwrap();

    let (lock, cvar) = &*is_playing.0;
    let mut playing = lock.lock().unwrap();
    *playing = !*playing;
    if *playing {
        cvar.notify_one();
    }
    
    if *first_click_lock {
        std::thread::spawn(move || {
            counter(tx, is_playing_clone);
        });

        std::thread::spawn(move || {
            while let Ok(seconds) = rx.recv() {
                app_handle.emit_all("update_time", seconds).expect("Failed to emit seconds");
            }
        });

        *first_click_lock = false;
    }
}

fn main() {

    let is_playing = Arc::new((Mutex::new(false), Condvar::new()));
    let first_click = Arc::new(Mutex::new(true));

    tauri::Builder::default()
        .manage(IsPlayingState(is_playing))
        .manage(FirstClickState(first_click))
        .invoke_handler(tauri::generate_handler![play])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.