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:
Mitchell Hashimoto
2024-11-25 19:57:29 -08:00
parent 3cf563ba5d
commit a482224da8
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 {
major: i64,
minor: i64,

View File

@ -4,8 +4,10 @@ pub const Thread = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const xev = @import("xev");
const crash = @import("../crash/main.zig");
const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");
const apprt = @import("../apprt.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
/// if we should be rendering or not.
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 {
@ -199,6 +205,9 @@ fn threadMain_(self: *Thread) !void {
};
defer crash.sentry.thread_state = null;
// Setup our thread QoS
self.setQosClass();
// Run our loop start/end callbacks if the renderer cares.
const has_loop = @hasDecl(renderer.Renderer, "loopEnter");
if (has_loop) try self.renderer.loopEnter(self);
@ -237,6 +246,36 @@ fn threadMain_(self: *Thread) !void {
_ = 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 {
// If our renderer doesn't support animations then we never run this.
if (!@hasDecl(renderer.Renderer, "hasAnimations")) return;
@ -273,10 +312,16 @@ fn drainMailbox(self: *Thread) !void {
switch (message) {
.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
self.flags.visible = v;
// Visibility affects our QoS class
self.setQosClass();
// If we became visible then we immediately trigger a draw.
// We don't need to update frame data because that should
// still be happening.
@ -293,7 +338,16 @@ fn drainMailbox(self: *Thread) !void {
// 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
try self.renderer.setFocus(v);