Merge pull request #4026 from dotCooCoo/fix/control-app-nap

Control: requests are answered at full speed while the window is hidden on macOS
This commit is contained in:
BFlat
2026-10-24 21:18:34 +07:00
committed by GitHub
3 changed files with 38 additions and 3 deletions
+2 -2
View File
@@ -62,8 +62,8 @@ windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_U
muda = { version = "=0.21.1", default-features = false }
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "block2", "objc2-core-foundation", "NSApplication", "NSButton", "NSColor", "NSColorSampler", "NSColorSpace", "NSControl", "NSMenu", "NSEvent", "NSPasteboard", "NSPopUpButton", "NSResponder", "NSSavePanel", "NSTextField", "NSTextInputContext", "NSView", "NSWindow"] }
# Files opened from Finder and the Dock (src/open_documents.rs) and pasted bitmaps (src/clipboard.rs); both already come with winit, muda and rfd.
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSData", "NSGeometry", "NSNotification", "NSOperation", "NSString", "NSURL"] }
# Files opened from Finder and the Dock (src/open_documents.rs), pasted bitmaps (src/clipboard.rs) and the control channel's App Nap activity (src/control_server.rs); both already come with winit, muda and rfd.
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSData", "NSGeometry", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
block2 = "0.6"
# The window's AppKit view, to centre its buttons on the app bar (src/mac_window.rs). MIT OR
# Apache-2.0 OR Zlib, already in the build through eframe and winit.
+34
View File
@@ -67,10 +67,31 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
}
};
eprintln!("vectorcraft: control server listening on 127.0.0.1:{port}");
#[cfg(target_os = "macos")]
keep_answering();
std::thread::spawn(move || accept_loop(listener, tx, ctx));
rx
}
/// The activity the control channel holds on macOS: work the user asked for, which may let the
/// system sleep when idle, with full timer precision. Without it, App Nap lowers every thread to
/// the lowest priority within about a minute of the window no longer being visible.
#[cfg(target_os = "macos")]
fn activity_options() -> objc2_foundation::NSActivityOptions {
use objc2_foundation::NSActivityOptions;
NSActivityOptions::UserInitiatedAllowingIdleSystemSleep | NSActivityOptions::LatencyCritical
}
/// Begins the control channel's activity. It lasts until the process ends.
#[cfg(target_os = "macos")]
fn keep_answering() {
use objc2_foundation::{NSProcessInfo, NSString};
let reason = NSString::from_str("Answering control requests");
let activity = NSProcessInfo::processInfo().beginActivityWithOptions_reason(activity_options(), &reason);
// The token is never released or passed to `endActivity`, so the activity lasts for the process's lifetime.
std::mem::forget(activity);
}
fn accept_loop(listener: TcpListener, tx: Sender<ControlRequest>, ctx: egui::Context) {
let open = Arc::new(AtomicUsize::new(0));
for stream in listener.incoming().flatten() {
@@ -642,4 +663,17 @@ mod tests {
assert!(line.contains("too many control connections"), "{line}");
drop(held);
}
// A test process is not an app that App Nap applies to, so this checks the activity the
// channel asks for; the throttling itself shows only in a running app with its window hidden.
#[cfg(target_os = "macos")]
#[test]
fn the_control_channel_keeps_the_app_at_full_priority_on_macos() {
use objc2_foundation::NSActivityOptions;
let options = activity_options();
assert!(options.contains(NSActivityOptions::LatencyCritical), "without it App Nap still lowers the priority to 20");
assert!(options.contains(NSActivityOptions::UserInitiatedAllowingIdleSystemSleep));
assert!(!options.contains(NSActivityOptions::IdleSystemSleepDisabled), "the channel must not keep the Mac awake");
keep_answering();
}
}
+2 -1
View File
@@ -51,7 +51,8 @@ A request the app hasn't answered after **60 s** gets `{"ok": false, "error": "t
app had not started it (it was busy, or its window wasn't being drawn), the request is withdrawn
and never runs, and the error says "will not run": retrying is safe. If the app was already
running it, the error says "still running": it may yet complete, so check the document before
retrying.
retrying. On macOS the app keeps itself out of App Nap while the control channel is on, so it keeps
answering with its window hidden, minimized or covered, or with the screen locked.
## Confining file access