perf(tui): render streaming token secara inkremental + kurangi redraw sia-sia
- fix(view): token streaming kini benar-benar tampil — sebelumnya cache display_lines tidak pernah di-rebuild saat pesan terakhir berubah (msg_count == cached_count), jadi teks AI streaming tidak pernah muncul sampai pesan baru/resize - perf(view): streaming kini hanya re-render pesan TERAKHIR (splice di batas cached_last_start) → O(konten baru) per token, bukan O(seluruh history); guard cached_last_len mencegah re-render pada frame spinner tanpa token baru - perf(run): skip chrono::Utc::now() + drain toasts saat tidak ada toast - perf(misc): drain_expired_toasts tidak lagi clone seluruh daftar toast - perf(view): render_toasts fast-path saat toasts kosong
This commit is contained in:
@@ -190,6 +190,8 @@ impl Component for ChatComponent {
|
||||
}
|
||||
state.display_lines_cache = all_lines;
|
||||
state.cached_msg_count = msg_count;
|
||||
state.cached_last_start = state.display_lines_cache.len();
|
||||
state.cached_last_len = last_msg_len(state);
|
||||
state.render_width_at_cache = state.last_render_width;
|
||||
state.transcript_cache.dirty = false;
|
||||
} else if state.transcript_cache.dirty && msg_count > cached_count {
|
||||
@@ -205,6 +207,27 @@ impl Component for ChatComponent {
|
||||
state.display_lines_cache.extend(msg_lines);
|
||||
}
|
||||
state.cached_msg_count = msg_count;
|
||||
state.cached_last_start = state.display_lines_cache.len();
|
||||
state.cached_last_len = last_msg_len(state);
|
||||
state.transcript_cache.dirty = false;
|
||||
} else if state.transcript_cache.dirty {
|
||||
// Streaming update: message count unchanged, but the last
|
||||
// assistant message's content grew (StreamToken). Re-render only
|
||||
// that message and splice its lines back into the cache — this
|
||||
// keeps updates O(new content), not O(whole history) per token.
|
||||
// Without this branch, streamed tokens would never appear (the
|
||||
// cache is only rebuilt on new-message or resize paths).
|
||||
let cur_len = last_msg_len(state);
|
||||
if cur_len != state.cached_last_len {
|
||||
if let Some(last_msg) = state.transcript_cache.messages.back() {
|
||||
if state.cached_last_start <= state.display_lines_cache.len() {
|
||||
let new_lines = render_one_message(last_msg, content_width);
|
||||
state.display_lines_cache.truncate(state.cached_last_start);
|
||||
state.display_lines_cache.extend(new_lines);
|
||||
}
|
||||
}
|
||||
state.cached_last_len = cur_len;
|
||||
}
|
||||
state.transcript_cache.dirty = false;
|
||||
}
|
||||
|
||||
@@ -326,3 +349,15 @@ fn split_spans_into_lines(spans: Vec<Span<'_>>) -> Vec<Line<'_>> {
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
/// Byte length of the last message's content + reasoning. Used as a cheap
|
||||
/// change detector for the streaming render branch — avoids re-rendering on
|
||||
/// spinner-only ticks where no new token arrived.
|
||||
fn last_msg_len(state: &crate::state::AppStateRest) -> usize {
|
||||
state
|
||||
.transcript_cache
|
||||
.messages
|
||||
.back()
|
||||
.map(|m| m.content.len() + m.reasoning.len())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user