Speed up crop preview and reduce timeline overhead#1707
Conversation
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Paragon SummaryThis pull request review analyzed 16 files and found no issues. The review examined code changes, potential bugs, security vulnerabilities, performance issues, and code quality concerns using automated analysis tools. Paragon did not detect any problems in the current diff. Proceed with merge after your normal checks. This PR speeds up the editor by showing an immediate crop preview from the current canvas frame, while keeping the accurate crop-frame fallback and removing the old preloader flow. It also cuts timeline overhead through memoization and adds debug-only devtools plus profiling logs for local performance investigation. Key changes:
Confidence score: 5/5
16 files reviewed, 0 comments |
| logCropProfile("dialog-opened", { | ||
| elapsedMs: Number((performance.now() - startedAt).toFixed(2)), | ||
| }); | ||
| await commands.stopPlayback(); |
There was a problem hiding this comment.
If stopPlayback() throws here, playing stays true (and the crop modal is already open). Making this try/catch/finally keeps state consistent.
| await commands.stopPlayback(); | |
| try { | |
| await commands.stopPlayback(); | |
| logCropProfile("playback-stopped", { | |
| elapsedMs: Number((performance.now() - startedAt).toFixed(2)), | |
| }); | |
| } catch (error) { | |
| console.warn("Failed to stop playback:", error); | |
| logCropProfile("playback-stop-failed", { | |
| elapsedMs: Number((performance.now() - startedAt).toFixed(2)), | |
| }); | |
| } finally { | |
| setEditorState("playing", false); | |
| } |
| ), | ||
| }); | ||
|
|
||
| scheduleAccurateFrame("immediate", { |
There was a problem hiding this comment.
Right now the accurate frame fetch gets queued on every crop-open (even when a fresh previewUrl is available). If the intent is mostly fallback/upgrade, a small delay when we already have a preview can avoid unnecessary work for quick open/close.
| scheduleAccurateFrame("immediate", { | |
| scheduleAccurateFrame(initialPreviewUrl ? "preview-available" : "immediate", { | |
| delayMs: initialPreviewUrl ? 250 : undefined, | |
| idleTimeoutMs: 500, | |
| fallbackDelayMs: 16, | |
| }); |
| function logCropProfile( | ||
| stage: string, | ||
| data: Record<string, number | string | boolean | null> = {}, | ||
| ) { | ||
| if (!import.meta.env.DEV) return; | ||
| console.info("[crop-profile]", stage, data); | ||
| } |
There was a problem hiding this comment.
logCropProfile duplicated across two files
logCropProfile is defined identically in both Player.tsx (lines 35–41) and Editor.tsx. If the signature or behaviour ever needs to change (e.g. adding a timestamp, swapping console.info for a structured logger, or changing the env guard), the two copies will drift. Consider extracting it to a shared utility module (e.g. cropProfile.ts) and importing it in both files.
// apps/desktop/src/routes/editor/cropProfile.ts
export function logCropProfile(
stage: string,
data: Record<string, number | string | boolean | null> = {},
) {
if (!import.meta.env.DEV) return;
console.info("[crop-profile]", stage, data);
}Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/routes/editor/Player.tsx
Line: 35-41
Comment:
**`logCropProfile` duplicated across two files**
`logCropProfile` is defined identically in both `Player.tsx` (lines 35–41) and `Editor.tsx`. If the signature or behaviour ever needs to change (e.g. adding a timestamp, swapping `console.info` for a structured logger, or changing the env guard), the two copies will drift. Consider extracting it to a shared utility module (e.g. `cropProfile.ts`) and importing it in both files.
```ts
// apps/desktop/src/routes/editor/cropProfile.ts
export function logCropProfile(
stage: string,
data: Record<string, number | string | boolean | null> = {},
) {
if (!import.meta.env.DEV) return;
console.info("[crop-profile]", stage, data);
}
```
How can I resolve this? If you propose a fix, please make it concise.
Greptile Summary
This PR has two independent improvements: a faster crop-dialog preview strategy and timeline computation memoization.
Crop preview UX —
cropVideoPreloader.tsis removed entirely. Instead,cropDialogHandlerinPlayer.tsxsynchronously captures the current canvas frame (canvasControls.captureFrame()) and passes the resulting blob URL aspreviewUrlin thecropdialog state. The dialog component inEditor.tsxdisplays this canvas snapshot immediately, then schedules an accurate backend frame fetch (getDisplayFrameForCropping) viarequestIdleCallback(500 ms timeout) or a 16 mssetTimeoutfallback. If the preview image fails to load,onErrortriggers the accurate-frame fetch as a fallback. URL lifecycle is well-managed: the initial preview URL is revoked bysetPreviewBlobwhen the accurate frame arrives, and byonCleanupwhen the dialog is dismissed.Timeline memoization —
trackOptions,textTrackRows,maskTrackRows,visibleTrackCount, andtrackHeightinTimeline/index.tsxare promoted tocreateMemo. In every track component, per-segmentisSelectedmemos now check against a pre-builtSetinstead of callingArray.includes.MaskTrackandTextTrackaddlaneSegmentPositionByIndex(Map) andsortedLaneSegmentsmemos to avoid repeated O(n log n) sorts infindPlacement.ClipTrackreplaces its O(n)reducein drag handlers with a memoizedtotalClipTimelineDuration.timelineTracks.tsreplaces spread-into-Math.maxwith explicit loops.Rust / Tauri —
get_display_frame_for_croppinggainsInstant-based phase profiling emitted atDEBUGlevel (no production overhead). ThedevtoolsTauri feature flag is removed fromCargo.tomland the window builder gates it oncfg!(debug_assertions).Findings:
logCropProfilehelper is copy-pasted identically into bothPlayer.tsxandEditor.tsx; a shared module would prevent drift.frameSourceis"accurate-frame"and the blob image firesonError,accurateFrameRequestedis alreadytruesorequestAccurateFrameis a no-op.frameBlobUrlremains non-null, soscreenshotSrcis never tried and the user sees a broken image.previewUrlis created beforesetDialogis called; a synchronous exception between the two (very unlikely in practice) would leave an unrevoked object URL.Confidence Score: 5/5
<Index>vs<For>index access patterns are used correctly throughout.Important Files Changed
canvasControls.captureFrame(); removespreloadCropVideoFullhooks;logCropProfileduplicated fromEditor.tsx.Player.tsx.trackOptions,textTrackRows,maskTrackRows,visibleTrackCount, andtrackHeightfrom plain reactive functions tocreateMemo, avoiding redundant recomputation on every render.selectedClipIndicesmemo (Set-based) andtotalClipTimelineDurationmemo, replacing inlineselection.indices.includesand per-renderreducecalls inside drag handlers.getUsedTrackCountandgetTrackRowsWithCountto use imperative loops instead of spread-into-Math.maxandArray.from, avoiding O(n) spread allocation on each call.Instant-based profiling spans (lookup / decode / convert / encode) toget_display_frame_for_cropping, emitted viadebug!— no production-log noise.cfg!(debug_assertions)so they are not exposed in release builds.Sequence Diagram
sequenceDiagram participant User participant Player as Player.tsx participant Canvas as canvasControls participant Editor as Editor.tsx (CropDialog) participant Backend as Tauri Backend User->>Player: Click "Crop" Player->>Canvas: hasRenderedFrame()? Canvas-->>Player: true Player->>Canvas: captureFrame() Canvas-->>Player: Blob (current frame) Player->>Player: URL.createObjectURL(blob) → previewUrl Player->>Editor: setDialog({ type:"crop", previewUrl }) Editor->>Editor: frameBlobUrl = previewUrl (immediate) Editor->>Editor: frameSource = "captured-preview" Editor-->>User: Show crop dialog with canvas preview Editor->>Editor: onMount → scheduleAccurateFrame(idleTimeoutMs=500) Note over Editor: requestIdleCallback / setTimeout(16ms) Editor->>Backend: getDisplayFrameForCropping(FPS) Backend-->>Editor: PNG bytes Editor->>Editor: setPreviewBlob → revokeObjectURL(previewUrl), set accurateFrameUrl Editor-->>User: Update with accurate frame alt Image load error (any source) Editor->>Editor: onError → requestAccurateFrame (if not already done) Editor->>Backend: getDisplayFrameForCropping(FPS) Backend-->>Editor: PNG bytes or error end User->>Editor: Close dialog Editor->>Editor: onCleanup → cancelled=true, revokeObjectURL(frameBlobUrl)Comments Outside Diff (2)
apps/desktop/src/routes/editor/Editor.tsx, line 1125-1163 (link)When
frameSourceis"accurate-frame"and the image firesonError(e.g. the backend returned bytes that the browser cannot decode as PNG),requestAccurateFrameis a no-op becauseaccurateFrameRequestedis alreadytrue. At that pointframeBlobUrl()is non-null, so the expressionframeBlobUrl() ?? screenshotSrcnever reaches the screenshot fallback, and the user is left with a broken image.To handle this gracefully you could reset
frameBlobUrltonullon an accurate-frame load failure so the screenshot path is tried:Prompt To Fix With AI
apps/desktop/src/routes/editor/Player.tsx, line 416-425 (link)previewUrlblob not revoked on capture failure pathpreviewUrlis created withURL.createObjectURL, then stored verbatim in the dialog state aspreviewUrl. TheonCleanupinside theShowblock inEditor.tsxrevokes the URL when the dialog unmounts — that path is correct. However, ifcaptureFrame()succeeds andpreviewUrlis set, butsetDialogis then not reached (e.g. an uncaught synchronous error between lines 175 and 185 — admittedly very unlikely), the URL would never be revoked.A small defensive measure is to unconditionally revoke
previewUrlin the crop dialog'sonCleanuppath, or alternatively create the blob URL inside the dialog component where it's more naturally owned. No immediate action required given the low probability, but worth keeping in mind ifcropDialogHandlergrows in complexity.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "Remove deferred accurate crop preview lo..." | Re-trigger Greptile