Improve timeline undo performance #1897

Open
ddennedy opened 2:39pm on August 16, 2026 wants to merge 1399 Ξ” into mltframework/shotcut master from
optimizied-timeline-undo

Pull Request Overview

  • Opened on August 17, 2026
  • Status Open
  • Commit count 13 with first commit August 10, 2026

Total Delta

1399 Total Diff Delta

Open Days

Open 25 weekdays

Test Delta

0 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Fraction of total time Business days Phase
 
4.1 days Authoring 10 commits before pull request opened for review
 
0.0 days Awaiting first review
 
25.0 days Revising work with 3 commits in response to 2 reviews that left 8 comments

Total time for pull request still awaiting merge (longer than repo's target): 29.1 business days

Author avatar

Improve timeline undo performance

This pull request significantly improves the undo/redo system for timeline editing commands by making undo operations more efficient and context-aware. The changes focus on capturing only the affected tracks or clips for undo, rather than restoring entire tracks unnecessarily. This results in faster, more reliable undos and better user experience. Additionally, command descriptions are now copied to UndoHelper to show on a progress dialogs when things are slow. Things are usually slow on big projects especially when Ripple All Tracks is on.

Undo/Redo System Improvements


  • Most timeline commands now call m_undoHelper.recordBeforeState() with a set of affected track indices, instead of always restoring whole tracks. This enables more efficient, fine-grained undo operations.


  • For commands that can affect multiple tracks (such as ripple edits or move/align commands), logic was added to determine the precise scope of affected tracks and fall back to whole-track restores only when necessary (e.g., ripple-all-tracks is enabled).



Here's the complete picture of every timeline command's undo strategy on the branch now.

Fine-grained (fast per-clip undo β€” no RestoreTracks)

Use UndoHelper with NoHints/SkipXML:

| Command | Notes |
|---|---|
| AppendCommand | NoHints |
| OverwriteCommand | NoHints (snapshots the replaced/split span) |
| LiftCommand | NoHints (snapshots lifted clip) |
| RemoveCommand | NoHints when not ripple-all |
| MergeCommand | NoHints |
| SplitCommand | NoHints (snapshots split clip) |
| InsertCommand | NoHints when not ripple-all (snapshots split clip) |
| AddTransitionCommand | NoHints when not ripple-all (snapshots mixed clips) |
| TrimClipInCommand | NoHints when not ripple |
| TrimClipOutCommand | SkipXML when not ripple |
| UpdateCommand | NoHints |
| DetachAudioCommand | NoHints |
| ReplaceCommand | NoHints |

Still RestoreTracks (whole-track rebuild)

These are the genuinely track-wide or cross-track cases where fine-grained doesn't apply:

| Command | When | Why it needs it |
|---|---|---|
| MoveClipCommand | always | can move across tracks / reorder |
| RemoveTrackCommand | always | the whole track is cleared |
| AlignClipsCommand | always | repositions many clips (lift+overwrite loop) |
| InsertCommand | ripple-all-tracks | shifts clips on other tracks |
| RemoveCommand | ripple-all-tracks | shifts other tracks |
| AddTransitionCommand | ripple-all-tracks | shifts other tracks |
| TrimClipInCommand | ripple | ripple shifts the track (and others if ripple-all) |

Don't use UndoHelper at all (own inverse logic)

GroupCommand, UngroupCommand, NameTrackCommand, MuteTrackCommand, HideTrackCommand, CompositeTrackCommand, LockTrackCommand, FadeInCommand, FadeOutCommand, TrimTransitionInCommand, TrimTransitionOutCommand, ResizeTransitionCommand, AddTransitionByTrimInCommand, AddTransitionByTrimOutCommand, RemoveTransitionByTrimInCommand, RemoveTransitionByTrimOutCommand, AddTrackCommand, InsertTrackCommand, MoveTrackCommand, ChangeBlendModeCommand, ChangeTransitionPropertyCommand, ApplyFiltersCommand, ChangeGainCommand.


Progress Dialog


  • All timeline commands now propagate their undo/redo text label via m_undoHelper.setText(text()) to show descriptions in the progress dialogs.

Improved Undo Data Capture


  • For commands that split, overwrite, or otherwise modify clips, the system now snapshots only the affected clips’ XML, enabling cheap and accurate restoration without parsing the entire track.

Refactoring for Move/Align Commands


  • The MoveClipCommand and AlignClipsCommand now defer the call to recordBeforeState() until the redo step, after the set of affected clips/tracks is known, further optimizing the undo scope.

Comments Threads Pending Resolution

ljharb reviewed on August 17, 2026
ljharb left a comment

## Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (1)

**src/docks/historyundoview.cpp:45**
* Every key press in the History view enters a bulk update, including keys such as Shift or an unhandled character that do not change the undo-stack index. `endBulkUpdate()` nevertheless always calls `MLT.refreshConsumer()` and emits `bulkUpdateFinished()`, so these no-op keys trigger an expensive timeline refresh. Restrict bulk updates to index-changing navigation, or track whether the stack index changed and skip the final flush when no command was replayed.
```
m_model->beginBulkUpdate();
QUndoView::keyPressEvent(event);
m_model->endBulkUpdate();
```

ljharb reviewed on August 17, 2026
ljharb left a comment

## Pull request overview

Optimizes timeline undo/redo by limiting snapshots to affected tracks or clips and batching History dock operations.

**Changes:**
- Adds fine-grained snapshot and restoration strategies.
- Batches multi-step History operations and defers expensive refreshes.
- Adds progress reporting and XML-call instrumentation.

### Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file

| File | Description |
| ---- | ----------- |
| `src/shotcut_mlt_properties.h` | Adds temporary serializable UUID property. |
| `src/models/multitrackmodel.h` | Declares bulk-update state and API. |
| `src/models/multitrackmodel.cpp` | Implements deferred updates and refreshes. |
| `src/mltcontroller.h` | Exposes XML-call instrumentation. |
| `src/mltcontroller.cpp` | Counts XML serialization calls. |
| `src/mainwindow.cpp` | Installs the specialized History view. |
| `src/docks/timelinedock.h` | Adds deferred selection state. |
| `src/docks/timelinedock.cpp` | Defers validation and scopes trim snapshots. |
| `src/docks/historyundoview.h` | Declares bulk-aware History view. |
| `src/docks/historyundoview.cpp` | Brackets History interactions as bulk updates. |
| `src/dialogs/longuitask.h` | Increases progress-dialog responsiveness. |
| `src/commands/undohelper.h` | Defines scoped snapshot strategies. |
| `src/commands/undohelper.cpp` | Implements fine-grained and whole-track restoration. |
| `src/commands/timelinecommands.h` | Adds move-command track scoping. |
| `src/commands/timelinecommands.cpp` | Applies scoped undo capture across commands. |
| `src/CMakeLists.txt` | Builds the new History view. |

Suppressed comments (1)

**src/commands/timelinecommands.cpp:1153**
* `TrimClipOutCommand` also never calls `setText()` on its helper. If its fine-grained undo falls back to `restoreAffectedTracks()`, the progress dialog therefore displays the incomplete label `Undo `. Propagate the command text when creating or injecting the trim helper.
```
m_undoHelper.reset(new UndoHelper(m_model));
if (!m_ripple)
m_undoHelper->setHints(UndoHelper::SkipXML);
m_undoHelper->recordBeforeState(m_rippleAllTracks ? QSet() : QSet{m_trackIndex});
```

---

πŸ’‘ Add a `code-review` agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Resolved Comment Threads

ljharb reviewed on August 17, 2026
ljharb left a comment

## Pull request overview

Optimizes timeline undo/redo by limiting snapshots to affected tracks or clips and batching History dock operations.

**Changes:**
- Adds fine-grained snapshot and restoration strategies.
- Batches multi-step History operations and defers expensive refreshes.
- Adds progress reporting and XML-call instrumentation.

### Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file

| File | Description |
| ---- | ----------- |
| `src/shotcut_mlt_properties.h` | Adds temporary serializable UUID property. |
| `src/models/multitrackmodel.h` | Declares bulk-update state and API. |
| `src/models/multitrackmodel.cpp` | Implements deferred updates and refreshes. |
| `src/mltcontroller.h` | Exposes XML-call instrumentation. |
| `src/mltcontroller.cpp` | Counts XML serialization calls. |
| `src/mainwindow.cpp` | Installs the specialized History view. |
| `src/docks/timelinedock.h` | Adds deferred selection state. |
| `src/docks/timelinedock.cpp` | Defers validation and scopes trim snapshots. |
| `src/docks/historyundoview.h` | Declares bulk-aware History view. |
| `src/docks/historyundoview.cpp` | Brackets History interactions as bulk updates. |
| `src/dialogs/longuitask.h` | Increases progress-dialog responsiveness. |
| `src/commands/undohelper.h` | Defines scoped snapshot strategies. |
| `src/commands/undohelper.cpp` | Implements fine-grained and whole-track restoration. |
| `src/commands/timelinecommands.h` | Adds move-command track scoping. |
| `src/commands/timelinecommands.cpp` | Applies scoped undo capture across commands. |
| `src/CMakeLists.txt` | Builds the new History view. |

Suppressed comments (1)

**src/commands/timelinecommands.cpp:1153**
* `TrimClipOutCommand` also never calls `setText()` on its helper. If its fine-grained undo falls back to `restoreAffectedTracks()`, the progress dialog therefore displays the incomplete label `Undo `. Propagate the command text when creating or injecting the trim helper.
```
m_undoHelper.reset(new UndoHelper(m_model));
if (!m_ripple)
m_undoHelper->setHints(UndoHelper::SkipXML);
m_undoHelper->recordBeforeState(m_rippleAllTracks ? QSet() : QSet{m_trackIndex});
```

---

πŸ’‘ Add a `code-review` agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.