Merge pull request #2833 from ghostty-org/push-wrstnvxstmpx

renderer: set QoS class of the renderer thread on macOS
This commit is contained in:
Mitchell Hashimoto
2024-11-26 15:53:35 -08:00
committed by GitHub
2 changed files with 97 additions and 2 deletions

View File

@ -62,6 +62,47 @@ pub fn appSupportDir(
}); });
} }
pub const SetQosClassError = error{
// The thread can't have its QoS class changed usually because
// a different pthread API was called that makes it an invalid
// target.
ThreadIncompatible,
};
/// Set the QoS class of the running thread.
///
/// https://developer.apple.com/documentation/apple-silicon/tuning-your-code-s-performance-for-apple-silicon?preferredLanguage=occ
pub fn setQosClass(class: QosClass) !void {
return switch (std.posix.errno(pthread_set_qos_class_self_np(
class,
0,
))) {
.SUCCESS => {},
.PERM => error.ThreadIncompatible,
// EPERM is the only known error that can happen based on
// the man pages for pthread_set_qos_class_self_np. I haven't
// checked the XNU source code to see if there are other
// possible errors.
else => @panic("unexpected pthread_set_qos_class_self_np error"),
};
}
/// https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW1
pub const QosClass = enum(c_uint) {
user_interactive = 0x21,
user_initiated = 0x19,
default = 0x15,
utility = 0x11,
background = 0x09,
unspecified = 0x00,
};
extern "c" fn pthread_set_qos_class_self_np(
qos_class: QosClass,
relative_priority: c_int,
) c_int;
pub const NSOperatingSystemVersion = extern struct { pub const NSOperatingSystemVersion = extern struct {
major: i64, major: i64,
minor: i64, minor: i64,

View File

@ -4,8 +4,10 @@ pub const Thread = @This();
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const assert = std.debug.assert;
const xev = @import("xev"); const xev = @import("xev");
const crash = @import("../crash/main.zig"); const crash = @import("../crash/main.zig");
const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig"); const renderer = @import("../renderer.zig");
const apprt = @import("../apprt.zig"); const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig"); const configpkg = @import("../config.zig");
@ -92,6 +94,10 @@ flags: packed struct {
/// This is true when the view is visible. This is used to determine /// This is true when the view is visible. This is used to determine
/// if we should be rendering or not. /// if we should be rendering or not.
visible: bool = true, visible: bool = true,
/// This is true when the view is focused. This defaults to true
/// and it is up to the apprt to set the correct value.
focused: bool = true,
} = .{}, } = .{},
pub const DerivedConfig = struct { pub const DerivedConfig = struct {
@ -199,6 +205,9 @@ fn threadMain_(self: *Thread) !void {
}; };
defer crash.sentry.thread_state = null; defer crash.sentry.thread_state = null;
// Setup our thread QoS
self.setQosClass();
// Run our loop start/end callbacks if the renderer cares. // Run our loop start/end callbacks if the renderer cares.
const has_loop = @hasDecl(renderer.Renderer, "loopEnter"); const has_loop = @hasDecl(renderer.Renderer, "loopEnter");
if (has_loop) try self.renderer.loopEnter(self); if (has_loop) try self.renderer.loopEnter(self);
@ -237,6 +246,36 @@ fn threadMain_(self: *Thread) !void {
_ = try self.loop.run(.until_done); _ = try self.loop.run(.until_done);
} }
fn setQosClass(self: *const Thread) void {
// Thread QoS classes are only relevant on macOS.
if (comptime !builtin.target.isDarwin()) return;
const class: internal_os.macos.QosClass = class: {
// If we aren't visible (our view is fully occluded) then we
// always drop our rendering priority down because it's just
// mostly wasted work.
//
// The renderer itself should be doing this as well (for example
// Metal will stop our DisplayLink) but this also helps with
// general forced updates and CPU usage i.e. a rebuild cells call.
if (!self.flags.visible) break :class .utility;
// If we're not focused, but we're visible, then we set a higher
// than default priority because framerates still matter but it isn't
// as important as when we're focused.
if (!self.flags.focused) break :class .user_initiated;
// We are focused and visible, we are the definition of user interactive.
break :class .user_interactive;
};
if (internal_os.macos.setQosClass(class)) {
log.debug("thread QoS class set class={}", .{class});
} else |err| {
log.warn("error setting QoS class err={}", .{err});
}
}
fn startDrawTimer(self: *Thread) void { fn startDrawTimer(self: *Thread) void {
// If our renderer doesn't support animations then we never run this. // If our renderer doesn't support animations then we never run this.
if (!@hasDecl(renderer.Renderer, "hasAnimations")) return; if (!@hasDecl(renderer.Renderer, "hasAnimations")) return;
@ -273,10 +312,16 @@ fn drainMailbox(self: *Thread) !void {
switch (message) { switch (message) {
.crash => @panic("crash request, crashing intentionally"), .crash => @panic("crash request, crashing intentionally"),
.visible => |v| { .visible => |v| visible: {
// If our state didn't change we do nothing.
if (self.flags.visible == v) break :visible;
// Set our visible state // Set our visible state
self.flags.visible = v; self.flags.visible = v;
// Visibility affects our QoS class
self.setQosClass();
// If we became visible then we immediately trigger a draw. // If we became visible then we immediately trigger a draw.
// We don't need to update frame data because that should // We don't need to update frame data because that should
// still be happening. // still be happening.
@ -293,7 +338,16 @@ fn drainMailbox(self: *Thread) !void {
// check the visible state themselves to control their behavior. // check the visible state themselves to control their behavior.
}, },
.focus => |v| { .focus => |v| focus: {
// If our state didn't change we do nothing.
if (self.flags.focused == v) break :focus;
// Set our state
self.flags.focused = v;
// Focus affects our QoS class
self.setQosClass();
// Set it on the renderer // Set it on the renderer
try self.renderer.setFocus(v); try self.renderer.setFocus(v);