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)
- Immediate rendering: Start drawing boxes as soon as first directory is scanned
- Progressive updates: Boxes dynamically resize as subdirectories are discovered
- Relative sizing: Boxes continuously adjust relative to siblings as scan progresses
- Visual feedback: User sees the file structure emerge in real-time
- 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_sizevsestimated_total_sizeper node - Mark nodes as
scan_completeflag - Use heuristics for estimation (e.g., average file size × file count estimate)
- Track
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_completeflag tofile_nodetype - Add
estimated_sizevsactual_sizefields - Modify
scan_directoryto be interruptible (return after N files) - Create
scan_statemodule to track progress
Phase 2: Idle Callback Scanning (1 day)
- Implement breadth-first scan queue
- Add
g_idle_addcallback 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
- Permission denied: Mark node as complete with error state
- Symlink loops: Detect and skip
- Very deep trees: Limit recursion depth
- Very wide directories: Limit children per node (group small files)
- Rapid navigation: Cancel in-progress scans
- Window resize during scan: Re-layout without restarting scan
Testing Strategy
-
Unit tests:
- Scan queue push/pop
- Message queue thread safety
- Layout diffing algorithm
-
Integration tests:
- Scan small directory (10 files)
- Scan deep directory (10 levels)
- Scan wide directory (1000 files)
- Navigate during scan
-
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
- SpaceSniffer behavior analysis: https://www.youtube.com/watch?v=... (TBD)
- GTK idle callbacks: https://docs.gtk.org/glib/func.idle_add.html
- Fortran OpenMP: https://www.openmp.org/wp-content/uploads/openmp-4.5.pdf
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 |