9.3 perf — collapse per-tick rope copy; add frame timing
Investigating the cursor slowdown reported after the wash became visible. Confident daemon-side win: `scoped_decorations` (run every tick per semantic frontend, in the daemon's single-threaded loop that also serves the TUI) was materializing the whole buffer via `buffer_source_bytes` — an O(n) rope→Vec copy — TWICE per tick: once in the 9.2 CurrentLine branch and again in the diagnostics branch. For an LSP buffer (diagnostics present, the common case) that doubled the per-tick copy cost, and the daemon's tick latency gates TUI cursor responsiveness. Now the source + line-start table is materialized at most once per call via `get_or_insert_with` and shared between both branches (and skipped entirely when neither branch needs it). Consumer instrumentation to localize any remaining cost: - `PMACS_GPU_DEBUG_FRAME=1` logs per-`render()` sub-phase timings (background rects / minimap rects / glyph prepare+submit / total / peer count). winit defaults to ControlFlow::Wait, so renders are on-demand (one per coalesced redraw request), not a continuous loop — the timing isolates the cost of a single cursor-driven frame. - The `PMACS_GPU_DEBUG_PRESENCE` check is now one-shot via OnceLock instead of a per-message `std::env::var_os` (which locks the global env table); same for the new frame flag. No behavior change to the rendered output. `render()` gains the clippy too_many_lines allow (now 115 lines with the timing block), matching the precedent on the other linear GPU-setup functions. Gates green: - cargo fmt --all -- --check - cargo clippy --all-targets --workspace -- -D warnings - cargo clippy --all-targets --workspace --features crdt -- -D warnings - pmacs lib 1329 + pmacs-protocol 11; pmacs-gpu unit 15 - m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ffdd5d801a
commit
8e29e8b90b
|
|
@ -777,7 +777,7 @@ impl State {
|
|||
// A `buf != current` line means the peer is on a buffer
|
||||
// this mirror isn't displaying (no wash expected); no
|
||||
// line at all means the message isn't reaching us.
|
||||
if std::env::var_os("PMACS_GPU_DEBUG_PRESENCE").is_some() {
|
||||
if debug_presence() {
|
||||
eprintln!(
|
||||
"pmacs-gpu presence: fid={frontend_id:?} buf={buffer_id:?} \
|
||||
current={:?} cursor={cursor} sel={selection:?}",
|
||||
|
|
@ -1020,6 +1020,7 @@ impl State {
|
|||
self.window.request_redraw();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)] // linear per-frame GPU sequence + optional timing.
|
||||
fn render(&mut self) {
|
||||
let frame = match self.surface.get_current_texture() {
|
||||
wgpu::CurrentSurfaceTexture::Success(frame)
|
||||
|
|
@ -1037,6 +1038,7 @@ impl State {
|
|||
let view = frame
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let frame_start = debug_frame().then(std::time::Instant::now);
|
||||
let bg_vertices = self.decoration_background_vertex_bytes();
|
||||
let bg_vertex_count = (bg_vertices.len() / QUAD_VERTEX_STRIDE as usize) as u32;
|
||||
let bg_buffer = (!bg_vertices.is_empty()).then(|| {
|
||||
|
|
@ -1047,6 +1049,7 @@ impl State {
|
|||
usage: wgpu::BufferUsages::VERTEX,
|
||||
})
|
||||
});
|
||||
let after_bg = debug_frame().then(std::time::Instant::now);
|
||||
let minimap_vertices = self.minimap_vertex_bytes();
|
||||
let minimap_vertex_count = (minimap_vertices.len() / QUAD_VERTEX_STRIDE as usize) as u32;
|
||||
let minimap_buffer = (!minimap_vertices.is_empty()).then(|| {
|
||||
|
|
@ -1057,6 +1060,7 @@ impl State {
|
|||
usage: wgpu::BufferUsages::VERTEX,
|
||||
})
|
||||
});
|
||||
let after_minimap = debug_frame().then(std::time::Instant::now);
|
||||
let text_bounds_right = self.text_bounds_right();
|
||||
|
||||
self.text_renderer
|
||||
|
|
@ -1126,6 +1130,21 @@ impl State {
|
|||
self.queue.submit(std::iter::once(encoder.finish()));
|
||||
frame.present();
|
||||
self.atlas.trim();
|
||||
|
||||
if let (Some(start), Some(after_bg), Some(after_minimap)) =
|
||||
(frame_start, after_bg, after_minimap)
|
||||
{
|
||||
let end = std::time::Instant::now();
|
||||
let us = |a: std::time::Instant, b: std::time::Instant| b.duration_since(a).as_micros();
|
||||
eprintln!(
|
||||
"pmacs-gpu frame: bg={}us minimap={}us prepare+submit={}us total={}us peers={}",
|
||||
us(start, after_bg),
|
||||
us(after_bg, after_minimap),
|
||||
us(after_minimap, end),
|
||||
us(start, end),
|
||||
self.peer_presences.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn text_bounds_right(&self) -> i32 {
|
||||
|
|
@ -1529,6 +1548,23 @@ fn rgb_to_minimap_color(r: u8, g: u8, b: u8) -> [f32; 4] {
|
|||
]
|
||||
}
|
||||
|
||||
/// One-shot env flag: `PMACS_GPU_DEBUG_PRESENCE=1` logs each received
|
||||
/// `PresenceUpdate`. Read once (the env lock is not free per call) and
|
||||
/// cached for the process lifetime.
|
||||
fn debug_presence() -> bool {
|
||||
static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*FLAG.get_or_init(|| std::env::var_os("PMACS_GPU_DEBUG_PRESENCE").is_some())
|
||||
}
|
||||
|
||||
/// One-shot env flag: `PMACS_GPU_DEBUG_FRAME=1` logs per-`render()`
|
||||
/// sub-phase timings (background rects, minimap rects, glyph prepare,
|
||||
/// total) so a perceived cursor-tracking slowdown can be localized to
|
||||
/// a specific phase.
|
||||
fn debug_frame() -> bool {
|
||||
static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*FLAG.get_or_init(|| std::env::var_os("PMACS_GPU_DEBUG_FRAME").is_some())
|
||||
}
|
||||
|
||||
/// Byte range `[start, end)` of the source line containing `cursor`:
|
||||
/// `start` is just after the previous `\n` (or 0), `end` is just after
|
||||
/// the next `\n` (or text length). Mirrors the producer's
|
||||
|
|
|
|||
|
|
@ -370,8 +370,18 @@ impl SemanticRenderState {
|
|||
/// contract boundary.
|
||||
fn scoped_decorations(&self, state: &EditorState, vp: &DeclaredViewport) -> Vec<Decoration> {
|
||||
let core = state.core.borrow();
|
||||
let registry = core.registry.clone();
|
||||
let reg = registry.borrow();
|
||||
let mut out = Vec::new();
|
||||
|
||||
// Byte<->line mapping is needed by both the CurrentLine
|
||||
// derivation and the diagnostics projection, and
|
||||
// `buffer_source_bytes` is an O(n) rope copy. This runs every
|
||||
// tick in the daemon's hot loop, so materialize at most once
|
||||
// per call and reuse — never twice (the pre-9.2 shape copied
|
||||
// separately in each branch).
|
||||
let mut line_info: Option<(Vec<u8>, Vec<u64>)> = None;
|
||||
|
||||
// Selection + CurrentLine — per-window (per-frontend) state.
|
||||
// Only this session's active window for the declared buffer
|
||||
// contributes either kind.
|
||||
|
|
@ -394,12 +404,13 @@ impl SemanticRenderState {
|
|||
kind: DecorationKind::Selection,
|
||||
});
|
||||
}
|
||||
let registry = core.registry.clone();
|
||||
let reg = registry.borrow();
|
||||
if let Ok(buf) = reg.get(vp.buffer_id) {
|
||||
let source = buffer_source_bytes(buf);
|
||||
let line_starts = line_start_offsets(&source);
|
||||
let (lo, hi) = current_line_range(&line_starts, source.len() as u64, win.cursor);
|
||||
let (source, line_starts) = line_info.get_or_insert_with(|| {
|
||||
let s = buffer_source_bytes(buf);
|
||||
let ls = line_start_offsets(&s);
|
||||
(s, ls)
|
||||
});
|
||||
let (lo, hi) = current_line_range(line_starts, source.len() as u64, win.cursor);
|
||||
if let Some(range) = clip_to_viewport(lo, hi, vp) {
|
||||
out.push(Decoration {
|
||||
range,
|
||||
|
|
@ -427,31 +438,24 @@ impl SemanticRenderState {
|
|||
let guard = store.lock().expect("diag store mutex poisoned");
|
||||
(guard.for_uri(&uri).to_vec(), guard.is_stale(&uri))
|
||||
};
|
||||
if !is_stale && !diags.is_empty() {
|
||||
let registry = core.registry.clone();
|
||||
let reg = registry.borrow();
|
||||
if let Ok(buf) = reg.get(vp.buffer_id) {
|
||||
let source = buffer_source_bytes(buf);
|
||||
let line_starts = line_start_offsets(&source);
|
||||
for d in &diags {
|
||||
let lo = line_col_to_byte(
|
||||
&line_starts,
|
||||
source.len() as u64,
|
||||
d.start_line,
|
||||
d.start_col,
|
||||
);
|
||||
let hi = line_col_to_byte(
|
||||
&line_starts,
|
||||
source.len() as u64,
|
||||
d.end_line,
|
||||
d.end_col,
|
||||
);
|
||||
if let Some(range) = clip_to_viewport(lo, hi, vp) {
|
||||
out.push(Decoration {
|
||||
range,
|
||||
kind: severity_to_kind(d.severity),
|
||||
});
|
||||
}
|
||||
if !is_stale
|
||||
&& !diags.is_empty()
|
||||
&& let Ok(buf) = reg.get(vp.buffer_id)
|
||||
{
|
||||
let (source, line_starts) = line_info.get_or_insert_with(|| {
|
||||
let s = buffer_source_bytes(buf);
|
||||
let ls = line_start_offsets(&s);
|
||||
(s, ls)
|
||||
});
|
||||
let source_len = source.len() as u64;
|
||||
for d in &diags {
|
||||
let lo = line_col_to_byte(line_starts, source_len, d.start_line, d.start_col);
|
||||
let hi = line_col_to_byte(line_starts, source_len, d.end_line, d.end_col);
|
||||
if let Some(range) = clip_to_viewport(lo, hi, vp) {
|
||||
out.push(Decoration {
|
||||
range,
|
||||
kind: severity_to_kind(d.severity),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue