Skip to content

Conversation

@bmr-cymru
Copy link
Contributor

@bmr-cymru bmr-cymru commented Dec 18, 2025

Resolves: #685
Resolves: #774
Resolves: #776

Summary by CodeRabbit

  • New Features

    • More granular move-detection phase with per-item progress, move counting and clearer lifecycle reporting.
    • Frame-rate throttling for progress rendering to smooth and limit update frequency.
  • Bug Fixes

    • Progress cancellation now finalises reliably and respects throttling.
    • Move-related diffs are correctly pruned during post-processing.
  • Tests

    • Tests updated to account for FPS-timed progress updates.
  • Documentation

    • Minor docstring typo corrected.

✏️ Tip: You can customize this high-level summary in your review settings.

@bmr-cymru bmr-cymru self-assigned this Dec 18, 2025
@bmr-cymru bmr-cymru added enhancement New feature or request UI/UX User interface and experience Cleanup DifferenceEngine labels Dec 18, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 18, 2025

Walkthrough

Compute_diff and _detect_moves now accept an optional term_control; move detection was extracted from the main diff pass into a separate post-processing phase that uses TermControl-driven progress reporting. Progress rendering was changed to FPS‑limited interval printing and a new _do_cancel finalisation path; tests updated to use DEFAULT_FPS.

Changes

Cohort / File(s) Summary
Progress FPS changes
snapm/progress.py
Rename DEFAULT_THROBBER_FPSDEFAULT_FPS; add _interval_us, _last, _last_message and a done counter; throttle output to interval based on FPS; add Progress._do_cancel for FPS-aware finalisation; propagate DEFAULT_FPS to ThrobberBase and __all__.
Detect-moves moved to post-phase
snapm/fsdiff/engine.py
Add term_control parameter to compute_diff and _detect_moves; remove in-place move detection from the main diff loop; add explicit moves phase with per-item progress updates, existence guards, move counting/timing, pruning of move-related diffs, and propagation of term_control.
Tests adjusted for FPS
tests/test_progress.py
Import and use DEFAULT_FPS; insert sleeps (1 / DEFAULT_FPS) in tests to align with FPS throttling.
Minor doc fix
snapm/fsdiff/tree.py
Fix docstring typo ("prefix sting" → "prefix string").

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DiffEngine
    participant TermControl
    participant Progress

    Client->>DiffEngine: compute_diff(tree_a, tree_b, options, term_control)
    DiffEngine->>DiffEngine: generate main diffs (compare trees)
    Note over DiffEngine: main diff phase completes

    alt move-detection required
        DiffEngine->>TermControl: request progress "Detecting moves"
        TermControl->>Progress: create/show progress
        DiffEngine->>DiffEngine: _detect_moves(diffs, tree_a, tree_b, options, term_control)
        loop per candidate path
            DiffEngine->>Progress: progress.update(item)
            Progress->>Progress: check elapsed vs interval (FPS)
            alt interval elapsed
                Progress->>Client: render update
            end
            DiffEngine->>DiffEngine: check source/dest existence, record moved_from/moved_to
        end
        Progress->>Progress: progress.cancel()/end (FPS-aware finalisation)
        DiffEngine->>DiffEngine: prune move-related diffs, record moves count
    end

    DiffEngine->>Client: return FsDiffResults
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect snapm/fsdiff/engine.py for correctness of move-candidate iteration, existence checks, mutation/pruning of diffs and proper propagation/use of term_control.
  • Verify timing/state initialisation and guarded rendering in snapm/progress.py, including _do_cancel and interaction with SimpleProgress/ThrobberBase.
  • Check tests/test_progress.py sleeps for flakiness and appropriate use of DEFAULT_FPS.

Suggested labels

bug, Python

Poem

🐰
I hopped the progress bar in measured, steady beats,
Throttled every throb so terminals spare their heats.
I sniffed the moved paths, noted where they roam,
Counted each small hop and brought the diffs back home.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly references the primary changes: fixing progress bar flicker and adding progress for move detection, which align with the main objectives.
Linked Issues check ✅ Passed All coding requirements from linked issues are met: #685 adds FPS limiting to Progress class, #774 adds progress reporting to _detect_moves, and #776 fixes the docstring typo.
Out of Scope Changes check ✅ Passed All changes align with the three linked issues; the rename of DEFAULT_THROBBER_FPS to DEFAULT_FPS and timing enhancements are necessary implementations for the FPS-limiting objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bmr-flicker-fix-and-move-progress

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 963a416 and 97c645f.

📒 Files selected for processing (4)
  • snapm/fsdiff/engine.py (5 hunks)
  • snapm/fsdiff/tree.py (1 hunks)
  • snapm/progress.py (8 hunks)
  • tests/test_progress.py (7 hunks)
✅ Files skipped from review due to trivial changes (1)
  • snapm/fsdiff/tree.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_progress.py
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-12-10T18:28:51.721Z
Learning: In the snapshotmanager/snapm codebase, `TreeWalker.walk_tree()` in `snapm/_fsdiff/treewalk.py` eagerly materializes all paths before processing (using `os.walk`) to compute the total count for progress reporting. This design is intentional and necessary because accurate progress updates require knowing the total upfront. The custom `snapm._progress` implementation is lightweight (~2000 lines with tests) and RHEL-compatible, unlike alternatives like `tqdm` (>8000 lines, not in RHEL). The eager materialization trade-off is well-justified for the UX benefit of progress reporting on large trees.
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 541
File: snapm/manager/_mounts.py:174-197
Timestamp: 2025-10-22T20:06:54.993Z
Learning: For snapshotmanager/snapm follow-up requests from bmr-cymru, create a GitHub issue with label "enhancement", set/ensure a "type: Task" label, and assign it to bmr-cymru. Include backlinks to the PR and the discussion comment in the issue body.
📚 Learning: 2025-10-22T20:06:54.993Z
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 541
File: snapm/manager/_mounts.py:174-197
Timestamp: 2025-10-22T20:06:54.993Z
Learning: For snapshotmanager/snapm follow-up requests from bmr-cymru, create a GitHub issue with label "enhancement", set/ensure a "type: Task" label, and assign it to bmr-cymru. Include backlinks to the PR and the discussion comment in the issue body.

Applied to files:

  • snapm/progress.py
  • snapm/fsdiff/engine.py
📚 Learning: 2025-12-10T18:28:51.721Z
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-12-10T18:28:51.721Z
Learning: In the snapshotmanager/snapm codebase, `TreeWalker.walk_tree()` in `snapm/_fsdiff/treewalk.py` eagerly materializes all paths before processing (using `os.walk`) to compute the total count for progress reporting. This design is intentional and necessary because accurate progress updates require knowing the total upfront. The custom `snapm._progress` implementation is lightweight (~2000 lines with tests) and RHEL-compatible, unlike alternatives like `tqdm` (>8000 lines, not in RHEL). The eager materialization trade-off is well-justified for the UX benefit of progress reporting on large trees.

Applied to files:

  • snapm/fsdiff/engine.py
📚 Learning: 2025-12-14T12:52:14.459Z
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 707
File: snapm/fsdiff/engine.py:327-405
Timestamp: 2025-12-14T12:52:14.459Z
Learning: In snapm/fsdiff/engine.py, render_unified_diff intentionally uses tc.WHITE instead of tc.NORMAL to reset colors after diff lines. This avoids breaking less -R output when piping through files, and is intended to support --color=always with less -R. When reviewing changes in this file, ensure any color reset logic preserves compatibility with downstream pagers/filters; if you introduce color resets, test with --color=always and piping to less -R. If you modify reset color behavior, consider updating tests and documenting rationale.

Applied to files:

  • snapm/fsdiff/engine.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (41)
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: virt_tests (bios, lvm, centos-stream9)
  • GitHub Check: virt_tests (bios, lvm-thin, fedora41)
  • GitHub Check: virt_tests (bios, lvm-thin, centos-stream9)
  • GitHub Check: virt_tests (uefi, lvm, centos-stream10)
  • GitHub Check: virt_tests (uefi, lvm-thin, centos-stream10)
  • GitHub Check: virt_tests (uefi, lvm, fedora42)
  • GitHub Check: virt_tests (bios, lvm-thin, fedora42)
  • GitHub Check: virt_tests (bios, lvm, fedora42)
  • GitHub Check: virt_tests (bios, lvm-thin, centos-stream10)
  • GitHub Check: virt_tests (bios, lvm, fedora41)
  • GitHub Check: virt_tests (uefi, lvm, centos-stream9)
  • GitHub Check: virt_tests (uefi, lvm-thin, fedora41)
  • GitHub Check: virt_tests (uefi, lvm-thin, centos-stream9)
  • GitHub Check: virt_tests (uefi, lvm-thin, fedora42)
  • GitHub Check: virt_tests (uefi, lvm, fedora41)
  • GitHub Check: virt_tests (bios, lvm, centos-stream10)
  • GitHub Check: python-basic-tests
🔇 Additional comments (8)
snapm/fsdiff/engine.py (3)

813-815: LGTM! Clear documentation of the new parameter.

The docstring correctly documents the term_control parameter and its purpose for colour output rendering.


997-1000: LGTM! Consistent threading of term_control.

The term_control instance is correctly passed to _detect_moves using keyword argument syntax, ensuring consistent terminal control settings between the main diff phase and move detection phase.


1101-1187: LGTM! Well-implemented progress reporting for move detection.

The progress reporting implementation is solid:

  • Early return guard for empty tree_a prevents unnecessary progress initialisation
  • Proper timing and per-item progress updates
  • Correct exception handling for KeyboardInterrupt and SystemExit
  • Accurate move count tracking and reporting

This effectively addresses issue #774 by providing feedback during the move detection phase.

snapm/progress.py (5)

34-34: LGTM! Appropriate constant rename.

Renaming DEFAULT_THROBBER_FPS to DEFAULT_FPS better reflects its broader use across both Progress and Throbber classes.


585-589: LGTM! Proper initialisation for FPS limiting.

The FPS-related fields are correctly initialised:

  • fps and _interval_us set up the timing threshold
  • _last initialised to force immediate first update
  • _last_message and done stored for cancel handling

Also applies to: 614-614


629-664: LGTM! Effective FPS throttling implementation.

The FPS limiting logic successfully addresses issue #685:

  • State tracking (done, _last_message) enables cancel handling
  • Time-based guard prevents excessive terminal updates
  • Rendering only occurs when sufficient time has elapsed

This should significantly reduce terminal flicker for large totals whilst maintaining responsive progress updates.

Based on learnings, the custom snapm._progress implementation is intentionally lightweight and RHEL-compatible.


705-719: LGTM! Robust cancellation handling.

The _do_cancel method correctly ensures the final progress state is visible before cleanup:

  • Forces an immediate update by resetting the timing guard
  • Renders the last known state
  • Prints a newline to prevent terminal control sequences from erasing the message
  • Delegates to _do_end for final cleanup

877-877: LGTM! Consistent adoption of the renamed constant.

ThrobberBase correctly uses DEFAULT_FPS, and the constant is properly exported in __all__ for external use.

Also applies to: 1346-1346


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@packit-as-a-service
Copy link

Congratulations! One of the builds has completed. 🍾

You can install the built RPMs by following these steps:

  • sudo dnf install -y 'dnf*-command(copr)'
  • dnf copr enable packit/snapshotmanager-snapm-775
  • And now you can install the packages.

