WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Conversation

@richiemcilroy
Copy link
Member

@richiemcilroy richiemcilroy commented Dec 18, 2025

The changes focus on virtualizing waveform rendering to handle large segments efficiently, batching timeline zoom and scroll updates for smoother user experience, and optimizing how timeline and segment markings are generated and displayed.

Summary by CodeRabbit

  • Performance Improvements
    • Optimized waveform rendering by displaying only visible portions of clips, reducing canvas redraw overhead during editing.
    • Enhanced zoom and scroll responsiveness through more efficient batching and processing of timeline navigation inputs.
    • Improved rendering of timeline markings with more efficient calculation methods.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

The changes introduce performance optimizations to the timeline editor by implementing viewport-aware waveform virtualization, RAF-based batching for zoom/scroll interactions, time-based sampling for waveform rendering, and dynamic marking calculations. Waveforms now render only visible segments based on canvas width thresholds, and wheel events are coalesced via scheduled RAF callbacks rather than immediate updates.

Changes

Cohort / File(s) Summary
Waveform Virtualization & Rendering
apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx
Implements virtualization-aware waveform rendering with segment offset support, introduces time-based amplitude sampling, adds rendering throttling via RAF with memoization, computes visible render segments, and replaces precomputed paths with dynamic drawWaveform helper; new constants MAX_WAVEFORM_SAMPLES, MAX_CANVAS_WIDTH, SAMPLES_PER_PIXEL added.
Timeline Interaction Batching & Markings
apps/desktop/src/routes/editor/Timeline/index.tsx
Replaces direct wheel-event handling with debounced RAF-based batching system for zoom/scroll updates; reworks TimelineMarkings from For-based to Index-based rendering with dynamic marking calculations via markingParams() and getMarkingTime().

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • ClipTrack virtualization logic: Segment offset calculations, visible-range math, time-based sampling contract changes, and RAF lifecycle management require careful verification of edge cases and rendering correctness.
  • index.tsx batching & scheduling: Ensure RAF flushing order, zoom/scroll coalescing behavior, and marking time calculations align with updated waveform sampling contract.
  • Public interface changes: WaveformCanvas now requires segmentOffset prop; createWaveformPath signature changed to accept targetSamples and undefined waveform; amplitudeAt shifted from index-based to time-based indexing.

Possibly related PRs

  • CapSoftware/Cap#1417: Both PRs virtualize timeline segments and update ClipTrack/Timeline context helpers for waveform rendering visibility.
  • CapSoftware/Cap#948: Both PRs modify timeline markings and wheel batching logic in index.tsx and related rendering code.
  • CapSoftware/Cap#1295: Both PRs switch marking rendering from For to Index, implement time-based marking logic, and touch Timeline/ClipTrack code.

Suggested labels

Desktop

Suggested reviewers

  • Brendonovich

Poem

🐰 A waveform hops through viewport panes,
RAF batches zoom and scroll's refrain,
Time-based samples paint the view,
Only visible portions gleam anew! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: optimizing waveform rendering (virtualization) and markings (batching and efficiency).
✨ 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 editor-frontend-speed

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.

Copy link
Contributor

@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: 1

🧹 Nitpick comments (4)
apps/desktop/src/routes/editor/Timeline/index.tsx (2)

218-267: RAF-based batching implementation looks solid for coalescing rapid wheel events.

The pattern correctly accumulates deltas and schedules a single RAF callback. However, there's a subtle issue: pendingZoomOrigin always overwrites to the latest origin rather than keeping the first origin of the batch. For zoom operations, the origin point matters - the first touch point in a gesture is typically the intended zoom anchor.

Consider preserving the first origin in a batch:

🔎 Suggested improvement
 function scheduleZoomUpdate(delta: number, origin: number) {
   pendingZoomDelta += delta;
-  pendingZoomOrigin = origin;
+  if (pendingZoomOrigin === null) {
+    pendingZoomOrigin = origin;
+  }

   if (zoomRafId === null) {
     zoomRafId = requestAnimationFrame(flushPendingZoom);
   }
 }

