From 3d8ae1942a843dd6e24b3ffa588064172155f9f8 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Sun, 10 May 2026 13:01:45 +0200 Subject: [PATCH] add: 20 seconds -> 2 seconds fix, for now behind a flag to be sure to be able to revert. --- DIAGNOSTICS_PLAN.md | 39 ++++ electron/ipc/recording/audioFilters.ts | 7 + electron/ipc/recording/diagnostics.ts | 13 +- electron/ipc/recording/mac.ts | 3 +- electron/ipc/recording/windows.ts | 83 ++++++-- electron/ipc/register/recording.ts | 264 +++++++++++++------------ electron/windows.ts | 6 +- src/components/ui/skeleton.tsx | 51 +++++ src/hooks/useScreenRecorder.ts | 32 +++ tailwind.config.cjs | 6 + 10 files changed, 361 insertions(+), 143 deletions(-) create mode 100644 DIAGNOSTICS_PLAN.md create mode 100644 src/components/ui/skeleton.tsx diff --git a/DIAGNOSTICS_PLAN.md b/DIAGNOSTICS_PLAN.md new file mode 100644 index 00000000..318af0e5 --- /dev/null +++ b/DIAGNOSTICS_PLAN.md @@ -0,0 +1,39 @@ +# Plan de Diagnostic de Performance : "Stop Recording" + +L'objectif est d'identifier précisément les goulots d'étranglement qui causent un délai de 10-15 secondes après l'arrêt d'un enregistrement natif sur Windows. + +## 1. Instrumentation Frontend (`src/hooks/useScreenRecorder.ts`) + +Nous allons mesurer le temps total côté UI et le temps d'attente pour chaque appel IPC majeur. + +- **Total Stop Sequence** : Du clic sur "Stop" à l'ouverture de l'éditeur. +- **IPC: stopNativeScreenRecording** : Temps d'arrêt du processus de capture natif. +- **IPC: muxNativeWindowsRecording** : Temps passé dans FFmpeg pour le padding et le muxing audio. +- **Store Sidecar** : Temps d'écriture de l'audio du microphone enregistré par le navigateur. + +## 2. Instrumentation Main Process IPC (`electron/ipc/register/recording.ts`) + +Nous allons mesurer le temps passé dans les handlers IPC pour différencier le temps de traitement de l'overhead de communication. + +- **Handler: stop-native-screen-recording** +- **Handler: mux-native-windows-recording** + +## 3. Instrumentation Logique Interne (`electron/ipc/recording/windows.ts`) + +C'est ici que se trouvent les opérations FFmpeg suspectées. + +- **extendNativeWindowsVideoToDuration** : Temps de re-encodage pour ajouter du padding (Suspect #1). +- **muxNativeWindowsVideoWithAudio** : Temps de muxing audio (Suspect #2). +- **Probing** : Temps passé dans `ffprobe`. + +## Méthodologie d'analyse + +Une fois les logs ajoutés : +1. Lancer l'app en mode dev. +2. Faire un enregistrement court (10s). +3. Arrêter l'enregistrement. +4. Récupérer les logs dans : + - La **Console du navigateur** (pour le frontend). + - Le **Terminal** (pour le processus main d'Electron). + +Les logs seront préfixés par `[PERF:RENDERER]` et `[PERF:MAIN]`. diff --git a/electron/ipc/recording/audioFilters.ts b/electron/ipc/recording/audioFilters.ts index b465f5f2..9f3ea4b1 100644 --- a/electron/ipc/recording/audioFilters.ts +++ b/electron/ipc/recording/audioFilters.ts @@ -38,3 +38,10 @@ export function shouldKeepRecordingAudioSidecars(env: NodeJS.ProcessEnv = proces const value = env[RECORDING_AUDIO_SIDECAR_DEBUG_ENV]?.trim().toLowerCase(); return value === "1" || value === "true" || value === "yes" || value === "on"; } + +/** + * Toggle for the recording finalization optimizations. + * - true: Fast probe (no -count_frames), separate audio tracks (no muxing). + * - false: slow probe (full decode), combined audio/video (heavy muxing). + */ +export const OPTIMIZE_RECORDING_FINALIZATION = true; diff --git a/electron/ipc/recording/diagnostics.ts b/electron/ipc/recording/diagnostics.ts index f097cea9..03299f0a 100644 --- a/electron/ipc/recording/diagnostics.ts +++ b/electron/ipc/recording/diagnostics.ts @@ -2,6 +2,7 @@ import { execFile } from "node:child_process"; import fs from "node:fs/promises"; import { promisify } from "node:util"; import { COMPANION_AUDIO_LAYOUTS } from "../constants"; +import { OPTIMIZE_RECORDING_FINALIZATION } from "./audioFilters"; import { getFfmpegBinaryPath, getFfprobeBinaryPath } from "../ffmpeg/binary"; import { lastNativeCaptureDiagnostics, setLastNativeCaptureDiagnostics } from "../state"; import type { CompanionAudioCandidate, NativeCaptureDiagnostics } from "../types"; @@ -190,6 +191,7 @@ export function summarizeMicrophoneChunkTiming( /** Probe the duration of a media file (in seconds) using the container header. */ export async function probeMediaDurationSeconds(filePath: string): Promise { + const start = Date.now(); const ffmpegPath = getFfmpegBinaryPath(); try { await execFileAsync(ffmpegPath, ["-i", filePath, "-hide_banner"], { timeout: 5000 }); @@ -199,6 +201,10 @@ export async function probeMediaDurationSeconds(filePath: string): Promise { + const start = Date.now(); try { const result = await execFileAsync( getFfprobeBinaryPath(), @@ -273,7 +280,7 @@ export async function probeVideoStreamDuration( "error", "-select_streams", "v:0", - "-count_frames", + ...(OPTIMIZE_RECORDING_FINALIZATION ? [] : ["-count_frames"]), "-show_entries", "stream=duration,nb_frames,nb_read_frames,avg_frame_rate,r_frame_rate", "-of", @@ -286,6 +293,10 @@ export async function probeVideoStreamDuration( return parseFfprobeVideoStreamDuration(stdout); } catch { return null; + } finally { + console.log( + `[PERF:MAIN] probeVideoStreamDuration: COMPLETED in ${Date.now() - start}ms`, + ); } } diff --git a/electron/ipc/recording/mac.ts b/electron/ipc/recording/mac.ts index 1227b0af..ef1b8b13 100644 --- a/electron/ipc/recording/mac.ts +++ b/electron/ipc/recording/mac.ts @@ -29,6 +29,7 @@ import { } from "../state"; import type { AudioSyncAdjustment } from "../types"; import { isAutoRecordingPath, moveFileWithOverwrite } from "../utils"; +import { OPTIMIZE_RECORDING_FINALIZATION } from "./audioFilters"; import { getFileSizeIfPresent, getRecordingAudioMuxTimeoutMs, @@ -318,7 +319,7 @@ export function attachNativeCaptureLifecycle(process: ChildProcessWithoutNullStr export async function finalizeStoredVideo(videoPath: string) { // Safety net: if companion audio files still exist, the mux was skipped — attempt it now - if (videoPath.endsWith(".mp4")) { + if (!OPTIMIZE_RECORDING_FINALIZATION && videoPath.endsWith(".mp4")) { const companionCandidates = await getUsableCompanionAudioCandidates(videoPath); for (const { systemPath, micPath, platform } of companionCandidates) { if (platform === "mac" || platform === "win") { diff --git a/electron/ipc/recording/windows.ts b/electron/ipc/recording/windows.ts index 5b41fc4d..2dc016bf 100644 --- a/electron/ipc/recording/windows.ts +++ b/electron/ipc/recording/windows.ts @@ -24,6 +24,7 @@ import { import type { AudioSyncAdjustment } from "../types"; import { moveFileWithOverwrite } from "../utils"; import { + OPTIMIZE_RECORDING_FINALIZATION, shouldKeepRecordingAudioSidecars, WINDOWS_NATIVE_MIC_PRE_FILTERS, } from "./audioFilters"; @@ -193,6 +194,9 @@ export async function extendNativeWindowsVideoToDuration( videoPath: string, targetDurationMs: number | null | undefined, ): Promise { + const start = Date.now(); + console.log("[PERF:MAIN] extendNativeWindowsVideoToDuration: STARTED"); + try { if (!Number.isFinite(targetDurationMs) || (targetDurationMs ?? 0) <= 0) { return { padded: false, @@ -267,8 +271,13 @@ export async function extendNativeWindowsVideoToDuration( padDurationSeconds, }; } catch (error) { - await fs.rm(paddedOutputPath, { force: true }).catch(() => undefined); - throw error; + await fs.rm(paddedOutputPath, { force: true }).catch(() => undefined); + throw error; + } + } finally { + console.log( + `[PERF:MAIN] extendNativeWindowsVideoToDuration: COMPLETED in ${Date.now() - start}ms`, + ); } } @@ -277,6 +286,9 @@ export async function muxNativeWindowsVideoWithAudio( systemAudioPath: string | null, micAudioPath: string | null, ): Promise { + const start = Date.now(); + console.log("[PERF:MAIN] muxNativeWindowsVideoWithAudio: STARTED"); + try { const ffmpegPath = getFfmpegBinaryPath(); const keepAudioSidecars = shouldKeepRecordingAudioSidecars(); const inputs: string[] = ["-i", videoPath]; @@ -385,6 +397,44 @@ export async function muxNativeWindowsVideoWithAudio( durationDeltaMs: 0, }; + if (OPTIMIZE_RECORDING_FINALIZATION) { + console.log("[mux-win] Optimization enabled: skipping heavy muxing, keeping tracks separate."); + const videoPathWithoutExt = videoPath.replace(/\.[^.]+$/u, ""); + + // Move audio sidecars to final companion paths if they exist + if (systemAudioPath) { + const finalSystemPath = `${videoPathWithoutExt}.system.wav`; + if (systemAudioPath !== finalSystemPath) { + try { + await moveFileWithOverwrite(systemAudioPath, finalSystemPath); + if (audio.system) audio.system.path = finalSystemPath; + } catch (err) { + console.error(`[mux-win] Failed to move system audio to ${finalSystemPath}:`, err); + } + } + } + if (micAudioPath) { + const finalMicPath = `${videoPathWithoutExt}.mic.wav`; + if (micAudioPath !== finalMicPath) { + try { + await moveFileWithOverwrite(micAudioPath, finalMicPath); + if (audio.mic) audio.mic.path = finalMicPath; + } catch (err) { + console.error(`[mux-win] Failed to move mic audio to ${finalMicPath}:`, err); + } + } + } + + return { + muxed: false, + videoDurationSeconds: videoDuration, + muxTimeoutMs, + audioInputs, + audio, + keptAudioSidecars: true, + }; + } + try { if (audioInputs.length === 2) { const filterParts: string[] = []; @@ -469,17 +519,22 @@ export async function muxNativeWindowsVideoWithAudio( await validateRecordedVideo(mixedOutputPath); await moveFileWithOverwrite(mixedOutputPath, videoPath); } catch (error) { - await fs.rm(mixedOutputPath, { force: true }).catch(() => undefined); - throw error; - } + await fs.rm(mixedOutputPath, { force: true }).catch(() => undefined); + throw error; + } - return { - muxed: true, - videoDurationSeconds: videoDuration, - muxTimeoutMs, - audioInputs, - audio, - outputPath: videoPath, - keptAudioSidecars: true, - }; + return { + muxed: true, + videoDurationSeconds: videoDuration, + muxTimeoutMs, + audioInputs, + audio, + outputPath: videoPath, + keptAudioSidecars: true, + }; + } finally { + console.log( + `[PERF:MAIN] muxNativeWindowsVideoWithAudio: COMPLETED in ${Date.now() - start}ms`, + ); + } } diff --git a/electron/ipc/register/recording.ts b/electron/ipc/register/recording.ts index 9e9e6e96..b2e9c9eb 100644 --- a/electron/ipc/register/recording.ts +++ b/electron/ipc/register/recording.ts @@ -40,6 +40,7 @@ import { import { rememberApprovedLocalReadPath } from "../project/manager"; import { getBrowserMicSidecarFilters, + OPTIMIZE_RECORDING_FINALIZATION, shouldKeepRecordingAudioSidecars, } from "../recording/audioFilters"; import { @@ -821,6 +822,9 @@ export function registerRecordingHandlers( ); ipcMain.handle("stop-native-screen-recording", async () => { + const start = Date.now(); + console.log("[PERF:MAIN] Handler: stop-native-screen-recording: STARTED"); + try { // Windows native capture stop path if (process.platform === "win32" && windowsNativeCaptureActive) { try { @@ -1093,11 +1097,16 @@ export function registerRecordingHandlers( return recovered; } - return { - success: false, - message: "Failed to stop native ScreenCaptureKit recording", - error: String(error), - }; + return { + success: false, + message: "Failed to stop native ScreenCaptureKit recording", + error: String(error), + }; + } + } finally { + console.log( + `[PERF:MAIN] Handler: stop-native-screen-recording: COMPLETED in ${Date.now() - start}ms`, + ); } }); @@ -1258,139 +1267,144 @@ export function registerRecordingHandlers( }); ipcMain.handle("mux-native-windows-recording", async (_event, expectedDurationMs?: number) => { - const videoPath = windowsPendingVideoPath; - const orphanedMicAudioPath = windowsOrphanedMicAudioPath; - const diagnosticsSystemAudioPath = windowsSystemAudioPath; - const diagnosticsMicAudioPath = windowsMicAudioPath; - setWindowsPendingVideoPath(null); - setWindowsOrphanedMicAudioPath(null); - - if (!videoPath) { - return { success: false, message: "No native Windows video pending for mux" }; - } - + const start = Date.now(); + console.log("[PERF:MAIN] Handler: mux-native-windows-recording: STARTED"); try { - await writeWindowsRecordingDiagnostics(videoPath, { - phase: "mux-start", - expectedDurationMs, - outputPath: videoPath, - systemAudioPath: diagnosticsSystemAudioPath, - microphonePath: diagnosticsMicAudioPath, - details: { - hasSystemAudio: Boolean(diagnosticsSystemAudioPath), - hasMicrophone: Boolean(diagnosticsMicAudioPath), - hasOrphanedMicrophone: Boolean(orphanedMicAudioPath), - }, - }); - try { - const padding = await extendNativeWindowsVideoToDuration( - videoPath, - expectedDurationMs, - ); - await writeWindowsRecordingDiagnostics(videoPath, { - phase: "pad", - expectedDurationMs, - outputPath: videoPath, - systemAudioPath: diagnosticsSystemAudioPath, - microphonePath: diagnosticsMicAudioPath, - details: { ...padding }, - }); - if (padding.padded) { - console.log( - `[mux-win] Extended native Windows video to ${padding.durationSeconds.toFixed(3)}s using the final frame`, - ); - } - } catch (paddingError) { - console.warn( - "[mux-win] Failed to extend native Windows video duration:", - paddingError, - ); - await writeWindowsRecordingDiagnostics(videoPath, { - phase: "pad", - expectedDurationMs, - outputPath: videoPath, - systemAudioPath: diagnosticsSystemAudioPath, - microphonePath: diagnosticsMicAudioPath, - error: String(paddingError), - }); + const videoPath = windowsPendingVideoPath; + const orphanedMicAudioPath = windowsOrphanedMicAudioPath; + const diagnosticsSystemAudioPath = windowsSystemAudioPath; + const diagnosticsMicAudioPath = windowsMicAudioPath; + setWindowsPendingVideoPath(null); + setWindowsOrphanedMicAudioPath(null); + + if (!videoPath) { + return { success: false, message: "No native Windows video pending for mux" }; } - let muxDetails: unknown = null; - if (diagnosticsSystemAudioPath || diagnosticsMicAudioPath) { - muxDetails = await muxNativeWindowsVideoWithAudio( - videoPath, - diagnosticsSystemAudioPath, - diagnosticsMicAudioPath, - ); + try { + await writeWindowsRecordingDiagnostics(videoPath, { + phase: "mux-start", + expectedDurationMs, + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + details: { + hasSystemAudio: Boolean(diagnosticsSystemAudioPath), + hasMicrophone: Boolean(diagnosticsMicAudioPath), + hasOrphanedMicrophone: Boolean(orphanedMicAudioPath), + }, + }); + if (!OPTIMIZE_RECORDING_FINALIZATION && expectedDurationMs) { + try { + const padding = await extendNativeWindowsVideoToDuration( + videoPath, + expectedDurationMs, + ); + await writeWindowsRecordingDiagnostics(videoPath, { + phase: "pad", + expectedDurationMs, + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + details: { ...padding }, + }); + } catch (paddingError) { + console.warn( + "[mux-win] Failed to extend native Windows video duration:", + paddingError, + ); + await writeWindowsRecordingDiagnostics(videoPath, { + phase: "pad", + expectedDurationMs, + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + error: String(paddingError), + }); + } + } + + let muxDetails: unknown = null; + if (diagnosticsSystemAudioPath || diagnosticsMicAudioPath) { + muxDetails = await muxNativeWindowsVideoWithAudio( + videoPath, + diagnosticsSystemAudioPath, + diagnosticsMicAudioPath, + ); + setWindowsSystemAudioPath(null); + setWindowsMicAudioPath(null); + } + + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "mux", + outputPath: videoPath, + fileSizeBytes: await getFileSizeIfPresent(videoPath), + }); + await writeWindowsRecordingDiagnostics(videoPath, { + phase: "mux-complete", + expectedDurationMs, + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + details: { + fileSizeBytes: await getFileSizeIfPresent(videoPath), + mux: muxDetails, + }, + }); + await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath); + return await finalizeStoredVideo(videoPath); + } catch (error) { + console.error("Failed to mux native Windows recording:", error); + recordNativeCaptureDiagnostics({ + backend: "windows-wgc", + phase: "mux", + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + fileSizeBytes: await getFileSizeIfPresent(videoPath), + error: String(error), + }); + await writeWindowsRecordingDiagnostics(videoPath, { + phase: "mux-error", + expectedDurationMs, + outputPath: videoPath, + systemAudioPath: diagnosticsSystemAudioPath, + microphonePath: diagnosticsMicAudioPath, + error: String(error), + details: { + fileSizeBytes: await getFileSizeIfPresent(videoPath), + }, + }); setWindowsSystemAudioPath(null); setWindowsMicAudioPath(null); - } - - recordNativeCaptureDiagnostics({ - backend: "windows-wgc", - phase: "mux", - outputPath: videoPath, - fileSizeBytes: await getFileSizeIfPresent(videoPath), - }); - await writeWindowsRecordingDiagnostics(videoPath, { - phase: "mux-complete", - expectedDurationMs, - outputPath: videoPath, - systemAudioPath: diagnosticsSystemAudioPath, - microphonePath: diagnosticsMicAudioPath, - details: { - fileSizeBytes: await getFileSizeIfPresent(videoPath), - mux: muxDetails, - }, - }); - await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath); - return await finalizeStoredVideo(videoPath); - } catch (error) { - console.error("Failed to mux native Windows recording:", error); - recordNativeCaptureDiagnostics({ - backend: "windows-wgc", - phase: "mux", - outputPath: videoPath, - systemAudioPath: diagnosticsSystemAudioPath, - microphonePath: diagnosticsMicAudioPath, - fileSizeBytes: await getFileSizeIfPresent(videoPath), - error: String(error), - }); - await writeWindowsRecordingDiagnostics(videoPath, { - phase: "mux-error", - expectedDurationMs, - outputPath: videoPath, - systemAudioPath: diagnosticsSystemAudioPath, - microphonePath: diagnosticsMicAudioPath, - error: String(error), - details: { - fileSizeBytes: await getFileSizeIfPresent(videoPath), - }, - }); - setWindowsSystemAudioPath(null); - setWindowsMicAudioPath(null); - await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath); - try { - return await finalizeStoredVideo(videoPath); - } catch { + await cleanupWindowsOrphanedMicAudioPath(orphanedMicAudioPath); try { - await validateRecordedVideo(videoPath); + return await finalizeStoredVideo(videoPath); + } catch { + try { + await validateRecordedVideo(videoPath); + return { + success: false, + path: videoPath, + message: "Failed to mux native Windows recording", + error: String(error), + }; + } catch { + // The fallback path is not safely playable; surface the original mux error. + } + return { success: false, - path: videoPath, message: "Failed to mux native Windows recording", error: String(error), }; - } catch { - // The fallback path is not safely playable; surface the original mux error. } - - return { - success: false, - message: "Failed to mux native Windows recording", - error: String(error), - }; } + } finally { + console.log( + `[PERF:MAIN] Handler: mux-native-windows-recording: COMPLETED in ${Date.now() - start}ms`, + ); } }); diff --git a/electron/windows.ts b/electron/windows.ts index f1b448b6..0925efc0 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -696,6 +696,8 @@ function loadPackagedEditorWindow(win: BrowserWindow) { } export function createEditorWindow(): BrowserWindow { + const perfStart = Date.now(); + console.log("[PERF:MAIN] createEditorWindow: STARTED"); const isMac = process.platform === "darwin"; const { workArea, workAreaSize } = getScreen().getPrimaryDisplay(); const initialWidth = isMac ? Math.round(workAreaSize.width * 0.85) : workArea.width; @@ -735,12 +737,12 @@ export function createEditorWindow(): BrowserWindow { }); win.once("ready-to-show", () => { - console.log("[editor-window] ready-to-show"); + console.log(`[PERF:MAIN] Editor Window: ready-to-show in ${Date.now() - perfStart}ms`); win.show(); }); win.webContents.on("did-finish-load", () => { - console.log("[editor-window] did-finish-load", win.webContents.getURL()); + console.log(`[PERF:MAIN] Editor Window: did-finish-load in ${Date.now() - perfStart}ms`); win?.webContents.send("main-process-message", new Date().toLocaleString()); // Fallback for Linux/Wayland where `ready-to-show` may not fire reliably. if (!win.isDestroyed() && !win.isVisible()) { diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 00000000..11349dc5 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const skeletonVariants = cva( + "relative overflow-hidden rounded-md transition-shadow", + { + variants: { + variant: { + default: "bg-muted/40", + glass: "bg-white/5 backdrop-blur-sm border border-white/10", + dark: "bg-black/20", + subtle: "bg-foreground/[0.03]", + }, + animation: { + none: "", + pulse: "animate-pulse", + shimmer: "before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-foreground/[0.04] before:to-transparent", + "shimmer-glass": "before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-white/[0.08] before:to-transparent", + }, + }, + defaultVariants: { + variant: "default", + animation: "shimmer", + }, + }, +); + +export interface SkeletonProps + extends React.HTMLAttributes, + VariantProps {} + +/** + * A magnificent Skeleton component designed with Apple-inspired aesthetics. + * Supports shimmer animations, pulse effects, and glassmorphism. + */ +const Skeleton = React.forwardRef( + ({ className, variant, animation, ...props }, ref) => { + return ( +
+ ); + }, +); + +Skeleton.displayName = "Skeleton"; + +export { Skeleton, skeletonVariants }; diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 40628d47..0fd2f1df 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -620,6 +620,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const finalizeRecordingSession = useCallback( async (videoPath: string, webcamPath: string | null) => { + const start = performance.now(); + console.log("[PERF:RENDERER] Finalize Session & Switch to Editor: STARTED"); const shouldHideOverlayCursor = hideEditorOverlayCursorByDefault.current; try { if (webcamPath) { @@ -648,6 +650,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setFinalizing(false); await window.electronAPI.switchToEditor(); + console.log( + `[PERF:RENDERER] Finalize Session & Switch to Editor: COMPLETED in ${(performance.now() - start).toFixed(2)}ms`, + ); }, [], ); @@ -1004,6 +1009,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setFinalizing(true); void (async () => { + const stopStart = performance.now(); + console.log("[PERF:RENDERER] Total Stop Sequence: STARTED"); + const fallbackStartDelayMs = micFallbackStartDelayMs.current; const fallbackTrackSettings = micFallbackTrackSettings.current; const stoppedAtMs = Date.now(); @@ -1014,7 +1022,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const isNativeWindows = nativeWindowsRecording.current; nativeWindowsRecording.current = false; + const ipcStopStart = performance.now(); + console.log("[PERF:RENDERER] IPC: stopNativeScreenRecording: STARTED"); const result = await window.electronAPI.stopNativeScreenRecording(); + console.log( + `[PERF:RENDERER] IPC: stopNativeScreenRecording: COMPLETED in ${(performance.now() - ipcStopStart).toFixed(2)}ms`, + ); + await window.electronAPI?.setRecordingState(false); const webcamPath = await webcamPathPromise; @@ -1030,6 +1044,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { fallbackStartDelayMs, ); if (recoveredPath) { + console.log( + `[PERF:RENDERER] Total Stop Sequence (RECOVERED) in ${(performance.now() - stopStart).toFixed(2)}ms`, + ); return; } } catch (recoveryError) { @@ -1049,8 +1066,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn { let finalPath = result.path; if (isNativeWindows) { + const ipcMuxStart = performance.now(); + console.log("[PERF:RENDERER] IPC: muxNativeWindowsRecording: STARTED"); const muxResult = await window.electronAPI.muxNativeWindowsRecording(expectedDurationMs); + console.log( + `[PERF:RENDERER] IPC: muxNativeWindowsRecording: COMPLETED in ${(performance.now() - ipcMuxStart).toFixed(2)}ms`, + ); + if (!muxResult?.success || !muxResult.path) { void logNativeCaptureDiagnostics("mux-native-windows-recording"); const fallbackPath = muxResult?.path ?? finalPath; @@ -1068,14 +1091,23 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } + const sidecarStart = performance.now(); + console.log("[PERF:RENDERER] Store Sidecar: STARTED"); await storeMicrophoneSidecar( micFallbackBlobPromise, finalPath, fallbackStartDelayMs, fallbackTrackSettings, ); + console.log( + `[PERF:RENDERER] Store Sidecar: COMPLETED in ${(performance.now() - sidecarStart).toFixed(2)}ms`, + ); await finalizeRecordingSession(finalPath, webcamPath); + + console.log( + `[PERF:RENDERER] Total Stop Sequence: COMPLETED in ${(performance.now() - stopStart).toFixed(2)}ms`, + ); })(); return; } diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 4b90a104..fbecd630 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -13,10 +13,16 @@ module.exports = { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, + shimmer: { + "100%": { + transform: "translateX(100%)", + }, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + shimmer: "shimmer 2s infinite", }, borderRadius: { lg: "var(--radius)",