Skip to content

Commit c3d130e

Browse files
committed
perf: share ScreenCaptureKit warm-up and prewarm early
1 parent 273f2db commit c3d130e

10 files changed

Lines changed: 406 additions & 23 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/src/lib.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ use tauri_plugin_notification::{NotificationExt, PermissionState};
7979
use tauri_plugin_opener::OpenerExt;
8080
use tauri_plugin_shell::ShellExt;
8181
use tauri_specta::Event;
82+
#[cfg(target_os = "macos")]
83+
use tokio::sync::Mutex;
8284
use tokio::sync::{RwLock, oneshot};
8385
use tracing::{error, trace};
8486
use upload::{S3UploadMeta, create_or_get_video, upload_image, upload_video};
@@ -320,13 +322,94 @@ pub struct RequestOpenSettings {
320322
page: String,
321323
}
322324

325+
#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
326+
pub struct RequestScreenCapturePrewarm {
327+
#[serde(default)]
328+
pub force: bool,
329+
}
330+
323331
#[derive(Deserialize, specta::Type, Serialize, tauri_specta::Event, Debug, Clone)]
324332
pub struct NewNotification {
325333
title: String,
326334
body: String,
327335
is_error: bool,
328336
}
329337

338+
#[cfg(target_os = "macos")]
339+
#[derive(Clone, Copy, PartialEq, Eq)]
340+
enum PrewarmState {
341+
Idle,
342+
Warming,
343+
Warmed,
344+
}
345+
346+
#[cfg(target_os = "macos")]
347+
pub(crate) struct ScreenCapturePrewarmer {
348+
state: Mutex<PrewarmState>,
349+
}
350+
351+
#[cfg(target_os = "macos")]
352+
impl Default for ScreenCapturePrewarmer {
353+
fn default() -> Self {
354+
Self {
355+
state: Mutex::new(PrewarmState::Idle),
356+
}
357+
}
358+
}
359+
360+
#[cfg(target_os = "macos")]
361+
impl ScreenCapturePrewarmer {
362+
async fn request(&self, force: bool) {
363+
let should_start = {
364+
let mut state = self.state.lock().await;
365+
366+
if force {
367+
*state = PrewarmState::Idle;
368+
}
369+
370+
match *state {
371+
PrewarmState::Idle => {
372+
*state = PrewarmState::Warming;
373+
true
374+
}
375+
PrewarmState::Warming => {
376+
trace!("ScreenCaptureKit prewarm already in progress");
377+
false
378+
}
379+
PrewarmState::Warmed => {
380+
if force {
381+
*state = PrewarmState::Warming;
382+
true
383+
} else {
384+
trace!("ScreenCaptureKit cache already warmed");
385+
false
386+
}
387+
}
388+
}
389+
};
390+
391+
if !should_start {
392+
return;
393+
}
394+
395+
let warm_start = std::time::Instant::now();
396+
let result = scap_targets::prewarm_shareable_content().await;
397+
398+
let mut state = self.state.lock().await;
399+
match result {
400+
Ok(()) => {
401+
let elapsed_ms = warm_start.elapsed().as_micros() as f64 / 1000.0;
402+
*state = PrewarmState::Warmed;
403+
trace!(elapsed_ms, "ScreenCaptureKit cache warmed");
404+
}
405+
Err(error) => {
406+
*state = PrewarmState::Idle;
407+
tracing::warn!(error = %error, "ScreenCaptureKit prewarm failed");
408+
}
409+
}
410+
}
411+
}
412+
330413
type ArcLock<T> = Arc<RwLock<T>>;
331414
pub type MutableState<'a, T> = State<'a, Arc<RwLock<T>>>;
332415

@@ -1937,6 +2020,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
19372020
RequestOpenRecordingPicker,
19382021
RequestNewScreenshot,
19392022
RequestOpenSettings,
2023+
RequestScreenCapturePrewarm,
19402024
NewNotification,
19412025
AuthenticationInvalid,
19422026
audio_meter::AudioInputLevelChange,
@@ -2075,6 +2159,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
20752159
fake_window::init(&app);
20762160
app.manage(target_select_overlay::WindowFocusManager::default());
20772161
app.manage(EditorWindowIds::default());
2162+
#[cfg(target_os = "macos")]
2163+
app.manage(ScreenCapturePrewarmer::default());
20782164

20792165
tokio::spawn({
20802166
let camera_feed = camera_feed.clone();
@@ -2204,6 +2290,12 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
22042290
.await;
22052291
});
22062292