Please note that the RPMs should be used only in a testing environment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
snapm/fsdiff/engine.py (1)

1099-1112: Consider checking tree_a before creating the progress bar.

The progress bar is created (lines 1099–1104), but if tree_a is empty, the early return on line 1108 skips progress.start() on line 1112. Whilst this doesn't cause errors, it's inefficient to construct the progress object when it won't be used.

🔎 Apply this diff to check tree_a earlier:
+        # Anything to do?
+        if not tree_a:
+            return diffs
+
         term_control = term_control or TermControl()
         progress = ProgressFactory.get_progress(
             "Detecting moves",
             quiet=options.quiet,
             term_control=term_control,
         )
 
-        # Anything to do?
-        if not tree_a:
-            return diffs
-
         moves = 0  # Count of move records
         start_time = datetime.now()
         progress.start(len(tree_a))
snapm/progress.py (1)

629-663: Verify FPS limiting correctly throttles updates.

The FPS-based guarding logic correctly limits progress bar updates. The implementation:

  1. Stores state (done, _last_message) for cancellation
  2. Checks elapsed time against the interval
  3. Outputs only when sufficient time has elapsed

Note: prefix calculation (lines 635-639) occurs before the FPS check, meaning it's computed even when updates are skipped. This is a minor inefficiency but doesn't affect correctness.

🔎 Optional optimization: move prefix calculation inside the FPS guard
         message = message or ""
         percent = float(done) / float(self.total)
         n = int((self.width - 10) * percent)
 
         # Used by cancel()
         self.done = done
 
         # Stash last message in case we need it later.
         self._last_message = message
 
+        now = datetime.now()
+        if (now - self._last).total_seconds() * _USECS_PER_SEC >= self._interval_us:
+            self._last = now
+            
             if self.first_update:
                 prefix = self.term.HIDE_CURSOR + self.term.BOL
                 self.first_update = False
             else:
                 prefix = 2 * (self.term.BOL + self.term.UP + self.term.CLEAR_EOL)
 
             if len(message) > self.budget:
                 message = message[0 : self.budget - 3] + "..."
 
-            now = datetime.now()
-            if (now - self._last).total_seconds() * _USECS_PER_SEC >= self._interval_us:
-                self._last = now
                 print(
                     prefix
                     + (
                         self.pbar
                         % (
                             self.header,
                             percent * 100,
                             self.did * n,
                             self.todo * (self.width - 10 - n),
                         )
                     )
                     + self.term.CLEAR_EOL
                     + message
                     + "\n",
                     file=self.stream,
                     end="",
                 )
         _flush_with_broken_pipe_guard(self.stream)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c1584b and 0ef074a.

📒 Files selected for processing (3)
  • snapm/fsdiff/engine.py (6 hunks)
  • snapm/progress.py (7 hunks)
  • tests/test_progress.py (7 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 541
File: snapm/manager/_mounts.py:174-197
Timestamp: 2025-10-22T20:06:54.993Z
Learning: For snapshotmanager/snapm follow-up requests from bmr-cymru, create a GitHub issue with label "enhancement", set/ensure a "type: Task" label, and assign it to bmr-cymru. Include backlinks to the PR and the discussion comment in the issue body.
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-12-10T18:28:51.721Z
Learning: In the snapshotmanager/snapm codebase, `TreeWalker.walk_tree()` in `snapm/_fsdiff/treewalk.py` eagerly materializes all paths before processing (using `os.walk`) to compute the total count for progress reporting. This design is intentional and necessary because accurate progress updates require knowing the total upfront. The custom `snapm._progress` implementation is lightweight (~2000 lines with tests) and RHEL-compatible, unlike alternatives like `tqdm` (>8000 lines, not in RHEL). The eager materialization trade-off is well-justified for the UX benefit of progress reporting on large trees.
📚 Learning: 2025-12-14T12:52:14.459Z
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 707
File: snapm/fsdiff/engine.py:327-405
Timestamp: 2025-12-14T12:52:14.459Z
Learning: In snapm/fsdiff/engine.py, render_unified_diff intentionally uses tc.WHITE instead of tc.NORMAL to reset colors after diff lines. This avoids breaking less -R output when piping through files, and is intended to support --color=always with less -R. When reviewing changes in this file, ensure any color reset logic preserves compatibility with downstream pagers/filters; if you introduce color resets, test with --color=always and piping to less -R. If you modify reset color behavior, consider updating tests and documenting rationale.

Applied to files:

  • snapm/fsdiff/engine.py
🧬 Code graph analysis (1)
snapm/fsdiff/engine.py (5)
snapm/progress.py (7)
  • TermControl (40-277)
  • progress (430-440)
  • ProgressFactory (1192-1342)
  • get_progress (1198-1269)
  • end (453-465)
  • end (947-960)
  • end (1184-1189)
snapm/fsdiff/treewalk.py (2)
  • _log_debug_fsdiff (42-44)
  • is_file (257-265)
snapm/fsdiff/tree.py (1)
  • _log_debug_fsdiff (32-34)
snapm/fsdiff/changes.py (2)
  • _log_debug_fsdiff (29-31)
  • detect_changes (376-525)
snapm/fsdiff/difftypes.py (1)
  • DiffType (14-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: virt_tests (uefi, lvm, centos-stream10)
  • GitHub Check: virt_tests (bios, lvm-thin, fedora41)
  • GitHub Check: virt_tests (bios, lvm, centos-stream10)
  • GitHub Check: virt_tests (bios, lvm-thin, centos-stream9)
  • GitHub Check: virt_tests (bios, lvm, fedora41)
  • GitHub Check: virt_tests (uefi, lvm, centos-stream9)
  • GitHub Check: virt_tests (bios, lvm, centos-stream9)
  • GitHub Check: virt_tests (bios, lvm, fedora42)
  • GitHub Check: virt_tests (uefi, lvm-thin, centos-stream9)
  • GitHub Check: virt_tests (uefi, lvm, fedora42)
  • GitHub Check: virt_tests (uefi, lvm-thin, fedora41)
  • GitHub Check: virt_tests (bios, lvm-thin, centos-stream10)
  • GitHub Check: virt_tests (bios, lvm-thin, fedora42)
  • GitHub Check: virt_tests (uefi, lvm, fedora41)
  • GitHub Check: virt_tests (uefi, lvm-thin, fedora42)
  • GitHub Check: virt_tests (uefi, lvm-thin, centos-stream10)
🔇 Additional comments (10)
snapm/fsdiff/engine.py (3)

813-815: Documentation updated correctly.

The docstring accurately reflects the new term_control parameter.


1024-1024: Documentation updated correctly.

The signature and docstring accurately reflect the new term_control parameter.

Also applies to: 1039-1041


1114-1177: Well-structured move detection with progress feedback.

The loop provides per-item progress updates and includes appropriate exception handling for keyboard interrupts and system exits.

tests/test_progress.py (2)

15-18: LGTM! Imports align with FPS limiting changes.

The addition of time and the import of DEFAULT_FPS (renamed from DEFAULT_THROBBER_FPS) correctly support the new FPS-based rate limiting tests.


255-256: LGTM! Sleep calls correctly honour FPS limiting.

The time.sleep(1 / DEFAULT_FPS) calls ensure sufficient time elapses between progress updates for the FPS limiting logic to take effect. This is necessary for reliable test behaviour.

Also applies to: 268-269, 283-284, 325-326, 338-339, 353-354

snapm/progress.py (5)

34-34: LGTM! Constant rename broadens applicability.

Renaming DEFAULT_THROBBER_FPS to DEFAULT_FPS is appropriate since the constant is now used for both Progress and Throbber classes.


585-589: LGTM! FPS tracking attributes properly initialised.

The new attributes correctly support FPS-based rate limiting:

  • fps and _interval_us define the update interval
  • _last tracks the last update time (initialised in _do_start)
  • _last_message and done support cancellation behaviour

614-614: LGTM! Initial timing state ensures first update displays.

Setting _last to now - interval guarantees the first progress() call will immediately display, which is correct behaviour.


877-877: LGTM! ThrobberBase updated to use renamed constant.

Correctly uses DEFAULT_FPS instead of the old DEFAULT_THROBBER_FPS.


773-798: Flag inconsistency: SimpleProgress lacks FPS limiting mentioned in summary.

The AI-generated summary states: "In SimpleProgress._do_start and SimpleProgress._do_progress, mirrors timing/state enhancements and implements the same guarded output behavior."

However, the code shows no FPS limiting changes in SimpleProgress. The class still outputs on every _do_progress() call without rate limiting. If SimpleProgress is also subject to high-frequency updates causing terminal flooding, it should receive similar FPS limiting logic.

Please verify whether SimpleProgress should also implement FPS-based rate limiting. If high-frequency progress() calls occur with SimpleProgress, it may exhibit similar terminal flooding behaviour.

@bmr-cymru bmr-cymru force-pushed the bmr-flicker-fix-and-move-progress branch from 0ef074a to 963a416 Compare December 18, 2025 10:52
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
snapm/progress.py (2)

629-664: Flush could be moved inside the conditional block for efficiency.

The _flush_with_broken_pipe_guard(self.stream) call on line 664 executes on every progress() call, even when no output is rendered due to FPS throttling. Moving it inside the conditional would avoid unnecessary flush syscalls.

That said, this is a minor optimisation and the current behaviour is functionally correct.

🔎 Optional diff:
         if (now - self._last).total_seconds() * _USECS_PER_SEC >= self._interval_us:
             self._last = now
             print(
                 prefix
                 + (
                     self.pbar
                     % (
                         self.header,
                         percent * 100,
                         self.did * n,
                         self.todo * (self.width - 10 - n),
                     )
                 )
                 + self.term.CLEAR_EOL
                 + message
                 + "\n",
                 file=self.stream,
                 end="",
             )
-        _flush_with_broken_pipe_guard(self.stream)
+            _flush_with_broken_pipe_guard(self.stream)

1345-1356: Consider adding DEFAULT_FPS to __all__.

DEFAULT_FPS is imported by the test suite and represents a public constant that external consumers might wish to reference (e.g., for coordinating timing in custom progress implementations). Adding it to __all__ would make the public API explicit.

🔎 Suggested addition:
 __all__ = [
+    "DEFAULT_FPS",
     "NullProgress",
     "NullThrobber",
     "Progress",
     "ProgressBase",
     "ProgressFactory",
     "SimpleProgress",
     "SimpleThrobber",
     "TermControl",
     "ThrobberBase",
     "Throbber",
 ]
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef074a and 963a416.

📒 Files selected for processing (4)
  • snapm/fsdiff/engine.py (5 hunks)
  • snapm/fsdiff/tree.py (1 hunks)
  • snapm/progress.py (7 hunks)
  • tests/test_progress.py (7 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 541
File: snapm/manager/_mounts.py:174-197
Timestamp: 2025-10-22T20:06:54.993Z
Learning: For snapshotmanager/snapm follow-up requests from bmr-cymru, create a GitHub issue with label "enhancement", set/ensure a "type: Task" label, and assign it to bmr-cymru. Include backlinks to the PR and the discussion comment in the issue body.
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-12-10T18:28:51.721Z
Learning: In the snapshotmanager/snapm codebase, `TreeWalker.walk_tree()` in `snapm/_fsdiff/treewalk.py` eagerly materializes all paths before processing (using `os.walk`) to compute the total count for progress reporting. This design is intentional and necessary because accurate progress updates require knowing the total upfront. The custom `snapm._progress` implementation is lightweight (~2000 lines with tests) and RHEL-compatible, unlike alternatives like `tqdm` (>8000 lines, not in RHEL). The eager materialization trade-off is well-justified for the UX benefit of progress reporting on large trees.
📚 Learning: 2025-10-22T20:06:54.993Z
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 541
File: snapm/manager/_mounts.py:174-197
Timestamp: 2025-10-22T20:06:54.993Z
Learning: For snapshotmanager/snapm follow-up requests from bmr-cymru, create a GitHub issue with label "enhancement", set/ensure a "type: Task" label, and assign it to bmr-cymru. Include backlinks to the PR and the discussion comment in the issue body.

Applied to files:

  • snapm/fsdiff/engine.py
📚 Learning: 2025-12-10T18:28:51.721Z
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-12-10T18:28:51.721Z
Learning: In the snapshotmanager/snapm codebase, `TreeWalker.walk_tree()` in `snapm/_fsdiff/treewalk.py` eagerly materializes all paths before processing (using `os.walk`) to compute the total count for progress reporting. This design is intentional and necessary because accurate progress updates require knowing the total upfront. The custom `snapm._progress` implementation is lightweight (~2000 lines with tests) and RHEL-compatible, unlike alternatives like `tqdm` (>8000 lines, not in RHEL). The eager materialization trade-off is well-justified for the UX benefit of progress reporting on large trees.

Applied to files:

  • snapm/fsdiff/engine.py
📚 Learning: 2025-12-14T12:52:14.459Z
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 707
File: snapm/fsdiff/engine.py:327-405
Timestamp: 2025-12-14T12:52:14.459Z
Learning: In snapm/fsdiff/engine.py, render_unified_diff intentionally uses tc.WHITE instead of tc.NORMAL to reset colors after diff lines. This avoids breaking less -R output when piping through files, and is intended to support --color=always with less -R. When reviewing changes in this file, ensure any color reset logic preserves compatibility with downstream pagers/filters; if you introduce color resets, test with --color=always and piping to less -R. If you modify reset color behavior, consider updating tests and documenting rationale.

Applied to files:

  • snapm/fsdiff/engine.py
🧬 Code graph analysis (1)
snapm/fsdiff/engine.py (3)
snapm/progress.py (7)
  • TermControl (40-277)
  • progress (430-440)
  • ProgressFactory (1192-1342)
  • get_progress (1198-1269)
  • end (453-465)
  • end (947-960)
  • end (1184-1189)
snapm/fsdiff/changes.py (2)
  • _log_debug_fsdiff (29-31)
  • detect_changes (376-525)
snapm/fsdiff/difftypes.py (1)
  • DiffType (14-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (41)
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-43-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:centos-stream-10-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:centos-stream-9-x86_64
  • GitHub Check: virt_tests (uefi, lvm-thin, fedora42)
  • GitHub Check: virt_tests (uefi, lvm-thin, fedora41)
  • GitHub Check: virt_tests (bios, lvm-thin, fedora41)
  • GitHub Check: virt_tests (bios, lvm, centos-stream10)
  • GitHub Check: virt_tests (uefi, lvm, centos-stream10)
  • GitHub Check: virt_tests (bios, lvm-thin, centos-stream10)
  • GitHub Check: virt_tests (uefi, lvm, fedora42)
  • GitHub Check: virt_tests (bios, lvm, centos-stream9)
  • GitHub Check: virt_tests (bios, lvm, fedora42)
  • GitHub Check: virt_tests (uefi, lvm, fedora41)
  • GitHub Check: virt_tests (uefi, lvm, centos-stream9)
  • GitHub Check: virt_tests (uefi, lvm-thin, centos-stream9)
  • GitHub Check: virt_tests (uefi, lvm-thin, centos-stream10)
  • GitHub Check: virt_tests (bios, lvm, fedora41)
  • GitHub Check: virt_tests (bios, lvm-thin, fedora42)
  • GitHub Check: virt_tests (bios, lvm-thin, centos-stream9)
  • GitHub Check: python-basic-tests
🔇 Additional comments (16)
snapm/fsdiff/tree.py (1)

181-181: LGTM!

Good catch on the typo fix in the docstring.

snapm/fsdiff/engine.py (7)

813-815: LGTM!

Docstring correctly documents the new term_control parameter for compute_diff.


997-1001: LGTM!

The term_control is now correctly passed to _detect_moves, ensuring consistent progress reporting between the diff computation and move detection phases.


1026-1046: LGTM!

The updated signature and docstring for _detect_moves correctly document the new term_control parameter.


1101-1114: LGTM!

The early return guard for empty tree_a is appropriate, and the progress lifecycle is correctly initialised with the source tree size as the total.


1116-1172: LGTM!

The move detection loop correctly reports progress for each source path and properly tracks the move count. The logic for selecting candidates and validating moves is well-structured.


1174-1179: LGTM!

Exception handling is consistent with the pattern used in compute_diff, ensuring proper progress bar cleanup on interruption.


1185-1187: LGTM!

The progress message now correctly reports the moves count rather than len(diffs), addressing the issue flagged in the previous review.

tests/test_progress.py (3)

15-18: LGTM!

The import changes correctly reference the renamed DEFAULT_FPS constant and add the time module needed for the FPS-aware test delays.


255-284: LGTM!

The time.sleep(1 / DEFAULT_FPS) calls correctly account for the new FPS-limiting behaviour, ensuring sufficient time elapses between progress updates for the renders to occur.


325-354: LGTM!

The FPS-aware delays in test_lifecycle_no_clear mirror those in test_lifecycle, maintaining test consistency.

snapm/progress.py (5)

34-34: LGTM!

The constant rename from DEFAULT_THROBBER_FPS to DEFAULT_FPS correctly reflects that it's now used by both Progress and Throbber classes.


585-589: LGTM!

The new FPS-related instance variables are correctly initialised. The interval calculation of round((1.0 / self.fps) * _USECS_PER_SEC) yields 100ms at 10 FPS, which should effectively eliminate terminal flicker from rapid updates.


614-614: LGTM!

Initialising _last to one interval in the past ensures the first progress() call renders immediately, providing good user feedback at startup.


705-719: LGTM!

The _do_cancel method correctly forces a final progress update before ending, ensuring the last state is visible. The typo flagged in the previous review ("erasue" → "erasure") has been fixed on line 716.


877-877: LGTM!

ThrobberBase correctly references the renamed DEFAULT_FPS constant.

Eagerly updating the progress bar on every Progress.progress() call
hammers the terminal and causes flicker when working with large total:
use the same mechanism as Throbber to limit the FPS to something
reasonable.

The compute_diffs() progress (which is normally very quick but takes ~30s
on bigger trees with 600k total paths and 400k paths in the tree dicts)
is flickery as heck. Here's how we can support it without the problems I
noted earlier in the issue:

  * Save the last status line in the class, even if we decide not to
    output it due to FPS limiting

  * On cancel (e.g. KeyboardInterrupt) re-draw the last status - that
    way the user knows what path we got stuck on if something hangs.

Resolves: #685

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
On large trees there's a noticeable pause after Comparing trees for
'...' due to move detection: add a separate progress bar for that with
"Detecting moves" / "Checking moves for '...'".

Resolves: #774

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
Resolves: #776

Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
@bmr-cymru bmr-cymru force-pushed the bmr-flicker-fix-and-move-progress branch from 963a416 to 97c645f Compare December 18, 2025 11:01
@bmr-cymru bmr-cymru merged commit 97c645f into main Dec 18, 2025
31 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Cleanup DifferenceEngine enhancement New feature or request UI/UX User interface and experience

Projects

2 participants