mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
@@ -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