mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(perf): reduce long-session stalls (exec + history)
This commit is contained in:
@@ -314,6 +314,10 @@ impl ModelClient {
|
||||
&self.config.code_home
|
||||
}
|
||||
|
||||
pub fn debug_enabled(&self) -> bool {
|
||||
self.config.debug
|
||||
}
|
||||
|
||||
pub fn build_tools_config_with_sandbox(
|
||||
&self,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
|
||||
+16
-10
@@ -11514,18 +11514,22 @@ async fn handle_container_exec_with_params(
|
||||
let sandbox_policy = sess.sandbox_policy.clone();
|
||||
let sandbox_cwd = sess.get_cwd().to_path_buf();
|
||||
let code_linux_sandbox_exe = sess.code_linux_sandbox_exe.clone();
|
||||
let exec_spool_dir = sess
|
||||
.client
|
||||
.code_home()
|
||||
.join("debug_logs")
|
||||
.join("exec");
|
||||
let exec_spool_dir_for_task = if sess.client.debug_enabled() {
|
||||
Some(
|
||||
sess.client
|
||||
.code_home()
|
||||
.join("debug_logs")
|
||||
.join("exec"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let result_cell_for_task = result_cell.clone();
|
||||
let notify_task = notify.clone();
|
||||
let tail_buf_task = tail_buf.clone();
|
||||
let backgrounded_task = backgrounded.clone();
|
||||
let suppress_event_flag_task = suppress_event_flag.clone();
|
||||
let display_label_task = display_label.clone();
|
||||
let exec_spool_dir_for_task = exec_spool_dir.clone();
|
||||
let task_handle = tokio::spawn(async move {
|
||||
// Build stdout stream with tail capture. We cannot stamp via `Session` here,
|
||||
// but deltas will be delivered with neutral ordering which the UI tolerates.
|
||||
@@ -11539,7 +11543,7 @@ async fn handle_container_exec_with_params(
|
||||
session: None,
|
||||
tail_buf: Some(tail_buf_task.clone()),
|
||||
order: Some(order_meta_for_deltas.clone()),
|
||||
spool_dir: Some(exec_spool_dir_for_task.clone()),
|
||||
spool_dir: exec_spool_dir_for_task.clone(),
|
||||
})
|
||||
};
|
||||
|
||||
@@ -11874,9 +11878,11 @@ async fn handle_sandbox_error(
|
||||
session: None,
|
||||
tail_buf: None,
|
||||
order: Some(crate::protocol::OrderMeta { request_ordinal: attempt_req, output_index: None, sequence_number: None }),
|
||||
spool_dir: Some(
|
||||
sess.client.code_home().join("debug_logs").join("exec"),
|
||||
),
|
||||
spool_dir: if sess.client.debug_enabled() {
|
||||
Some(sess.client.code_home().join("debug_logs").join("exec"))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -690,6 +690,10 @@ fn retained_stream_len(chunks: &[ExecStreamChunk]) -> usize {
|
||||
stream_len(chunks).saturating_sub(first_offset)
|
||||
}
|
||||
|
||||
fn retained_exec_record_bytes(record: &ExecRecord) -> usize {
|
||||
retained_stream_len(&record.stdout_chunks).saturating_add(retained_stream_len(&record.stderr_chunks))
|
||||
}
|
||||
|
||||
fn truncated_prefix_len(chunks: &[ExecStreamChunk]) -> usize {
|
||||
chunks.first().map(|chunk| chunk.offset).unwrap_or(0)
|
||||
}
|
||||
@@ -1392,6 +1396,10 @@ pub struct HistoryState {
|
||||
id_index: HashMap<HistoryId, usize>,
|
||||
#[serde(skip)]
|
||||
usage_tracker: HistoryUsageTracker,
|
||||
#[serde(skip)]
|
||||
exec_stream_retained_total_bytes: usize,
|
||||
#[serde(skip)]
|
||||
exec_stream_retained_by_id: HashMap<HistoryId, usize>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -1405,6 +1413,8 @@ impl HistoryState {
|
||||
stream_lookup: HashMap::new(),
|
||||
id_index: HashMap::new(),
|
||||
usage_tracker: HistoryUsageTracker::default(),
|
||||
exec_stream_retained_total_bytes: 0,
|
||||
exec_stream_retained_by_id: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1599,6 +1609,58 @@ impl HistoryState {
|
||||
for record in &self.records {
|
||||
self.usage_tracker.on_insert(record);
|
||||
}
|
||||
self.rebuild_exec_stream_retained_totals();
|
||||
}
|
||||
|
||||
fn rebuild_exec_stream_retained_totals(&mut self) {
|
||||
let mut total = 0usize;
|
||||
let mut by_id: HashMap<HistoryId, usize> = HashMap::new();
|
||||
|
||||
for record in &self.records {
|
||||
let HistoryRecord::Exec(exec) = record else {
|
||||
continue;
|
||||
};
|
||||
if exec.id == HistoryId::ZERO {
|
||||
continue;
|
||||
}
|
||||
let retained = retained_exec_record_bytes(exec);
|
||||
by_id.insert(exec.id, retained);
|
||||
total = total.saturating_add(retained);
|
||||
}
|
||||
|
||||
self.exec_stream_retained_total_bytes = total;
|
||||
self.exec_stream_retained_by_id = by_id;
|
||||
}
|
||||
|
||||
fn set_exec_stream_retained(&mut self, id: HistoryId, retained: usize) {
|
||||
if id == HistoryId::ZERO {
|
||||
return;
|
||||
}
|
||||
let prev = self.exec_stream_retained_by_id.insert(id, retained).unwrap_or(0);
|
||||
match retained.cmp(&prev) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
self.exec_stream_retained_total_bytes = self
|
||||
.exec_stream_retained_total_bytes
|
||||
.saturating_add(retained.saturating_sub(prev));
|
||||
}
|
||||
std::cmp::Ordering::Less => {
|
||||
self.exec_stream_retained_total_bytes = self
|
||||
.exec_stream_retained_total_bytes
|
||||
.saturating_sub(prev.saturating_sub(retained));
|
||||
}
|
||||
std::cmp::Ordering::Equal => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_exec_stream_retained(&mut self, id: HistoryId) {
|
||||
if id == HistoryId::ZERO {
|
||||
return;
|
||||
}
|
||||
if let Some(prev) = self.exec_stream_retained_by_id.remove(&id) {
|
||||
self.exec_stream_retained_total_bytes = self
|
||||
.exec_stream_retained_total_bytes
|
||||
.saturating_sub(prev);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn truncate_after(&mut self, id: HistoryId) -> Vec<HistoryRecord> {
|
||||
@@ -1609,6 +1671,8 @@ impl HistoryState {
|
||||
self.stream_lookup.clear();
|
||||
self.next_id = 1;
|
||||
self.usage_tracker.reset();
|
||||
self.exec_stream_retained_total_bytes = 0;
|
||||
self.exec_stream_retained_by_id.clear();
|
||||
return removed;
|
||||
}
|
||||
|
||||
@@ -1662,6 +1726,7 @@ impl HistoryState {
|
||||
if let Some(call_id) = state.call_id.as_ref() {
|
||||
self.exec_call_lookup.insert(call_id.clone(), state.id);
|
||||
}
|
||||
self.set_exec_stream_retained(state.id, retained_exec_record_bytes(state));
|
||||
}
|
||||
HistoryRecord::MergedExec(state) => {
|
||||
for segment in &state.segments {
|
||||
@@ -1701,6 +1766,7 @@ impl HistoryState {
|
||||
self.exec_call_lookup.remove(call_id);
|
||||
}
|
||||
}
|
||||
self.remove_exec_stream_retained(state.id);
|
||||
}
|
||||
HistoryRecord::MergedExec(state) => {
|
||||
for segment in &state.segments {
|
||||
@@ -1850,7 +1916,7 @@ impl HistoryState {
|
||||
stdout_chunk,
|
||||
stderr_chunk,
|
||||
} => {
|
||||
let id = match self.records.get_mut(index) {
|
||||
let (id, retained) = match self.records.get_mut(index) {
|
||||
Some(HistoryRecord::Exec(existing)) => {
|
||||
if let Some(chunk) = stdout_chunk {
|
||||
let chunk_len = chunk.content.len();
|
||||
@@ -1864,11 +1930,14 @@ impl HistoryState {
|
||||
}
|
||||
self.usage_tracker
|
||||
.observe_exec(existing, "domain:update-exec-stream");
|
||||
existing.id
|
||||
let retained = retained_exec_record_bytes(existing);
|
||||
(existing.id, retained)
|
||||
}
|
||||
_ => return HistoryMutation::Noop,
|
||||
};
|
||||
|
||||
self.set_exec_stream_retained(id, retained);
|
||||
|
||||
self.enforce_exec_stream_global_limit();
|
||||
|
||||
if let Some(record) = self.records.get(index).cloned() {
|
||||
@@ -2115,44 +2184,52 @@ impl HistoryState {
|
||||
/// `GLOBAL_EXEC_STREAM_RETAINED_BYTES`, progressively prune the oldest exec
|
||||
/// records down to `EXEC_STREAM_PRUNE_TARGET_BYTES` per stream.
|
||||
fn enforce_exec_stream_global_limit(&mut self) {
|
||||
let mut total_retained: usize = 0;
|
||||
let mut exec_indices: Vec<usize> = Vec::new();
|
||||
|
||||
for (idx, record) in self.records.iter().enumerate() {
|
||||
if let HistoryRecord::Exec(exec) = record {
|
||||
let retained = retained_stream_len(&exec.stdout_chunks)
|
||||
.saturating_add(retained_stream_len(&exec.stderr_chunks));
|
||||
if retained > 0 {
|
||||
total_retained = total_retained.saturating_add(retained);
|
||||
exec_indices.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if total_retained <= GLOBAL_EXEC_STREAM_RETAINED_BYTES {
|
||||
if self.exec_stream_retained_total_bytes <= GLOBAL_EXEC_STREAM_RETAINED_BYTES {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut bytes_to_drop = total_retained - GLOBAL_EXEC_STREAM_RETAINED_BYTES;
|
||||
let mut bytes_to_drop =
|
||||
self.exec_stream_retained_total_bytes - GLOBAL_EXEC_STREAM_RETAINED_BYTES;
|
||||
|
||||
for idx in exec_indices {
|
||||
let retained_by_id = &mut self.exec_stream_retained_by_id;
|
||||
let total_retained = &mut self.exec_stream_retained_total_bytes;
|
||||
|
||||
for record in &mut self.records {
|
||||
if bytes_to_drop == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(HistoryRecord::Exec(exec)) = self.records.get_mut(idx) {
|
||||
let before = retained_stream_len(&exec.stdout_chunks)
|
||||
.saturating_add(retained_stream_len(&exec.stderr_chunks));
|
||||
let HistoryRecord::Exec(exec) = record else {
|
||||
continue;
|
||||
};
|
||||
|
||||
prune_exec_stream(&mut exec.stdout_chunks, EXEC_STREAM_PRUNE_TARGET_BYTES);
|
||||
prune_exec_stream(&mut exec.stderr_chunks, EXEC_STREAM_PRUNE_TARGET_BYTES);
|
||||
|
||||
let after = retained_stream_len(&exec.stdout_chunks)
|
||||
.saturating_add(retained_stream_len(&exec.stderr_chunks));
|
||||
|
||||
let dropped = before.saturating_sub(after);
|
||||
bytes_to_drop = bytes_to_drop.saturating_sub(dropped);
|
||||
let before = retained_by_id
|
||||
.get(&exec.id)
|
||||
.copied()
|
||||
.unwrap_or_else(|| retained_exec_record_bytes(exec));
|
||||
if before == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
prune_exec_stream(&mut exec.stdout_chunks, EXEC_STREAM_PRUNE_TARGET_BYTES);
|
||||
prune_exec_stream(&mut exec.stderr_chunks, EXEC_STREAM_PRUNE_TARGET_BYTES);
|
||||
|
||||
let after = retained_exec_record_bytes(exec);
|
||||
let prev = retained_by_id.insert(exec.id, after).unwrap_or(0);
|
||||
match after.cmp(&prev) {
|
||||
std::cmp::Ordering::Greater => {
|
||||
*total_retained = (*total_retained)
|
||||
.saturating_add(after.saturating_sub(prev));
|
||||
}
|
||||
std::cmp::Ordering::Less => {
|
||||
*total_retained = (*total_retained)
|
||||
.saturating_sub(prev.saturating_sub(after));
|
||||
}
|
||||
std::cmp::Ordering::Equal => {}
|
||||
}
|
||||
|
||||
let dropped = before.saturating_sub(after);
|
||||
bytes_to_drop = bytes_to_drop.saturating_sub(dropped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8721,13 +8721,22 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
// Determine insertion position across the entire history
|
||||
// Determine insertion position across the entire history.
|
||||
// Most ordered inserts are monotonic tail-appends (we bump non-background
|
||||
// keys to keep them strictly increasing), so avoid an O(n) scan in the
|
||||
// common case.
|
||||
//
|
||||
// Exception: some early, non-background system cells (e.g. the context
|
||||
// summary) are inserted with a low order key before any ordering state
|
||||
// has been established. In that phase, we must still respect the order.
|
||||
let mut pos = self.history_cells.len();
|
||||
for i in 0..self.history_cells.len() {
|
||||
if let Some(existing) = self.cell_order_seq.get(i) {
|
||||
if *existing > key {
|
||||
pos = i;
|
||||
break;
|
||||
if is_background_cell || self.last_assigned_order.is_none() {
|
||||
for i in 0..self.history_cells.len() {
|
||||
if let Some(existing) = self.cell_order_seq.get(i) {
|
||||
if *existing > key {
|
||||
pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8809,8 +8818,12 @@ impl ChatWidget<'_> {
|
||||
|
||||
let mut cell = cell;
|
||||
|
||||
let record_index = self.record_index_for_position(pos);
|
||||
let mutation = if let Some(domain_record) = record {
|
||||
let record_index = if pos == self.history_cells.len() {
|
||||
self.history_state.records.len()
|
||||
} else {
|
||||
self.record_index_for_position(pos)
|
||||
};
|
||||
let event = match domain_record {
|
||||
HistoryDomainRecord::Exec(ref exec_record) => {
|
||||
HistoryDomainEvent::StartExec {
|
||||
@@ -8832,6 +8845,11 @@ impl ChatWidget<'_> {
|
||||
};
|
||||
Some(self.history_state.apply_domain_event(event))
|
||||
} else if let Some(record) = history_cell::record_from_cell(cell.as_ref()) {
|
||||
let record_index = if pos == self.history_cells.len() {
|
||||
self.history_state.records.len()
|
||||
} else {
|
||||
self.record_index_for_position(pos)
|
||||
};
|
||||
let event = match HistoryDomainRecord::from(record) {
|
||||
HistoryDomainRecord::Exec(exec_record) => HistoryDomainEvent::StartExec {
|
||||
index: record_index,
|
||||
@@ -8861,8 +8879,14 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
self.history_cells.insert(pos, cell);
|
||||
self.history_cell_ids.insert(pos, maybe_id);
|
||||
let append = pos == self.history_cells.len();
|
||||
if append {
|
||||
self.history_cells.push(cell);
|
||||
self.history_cell_ids.push(maybe_id);
|
||||
} else {
|
||||
self.history_cells.insert(pos, cell);
|
||||
self.history_cell_ids.insert(pos, maybe_id);
|
||||
}
|
||||
// In terminal mode, App mirrors history lines into the native buffer.
|
||||
// Ensure order vector is also long enough for position after cell insert
|
||||
if self.cell_order_seq.len() < pos {
|
||||
@@ -8875,7 +8899,11 @@ impl ChatWidget<'_> {
|
||||
},
|
||||
);
|
||||
}
|
||||
self.cell_order_seq.insert(pos, key);
|
||||
if append {
|
||||
self.cell_order_seq.push(key);
|
||||
} else {
|
||||
self.cell_order_seq.insert(pos, key);
|
||||
}
|
||||
if key_bumped {
|
||||
if let Some(stream) = self.history_cells[pos]
|
||||
.as_any()
|
||||
@@ -8916,7 +8944,11 @@ impl ChatWidget<'_> {
|
||||
if self.cell_order_dbg.len() < pos {
|
||||
self.cell_order_dbg.resize(pos, None);
|
||||
}
|
||||
self.cell_order_dbg.insert(pos, Some(dbg));
|
||||
if append {
|
||||
self.cell_order_dbg.push(Some(dbg));
|
||||
} else {
|
||||
self.cell_order_dbg.insert(pos, Some(dbg));
|
||||
}
|
||||
self.invalidate_height_cache();
|
||||
self.autoscroll_if_near_bottom();
|
||||
self.bottom_pane.set_has_chat_history(true);
|
||||
@@ -8971,11 +9003,13 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
let mut pos = self.history_cells.len();
|
||||
for i in 0..self.history_cells.len() {
|
||||
if let Some(existing) = self.cell_order_seq.get(i) {
|
||||
if *existing > key {
|
||||
pos = i;
|
||||
break;
|
||||
if is_background_cell || self.last_assigned_order.is_none() {
|
||||
for i in 0..self.history_cells.len() {
|
||||
if let Some(existing) = self.cell_order_seq.get(i) {
|
||||
if *existing > key {
|
||||
pos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9045,8 +9079,14 @@ impl ChatWidget<'_> {
|
||||
|
||||
Self::assign_history_id_inner(&mut cell, id);
|
||||
|
||||
self.history_cells.insert(pos, cell);
|
||||
self.history_cell_ids.insert(pos, Some(id));
|
||||
let append = pos == self.history_cells.len();
|
||||
if append {
|
||||
self.history_cells.push(cell);
|
||||
self.history_cell_ids.push(Some(id));
|
||||
} else {
|
||||
self.history_cells.insert(pos, cell);
|
||||
self.history_cell_ids.insert(pos, Some(id));
|
||||
}
|
||||
if self.cell_order_seq.len() < pos {
|
||||
self.cell_order_seq.resize(
|
||||
pos,
|
||||
@@ -9057,7 +9097,11 @@ impl ChatWidget<'_> {
|
||||
},
|
||||
);
|
||||
}
|
||||
self.cell_order_seq.insert(pos, key);
|
||||
if append {
|
||||
self.cell_order_seq.push(key);
|
||||
} else {
|
||||
self.cell_order_seq.insert(pos, key);
|
||||
}
|
||||
if key_bumped {
|
||||
if let Some(stream) = self.history_cells[pos]
|
||||
.as_any()
|
||||
@@ -9097,7 +9141,11 @@ impl ChatWidget<'_> {
|
||||
if self.cell_order_dbg.len() < pos {
|
||||
self.cell_order_dbg.resize(pos, None);
|
||||
}
|
||||
self.cell_order_dbg.insert(pos, Some(dbg));
|
||||
if append {
|
||||
self.cell_order_dbg.push(Some(dbg));
|
||||
} else {
|
||||
self.cell_order_dbg.insert(pos, Some(dbg));
|
||||
}
|
||||
self.invalidate_height_cache();
|
||||
self.autoscroll_if_near_bottom();
|
||||
self.bottom_pane.set_has_chat_history(true);
|
||||
@@ -11147,7 +11195,7 @@ impl ChatWidget<'_> {
|
||||
|
||||
fn mark_history_dirty(&mut self) {
|
||||
self.history_snapshot_dirty = true;
|
||||
self.flush_history_snapshot_if_needed(true);
|
||||
self.flush_history_snapshot_if_needed(false);
|
||||
}
|
||||
|
||||
fn flush_history_snapshot_if_needed(&mut self, force: bool) {
|
||||
|
||||
Reference in New Issue
Block a user