diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index 610cbf8..47f55ef 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -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 = 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 = 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 diff --git a/src/semantic_render.rs b/src/semantic_render.rs index 26fa451..b1d7bc6 100644 --- a/src/semantic_render.rs +++ b/src/semantic_render.rs @@ -370,8 +370,18 @@ impl SemanticRenderState { /// contract boundary. fn scoped_decorations(&self, state: &EditorState, vp: &DeclaredViewport) -> Vec { 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, Vec)> = 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), + }); } } }