2293+
#[cfg(target_os = "macos")]
2294+
RequestScreenCapturePrewarm::listen_any_spawn(&app, async |event, app| {
2295+
let prewarmer = app.state::<ScreenCapturePrewarmer>();
2296+
prewarmer.request(event.force).await;
2297+
});
2298+
22072299
let app_handle = app.clone();
22082300
app.deep_link().on_open_url(move |event| {
22092301
deeplink_actions::handle(&app_handle, event.urls());

apps/desktop/src-tauri/src/windows.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ use tauri::{
1616
AppHandle, LogicalPosition, Manager, Monitor, PhysicalPosition, PhysicalSize, WebviewUrl,
1717
WebviewWindow, WebviewWindowBuilder, Wry,
1818
};
19+
use tauri_specta::Event;
1920
use tokio::sync::RwLock;
20-
use tracing::{debug, error};
21+
use tracing::{debug, error, warn};
2122

2223
use crate::{
23-
App, ArcLock, fake_window,
24+
App, ArcLock, RequestScreenCapturePrewarm, ScreenCapturePrewarmer, fake_window,
2425
general_settings::{AppTheme, GeneralSettingsStore},
2526
permissions,
2627
recording_settings::RecordingTargetMode,
@@ -274,6 +275,19 @@ impl ShowCapWindow {
274275
crate::platform::set_window_level(window.as_ref().window(), 50);
275276
}
276277

278+
#[cfg(target_os = "macos")]
279+
{
280+
let app_handle = app.clone();
281+
tauri::async_runtime::spawn(async move {
282+
let prewarmer = app_handle.state::<ScreenCapturePrewarmer>();
283+
prewarmer.request(false).await;
284+
});
285+
286+
if let Err(error) = (RequestScreenCapturePrewarm { force: false }).emit(app) {
287+
warn!(%error, "Failed to emit ScreenCaptureKit prewarm event");
288+
}
289+
}
290+
277291
window
278292
}
279293
Self::TargetSelectOverlay { display_id } => {

apps/desktop/src/utils/tauri.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ renderFrameEvent: RenderFrameEvent,
290290
requestNewScreenshot: RequestNewScreenshot,
291291
requestOpenRecordingPicker: RequestOpenRecordingPicker,
292292
requestOpenSettings: RequestOpenSettings,
293+
requestScreenCapturePrewarm: RequestScreenCapturePrewarm,
293294
requestStartRecording: RequestStartRecording,
294295
targetUnderCursor: TargetUnderCursor
295296
}>({
@@ -311,6 +312,7 @@ renderFrameEvent: "render-frame-event",
311312
requestNewScreenshot: "request-new-screenshot",
312313
requestOpenRecordingPicker: "request-open-recording-picker",
313314
requestOpenSettings: "request-open-settings",
315+
requestScreenCapturePrewarm: "request-screen-capture-prewarm",
314316
requestStartRecording: "request-start-recording",
315317
targetUnderCursor: "target-under-cursor"
316318
})
@@ -432,6 +434,7 @@ export type RenderFrameEvent = { frame_number: number; fps: number; resolution_b
432434
export type RequestNewScreenshot = null
433435
export type RequestOpenRecordingPicker = { target_mode: RecordingTargetMode | null }
434436
export type RequestOpenSettings = { page: string }
437+
export type RequestScreenCapturePrewarm = { force?: boolean }
435438
export type RequestStartRecording = { mode: RecordingMode }
436439
export type S3UploadMeta = { id: string }
437440
export type SceneMode = "default" | "cameraOnly" | "hideCamera"

crates/recording/src/capture_pipeline.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,21 @@ pub async fn create_screen_capture(
787787
) -> Result<ScreenCaptureReturn<ScreenCaptureMethod>, RecordingError> {
788788
let (video_tx, video_rx) = flume::bounded(16);
789789

790+
#[cfg(target_os = "macos")]
791+
{
792+
let warm_start = std::time::Instant::now();
793+
match scap_targets::prewarm_shareable_content().await {
794+
Ok(()) => tracing::trace!(
795+
elapsed_ms = warm_start.elapsed().as_micros() as f64 / 1000.0,
796+
"ScreenCaptureKit cache ensured before capture"
797+
),
798+
Err(error) => tracing::warn!(
799+
error = %error,
800+
"ScreenCaptureKit prewarm failed before capture"
801+
),
802+
}
803+
}
804+
790805
ScreenCaptureSource::<ScreenCaptureMethod>::init(
791806
capture_target,
792807
force_show_cursor,

crates/recording/src/pipeline/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ impl PipelineBuilder {
122122
// TODO: Shut down tasks if launch failed.
123123
for (name, task) in tasks.into_iter() {
124124
// TODO: Wait for these in parallel?
125-
tokio::time::timeout(Duration::from_secs(5), task.ready_signal.recv_async())
125+
tokio::time::timeout(Duration::from_secs(15), task.ready_signal.recv_async())
126126
.await
127127
.map_err(|_| MediaError::TaskLaunch(format!("task timed out: '{name}'")))?
128128
.map_err(|e| MediaError::TaskLaunch(format!("'{name}' build / {e}")))??;

crates/scap-targets/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ core-graphics = "0.24.0"
1818
core-foundation = "0.10.0"
1919
cocoa = "0.26.0"
2020
objc = "0.2.7"
21+
tokio = { workspace = true, features = ["sync"] }
2122

2223
[target.'cfg(target_os = "windows")'.dependencies]
2324
windows = { workspace = true, features = [

crates/scap-targets/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pub mod bounds;
22
pub mod platform;
33

44
use bounds::*;
5+
#[cfg(target_os = "macos")]
6+
pub use platform::prewarm_shareable_content;
57
pub use platform::{DisplayIdImpl, DisplayImpl, WindowIdImpl, WindowImpl};
68
use serde::{Deserialize, Serialize};
79
use specta::Type;

crates/scap-targets/src/platform/macos.rs

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ use core_graphics::{
1515
};
1616

1717
use crate::bounds::{LogicalBounds, LogicalPosition, LogicalSize, PhysicalSize};
18+
use tracing::{trace, warn};
19+
20+
mod cache;
21+
22+
pub async fn prewarm_shareable_content() -> Result<(), arc::R<ns::Error>> {
23+
cache::prewarm_shareable_content().await
24+
}
1825

1926
#[derive(Clone, Copy)]
2027
pub struct DisplayImpl(CGDisplay);
@@ -174,13 +181,17 @@ impl DisplayImpl {
174181

175182
impl DisplayImpl {
176183
pub async fn as_sc(&self) -> Option<arc::R<sc::Display>> {
177-
sc::ShareableContent::current()
178-
.await
179-
.ok()?
180-
.displays()
181-
.iter()
182-
.find(|d| d.display_id().0 == self.0.id)
183-
.map(|v| v.retained())
184+
match cache::get_display(self.0.id).await {
185+
Ok(display) => display,
186+
Err(error) => {
187+
warn!(
188+
display_id = self.0.id,
189+
error = %error,
190+
"Failed to access ScreenCaptureKit display cache"
191+
);
192+
None
193+
}
194+
}
184195
}
185196

186197
pub async fn as_content_filter(&self) -> Option<arc::R<sc::ContentFilter>> {
@@ -191,13 +202,40 @@ impl DisplayImpl {
191202
&self,
192203
windows: Vec<arc::R<sc::Window>>,
193204
) -> Option<arc::R<sc::ContentFilter>> {
194-
let excluded_windows =
195-
ns::Array::from_slice_retained(windows.into_iter().collect::<Vec<_>>().as_slice());
205+
let lookup_start = std::time::Instant::now();
206+
207+
let display = match cache::get_display(self.0.id).await {
208+
Ok(Some(display)) => display,
209+
Ok(None) => {
210+
warn!(
211+
display_id = self.0.id,
212+
"Display missing from ScreenCaptureKit cache"
213+
);
214+
return None;
215+
}
216+
Err(error) => {
217+
warn!(
218+
display_id = self.0.id,
219+
error = %error,
220+
"Failed to resolve ScreenCaptureKit display"
221+
);
222+
return None;
223+
}
224+
};
225+
226+
let windows = windows.into_iter().collect::<Vec<_>>();
227+
let excluded_windows = ns::Array::from_slice_retained(windows.as_slice());
196228

197-
Some(sc::ContentFilter::with_display_excluding_windows(
198-
self.as_sc().await?.as_ref(),
199-
&excluded_windows,
200-
))
229+
let filter =
230+
sc::ContentFilter::with_display_excluding_windows(display.as_ref(), &excluded_windows);
231+
232+
trace!(
233+
display_id = self.0.id,
234+
elapsed_ms = lookup_start.elapsed().as_micros() as f64 / 1000.0,
235+
"Created ScreenCaptureKit content filter"
236+
);
237+
238+
Some(filter)
201239
}
202240
}
203241

@@ -449,13 +487,17 @@ impl WindowImpl {
449487
}
450488

451489
pub async fn as_sc(&self) -> Option<arc::R<sc::Window>> {
452-
sc::ShareableContent::current()
453-
.await
454-
.ok()?
455-
.windows()
456-
.iter()
457-
.find(|w| w.id() == self.0)
458-
.map(|v| v.retained())
490+
match cache::get_window(self.0).await {
491+
Ok(window) => window,
492+
Err(error) => {
493+
warn!(
494+
window_id = self.0,
495+
error = %error,
496+
"Failed to access ScreenCaptureKit window cache"
497+
);
498+
None
499+
}
500+
}
459501
}
460502

461503
pub fn display(&self) -> Option<DisplayImpl> {

0 commit comments

Comments
 (0)