613-637: Switch from For to Index is appropriate for stable indices, but the array creation pattern can be optimized.

Using Index is correct here since the array items have no identity - only their indices matter. However, Array.from({ length: markingCount() }) creates a new array on every reactive update, which triggers unnecessary re-renders.

🔎 Use a number range signal instead

Consider creating a simple range accessor that doesn't allocate:

-<Index each={Array.from({ length: markingCount() })}>
-  {(_, index) => {
+<Index each={new Array(markingCount())}>
+  {(_, index) => {

Or better yet, since Index tracks by index anyway, you could use a keyed For with a stable range memo:

const markingIndices = createMemo(() => 
  Array.from({ length: markingCount() }, (_, i) => i)
);

// Then in JSX:
<For each={markingIndices()}>

This memoizes the array creation, avoiding allocations on every render cycle.

apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (2)

72-82: The amplitudeAt function uses a hardcoded sample rate assumption.

The index calculation Math.floor(time * 10) assumes waveform data is sampled at 10 samples per second (matching WAVEFORM_SAMPLE_STEP = 0.1). This coupling is implicit and could break if the sample step changes.

🔎 Consider making the sample rate explicit
+ const samplesPerSecond = 1 / WAVEFORM_SAMPLE_STEP;
+
  const amplitudeAt = (time: number) => {
-   const index = Math.floor(time * 10);
+   const index = Math.floor(time * samplesPerSecond);
    const sample = waveform[index];

912-927: Same array allocation pattern as Timeline/index.tsx - consider memoizing.

Similar to the TimelineMarkings component, Array.from({ length: markingParams().count }) creates a new array on each reactive update.

🔎 Memoize the marking indices
const markingIndices = createMemo(() => 
  Array.from({ length: markingParams().count }, (_, i) => i)
);

// Then in JSX:
<For each={markingIndices()}>
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 828b874 and 36cc6d5.

📒 Files selected for processing (2)
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (7 hunks)
  • apps/desktop/src/routes/editor/Timeline/index.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use React Query hooks with Server Actions for mutations and perform precise cache updates using setQueryData/setQueriesData instead of broad invalidations

Files:

  • apps/desktop/src/routes/editor/Timeline/index.tsx
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Use strict TypeScript; avoid any type; leverage shared types from @cap/* packages
Follow camelCase naming for variables and functions; PascalCase for components; hooks must start with 'use' prefix
Use Biome for linting and formatting; match existing formatting conventions in the codebase
Use Tailwind CSS for styling in web components; stay consistent with spacing and tokens
Use static skeletons for loading states that mirror content; avoid bouncing animations
Memoize expensive work, code-split naturally, and use Next/Image for remote assets

**/*.{ts,tsx,js,jsx}: Use 2-space indent for TypeScript files; format with Biome using pnpm format
Use Biome for code formatting and linting; run pnpm format regularly
Use kebab-case for file names (e.g., user-menu.tsx); use PascalCase for components

Files:

  • apps/desktop/src/routes/editor/Timeline/index.tsx
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add any form of comments to code (single-line //, multi-line /* /, JSDoc /* */, or any other comment syntax); code must be self-explanatory through naming, types, and structure
Directory naming must use lowercase-dashed convention

Files:

  • apps/desktop/src/routes/editor/Timeline/index.tsx
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx
**/*.{ts,tsx,js,jsx,rs}

📄 CodeRabbit inference engine (AGENTS.md)

Never add comments to code (//, /* */, ///, //!, #, etc.); code must be self-explanatory through naming, types, and structure

Files:

  • apps/desktop/src/routes/editor/Timeline/index.tsx
  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx
🧠 Learnings (2)
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to apps/web/**/*.tsx : Minimize useEffect usage in React components; compute during render, handle logic in event handlers, and ensure cleanups for subscriptions/timers

Applied to files:

  • apps/desktop/src/routes/editor/Timeline/index.tsx
📚 Learning: 2025-12-07T14:29:19.180Z
Learnt from: CR
Repo: CapSoftware/Cap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-07T14:29:19.180Z
Learning: Applies to apps/desktop/**/*.ts : Use tanstack/solid-query for server state management in SolidJS components

Applied to files:

  • apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx
🧬 Code graph analysis (1)
apps/desktop/src/routes/editor/Timeline/index.tsx (1)
apps/desktop/src/routes/editor/utils.ts (1)
  • formatTime (1-13)
⏰ 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). (1)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (8)
apps/desktop/src/routes/editor/Timeline/index.tsx (2)

448-464: Wheel event handling correctly routes to RAF-based schedulers.

The logic properly distinguishes between ctrl+wheel (zoom) and regular wheel (scroll), with platform-specific handling for macOS. The delta calculations are correctly passed to the scheduling functions.


600-606: Marking calculation functions are clean and correctly compute time-based positions.

The markingCount, markingOffset, and getMarkingTime functions properly calculate the number of markings needed and their time positions based on the current viewport transform.

apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx (6)

47-65: Time-based sampling logic is well-designed with appropriate bounds.

The numSamples calculation correctly bounds between a minimum of 50, the target samples, the native sample count, and the global maximum. This prevents both under-sampling and excessive computation.


129-135: RAF tracking variables are correctly scoped for lifecycle management.

The rafId and lastRenderKey variables properly track pending animation frames and prevent redundant renders.


157-203: Virtualization logic correctly calculates visible segment bounds.

The viewport intersection calculation properly determines which portion of the segment is visible and computes appropriate canvas dimensions. The early return for non-visible segments (line 169-174) is a good optimization.


254-270: The createEffect has many dependencies but no explicit dependency array - this is intentional for SolidJS.

The effect correctly tracks all reactive values that should trigger a re-render. The RAF scheduling inside the effect properly cancels any pending frame before scheduling a new one, preventing frame accumulation.


640-640: Passing segmentOffset to WaveformCanvas correctly enables viewport-aware virtualization.

The prevDuration() value provides the necessary context for the canvas to calculate its position relative to the overall timeline viewport.


896-909: Marking calculation correctly scopes visibility to the segment bounds.

The markingParams function properly calculates the visible range within the segment context, and getMarkingTime returns absolute times for each marking index.

Comment on lines +272 to +280
onMount(() => {
setTimeout(() => {
lastRenderKey = "";
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(renderCanvas);
}, 300);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The setTimeout in onMount introduces a potential race condition.

The 300ms delay with lastRenderKey = "" forces a re-render after mount, but if the component unmounts within that window, the timeout callback will still execute. While onCleanup cancels the RAF, it doesn't cancel this timeout.

🔎 Clear the timeout on cleanup
+ let mountTimeoutId: number | null = null;
+
  onMount(() => {
-   setTimeout(() => {
+   mountTimeoutId = window.setTimeout(() => {
      lastRenderKey = "";
      if (rafId !== null) {
        cancelAnimationFrame(rafId);
      }
      rafId = requestAnimationFrame(renderCanvas);
    }, 300);
  });

  onCleanup(() => {
+   if (mountTimeoutId !== null) {
+     clearTimeout(mountTimeoutId);
+   }
    if (rafId !== null) {
      cancelAnimationFrame(rafId);
    }
  });
🤖 Prompt for AI Agents
In apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx around lines 272 to
280, the onMount uses setTimeout which can fire after the component unmounts
causing a race; store the timeout id (e.g. let mountTimer: number | null),
assign mountTimer = window.setTimeout(...), and in onCleanup call
clearTimeout(mountTimer) (and set mountTimer = null) in addition to the existing
cancelAnimationFrame logic so the delayed callback cannot run after unmount.

@richiemcilroy richiemcilroy merged commit a49cb4a into main Dec 18, 2025
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants