mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
renderer: set QoS class of the renderer thread on macOS
This sets the macOS QoS class of the renderer thread. Apple recommends[1] that all threads should have a QoS class set, and there are many benefits[2] to that, mainly around power management moreso than performance I'd expect. In this commit, I start by setting the QoS class of the renderer thread. By default, the renderer thread is set to user interactive, because it is a UI thread after all. But under some conditions we downgrade: - If the surface is not visible at all (i.e. another window is fully covering it or its minimized), we set the QoS class to utility. This is lower than the default, previous QoS and should help macOS unschedule the workload or move it to a different core. - If the surface is visible but not focused, we set the QoS class to user initiated. This is lower than user interactive but higher than default. The renderer should remain responsive but not consume as much time as it would if it was user interactive. I'm unable to see any noticable difference in anything from these changes. Unfortunately it doesn't seem like Apple provides good tools to play around with this. We should continue to apply QoS classes to our other threads on macOS. [1]: https://developer.apple.com/documentation/apple-silicon/tuning-your-code-s-performance-for-apple-silicon?preferredLanguage=occl [2]: https://blog.xoria.org/macos-tips-threading/
This commit is contained in:
@ -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,
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user