fix: throttle ticket progress events to prevent UI freeze
This commit is contained in:
parent
9054c252ab
commit
955543d740
1 changed files with 69 additions and 7 deletions
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue