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