1{ config, lib, pkgs, ... }:
2
3let
4 cfg = config.services.system76-scheduler;
5
6 inherit (builtins) concatStringsSep map toString attrNames;
7 inherit (lib) boolToString types mkOption literalExpression mdDoc optional mkIf mkMerge;
8 inherit (types) nullOr listOf bool int ints float str enum;
9
10 withDefaults = optionSpecs: defaults:
11 lib.genAttrs (attrNames optionSpecs) (name:
12 mkOption (optionSpecs.${name} // {
13 default = optionSpecs.${name}.default or defaults.${name} or null;
14 }));
15
16 latencyProfile = withDefaults {
17 latency = {
18 type = int;
19 description = mdDoc "`sched_latency_ns`.";
20 };
21 nr-latency = {
22 type = int;
23 description = mdDoc "`sched_nr_latency`.";
24 };
25 wakeup-granularity = {
26 type = float;
27 description = mdDoc "`sched_wakeup_granularity_ns`.";
28 };
29 bandwidth-size = {
30 type = int;
31 description = mdDoc "`sched_cfs_bandwidth_slice_us`.";
32 };
33 preempt = {
34 type = enum [ "none" "voluntary" "full" ];
35 description = mdDoc "Preemption mode.";
36 };
37 };
38 schedulerProfile = withDefaults {
39 nice = {
40 type = nullOr (ints.between (-20) 19);
41 description = mdDoc "Niceness.";
42 };
43 class = {
44 type = nullOr (enum [ "idle" "batch" "other" "rr" "fifo" ]);
45 example = literalExpression "\"batch\"";
46 description = mdDoc "CPU scheduler class.";
47 };
48 prio = {
49 type = nullOr (ints.between 1 99);
50 example = literalExpression "49";
51 description = mdDoc "CPU scheduler priority.";
52 };
53 ioClass = {
54 type = nullOr (enum [ "idle" "best-effort" "realtime" ]);
55 example = literalExpression "\"best-effort\"";
56 description = mdDoc "IO scheduler class.";
57 };
58 ioPrio = {
59 type = nullOr (ints.between 0 7);
60 example = literalExpression "4";
61 description = mdDoc "IO scheduler priority.";
62 };
63 matchers = {
64 type = nullOr (listOf str);
65 default = [];
66 example = literalExpression ''
67 [
68 "include cgroup=\"/user.slice/*.service\" parent=\"systemd\""
69 "emacs"
70 ]
71 '';
72 description = mdDoc "Process matchers.";
73 };
74 };
75
76 cfsProfileToString = name: let
77 p = cfg.settings.cfsProfiles.${name};
78 in
79 "${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}\"";
80
81 prioToString = class: prio: if prio == null then "\"${class}\"" else "(${class})${toString prio}";
82
83 schedulerProfileToString = name: a: indent:
84 concatStringsSep " "
85 (["${indent}${name}"]
86 ++ (optional (a.nice != null) "nice=${toString a.nice}")
87 ++ (optional (a.class != null) "sched=${prioToString a.class a.prio}")
88 ++ (optional (a.ioClass != null) "io=${prioToString a.ioClass a.ioPrio}")
89 ++ (optional ((builtins.length a.matchers) != 0) ("{\n${concatStringsSep "\n" (map (m: " ${indent}${m}") a.matchers)}\n${indent}}")));
90
91in {
92 options = {
93 services.system76-scheduler = {
94 enable = lib.mkEnableOption (lib.mdDoc "system76-scheduler");
95
96 package = mkOption {
97 type = types.package;
98 default = config.boot.kernelPackages.system76-scheduler;
99 defaultText = literalExpression "config.boot.kernelPackages.system76-scheduler";
100 description = mdDoc "Which System76-Scheduler package to use.";
101 };
102
103 useStockConfig = mkOption {
104 type = bool;
105 default = true;
106 description = mdDoc ''
107 Use the (reasonable and featureful) stock configuration.
108
109 When this option is `true`, `services.system76-scheduler.settings`
110 are ignored.
111 '';
112 };
113
114 settings = {
115 cfsProfiles = {
116 enable = mkOption {
117 type = bool;
118 default = true;
119 description = mdDoc "Tweak CFS latency parameters when going on/off battery";
120 };
121
122 default = latencyProfile {
123 latency = 6;
124 nr-latency = 8;
125 wakeup-granularity = 1.0;
126 bandwidth-size = 5;
127 preempt = "voluntary";
128 };
129 responsive = latencyProfile {
130 latency = 4;
131 nr-latency = 10;
132 wakeup-granularity = 0.5;
133 bandwidth-size = 3;
134 preempt = "full";
135 };
136 };
137
138 processScheduler = {
139 enable = mkOption {
140 type = bool;
141 default = true;
142 description = mdDoc "Tweak scheduling of individual processes in real time.";
143 };
144
145 useExecsnoop = mkOption {
146 type = bool;
147 default = true;
148 description = mdDoc "Use execsnoop (otherwise poll the precess list periodically).";
149 };
150
151 refreshInterval = mkOption {
152 type = int;
153 default = 60;
154 description = mdDoc "Process list poll interval, in seconds";
155 };
156
157 foregroundBoost = {
158 enable = mkOption {
159 type = bool;
160 default = true;
161 description = mdDoc ''
162 Boost foreground process priorities.
163
164 (And de-boost background ones). Note that this option needs cooperation
165 from the desktop environment to work. On Gnome the client side is
166 implemented by the "System76 Scheduler" shell extension.
167 '';
168 };
169 foreground = schedulerProfile {
170 nice = 0;
171 ioClass = "best-effort";
172 ioPrio = 0;
173 };
174 background = schedulerProfile {
175 nice = 6;
176 ioClass = "idle";
177 };
178 };
179
180 pipewireBoost = {
181 enable = mkOption {
182 type = bool;
183 default = true;
184 description = mdDoc "Boost Pipewire client priorities.";
185 };
186 profile = schedulerProfile {
187 nice = -6;
188 ioClass = "best-effort";
189 ioPrio = 0;
190 };
191 };
192 };
193 };
194
195 assignments = mkOption {
196 type = types.attrsOf (types.submodule {
197 options = schedulerProfile { };
198 });
199 default = {};
200 example = literalExpression ''
201 {
202 nix-builds = {
203 nice = 15;
204 class = "batch";
205 ioClass = "idle";
206 matchers = [
207 "nix-daemon"
208 ];
209 };
210 }
211 '';
212 description = mdDoc "Process profile assignments.";
213 };
214
215 exceptions = mkOption {
216 type = types.listOf str;
217 default = [];
218 example = literalExpression ''
219 [
220 "include descends=\"schedtool\""
221 "schedtool"
222 ]
223 '';
224 description = mdDoc "Processes that are left alone.";
225 };
226 };
227 };
228
229 config = mkIf cfg.enable {
230 environment.systemPackages = [ cfg.package ];
231 services.dbus.packages = [ cfg.package ];
232
233 systemd.services.system76-scheduler = {
234 description = "Manage process priorities and CFS scheduler latencies for improved responsiveness on the desktop";
235 wantedBy = [ "multi-user.target" ];
236 path = [
237 # execsnoop needs those to extract kernel headers:
238 pkgs.kmod
239 pkgs.gnutar
240 pkgs.xz
241 ];
242 serviceConfig = {
243 Type = "dbus";
244 BusName= "com.system76.Scheduler";
245 ExecStart = "${cfg.package}/bin/system76-scheduler daemon";
246 ExecReload = "${cfg.package}/bin/system76-scheduler daemon reload";
247 };
248 };
249
250 environment.etc = mkMerge [
251 (mkIf cfg.useStockConfig {
252 # No custom settings: just use stock configuration with a fix for Pipewire
253 "system76-scheduler/config.kdl".source = "${cfg.package}/data/config.kdl";
254 "system76-scheduler/process-scheduler/00-dist.kdl".source = "${cfg.package}/data/pop_os.kdl";
255 "system76-scheduler/process-scheduler/01-fix-pipewire-paths.kdl".source = ../../../../pkgs/os-specific/linux/system76-scheduler/01-fix-pipewire-paths.kdl;
256 })
257
258 (let
259 settings = cfg.settings;
260 cfsp = settings.cfsProfiles;
261 ps = settings.processScheduler;
262 in mkIf (!cfg.useStockConfig) {
263 "system76-scheduler/config.kdl".text = ''
264 version "2.0"
265 autogroup-enabled false
266 cfs-profiles enable=${boolToString cfsp.enable} {
267 ${cfsProfileToString "default"}
268 ${cfsProfileToString "responsive"}
269 }
270 process-scheduler enable=${boolToString ps.enable} {
271 execsnoop ${boolToString ps.useExecsnoop}
272 refresh-rate ${toString ps.refreshInterval}
273 assignments {
274 ${if ps.foregroundBoost.enable then (schedulerProfileToString "foreground" ps.foregroundBoost.foreground " ") else ""}
275 ${if ps.foregroundBoost.enable then (schedulerProfileToString "background" ps.foregroundBoost.background " ") else ""}
276 ${if ps.pipewireBoost.enable then (schedulerProfileToString "pipewire" ps.pipewireBoost.profile " ") else ""}
277 }
278 }
279 '';
280 })
281
282 {
283 "system76-scheduler/process-scheduler/02-config.kdl".text =
284 "exceptions {\n${concatStringsSep "\n" (map (e: " ${e}") cfg.exceptions)}\n}\n"
285 + "assignments {\n"
286 + (concatStringsSep "\n" (map (name: schedulerProfileToString name cfg.assignments.${name} " ")
287 (attrNames cfg.assignments)))
288 + "\n}\n";
289 }
290 ];
291 };
292
293 meta = {
294 maintainers = [ lib.maintainers.cmm ];
295 };
296}