nixpkgs/nixos/modules/services/desktops/system76-scheduler.nix
2024-10-17 18:22:34 +02:00

297 lines
9.3 KiB
Nix

{ config, lib, pkgs, ... }:
let
cfg = config.services.system76-scheduler;
inherit (builtins) concatStringsSep map toString attrNames;
inherit (lib) boolToString types mkOption literalExpression optional mkIf mkMerge;
inherit (types) nullOr listOf bool int ints float str enum;
withDefaults = optionSpecs: defaults:
lib.genAttrs (attrNames optionSpecs) (name:
mkOption (optionSpecs.${name} // {
default = optionSpecs.${name}.default or defaults.${name} or null;
}));
latencyProfile = withDefaults {
latency = {
type = int;
description = "`sched_latency_ns`.";
};
nr-latency = {
type = int;
description = "`sched_nr_latency`.";
};
wakeup-granularity = {
type = float;
description = "`sched_wakeup_granularity_ns`.";
};
bandwidth-size = {
type = int;
description = "`sched_cfs_bandwidth_slice_us`.";
};
preempt = {
type = enum [ "none" "voluntary" "full" ];
description = "Preemption mode.";
};
};
schedulerProfile = withDefaults {
nice = {
type = nullOr (ints.between (-20) 19);
description = "Niceness.";
};
class = {
type = nullOr (enum [ "idle" "batch" "other" "rr" "fifo" ]);
example = literalExpression "\"batch\"";
description = "CPU scheduler class.";
};
prio = {
type = nullOr (ints.between 1 99);
example = literalExpression "49";
description = "CPU scheduler priority.";
};
ioClass = {
type = nullOr (enum [ "idle" "best-effort" "realtime" ]);
example = literalExpression "\"best-effort\"";
description = "IO scheduler class.";
};
ioPrio = {
type = nullOr (ints.between 0 7);
example = literalExpression "4";
description = "IO scheduler priority.";
};
matchers = {
type = nullOr (listOf str);
default = [];
example = literalExpression ''
[
"include cgroup=\"/user.slice/*.service\" parent=\"systemd\""
"emacs"
]
'';
description = "Process matchers.";
};
};
cfsProfileToString = name: let
p = cfg.settings.cfsProfiles.${name};
in
"${name} latency=${toString p.latency} nr-latency=${toString p.nr-latency} wakeup-granularity=${toString p.wakeup-granularity} bandwidth-size=${toString p.bandwidth-size} preempt=\"${p.preempt}\"";
prioToString = class: prio: if prio == null then "\"${class}\"" else "(${class})${toString prio}";
schedulerProfileToString = name: a: indent:
concatStringsSep " "
(["${indent}${name}"]
++ (optional (a.nice != null) "nice=${toString a.nice}")
++ (optional (a.class != null) "sched=${prioToString a.class a.prio}")
++ (optional (a.ioClass != null) "io=${prioToString a.ioClass a.ioPrio}")
++ (optional ((builtins.length a.matchers) != 0) ("{\n${concatStringsSep "\n" (map (m: " ${indent}${m}") a.matchers)}\n${indent}}")));
in {
options = {
services.system76-scheduler = {
enable = lib.mkEnableOption "system76-scheduler";
package = mkOption {
type = types.package;
default = pkgs.system76-scheduler;
defaultText = literalExpression "pkgs.system76-scheduler";
description = "Which System76-Scheduler package to use.";
};
useStockConfig = mkOption {
type = bool;
default = true;
description = ''
Use the (reasonable and featureful) stock configuration.
When this option is `true`, `services.system76-scheduler.settings`
are ignored.
'';
};
settings = {
cfsProfiles = {
enable = mkOption {
type = bool;
default = true;
description = "Tweak CFS latency parameters when going on/off battery";
};
default = latencyProfile {
latency = 6;
nr-latency = 8;
wakeup-granularity = 1.0;
bandwidth-size = 5;
preempt = "voluntary";
};
responsive = latencyProfile {
latency = 4;
nr-latency = 10;
wakeup-granularity = 0.5;
bandwidth-size = 3;
preempt = "full";
};
};
processScheduler = {
enable = mkOption {
type = bool;
default = true;
description = "Tweak scheduling of individual processes in real time.";
};
useExecsnoop = mkOption {
type = bool;
default = true;
description = "Use execsnoop (otherwise poll the precess list periodically).";
};
refreshInterval = mkOption {
type = int;
default = 60;
description = "Process list poll interval, in seconds";
};
foregroundBoost = {
enable = mkOption {
type = bool;
default = true;
description = ''
Boost foreground process priorities.
(And de-boost background ones). Note that this option needs cooperation
from the desktop environment to work. On Gnome the client side is
implemented by the "System76 Scheduler" shell extension.
'';
};
foreground = schedulerProfile {
nice = 0;
ioClass = "best-effort";
ioPrio = 0;
};
background = schedulerProfile {
nice = 6;
ioClass = "idle";
};
};
pipewireBoost = {
enable = mkOption {
type = bool;
default = true;
description = "Boost Pipewire client priorities.";
};
profile = schedulerProfile {
nice = -6;
ioClass = "best-effort";
ioPrio = 0;
};
};
};
};
assignments = mkOption {
type = types.attrsOf (types.submodule {
options = schedulerProfile { };
});
default = {};
example = literalExpression ''
{
nix-builds = {
nice = 15;
class = "batch";
ioClass = "idle";
matchers = [
"nix-daemon"
];
};
}
'';
description = "Process profile assignments.";
};
exceptions = mkOption {
type = types.listOf str;
default = [];
example = literalExpression ''
[
"include descends=\"schedtool\""
"schedtool"
]
'';
description = "Processes that are left alone.";
};
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
services.dbus.packages = [ cfg.package ];
systemd.services.system76-scheduler = {
description = "Manage process priorities and CFS scheduler latencies for improved responsiveness on the desktop";
wantedBy = [ "multi-user.target" ];
path = [
# execsnoop needs those to extract kernel headers:
pkgs.kmod
pkgs.gnutar
pkgs.xz
];
serviceConfig = {
Type = "dbus";
BusName= "com.system76.Scheduler";
ExecStart = "${cfg.package}/bin/system76-scheduler daemon";
ExecReload = "${cfg.package}/bin/system76-scheduler daemon reload";
};
};
environment.etc = mkMerge [
(mkIf cfg.useStockConfig {
# No custom settings: just use stock configuration with a fix for Pipewire
"system76-scheduler/config.kdl".source = "${cfg.package}/data/config.kdl";
"system76-scheduler/process-scheduler/00-dist.kdl".source = "${cfg.package}/data/pop_os.kdl";
"system76-scheduler/process-scheduler/01-fix-pipewire-paths.kdl".source = ../../../../pkgs/by-name/sy/system76-scheduler/01-fix-pipewire-paths.kdl;
})
(let
settings = cfg.settings;
cfsp = settings.cfsProfiles;
ps = settings.processScheduler;
in mkIf (!cfg.useStockConfig) {
"system76-scheduler/config.kdl".text = ''
version "2.0"
autogroup-enabled false
cfs-profiles enable=${boolToString cfsp.enable} {
${cfsProfileToString "default"}
${cfsProfileToString "responsive"}
}
process-scheduler enable=${boolToString ps.enable} {
execsnoop ${boolToString ps.useExecsnoop}
refresh-rate ${toString ps.refreshInterval}
assignments {
${if ps.foregroundBoost.enable then (schedulerProfileToString "foreground" ps.foregroundBoost.foreground " ") else ""}
${if ps.foregroundBoost.enable then (schedulerProfileToString "background" ps.foregroundBoost.background " ") else ""}
${if ps.pipewireBoost.enable then (schedulerProfileToString "pipewire" ps.pipewireBoost.profile " ") else ""}
}
}
'';
})
{
"system76-scheduler/process-scheduler/02-config.kdl".text =
"exceptions {\n${concatStringsSep "\n" (map (e: " ${e}") cfg.exceptions)}\n}\n"
+ "assignments {\n"
+ (concatStringsSep "\n" (map (name: schedulerProfileToString name cfg.assignments.${name} " ")
(attrNames cfg.assignments)))
+ "\n}\n";
}
];
};
meta = {
maintainers = [ lib.maintainers.cmm ];
};
}