markdown · 9936 bytes Raw Blame History

Dynamic Rendering Plan - SpaceSniffer-Style Progressive Scanning

Goal

Implement real-time progressive rendering where treemap boxes grow, shrink, and reposition as directories are scanned, similar to SpaceSniffer's iconic behavior.

Current State

  • Synchronous scanning: Full directory tree is scanned before any rendering
  • Static layout: Once rendered, boxes don't change size
  • Progress bar only: User sees only a progress bar during scan, no visual feedback of structure

Target Behavior (SpaceSniffer-like)

  1. Immediate rendering: Start drawing boxes as soon as first directory is scanned
  2. Progressive updates: Boxes dynamically resize as subdirectories are discovered
  3. Relative sizing: Boxes continuously adjust relative to siblings as scan progresses
  4. Visual feedback: User sees the file structure emerge in real-time
  5. Smooth experience: No jarring jumps, boxes flow and adapt gracefully

Technical Challenges

1. Asynchronous Scanning

  • Current: Recursive scan_directory() blocks until complete
  • Needed: Non-blocking scan that yields partial results
  • Options:
    • Thread-based: Scan in background thread, communicate via shared state
    • Idle callback: Scan one directory per GTK idle iteration
    • Generator pattern: Yield after each directory scan

2. Incremental Layout Updates

  • Current: squarify_layout() computes entire layout at once
  • Needed: Re-layout on every size update
  • Performance: Must be fast enough for 30-60 FPS during scan
  • Solution approaches:
    • Cached layouts with partial invalidation
    • Incremental squarify (update only affected nodes)
    • Layout diffing to minimize recalculation

3. Size Estimation

  • Problem: When scanning /foo, we don't know final size until all children scanned
  • SpaceSniffer approach: Use "estimated" sizes that grow as scan progresses
  • Implementation:
    • Track scanned_size vs estimated_total_size per node
    • Mark nodes as scan_complete flag
    • Use heuristics for estimation (e.g., average file size × file count estimate)

