mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Control: requests are answered at full speed while the window is hidden on macOS
Within about a minute of its window no longer being visible, macOS App Nap lowers the app's threads to the lowest priority and coalesces its timers. On a loaded machine, control requests then took seconds to answer, and the 60 s timeout reply arrived up to 26 s late. When the control channel is on (--control or VECTORCRAFT_CONTROL_PORT), the app now holds an NSProcessInfo activity (user initiated, allowing idle system sleep, latency critical) for as long as it runs, so App Nap does not apply. The activity does not keep the Mac awake. docs/control-protocol.md states this in the paragraph on the 60 s timeout.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user