From fd6b17bf20dc0cec2b4edff2928f899ebd2741cc Mon Sep 17 00:00:00 2001 From: Giacomo Leidi Date: Sun, 4 Jan 2026 23:58:56 +0100 Subject: [PATCH] gnu: Add tuned-service-type. * gnu/services/linux.scm (tuned-configuration,tuned-settings,tuned-ppd-settings): New configuration records. (tuned-file-systems,tuned-activation,tuned-shepherd-services, tuned-kernel-modules): New procedures. (tuned-service-type): New service type. * doc/guix.texi: Add service documentation. Change-Id: I6c8d54c23175c2ea133d99965641c548fb1d6452 --- doc/guix.texi | 198 +++++++++++++++++- gnu/services/linux.scm | 448 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 645 insertions(+), 1 deletion(-) diff --git a/doc/guix.texi b/doc/guix.texi index 15ed74ed0c..d7f4c41d57 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -111,7 +111,7 @@ Copyright @copyright{} 2022 (@* Copyright @copyright{} 2022 John Kehayias@* Copyright @copyright{} 2022⁠–⁠2023 Bruno Victal@* Copyright @copyright{} 2022 Ivan Vilata-i-Balaguer@* -Copyright @copyright{} 2023-2025 Giacomo Leidi@* +Copyright @copyright{} 2023-2026 Giacomo Leidi@* Copyright @copyright{} 2022 Antero Mejr@* Copyright @copyright{} 2023 Karl Hallsby@* Copyright @copyright{} 2023 Nathaniel Nicandro@* @@ -44972,6 +44972,202 @@ The database location is hard-coded to @file{/var/lib/rasdaemon/ras-mc_event.db} @end table @end deftp +@cindex tuned +@cindex system tuning +@cindex System tuning service for Linux +@subsubheading TuneD Service + +@url{https://tuned-project.org,TuneD} is a system tuning service for Linux. It +monitors connected devices using the udev device manager and tunes system +settings according to a selected profile. + +It can be integrated with desktop environments like GNOME and KDE: it replaces +@code{power-profiles-daemon} by implementing the same D-Bus API that they +already use. + +The following is an example configuration that could be suitable for a laptop: + +@lisp +(service tuned-service-type + (tuned-configuration + (power-profiles-daemon-support? #t) + (ppd-settings + (tuned-ppd-settings + ;; Customize default profiles to use laptop specific ones. + (profiles + '(("power-saver" . "laptop-ac-powersave") + ("balanced" . "balanced") + ("performance" . "throughput-performance"))) + (battery + ;; Customize battery profiles to use laptop specific ones. + '(("power-saver" . "laptop-battery-powersave") + ("balanced" . "balanced-battery"))))))) +@end lisp + +For more information, refer to @code{tuned-main.conf(5)}. + +@defvar tuned-service-type +This service spawns and configure the TuneD daemon. The service's value is a +@code{tuned-configuration} record. + +@c %start of fragment + +@deftp {Data Type} tuned-configuration +Available @code{tuned-configuration} fields are: + +@table @asis +@item @code{tuned} (default: @code{tuned}) (type: package) +The TuneD package. + +@item @code{auto-start?} (default: @code{#t}) (type: boolean) +Whether this service should be started automatically by the Shepherd. If +it is @code{#f} the service has to be started manually with +@command{herd start}. + +@item @code{power-profiles-daemon-support?} (default: @code{#f}) (type: boolean) +Whether the power-profiles-daemon emulation layer should be +enabled. + +@item @code{profiles} (default: @code{'()}) (type: list-of-tuned-plugins) +User provided profiles for TuneD. Each element of the list is supposed +to be a list where the first element is the name of the directory where +plugin files will be placed under @file{/etc/tuned/profiles} and the +second a file like object containing the plugin files: + +@lisp +(list (list "plugin-name" (plain-file "plugin.conf" "content")) + (list "other-plugin" + (file-union "plugin-data" + (list + (list "other-plugin.conf" + (plain-file "other-plugin.conf" "content")) + (list "other-plugin.scm" + (program-file "other-plugin.scm" + #~(display "content"))))))) +@end lisp + +@item @code{settings} (type: tuned-settings) +Configuration for TuneD. + +@item @code{ppd-settings} (type: tuned-ppd-settings) +Configuration for the @code{power-profiles-daemon} compatibility layer +of TuneD. + +@item @code{recommend.conf} (type: file-like) +File like object containing the recommended profile configuration. +Defaults to @code{%default-tuned-configuration-recommend.conf}. + +@end table + +@end deftp + + +@c %end of fragment + + +@c %start of fragment + +@deftp {Data Type} tuned-settings +Available @code{tuned-settings} fields are: + +@table @asis +@item @code{daemon?} (default: @code{#t}) (type: boolean) +Whether to use daemon. Without daemon TuneD just applies tuning. + +@item @code{dynamic-tuning?} (default: @code{#f}) (type: boolean) +Dynamically tune devices, if disabled only static tuning will be used. + +@item @code{default-instance-priority} (default: @code{0}) (type: integer) +Default priority assigned to instances. + +@item @code{recommend-command?} (default: @code{#t}) (type: boolean) +Recommend functionality, if disabled @code{recommend} command will be +not available in CLI, daemon will not parse @file{recommend.conf} but +will return one hardcoded profile (by default @code{balanced}). + +@item @code{sleep-interval} (default: @code{1}) (type: integer) +How long to sleep before checking for events (in seconds), higher number +means lower overhead but longer response time. + +@item @code{update-interval} (default: @code{10}) (type: integer) +Update interval for dynamic tunings (in seconds). It must be a multiple +of the @code{sleep-interval}. + +@item @code{profile-dirs} (type: list-of-profile-dirs) +List of strings or gexps representing directories to search for +profiles. In case of collisions in profile names, the latter directory +takes precedence. + +@item @code{extra-content} (type: text-config) +A list of file-like objects that are appended to the configuration file. + +@end table + +@end deftp + + +@c %end of fragment + + +@c %start of fragment + +@deftp {Data Type} tuned-ppd-settings +Available @code{tuned-ppd-settings} fields are: + +@table @asis +@item @code{default} (default: @code{"balanced"}) (type: string) +Default PPD profile. + +@item @code{battery-detection?} (default: @code{#t}) (type: boolean) +Whether to enable battery detection. + +@item @code{sysfs-acpi-monitor?} (default: @code{#t}) (type: boolean) +Whether to react to changes of ACPI platform profile done via function +keys (e.g., Fn-L). This is marked upstream as an experimental feature. + +@item @code{profiles} (type: mixed-list) +Map of PPD profiles states to TuneD profiles. It's supposed to be a +list of pairs, pair members are supposed to be string. It defaults to +@code{%default-tuned-ppd-settings-profiles}: + +@lisp +'(("power-saver" . "powersave") + ("balanced" . "balanced") + ("performance" . "throughput-performance")) +@end lisp + +Elements can be pairs or strings. Pair members can be either strings, gexps or +file like objects. Strings are directly passed to the serializer. This can be +an escape hatch in case the underlying syntax of the output file changes +slightly and the Scheme API is not adequated in time. This way there is always +a way to work around Scheme records. + +@item @code{battery} (type: mixed-list) +Map of PPD battery states to TuneD profiles. It's supposed to be a list +of pairs, pair members are supposed to be string. It defaults to +@code{%default-tuned-ppd-settings-battery}: + +@lisp +'(("balanced" . "balanced-battery")) +@end lisp + +Elements can be pairs or strings. Pair members can be either strings, gexps or +file like objects. Strings are directly passed to the serializer. This can be +an escape hatch in case the underlying syntax of the output file changes +slightly and the Scheme API is not adequated in time. This way there is always +a way to work around Scheme records. + +@item @code{extra-content} (type: text-config) +A list of file-like objects that are appended to the configuration file. + +@end table + +@end deftp + + +@c %end of fragment +@end defvar + @cindex zram @cindex compressed swap @cindex Compressed RAM-based block devices diff --git a/gnu/services/linux.scm b/gnu/services/linux.scm index 90693b77eb..2284741a65 100644 --- a/gnu/services/linux.scm +++ b/gnu/services/linux.scm @@ -8,6 +8,7 @@ ;;; Copyright © 2023 Bruno Victal ;;; Copyright © 2023 Felix Lechner ;;; Copyright © 2025 Edouard Klein +;;; Copyright © 2026 Giacomo Leidi ;;; ;;; This file is part of GNU Guix. ;;; @@ -30,13 +31,18 @@ #:use-module (guix records) #:use-module (guix modules) #:use-module (guix i18n) + #:use-module (guix packages) #:use-module (guix ui) #:use-module (gnu services) #:use-module (gnu services admin) #:use-module (gnu services base) #:use-module (gnu services configuration) + #:use-module (gnu services dbus) #:use-module (gnu services shepherd) + #:use-module (gnu system file-systems) + #:use-module (gnu packages base) #:use-module (gnu packages linux) + #:use-module (gnu packages power) #:use-module (gnu packages file-systems) #:use-module (srfi srfi-1) #:use-module (srfi srfi-26) @@ -45,6 +51,7 @@ #:use-module (srfi srfi-171) #:use-module (ice-9 format) #:use-module (ice-9 match) + #:use-module (ice-9 string-fun) #:export (earlyoom-configuration earlyoom-configuration? earlyoom-configuration-earlyoom @@ -97,6 +104,49 @@ rasdaemon-configuration-record? rasdaemon-service-type + %default-tuned-configuration-recommend.conf + %default-tuned-ppd-settings-battery + %default-tuned-ppd-settings-profiles + + tuned-configuration + tuned-configuration? + tuned-configuration-fields + tuned-configuration-tuned + tuned-configuration-auto-start? + tuned-configuration-power-profiles-daemon-support? + tuned-configuration-profiles + tuned-configuration-settings + tuned-configuration-ppd-settings + tuned-configuration-recommend.conf + + tuned-settings + tuned-settings? + tuned-settings-fields + tuned-settings-daemon? + tuned-settings-dynamic-tuning? + tuned-settings-default-instance-priority + tuned-settings-recommend-command? + tuned-settings-sleep-interval + tuned-settings-update-interval + tuned-settings-profile-dirs + tuned-settings-extra-content + + tuned-ppd-settings + tuned-ppd-settings? + tuned-ppd-settings-fields + tuned-ppd-settings-default + tuned-ppd-settings-battery-detection? + tuned-ppd-settings-sysfs-acpi-monitor? + tuned-ppd-settings-profiles + tuned-ppd-settings-battery + tuned-ppd-settings-extra-content + + tuned-file-systems + tuned-activation + tuned-shepherd-services + + tuned-service-type + zram-device-configuration zram-device-configuration? zram-device-configuration-size @@ -549,6 +599,404 @@ the Linux @code{cachefiles} module.") (compose concatenate) (description "Run @command{rasdaemon}, the RAS monitor"))) + +;;; +;;; TuneD. +;;; + +(define (uglify-snake-case field-name) + "Serializes FIELD-NAME, a field name from @code{(gnu services configuration)}, +to a snake case string representation of the field name. Trailing @code{?} in +the name are dropped and @code{-} get replaced by @code{_}. + +For example the procedure would convert @code{'A-Field?} to @code{\"a_field\"}." + (define str (symbol->string field-name)) + (string-downcase + (string-replace-substring + (if (string-suffix? "?" str) + (string-drop-right str 1) + str) + "-" "_"))) + +(define* (tuned-serialize-pair pair #:key (separator "=")) + (define (valid? member) + (or (string? member) + (gexp? member) + (file-like? member))) + (match pair + (((? valid? key) . (? valid? value)) + #~(string-append #$key #$separator #$value)) + (_ + (raise + (formatted-message + (G_ "pair members must contain only strings, gexps or file-like objects +but ~a was found") + pair))))) + +(define* (tuned-ppd-serialize-mixed-list name value #:key (separator " = ")) + (if (zero? (length value)) + "" + #~(string-append "\n[" #$(uglify-snake-case name) "]\n" + (string-join + (list #$@(map tuned-serialize-pair value)) "\n") + "\n"))) + +(define (mixed-list? value) + ;; Expected spec format: + ;; '(("name" . "value") "name=value") + (for-each + (lambda (el) + (cond ((string? el) el) + ((pair? el) (tuned-serialize-pair el)) + (else + (raise + (formatted-message + (G_ "members must be either strings or pairs but ~a was +found!") + el))))) + value) + #t) + +(define (tuned-ppd-serialize-string name value) + (format #f "~a=~a" (uglify-snake-case name) value)) + +(define (tuned-ppd-serialize-boolean name value) + (format #f "~a=~a" (uglify-snake-case name) (if value "true" "false"))) + +(define %default-tuned-ppd-settings-profiles + '(("power-saver" . "powersave") + ("balanced" . "balanced") + ("performance" . "throughput-performance"))) + +(define %default-tuned-ppd-settings-battery + '(("balanced" . "balanced-battery"))) + +(define-configuration tuned-ppd-settings + (default + (string "balanced") + "Default PPD profile.") + (battery-detection? + (boolean #t) + "Whether to enable battery detection.") + (sysfs-acpi-monitor? + (boolean #t) + "Whether to react to changes of ACPI platform profile done via function keys +(e.g., Fn-L). This is marked upstream as an experimental feature.") + (profiles + (mixed-list %default-tuned-ppd-settings-profiles) + "Map of PPD profiles states to TuneD profiles. It's supposed to be a list of +pairs, pair members are supposed to be string. It defaults to +@code{%default-tuned-ppd-settings-profiles}: + +@lisp +'((\"power-saver\" . \"powersave\") + (\"balanced\" . \"balanced\") + (\"performance\" . \"throughput-performance\")) +@end lisp + +Elements can be pairs or strings. Pair members can be either strings, gexps or +file like objects. Strings are directly passed to the serializer. This can be +an escape hatch in case the underlying syntax of the output file changes +slightly and the Scheme API is not adequated in time. This way there is always +a way to work around Scheme records.") + (battery + (mixed-list %default-tuned-ppd-settings-battery) + "Map of PPD battery states to TuneD profiles. It's supposed to be a list of +pairs, pair members are supposed to be string. It defaults to +@code{%default-tuned-ppd-settings-battery}: + +@lisp +'((\"balanced\" . \"balanced-battery\")) +@end lisp + +Elements can be pairs or strings. Pair members can be either strings, gexps or +file like objects. Strings are directly passed to the serializer. This can be +an escape hatch in case the underlying syntax of the output file changes +slightly and the Scheme API is not adequated in time. This way there is always +a way to work around Scheme records.") + (extra-content + (text-config + (list (plain-file "tuned-ppd-settings-extra-content" ""))) + "A list of file-like objects that are appended to the configuration file." + (serializer serialize-text-config)) + (prefix tuned-ppd-)) + +(define (serialize-tuned-ppd-settings config) + (define fields + (filter-configuration-fields + tuned-ppd-settings-fields + '(default battery-detection? sysfs-acpi-monitor? profiles battery + extra-content))) + (define getters + (map configuration-field-getter fields)) + (define names + (map configuration-field-name fields)) + (define serializers + (map configuration-field-serializer fields)) + (define values + (map (match-lambda ((serializer name getter) + (serializer name (getter config)))) + (zip serializers names getters))) + + (match values + ((default battery-detection? sysfs-acpi-monitor? profiles battery + extra-content) + + (mixed-text-file "ppd.conf" + "[main]\n" + (string-append default "\n") + (string-append battery-detection? "\n") + (string-append sysfs-acpi-monitor? "\n") + "\n" + profiles + battery + extra-content + "\n")))) + +(define (serialize-list-of-profile-dirs name value) + (if (zero? (length value)) + "" + #~(string-append + #$(uglify-snake-case name) " = " (string-join (list #$@value) ",")))) + +(define (list-of-profile-dirs? value) + (for-each + (lambda (el) + (unless (or (string? el) (file-like? el)) + (raise + (formatted-message + (G_ "tuned-settings profile-dirs members must be either a string +or a file-like object but ~a was found!") + el)))) + value) + #t) + +(define tuned-serialize-boolean tuned-ppd-serialize-boolean) +(define tuned-serialize-integer tuned-ppd-serialize-string) + +(define-configuration tuned-settings + (daemon? + (boolean #t) + "Whether to use daemon. Without daemon TuneD just applies tuning.") + (dynamic-tuning? + (boolean #f) + "Dynamically tune devices, if disabled only static tuning will be used.") + (default-instance-priority + (integer 0) + "Default priority assigned to instances.") + (recommend-command? + (boolean #t) + "Recommend functionality, if disabled @code{recommend} command will be not +available in CLI, daemon will not parse @file{recommend.conf} but will return +one hardcoded profile (by default @code{balanced}).") + (sleep-interval + (integer 1) + "How long to sleep before checking for events (in seconds), +higher number means lower overhead but longer response time.") + (update-interval + (integer 10) + "Update interval for dynamic tunings (in seconds). It must be a multiple of +the @code{sleep-interval}.") + (profile-dirs + (list-of-profile-dirs + (list (file-append tuned "/lib/tuned/profiles") "/etc/tuned/profiles")) + "List of strings or gexps representing directories to search for +profiles. In case of collisions in profile names, the latter directory takes +precedence." + (serializer serialize-list-of-profile-dirs)) + (extra-content + (text-config + (list (plain-file "tuned-settings-extra-content" ""))) + "A list of file-like objects that are appended to the configuration file." + (serializer serialize-text-config)) + (prefix tuned-)) + +(define (serialize-tuned-settings config) + (define fields + (filter-configuration-fields + tuned-settings-fields + '(daemon? dynamic-tuning? default-instance-priority + recommend-command? sleep-interval update-interval profile-dirs + extra-content))) + (define getters + (map configuration-field-getter fields)) + (define names + (map configuration-field-name fields)) + (define serializers + (map configuration-field-serializer fields)) + (define values + (map (match-lambda ((serializer name getter) + (serializer name (getter config)))) + (zip serializers names getters))) + + (match values + ((daemon? dynamic-tuning? default-instance-priority + recommend-command? sleep-interval update-interval profile-dirs + extra-content) + (mixed-text-file + "tuned-main.conf" + (string-append daemon? "\n\n") + (string-append dynamic-tuning? "\n\n") + (string-append default-instance-priority "\n\n") + (string-append recommend-command? "\n\n") + (string-append sleep-interval "\n\n") + (string-append update-interval "\n\n") + profile-dirs + extra-content + "\n")))) + +(define (tuned-plugin? value) + (if (and (= 2 (length value)) + (string? (first value)) + (file-like? (second value))) + #t + (raise + (formatted-message + (G_ "tuned-configuration profiles members must be lists with two +elements, the first being a string and the second a file-like object, but ~a was +found!") + value)))) + +(define (list-of-tuned-plugins? value) + (list-of tuned-plugin?)) + +(define %default-tuned-configuration-recommend.conf + (file-append tuned "/lib/tuned/recommend.d/50-tuned.conf")) + +(define-configuration/no-serialization tuned-configuration + (tuned + (package tuned) + "The TuneD package.") + (auto-start? + (boolean #t) + "Whether this service should be started automatically by the Shepherd. If it +is @code{#f} the service has to be started manually with @command{herd start}.") + (power-profiles-daemon-support? + (boolean #f) + "Whether the power-profiles-daemon emulation layer should be +enabled.") + (profiles + (list-of-tuned-plugins '()) + "User provided profiles for TuneD. Each element of the list is supposed to be +a list where the first element is the name of the directory where plugin files +will be placed under @file{/etc/tuned/profiles} and the second a file like +object containing the plugin files: + +@lisp +(list + (list \"plugin-name\" + (plain-file \"plugin.conf\" \"content\")) + (list \"other-plugin\" + (file-union \"plugin-data\" + (list (list \"other-plugin.conf\" + (plain-file \"other-plugin.conf\" \"content\")) + (list \"other-plugin.scm\" + (program-file \"other-plugin.scm\" + #~(display \"content\"))))))) +@end lisp") + (settings + (tuned-settings (tuned-settings)) + "Configuration for TuneD.") + (ppd-settings + (tuned-ppd-settings (tuned-ppd-settings)) + "Configuration for the @code{power-profiles-daemon} compatibility layer of +TuneD.") + (recommend.conf + (file-like %default-tuned-configuration-recommend.conf) + "File like object containing the recommended profile configuration. Defaults +to @code{%default-tuned-configuration-recommend.conf}.")) + +(define (tuned-file-systems config) + (list + (file-system + (device "none") + (mount-point "/run/tuned") + (type "ramfs") + (check? #f)))) + +(define (tuned-activation config) + (match-record config + (profiles settings ppd-settings recommend.conf) + #~(begin + (use-modules (guix build utils)) + (let ((data-directory "/var/lib/tuned") + (config-directory "/etc/tuned")) + ;; Setup TuneD directories. + (for-each + (lambda (dir) + (mkdir-p dir) + (chmod dir #o755)) + (list data-directory config-directory + ;; the directory where TuneD will look for kernel modules for + ;; plugins. + "/etc/tuned/modprobe.d")) + ;; Create plugins kernel modules configuration. + (invoke #$(file-append coreutils-minimal "/bin/touch") + "/etc/tuned/modprobe.d/tuned.conf") + ;; Generate and activate TuneD configuration files. + ;; TuneD needs to write in /etc/tuned, special-files-service-type + ;; creates a file union, so /etc/tuned is read-only and TuneD crashes. + ;; activate-special-files creates a single symlink to the store for + ;; each file so TuneD doesn't notice and runs successfully. + (activate-special-files + '(("/etc/tuned/recommend.conf" #$recommend.conf) + ("/etc/tuned/profiles" + #$(file-union "tuned-profiles" profiles)) + ("/etc/tuned/tuned-main.conf" + #$(serialize-tuned-settings settings)) + ("/etc/tuned/ppd.conf" + #$(serialize-tuned-ppd-settings ppd-settings)))))))) + +(define (tuned-shepherd-services config) + (match-record config + (tuned auto-start? power-profiles-daemon-support?) + (append + (list + (shepherd-service + (documentation "TuneD daemon") + (provision '(tuned)) + (requirement '(dbus-system user-processes udev)) + (start #~(make-forkexec-constructor + '(#$(file-append tuned "/sbin/tuned")) + #:log-file "/var/log/tuned/tuned.log")) + (stop #~(make-kill-destructor)) + (auto-start? auto-start?))) + (if power-profiles-daemon-support? + (list (shepherd-service + (documentation "TuneD power-profiles-daemon emulation daemon") + (provision '(tuned-ppd power-profiles-daemon)) + (requirement '(dbus-system user-processes udev tuned)) + (start + #~(make-forkexec-constructor + '(#$(file-append tuned "/sbin/tuned-ppd")) + #:log-file "/var/log/tuned/tuned-ppd.log")) + (stop #~(make-kill-destructor)) + (auto-start? auto-start?))) + '())))) + +(define tuned-service-type + (let ((config->package + (compose list tuned-configuration-tuned))) + (service-type + (name 'tuned) + (extensions (list + (service-extension shepherd-root-service-type + tuned-shepherd-services) + (service-extension dbus-root-service-type + config->package) + (service-extension polkit-service-type + config->package) + (service-extension profile-service-type + config->package) + (service-extension file-system-service-type + tuned-file-systems) + (service-extension activation-service-type + tuned-activation))) + (default-value (tuned-configuration)) + (description "Run the TuneD daemon. It is daemon that tunes system settings +dynamically. It does so by monitoring the usage of several system components +periodically.")))) + ;;; ;;; Zram device