4. Treemap Algorithm Modifications

  • Standard squarify: Assumes all sizes are final
  • Dynamic squarify: Must handle changing sizes gracefully
  • Requirements:
    • Stable positioning (boxes shouldn't jump around too much)
    • Smooth transitions (animate size changes)
    • Handle new children appearing mid-render

5. Threading & GTK Integration

  • GTK is not thread-safe: All widget updates must be on main thread
  • Architecture:
    Main Thread (GTK)              Background Thread (Scanner)
    ┌──────────────┐               ┌────────────────┐
    │ Render loop  │◄──Messages────│ scan_directory │
    │ (60 FPS)     │               │ (breadth-first)│
    │              │───Request────►│                │
    │ Layout calc  │               │ File I/O       │
    └──────────────┘               └────────────────┘
    

6. State Synchronization

  • Problem: Scan thread modifies tree while render thread reads it
  • Solution options:
    • Double buffering: Scan writes to buffer, swap atomically
    • Read-write locks: Readers don't block each other
    • Message passing: Scan sends deltas, render applies them

Proposed Architecture

Phase 1: Non-Blocking Scan (Idle Callback Approach)

Simplest, no threading

module progressive_scanner
  type :: scan_state
    character(len=512) :: current_path
    integer :: depth
    type(file_node), pointer :: current_node
    logical :: complete
    ! Queue of directories to scan
    character(len=512), dimension(1000) :: pending_dirs
    integer :: queue_head, queue_tail
  end type scan_state

  type(scan_state), save :: scanner

contains

  ! Initialize scan
  subroutine start_progressive_scan(root_path)
    scanner%queue_head = 1
    scanner%queue_tail = 1
    scanner%pending_dirs(1) = root_path
    scanner%complete = .false.

    ! Register idle callback
    call g_idle_add(c_funloc(scan_one_directory), c_null_ptr)
  end subroutine

  ! Scan one directory per idle iteration
  function scan_one_directory(user_data) bind(c) result(continue)
    type(c_ptr), value :: user_data
    integer(c_int) :: continue

    if (scanner%queue_head > scanner%queue_tail) then
      scanner%complete = .true.
      continue = 0_c_int  ! Stop calling
      return
    end if

    ! Scan one directory
    call scan_single_directory(scanner%pending_dirs(scanner%queue_head))
    scanner%queue_head = scanner%queue_head + 1

    ! Request redraw
    call gtk_widget_queue_draw(widget_ptr)

    continue = 1_c_int  ! Keep calling
  end function

end module progressive_scanner

Pros:

  • Simple, no threading complexity
  • GTK-friendly
  • Easy to debug

Cons:

  • Slower than threaded (I/O blocks GUI)
  • May feel sluggish on slow disks

Phase 2: Background Thread Scan

Better performance, more complex

module threaded_scanner
  use omp_lib

  type :: scan_message
    integer :: msg_type  ! 1=new_node, 2=update_size, 3=complete
    character(len=512) :: path
    integer(int64) :: size
    integer :: parent_id, node_id
  end type

  ! Thread-safe message queue
  type(scan_message), dimension(10000) :: message_queue
  integer :: queue_read_pos = 1, queue_write_pos = 1
  !$omp threadprivate(queue_read_pos, queue_write_pos)

contains

  subroutine start_background_scan(root_path)
    !$omp parallel
    !$omp single
    call scan_directory_async(root_path)
    !$omp end single
    !$omp end parallel

    ! Register timer to process messages on main thread
    call g_timeout_add(16_c_int, c_funloc(process_scan_messages), c_null_ptr)
  end subroutine

  recursive subroutine scan_directory_async(path)
    ! Scan directory, send messages for each discovery
    ! ...
    call send_message(NEW_NODE, path, size, parent_id)
    !$omp task
    call scan_directory_async(child_path)
    !$omp end task
  end subroutine

  function process_scan_messages(user_data) bind(c) result(continue)
    ! Process up to 100 messages per frame
    do i = 1, 100
      if (has_messages()) then
        call apply_message(read_message())
      end if
    end do
    call gtk_widget_queue_draw(widget_ptr)
    continue = 1_c_int
  end function

end module threaded_scanner

Implementation Phases

Phase 1: Foundation (1-2 days)

  • Add scan_complete flag to file_node type
  • Add estimated_size vs actual_size fields
  • Modify scan_directory to be interruptible (return after N files)
  • Create scan_state module to track progress

Phase 2: Idle Callback Scanning (1 day)

  • Implement breadth-first scan queue
  • Add g_idle_add callback for progressive scanning
  • Update sizes incrementally as scan progresses
  • Trigger redraws after each directory

Phase 3: Dynamic Layout (2-3 days)

  • Cache last layout in tree nodes
  • Implement layout diffing (detect what changed)
  • Partial layout recalculation
  • Smooth transitions (animate box movements)

Phase 4: Performance Optimization (1-2 days)

  • Throttle redraws (max 30 FPS)
  • Batch size updates before layout
  • Only re-layout visible nodes
  • Add "scan paused" state for user interaction

Phase 5: Threading (Optional, 2-3 days)

  • Implement background thread scanner
  • Message queue for thread communication
  • Lock-free or minimal locking strategy
  • Test on multi-core systems

Visual Design Decisions

1. Incomplete Nodes Visual Indicator

  • Option A: Pulsing border while scanning
  • Option B: Different color saturation (dimmer until complete)
  • Option C: Animated gradient sweep
  • Recommendation: Subtle pulse + slightly dimmer

2. Transition Animation

  • Duration: 100-200ms per size update
  • Easing: Ease-out for natural feel
  • Method: Linear interpolation in render_node()

3. Performance Targets

  • Min framerate: 30 FPS during scan
  • Max layout time: 16ms per frame (60 FPS budget)
  • Redraw throttle: Every 3-5 directories scanned

Edge Cases to Handle

  1. Permission denied: Mark node as complete with error state
  2. Symlink loops: Detect and skip
  3. Very deep trees: Limit recursion depth
  4. Very wide directories: Limit children per node (group small files)
  5. Rapid navigation: Cancel in-progress scans
  6. Window resize during scan: Re-layout without restarting scan

Testing Strategy

  1. Unit tests:

    • Scan queue push/pop
    • Message queue thread safety
    • Layout diffing algorithm
  2. Integration tests:

    • Scan small directory (10 files)
    • Scan deep directory (10 levels)
    • Scan wide directory (1000 files)
    • Navigate during scan
  3. Performance tests:

    • Measure framerate on large directory
    • Profile layout recalculation time
    • Memory leak detection

Success Metrics

  • ✅ Boxes appear within 100ms of starting scan
  • ✅ Smooth animation (30+ FPS) throughout scan
  • ✅ No visual artifacts or flickering
  • ✅ User can interact (click, navigate) during scan
  • ✅ Memory usage stays reasonable (< 500MB for typical home directory)

Future Enhancements

  • Scan prioritization: Scan visible areas first
  • Incremental updates: Update only changed subtrees
  • Predictive scanning: Prefetch likely navigation targets
  • Network filesystem awareness: Adjust timeouts for slow disks
  • Cancellation: Allow user to stop scan mid-progress

References

View source
1 # Dynamic Rendering Plan - SpaceSniffer-Style Progressive Scanning
2
3 ## Goal
4 Implement real-time progressive rendering where treemap boxes grow, shrink, and reposition as directories are scanned, similar to SpaceSniffer's iconic behavior.
5
6 ## Current State
7 - **Synchronous scanning**: Full directory tree is scanned before any rendering
8 - **Static layout**: Once rendered, boxes don't change size
9 - **Progress bar only**: User sees only a progress bar during scan, no visual feedback of structure
10
11 ## Target Behavior (SpaceSniffer-like)
12 1. **Immediate rendering**: Start drawing boxes as soon as first directory is scanned
13 2. **Progressive updates**: Boxes dynamically resize as subdirectories are discovered
14 3. **Relative sizing**: Boxes continuously adjust relative to siblings as scan progresses
15 4. **Visual feedback**: User sees the file structure emerge in real-time
16 5. **Smooth experience**: No jarring jumps, boxes flow and adapt gracefully
17
18 ## Technical Challenges
19
20 ### 1. **Asynchronous Scanning**
21 - **Current**: Recursive `scan_directory()` blocks until complete
22 - **Needed**: Non-blocking scan that yields partial results
23 - **Options**:
24 - Thread-based: Scan in background thread, communicate via shared state
25 - Idle callback: Scan one directory per GTK idle iteration
26 - Generator pattern: Yield after each directory scan
27
28 ### 2. **Incremental Layout Updates**
29 - **Current**: `squarify_layout()` computes entire layout at once
30 - **Needed**: Re-layout on every size update
31 - **Performance**: Must be fast enough for 30-60 FPS during scan
32 - **Solution approaches**:
33 - Cached layouts with partial invalidation
34 - Incremental squarify (update only affected nodes)
35 - Layout diffing to minimize recalculation
36
37 ### 3. **Size Estimation**
38 - **Problem**: When scanning `/foo`, we don't know final size until all children scanned
39 - **SpaceSniffer approach**: Use "estimated" sizes that grow as scan progresses
40 - **Implementation**:
41 - Track `scanned_size` vs `estimated_total_size` per node
42 - Mark nodes as `scan_complete` flag
43 - Use heuristics for estimation (e.g., average file size × file count estimate)
44
45 ### 4. **Treemap Algorithm Modifications**
46 - **Standard squarify**: Assumes all sizes are final
47 - **Dynamic squarify**: Must handle changing sizes gracefully
48 - **Requirements**:
49 - Stable positioning (boxes shouldn't jump around too much)
50 - Smooth transitions (animate size changes)
51 - Handle new children appearing mid-render
52
53 ### 5. **Threading & GTK Integration**
54 - **GTK is not thread-safe**: All widget updates must be on main thread
55 - **Architecture**:
56 ```
57 Main Thread (GTK) Background Thread (Scanner)
58 ┌──────────────┐ ┌────────────────┐
59 │ Render loop │◄──Messages────│ scan_directory │
60 │ (60 FPS) │ │ (breadth-first)│
61 │ │───Request────►│ │
62 │ Layout calc │ │ File I/O │
63 └──────────────┘ └────────────────┘
64 ```
65
66 ### 6. **State Synchronization**
67 - **Problem**: Scan thread modifies tree while render thread reads it
68 - **Solution options**:
69 - Double buffering: Scan writes to buffer, swap atomically
70 - Read-write locks: Readers don't block each other
71 - Message passing: Scan sends deltas, render applies them
72
73 ## Proposed Architecture
74
75 ### Phase 1: Non-Blocking Scan (Idle Callback Approach)
76 **Simplest, no threading**
77
78 ```fortran
79 module progressive_scanner
80 type :: scan_state
81 character(len=512) :: current_path
82 integer :: depth
83 type(file_node), pointer :: current_node
84 logical :: complete
85 ! Queue of directories to scan
86 character(len=512), dimension(1000) :: pending_dirs
87 integer :: queue_head, queue_tail
88 end type scan_state
89
90 type(scan_state), save :: scanner
91
92 contains
93
94 ! Initialize scan
95 subroutine start_progressive_scan(root_path)
96 scanner%queue_head = 1
97 scanner%queue_tail = 1
98 scanner%pending_dirs(1) = root_path
99 scanner%complete = .false.
100
101 ! Register idle callback
102 call g_idle_add(c_funloc(scan_one_directory), c_null_ptr)
103 end subroutine
104
105 ! Scan one directory per idle iteration
106 function scan_one_directory(user_data) bind(c) result(continue)
107 type(c_ptr), value :: user_data
108 integer(c_int) :: continue
109
110 if (scanner%queue_head > scanner%queue_tail) then
111 scanner%complete = .true.
112 continue = 0_c_int ! Stop calling
113 return
114 end if
115
116 ! Scan one directory
117 call scan_single_directory(scanner%pending_dirs(scanner%queue_head))
118 scanner%queue_head = scanner%queue_head + 1
119
120 ! Request redraw
121 call gtk_widget_queue_draw(widget_ptr)
122
123 continue = 1_c_int ! Keep calling
124 end function
125
126 end module progressive_scanner
127 ```
128
129 **Pros**:
130 - Simple, no threading complexity
131 - GTK-friendly
132 - Easy to debug
133
134 **Cons**:
135 - Slower than threaded (I/O blocks GUI)
136 - May feel sluggish on slow disks
137
138 ### Phase 2: Background Thread Scan
139 **Better performance, more complex**
140
141 ```fortran
142 module threaded_scanner
143 use omp_lib
144
145 type :: scan_message
146 integer :: msg_type ! 1=new_node, 2=update_size, 3=complete
147 character(len=512) :: path
148 integer(int64) :: size
149 integer :: parent_id, node_id
150 end type
151
152 ! Thread-safe message queue
153 type(scan_message), dimension(10000) :: message_queue
154 integer :: queue_read_pos = 1, queue_write_pos = 1
155 !$omp threadprivate(queue_read_pos, queue_write_pos)
156
157 contains
158
159 subroutine start_background_scan(root_path)
160 !$omp parallel
161 !$omp single
162 call scan_directory_async(root_path)
163 !$omp end single
164 !$omp end parallel
165
166 ! Register timer to process messages on main thread
167 call g_timeout_add(16_c_int, c_funloc(process_scan_messages), c_null_ptr)
168 end subroutine
169
170 recursive subroutine scan_directory_async(path)
171 ! Scan directory, send messages for each discovery
172 ! ...
173 call send_message(NEW_NODE, path, size, parent_id)
174 !$omp task
175 call scan_directory_async(child_path)
176 !$omp end task
177 end subroutine
178
179 function process_scan_messages(user_data) bind(c) result(continue)
180 ! Process up to 100 messages per frame
181 do i = 1, 100
182 if (has_messages()) then
183 call apply_message(read_message())
184 end if
185 end do
186 call gtk_widget_queue_draw(widget_ptr)
187 continue = 1_c_int
188 end function
189
190 end module threaded_scanner
191 ```
192
193 ## Implementation Phases
194
195 ### Phase 1: Foundation (1-2 days)
196 - [ ] Add `scan_complete` flag to `file_node` type
197 - [ ] Add `estimated_size` vs `actual_size` fields
198 - [ ] Modify `scan_directory` to be interruptible (return after N files)
199 - [ ] Create `scan_state` module to track progress
200
201 ### Phase 2: Idle Callback Scanning (1 day)
202 - [ ] Implement breadth-first scan queue
203 - [ ] Add `g_idle_add` callback for progressive scanning
204 - [ ] Update sizes incrementally as scan progresses
205 - [ ] Trigger redraws after each directory
206
207 ### Phase 3: Dynamic Layout (2-3 days)
208 - [ ] Cache last layout in tree nodes
209 - [ ] Implement layout diffing (detect what changed)
210 - [ ] Partial layout recalculation
211 - [ ] Smooth transitions (animate box movements)
212
213 ### Phase 4: Performance Optimization (1-2 days)
214 - [ ] Throttle redraws (max 30 FPS)
215 - [ ] Batch size updates before layout
216 - [ ] Only re-layout visible nodes
217 - [ ] Add "scan paused" state for user interaction
218
219 ### Phase 5: Threading (Optional, 2-3 days)
220 - [ ] Implement background thread scanner
221 - [ ] Message queue for thread communication
222 - [ ] Lock-free or minimal locking strategy
223 - [ ] Test on multi-core systems
224
225 ## Visual Design Decisions
226
227 ### 1. **Incomplete Nodes Visual Indicator**
228 - **Option A**: Pulsing border while scanning
229 - **Option B**: Different color saturation (dimmer until complete)
230 - **Option C**: Animated gradient sweep
231 - **Recommendation**: Subtle pulse + slightly dimmer
232
233 ### 2. **Transition Animation**
234 - **Duration**: 100-200ms per size update
235 - **Easing**: Ease-out for natural feel
236 - **Method**: Linear interpolation in `render_node()`
237
238 ### 3. **Performance Targets**
239 - **Min framerate**: 30 FPS during scan
240 - **Max layout time**: 16ms per frame (60 FPS budget)
241 - **Redraw throttle**: Every 3-5 directories scanned
242
243 ## Edge Cases to Handle
244
245 1. **Permission denied**: Mark node as complete with error state
246 2. **Symlink loops**: Detect and skip
247 3. **Very deep trees**: Limit recursion depth
248 4. **Very wide directories**: Limit children per node (group small files)
249 5. **Rapid navigation**: Cancel in-progress scans
250 6. **Window resize during scan**: Re-layout without restarting scan
251
252 ## Testing Strategy
253
254 1. **Unit tests**:
255 - Scan queue push/pop
256 - Message queue thread safety
257 - Layout diffing algorithm
258
259 2. **Integration tests**:
260 - Scan small directory (10 files)
261 - Scan deep directory (10 levels)
262 - Scan wide directory (1000 files)
263 - Navigate during scan
264
265 3. **Performance tests**:
266 - Measure framerate on large directory
267 - Profile layout recalculation time
268 - Memory leak detection
269
270 ## Success Metrics
271
272 - ✅ Boxes appear within 100ms of starting scan
273 - ✅ Smooth animation (30+ FPS) throughout scan
274 - ✅ No visual artifacts or flickering
275 - ✅ User can interact (click, navigate) during scan
276 - ✅ Memory usage stays reasonable (< 500MB for typical home directory)
277
278 ## Future Enhancements
279
280 - **Scan prioritization**: Scan visible areas first
281 - **Incremental updates**: Update only changed subtrees
282 - **Predictive scanning**: Prefetch likely navigation targets
283 - **Network filesystem awareness**: Adjust timeouts for slow disks
284 - **Cancellation**: Allow user to stop scan mid-progress
285
286 ## References
287
288 - SpaceSniffer behavior analysis: https://www.youtube.com/watch?v=... (TBD)
289 - GTK idle callbacks: https://docs.gtk.org/glib/func.idle_add.html
290 - Fortran OpenMP: https://www.openmp.org/wp-content/uploads/openmp-4.5.pdf