|
6 | 6 | "slices" |
7 | 7 | "sort" |
8 | 8 | "strings" |
| 9 | + "sync/atomic" |
9 | 10 | "time" |
10 | 11 | "zfs-file-history/internal/data" |
11 | 12 | "zfs-file-history/internal/data/diff_state" |
@@ -40,6 +41,9 @@ type SnapshotBrowserComponent struct { |
40 | 41 | selectedSnapshotMemory *uiutil.SelectionMemory[data.SnapshotBrowserEntry] |
41 | 42 |
|
42 | 43 | isRestoringSelection bool |
| 44 | + |
| 45 | + diffCancel context.CancelFunc |
| 46 | + diffSeq atomic.Uint64 |
43 | 47 | } |
44 | 48 |
|
45 | 49 | type snapshotLoadResult struct { |
@@ -270,39 +274,115 @@ func (snapshotBrowser *SnapshotBrowserComponent) updateCurrentSnapshotEntries(qu |
270 | 274 | } |
271 | 275 |
|
272 | 276 | func (snapshotBrowser *SnapshotBrowserComponent) updateTableEntries() { |
273 | | - newEntries := snapshotBrowser.computeTableEntries(snapshotBrowser.currentSnapshots) |
274 | | - snapshotBrowser.tableContainer.SetData(newEntries) |
275 | | - snapshotBrowser.updateTableTitle() |
| 277 | + snapshotBrowser.startAsyncDiffCalculation() |
276 | 278 | } |
277 | 279 |
|
278 | | -func (snapshotBrowser *SnapshotBrowserComponent) Focus() { |
279 | | - snapshotBrowser.application.SetFocus(snapshotBrowser.container) |
| 280 | +func (snapshotBrowser *SnapshotBrowserComponent) cancelDiffCalculation() { |
| 281 | + if snapshotBrowser.diffCancel != nil { |
| 282 | + snapshotBrowser.diffCancel() |
| 283 | + snapshotBrowser.diffCancel = nil |
| 284 | + } |
280 | 285 | } |
281 | 286 |
|
282 | | -func (snapshotBrowser *SnapshotBrowserComponent) HasFocus() bool { |
283 | | - return snapshotBrowser.container.HasFocus() |
284 | | -} |
| 287 | +func (snapshotBrowser *SnapshotBrowserComponent) startAsyncDiffCalculation() { |
| 288 | + snapshotBrowser.cancelDiffCalculation() |
285 | 289 |
|
286 | | -func (snapshotBrowser *SnapshotBrowserComponent) computeTableEntries(snapshots []*zfs.Snapshot) []*data.SnapshotBrowserEntry { |
287 | | - result := []*data.SnapshotBrowserEntry{} |
| 290 | + snapshots := snapshotBrowser.currentSnapshots |
| 291 | + fileEntry := snapshotBrowser.currentFileEntry |
288 | 292 |
|
289 | | - if snapshotBrowser.hostDataset == nil { |
290 | | - return result |
| 293 | + // If no snapshots, clear the table instantly |
| 294 | + if len(snapshots) == 0 { |
| 295 | + snapshotBrowser.tableContainer.SetData([]*data.SnapshotBrowserEntry{}) |
| 296 | + snapshotBrowser.updateTableTitle() |
| 297 | + return |
291 | 298 | } |
292 | 299 |
|
293 | | - for _, snapshot := range snapshots { |
294 | | - diffState := diff_state.Unknown |
295 | | - if snapshotBrowser.currentFileEntry != nil { |
296 | | - filePath := snapshotBrowser.currentFileEntry.GetRealPath() |
297 | | - diffState = snapshot.DetermineDiffState(filePath) |
| 300 | + // Create cancellable context and increment sequence |
| 301 | + ctx, cancel := context.WithCancel(context.Background()) |
| 302 | + snapshotBrowser.diffCancel = cancel |
| 303 | + seq := snapshotBrowser.diffSeq.Add(1) |
| 304 | + |
| 305 | + // Step 1: Instantly render table with Unknown ("N/A") states |
| 306 | + initialEntries := make([]*data.SnapshotBrowserEntry, len(snapshots)) |
| 307 | + for i, snap := range snapshots { |
| 308 | + initialEntries[i] = &data.SnapshotBrowserEntry{ |
| 309 | + Snapshot: snap, |
| 310 | + DiffState: diff_state.Unknown, |
298 | 311 | } |
299 | | - result = append(result, &data.SnapshotBrowserEntry{ |
300 | | - Snapshot: snapshot, |
301 | | - DiffState: diffState, |
302 | | - }) |
303 | 312 | } |
| 313 | + snapshotBrowser.tableContainer.SetData(initialEntries) |
| 314 | + snapshotBrowser.updateTableTitle() |
| 315 | + |
| 316 | + // Get the sorted entries from the table container (determines screen top-to-bottom layout) |
| 317 | + sortedEntries := snapshotBrowser.tableContainer.GetEntries() |
| 318 | + |
| 319 | + // Step 2: Compute actual diffs in background goroutine, processing them in sorted order |
| 320 | + go func() { |
| 321 | + filePath := "" |
| 322 | + if fileEntry != nil { |
| 323 | + filePath = fileEntry.GetRealPath() |
| 324 | + } |
| 325 | + |
| 326 | + // Keep a local working copy in the exact sorted order |
| 327 | + localEntries := make([]*data.SnapshotBrowserEntry, len(sortedEntries)) |
| 328 | + for i, entry := range sortedEntries { |
| 329 | + localEntries[i] = &data.SnapshotBrowserEntry{ |
| 330 | + Snapshot: entry.Snapshot, |
| 331 | + DiffState: diff_state.Unknown, |
| 332 | + } |
| 333 | + } |
| 334 | + |
| 335 | + // Closure to safely push local updates to UI thread |
| 336 | + updateUI := func() { |
| 337 | + entriesCopy := make([]*data.SnapshotBrowserEntry, len(localEntries)) |
| 338 | + for i, entry := range localEntries { |
| 339 | + entriesCopy[i] = &data.SnapshotBrowserEntry{ |
| 340 | + Snapshot: entry.Snapshot, |
| 341 | + DiffState: entry.DiffState, |
| 342 | + } |
| 343 | + } |
| 344 | + |
| 345 | + snapshotBrowser.application.QueueUpdateDraw(func() { |
| 346 | + // Discard update if a newer selection calculation has started |
| 347 | + if seq != snapshotBrowser.diffSeq.Load() { |
| 348 | + return |
| 349 | + } |
| 350 | + snapshotBrowser.tableContainer.SetData(entriesCopy) |
| 351 | + snapshotBrowser.updateTableTitle() |
| 352 | + }) |
| 353 | + } |
| 354 | + |
| 355 | + for i, entry := range localEntries { |
| 356 | + // Abort immediately if user changed selection and cancelled context |
| 357 | + if ctx.Err() != nil { |
| 358 | + return |
| 359 | + } |
| 360 | + |
| 361 | + if filePath != "" { |
| 362 | + entry.DiffState = entry.Snapshot.DetermineDiffState(filePath) |
| 363 | + } |
| 364 | + |
| 365 | + // Incremental Redraw Logic: |
| 366 | + // - First 5 entries: Redraw after each (gives instant feedback on visible entries) |
| 367 | + // - Remaining entries: Batch update every 10 entries (optimizes UI rendering) |
| 368 | + // - Final entry: Always push the final update |
| 369 | + isFirstFew := i < 5 |
| 370 | + isBatchEnd := (i+1)%10 == 0 |
| 371 | + isLast := i == len(localEntries)-1 |
304 | 372 |
|
305 | | - return result |
| 373 | + if isFirstFew || isBatchEnd || isLast { |
| 374 | + updateUI() |
| 375 | + } |
| 376 | + } |
| 377 | + }() |
| 378 | +} |
| 379 | + |
| 380 | +func (snapshotBrowser *SnapshotBrowserComponent) Focus() { |
| 381 | + snapshotBrowser.application.SetFocus(snapshotBrowser.container) |
| 382 | +} |
| 383 | + |
| 384 | +func (snapshotBrowser *SnapshotBrowserComponent) HasFocus() bool { |
| 385 | + return snapshotBrowser.container.HasFocus() |
306 | 386 | } |
307 | 387 |
|
308 | 388 | func (snapshotBrowser *SnapshotBrowserComponent) updateTableTitle() { |
|
0 commit comments