fix: throttle ticket progress events to prevent UI freeze

This commit is contained in:
thibaud-lclr 2026-04-20 14:38:38 +02:00
parent 9054c252ab
commit 955543d740

View file

@ -10,6 +10,7 @@ use crate::models::worktree::Worktree;
use crate::services::process_registry::ProcessRegistry; use crate::services::process_registry::ProcessRegistry;
use crate::services::{cli_process, notifier, worktree_manager}; use crate::services::{cli_process, notifier, worktree_manager};
use rusqlite::Connection; use rusqlite::Connection;
use std::mem;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
@ -17,6 +18,9 @@ use tokio::process::Command;
use tokio::sync::Mutex as AsyncMutex; use tokio::sync::Mutex as AsyncMutex;
use tokio::time::{interval, timeout, Duration}; use tokio::time::{interval, timeout, Duration};
const PROGRESS_BUFFER_MAX_BYTES: usize = 2048;
const PROGRESS_EMIT_INTERVAL_MS: u128 = 250;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Verdict { pub enum Verdict {
FixNeeded, FixNeeded,
@ -207,6 +211,15 @@ fn validate_developer_completion(
evaluate_developer_completion(developer_report, commits.len(), !diff.trim().is_empty()) evaluate_developer_completion(developer_report, commits.len(), !diff.trim().is_empty())
} }
fn should_flush_progress_buffer(buffer: &str, elapsed_since_last_emit: std::time::Duration) -> bool {
if buffer.is_empty() {
return false;
}
buffer.len() >= PROGRESS_BUFFER_MAX_BYTES
|| elapsed_since_last_emit.as_millis() >= PROGRESS_EMIT_INTERVAL_MS
}
fn recover_interrupted_tickets(db: &Arc<Mutex<Connection>>) -> Result<usize, String> { fn recover_interrupted_tickets(db: &Arc<Mutex<Connection>>) -> Result<usize, String> {
let inflight = { let inflight = {
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
@ -305,14 +318,27 @@ pub async fn run_cli_command(
"Failed to capture stderr".to_string() "Failed to capture stderr".to_string()
})?; })?;
let mut progress_buffer = String::new();
let mut last_progress_emit = std::time::Instant::now();
let read_future = cli_process::collect_process_output(child.clone(), stdout, stderr, |line| { let read_future = cli_process::collect_process_output(child.clone(), stdout, stderr, |line| {
let _ = app_handle.emit( if !line.is_empty() {
"ticket-processing-progress", if !progress_buffer.is_empty() {
serde_json::json!({ progress_buffer.push('\n');
"ticket_id": ticket_id, }
"output_chunk": line, progress_buffer.push_str(line);
}), }
);
if should_flush_progress_buffer(&progress_buffer, last_progress_emit.elapsed()) {
let chunk = mem::take(&mut progress_buffer);
let _ = app_handle.emit(
"ticket-processing-progress",
serde_json::json!({
"ticket_id": ticket_id,
"output_chunk": chunk,
}),
);
last_progress_emit = std::time::Instant::now();
}
Ok(()) Ok(())
}); });
@ -336,6 +362,17 @@ pub async fn run_cli_command(
} }
}; };
if !progress_buffer.is_empty() {
let chunk = mem::take(&mut progress_buffer);
let _ = app_handle.emit(
"ticket-processing-progress",
serde_json::json!({
"ticket_id": ticket_id,
"output_chunk": chunk,
}),
);
}
process_registry.unregister_ticket(ticket_id); process_registry.unregister_ticket(ticket_id);
if cancellation_requested.load(Ordering::SeqCst) { if cancellation_requested.load(Ordering::SeqCst) {
@ -959,6 +996,31 @@ mod tests {
assert_eq!(updated.status, "Pending"); assert_eq!(updated.status, "Pending");
} }
#[test]
fn test_should_flush_progress_buffer_when_elapsed_interval_exceeded() {
let should_flush = should_flush_progress_buffer(
"some progress",
std::time::Duration::from_millis(300),
);
assert!(should_flush);
}
#[test]
fn test_should_flush_progress_buffer_when_buffer_reaches_size_limit() {
let payload = "x".repeat(2048);
let should_flush = should_flush_progress_buffer(
&payload,
std::time::Duration::from_millis(10),
);
assert!(should_flush);
}
#[test]
fn test_should_not_flush_progress_buffer_when_empty() {
let should_flush = should_flush_progress_buffer("", std::time::Duration::from_secs(1));
assert!(!should_flush);
}
#[test] #[test]
fn test_evaluate_developer_completion_rejects_permission_block_report() { fn test_evaluate_developer_completion_rejects_permission_block_report() {
let result = evaluate_developer_completion( let result = evaluate_developer_completion(