git.lucas.co / cce-data-editor
structured data editor
git clone https://git.lucas.co/cce-data-editor.git

commit1a6a0410a1b6e6d4ab53e25ca5b9e498911155b7
parent7f559cc683
authorLucas Galante <[email protected]>
date2026-09-11 13:50
Poll the open file on a wall clock, not tick's dt

The disk-sync watch accumulated tick's `dt` and fired at 1.0. But `dt`
is animation time: with nothing to redraw the runner parks on a 1 s idle
sleep and clamps `dt` to one frame on waking, so an editor sitting idle
— exactly when another writer is the one touching the file — polled
about once a minute. Measured in a shadow: 32 s to notice an external
edit, against the 1 s the comment promises.

Hold a deadline instead, and override `idle_poll_interval` to name the
cadence rather than inheriting the runner's default idle cap. Same
measurement after: 790 ms.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/main.rs | 26 +++++++++++++++++++++-----
 1 file changed, 21 insertions(+), 5 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 81887d8..23a2638 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -59,6 +59,11 @@ fn parse_path(path: &str) -> Vec<PathToken> {
     tokens
 }
 
+/// How often the open file is compared against its on-disk state. The runner
+/// wakes an idle app once a second by itself, so this costs no extra frames;
+/// `idle_poll_interval` below pins that cadence rather than inheriting it.
+const DISK_POLL_EVERY: std::time::Duration = std::time::Duration::from_secs(1);
+
 /// KDL spans are byte offsets; the TextBox cursor/selection are char indices.
 /// Convert one to the other (clamping past-the-end offsets to the char count).
 fn byte_to_char_idx(content: &str, byte_idx: usize) -> usize {
@@ -425,7 +430,11 @@ struct DataEditorApp {
     disk_state: Option<(std::time::SystemTime, u64)>,
     disk_hash: u64,
     file_outdated: bool,
-    disk_poll: f32,
+    /// When the disk-sync watch below may run again. A wall clock, not an
+    /// accumulation of `tick`'s `dt`: `dt` is animation time, clamped to one
+    /// frame after an idle sleep, so an editor sitting idle — exactly when
+    /// another writer is the one touching the file — polled once a MINUTE.
+    disk_poll_at: std::time::Instant,
     // File pickers run on a thread and report back over the engine's message
     // channel (OpenPicked / SaveAsPicked) so the event loop keeps animating.
     msg_sender: calloop::channel::Sender<AppMessage>,
@@ -1005,7 +1014,7 @@ impl Application for DataEditorApp {
                 disk_state,
                 disk_hash,
                 file_outdated: false,
-                disk_poll: 0.0,
+                disk_poll_at: std::time::Instant::now(),
                 msg_sender: sender,
                 file_dialog_open: false,
                 width: 800,
@@ -1345,6 +1354,13 @@ impl Application for DataEditorApp {
         }
     }
 
+    /// The disk-sync watch in `tick` is work the runner cannot see — nothing
+    /// redraws until the file actually changes — so say how often the loop
+    /// must come back rather than relying on its default idle cap.
+    fn idle_poll_interval(&self) -> Option<std::time::Duration> {
+        Some(DISK_POLL_EVERY)
+    }
+
     fn tick(&mut self, _dt: f32, needs_rebuild: &mut bool) {
         if self.ui_context.tick(_dt) {
             *needs_rebuild = true;
@@ -1411,9 +1427,9 @@ impl Application for DataEditorApp {
         // against the recorded loaded/saved state. Another writer touched it:
         // reload in place when the in-memory document is clean, else raise the
         // outdated indicator and leave the local edits alone.
-        self.disk_poll += _dt;
-        if self.disk_poll >= 1.0 {
-            self.disk_poll = 0.0;
+        let now = std::time::Instant::now();
+        if now >= self.disk_poll_at {
+            self.disk_poll_at = now + DISK_POLL_EVERY;
             if let (Some(path), Some(recorded)) = (self.current_file_path.clone(), self.disk_state) {
                 let on_disk = std::fs::metadata(&path)
                     .ok()