├── .gitignore ├── doc └── .gitignore ├── obj └── .gitignore ├── src ├── DIR.dox ├── sys │ ├── DIR.dox │ ├── error.hpp │ ├── signal.hpp │ ├── pidfile.hpp │ ├── env.hpp │ └── sysctl.hpp ├── types.hpp ├── version.hpp ├── constants.hpp ├── utility.cpp ├── errors.hpp ├── Cycle.hpp ├── loadplay.cpp ├── clas.hpp ├── clas.cpp ├── loadrec.cpp ├── utility.hpp └── Options.hpp ├── doxy ├── cppfilter.awk ├── mandoc.sh ├── githublabels.awk └── mantohtml.awk ├── .gitmodules ├── pkg ├── deinstall.sh ├── common.sh ├── files └── install.sh ├── powerd++.rc ├── loads └── 5s.load ├── LICENSE.md ├── tools ├── playdiff ├── playfilter.movingavg.awk ├── playfilter.horiz.awk ├── playfilter └── README.md ├── man ├── loadrec.1 ├── loadplay.1 └── powerd++.8 ├── Makefile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /obj/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/DIR.dox: -------------------------------------------------------------------------------- 1 | /** 2 | * C++ source file root. 3 | * 4 | * @dir 5 | */ 6 | -------------------------------------------------------------------------------- /doxy/cppfilter.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | /\*\//{print;next}/\/\*\*/,/\*\//{sub(/^[ \t]*\* ?/,"")}1 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gh-pages"] 2 | path = gh-pages 3 | url = ../powerdxx.git 4 | branch = gh-pages 5 | -------------------------------------------------------------------------------- /src/sys/DIR.dox: -------------------------------------------------------------------------------- 1 | /** 2 | * C++ wrappers for common C interfaces. 3 | * 4 | * @dir 5 | */ 6 | 7 | /** 8 | * Wrappers around native system interfaces. 9 | * 10 | * @namespace sys 11 | */ 12 | -------------------------------------------------------------------------------- /pkg/deinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "${0%/*}/common.sh" "$@" 3 | sed "${sedprog}" | while read type src tgt; do 4 | echo rm "${DESTDIR:+${DESTDIR%/}/}${tgt}" 5 | rm "${DESTDIR:+${DESTDIR%/}/}${tgt}" 6 | while rmdir "${DESTDIR:+${DESTDIR%/}/}${tgt%/*}" 2> /dev/null; do 7 | tgt="${tgt%/*}" 8 | done 9 | done 10 | -------------------------------------------------------------------------------- /powerd++.rc: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # PROVIDE: powerdxx 4 | # REQUIRE: DAEMON 5 | # BEFORE: LOGIN 6 | # KEYWORD: nojail shutdown 7 | 8 | . /etc/rc.subr 9 | 10 | name="powerdxx" 11 | rcvar="powerdxx_enable" 12 | command="%%PREFIX%%/sbin/powerd++" 13 | pidfile="/var/run/powerd.pid" 14 | 15 | load_rc_config $name 16 | run_rc_command "$1" 17 | -------------------------------------------------------------------------------- /doxy/mandoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -fe 3 | 4 | name="${1##*/}" 5 | name="${name%.*}" 6 | sane="$(echo "$name" | tr '+' 'x')" 7 | sect="${1##*.}" 8 | echo "\\page man_${sect}_$sane Manual $name($sect) 9 | " 10 | awk '{for (name in ENVIRON) gsub("%%" name "%%", ENVIRON[name])}1' "$1" | \ 11 | mandoc -Kutf-8 -Tutf8 -mdoc | \ 12 | "${0%/*}/mantohtml.awk" -vESCAPE='\@&$#<>%".=|-' 13 | -------------------------------------------------------------------------------- /pkg/common.sh: -------------------------------------------------------------------------------- 1 | set -f 2 | export "$@" 3 | sedprog= 4 | for envvar in CURDIR OBJDIR PREFIX DOCSDIR; do 5 | eval "sedprog=\"${sedprog};s!%%${envvar}%%!\${${envvar}%/}!g\"" 6 | done 7 | IFS=':' 8 | : ${BSD_INSTALL_PROGRAM=install -s -m 555} 9 | : ${BSD_INSTALL_MAN=install -m 444} 10 | : ${BSD_INSTALL_SCRIPT=install -m 555} 11 | : ${BSD_INSTALL_LIB=install -s -m 444} 12 | : ${BSD_INSTALL_SYMLINK=install -l s} 13 | : ${GZIP_CMD=gzip -nf9} 14 | -------------------------------------------------------------------------------- /loads/5s.load: -------------------------------------------------------------------------------- 1 | hw.machine=amd64 2 | hw.model=Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz 3 | hw.ncpu=4 4 | hw.acpi.acline=1 5 | dev.cpu.0.freq=1800 6 | dev.cpu.0.freq_levels=2401/15000 2400/15000 2300/14088 2200/13340 2000/11888 1900/11184 1800/10495 1700/9680 1500/8372 1400/7738 1300/7119 1200/6511 1100/5789 900/4643 800/4090 768/3550 7 | 0 37569 18144 18305 6386 293895 41884 22207 17069 610 292481 37564 20841 15889 1768 298189 37980 22174 16452 1293 296352 8 | 5000 26 0 16 10 587 16 0 15 0 607 19 0 16 0 602 15 0 9 0 614 9 | -------------------------------------------------------------------------------- /pkg/files: -------------------------------------------------------------------------------- 1 | PROGRAM:%%OBJDIR%%/powerd++:%%PREFIX%%/sbin/powerd++ 2 | SYMLINK:powerd++:%%PREFIX%%/sbin/powerdxx 3 | PROGRAM:%%OBJDIR%%/loadrec:%%PREFIX%%/bin/loadrec 4 | PROGRAM:%%OBJDIR%%/loadplay:%%PREFIX%%/bin/loadplay 5 | LIB:%%OBJDIR%%/libloadplay.so:%%PREFIX%%/lib/libloadplay.so 6 | MAN:%%CURDIR%%/README.md:%%DOCSDIR%%/README.md 7 | MAN:%%CURDIR%%/man/powerd++.8:%%PREFIX%%/man/man8/powerd++.8.gz 8 | SYMLINK:powerd++.8.gz:%%PREFIX%%/man/man8/powerdxx.8.gz 9 | MAN:%%CURDIR%%/man/loadrec.1:%%PREFIX%%/man/man1/loadrec.1.gz 10 | MAN:%%CURDIR%%/man/loadplay.1:%%PREFIX%%/man/man1/loadplay.1.gz 11 | SCRIPT:%%CURDIR%%/powerd++.rc:%%PREFIX%%/etc/rc.d/powerdxx 12 | -------------------------------------------------------------------------------- /pkg/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "${0%/*}/common.sh" "$@" 3 | sed "${sedprog}" | while read type src tgt; do 4 | eval echo "\${BSD_INSTALL_${type}} \"\${src}\" \"\${DESTDIR:+\${DESTDIR%/}/}\${tgt}\"" 5 | mkdir -p "${DESTDIR:+${DESTDIR%/}/}${tgt%/*}" 6 | eval eval "\${BSD_INSTALL_${type}} \"\${src}\" \"\${DESTDIR:+\${DESTDIR%/}/}\${tgt%.gz}\"" 7 | case ${type} in 8 | MAN | SCRIPT) 9 | sed -i '' "/#RM\$/d${sedprog}" "${DESTDIR:+${DESTDIR%/}/}${tgt%.gz}" 10 | ;; 11 | SYMLINK) 12 | mv "${DESTDIR:+${DESTDIR%/}/}${tgt%.gz}" "${DESTDIR:+${DESTDIR%/}/}${tgt}" 13 | continue 14 | ;; 15 | esac 16 | if [ -z "${tgt##*.gz}" ]; then 17 | eval ${GZIP_CMD} "${DESTDIR:+${DESTDIR%/}/}${tgt%.gz}" 18 | fi 19 | done 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2016 - 2020 Dominic Fandrey 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/types.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * A collection of type aliases. 3 | * 4 | * @file 5 | */ 6 | 7 | #include /* std::chrono::milliseconds */ 8 | 9 | #ifndef _POWERDXX_TYPES_HPP_ 10 | #define _POWERDXX_TYPES_HPP_ 11 | 12 | /** 13 | * A collection of type aliases. 14 | */ 15 | namespace types { 16 | 17 | /** 18 | * Millisecond type for polling intervals. 19 | */ 20 | typedef std::chrono::milliseconds ms; 21 | 22 | /** 23 | * Type for CPU core indexing. 24 | */ 25 | typedef int coreid_t; 26 | 27 | /** 28 | * Type for load counting. 29 | * 30 | * According to src/sys/kern/kern_clock.c the type is `long` (an array of 31 | * loads `long[CPUSTATES]` is defined). 32 | * But in order to have defined wrapping characteristics `unsigned long` 33 | * will be used here. 34 | */ 35 | typedef unsigned long cptime_t; 36 | 37 | /** 38 | * Type for CPU frequencies in MHz. 39 | */ 40 | typedef unsigned int mhz_t; 41 | 42 | /** 43 | * Type for temperatures in dK. 44 | */ 45 | typedef int decikelvin_t; 46 | 47 | } /* namespace types */ 48 | 49 | #endif /* _POWERDXX_TYPES_HPP_ */ 50 | -------------------------------------------------------------------------------- /src/sys/error.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides system call error handling. 3 | * 4 | * @file 5 | */ 6 | 7 | #ifndef _POWERDXX_SYS_ERROR_HPP_ 8 | #define _POWERDXX_SYS_ERROR_HPP_ 9 | 10 | #include /* errno */ 11 | #include /* strerror() */ 12 | 13 | namespace sys { 14 | 15 | /** 16 | * Can be thrown by syscall function wrappers if the function returned 17 | * with an error. 18 | * 19 | * This is its own type for easy catching, but implicitly casts to 20 | * int for easy comparison. 21 | * 22 | * @tparam Domain 23 | * A type marking the domain the error comes from, e.g. sys::ctl::error 24 | */ 25 | template 26 | struct sc_error { 27 | /** 28 | * The errno set by the native C function. 29 | */ 30 | int error; 31 | 32 | /** 33 | * Cast to integer. 34 | * 35 | * @return 36 | * The errno code 37 | */ 38 | operator int() const { 39 | return this->error; 40 | } 41 | 42 | /** 43 | * Return c style string. 44 | * 45 | * @return 46 | * A string representation of the error 47 | */ 48 | char const * c_str() const { 49 | return ::strerror(this->error); 50 | } 51 | }; 52 | 53 | } /* namespace sys */ 54 | 55 | #endif /* _POWERDXX_SYS_ERROR_HPP_ */ 56 | -------------------------------------------------------------------------------- /src/version.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines types and constants used for version management. 3 | * 4 | * @file 5 | */ 6 | 7 | #ifndef _POWERDXX_VERSION_HPP_ 8 | #define _POWERDXX_VERSION_HPP_ 9 | 10 | #include "utility.hpp" /* utility::to_value() */ 11 | 12 | #include /* uint64_t, uint8_t */ 13 | 14 | /** 15 | * Version information constants and types. 16 | */ 17 | namespace version { 18 | 19 | /** 20 | * The pseudo MIB name for the load recording feature flags. 21 | */ 22 | char const * const LOADREC_FEATURES = "usr.app.powerdxx.loadrec.features"; 23 | 24 | /** 25 | * The data type to use for feature flags. 26 | */ 27 | typedef uint64_t flag_t; 28 | 29 | /** 30 | * Feature flags for load recordings. 31 | */ 32 | enum class LoadrecBits { 33 | FREQ_TRACKING, /**< Record clock frequencies per frame. */ 34 | }; 35 | 36 | /** 37 | * Literals to set flag bits. 38 | */ 39 | namespace literals { 40 | 41 | /** 42 | * Set the FREQ_TRACKING bit. 43 | * 44 | * @param value 45 | * The bit value 46 | * @return 47 | * The flag at the correct bit position 48 | */ 49 | constexpr flag_t operator ""_FREQ_TRACKING(unsigned long long int value) { 50 | return static_cast(value > 0) << 51 | utility::to_value(LoadrecBits::FREQ_TRACKING); 52 | } 53 | 54 | } /* namespace literals */ 55 | 56 | } /* namespace version */ 57 | 58 | #endif /* _POWERDXX_VERSION_HPP_ */ 59 | -------------------------------------------------------------------------------- /src/sys/signal.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements a c++ wrapper for the signal(3) call. 3 | * 4 | * @file 5 | */ 6 | 7 | #ifndef _POWERDXX_SYS_SIGNAL_HPP_ 8 | #define _POWERDXX_SYS_SIGNAL_HPP_ 9 | 10 | #include "error.hpp" /* sys::sc_error */ 11 | 12 | #include 13 | 14 | namespace sys { 15 | 16 | /** 17 | * This namespace provides c++ wrappers for signal(3). 18 | */ 19 | namespace sig { 20 | 21 | /** 22 | * The domain error type. 23 | */ 24 | struct error {}; 25 | 26 | /** 27 | * Convenience type for signal handlers. 28 | */ 29 | using sig_t = void (*)(int); 30 | 31 | /** 32 | * Sets up a given signal handler and restores the old handler when 33 | * going out of scope. 34 | */ 35 | class Signal { 36 | private: 37 | /** 38 | * The signal this handler is handling. 39 | */ 40 | int const sig; 41 | 42 | /** 43 | * The previous signal handler. 44 | */ 45 | sig_t const handler; 46 | 47 | public: 48 | /** 49 | * Sets up the given handler. 50 | * 51 | * @param sig 52 | * The signal to set a handler for 53 | * @param handler 54 | * The signal handling function 55 | * @throws sys::sc_error 56 | * Throws with the errno of signal() 57 | */ 58 | Signal(int const sig, sig_t const handler) : 59 | sig{sig}, handler{::signal(sig, handler)} { 60 | if (this->handler == SIG_ERR) { 61 | throw sc_error{errno}; 62 | } 63 | } 64 | 65 | /** 66 | * Restore previous signal handler. 67 | */ 68 | ~Signal() { 69 | ::signal(this->sig, this->handler); 70 | } 71 | }; 72 | 73 | } /* namespace sig */ 74 | 75 | } /* namespace sys */ 76 | 77 | #endif /* _POWERDXX_SYS_SIGNAL_HPP_ */ 78 | -------------------------------------------------------------------------------- /tools/playdiff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | # 3 | # @see README.md#playdiff 4 | # 5 | 6 | # 7 | # Print usage in case of missing input files. 8 | # 9 | BEGIN { 10 | if (ARGC < 3) { 11 | print "usage: playdiff file1 file2 ..." 12 | exit 1 13 | } 14 | } 15 | 16 | # 17 | # Record the first file data. 18 | # 19 | FNR == NR { 20 | ORIG_NAME = FILENAME 21 | ORIG_TIME = $1 22 | i = 0 23 | while (i <= NF) { 24 | ORIG_DATA[FNR, i] = $i 25 | ++i 26 | } 27 | } 28 | 29 | # 30 | # Reset the sum of absolute deviations and the sum of deviations 31 | # in the first line of a new file. 32 | # 33 | NR != FNR && FNR == 1 { 34 | print "--- " ORIG_NAME 35 | print "+++ " FILENAME 36 | i = 2 37 | while (i <= NF) { 38 | SUMABSDEV[i] = 0 39 | SUMDEV[i] = 0 40 | ++i 41 | } 42 | } 43 | 44 | # 45 | # Update the sum of deviations and absolute deviations. 46 | # 47 | NR != FNR && FNR > 1 { 48 | if ($1 != ORIG_DATA[FNR, 1]) { 49 | print "time reference mismatch, skip to next file" 50 | nextfile 51 | } 52 | dt = ORIG_DATA[FNR, 1] - ORIG_DATA[FNR - 1, 1] 53 | i = 2 54 | while (i <= NF) { 55 | $i = dt * ($i - ORIG_DATA[FNR, i]) 56 | SUMABSDEV[i] += $i < 0 ? -$i : $i 57 | SUMDEV[i] += $i 58 | ++i 59 | } 60 | } 61 | 62 | # 63 | # Output the recorded deviations for each column when the last frame 64 | # is encountered. 65 | # 66 | NR != FNR && $1 == ORIG_TIME { 67 | i = 1 68 | printf("%-20.20s %12.12s %12.12s %12.12s %12.12s\n", 69 | "", "ID", "MD", "IAD", "MAD") 70 | while (i <= NF) { 71 | printf("%-20.20s %12.1f %12.1f %12.1f %12.1f\n", 72 | ORIG_DATA[1, i], 73 | SUMDEV[i], 74 | SUMDEV[i] / ORIG_TIME, 75 | SUMABSDEV[i], 76 | SUMABSDEV[i] / ORIG_TIME) 77 | ++i 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /doxy/githublabels.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | # 3 | # Adds github compatible labels to level 1 and 2 markdown headings 4 | # in underline style. 5 | # 6 | 7 | # 8 | # Get file name prefix for references. 9 | # 10 | # The prefix mimics the doxygen file naming scheme, 11 | # e.g. `man/loadrec.1` becomse `man_1_loadrec`. 12 | # 13 | # Equivalently the prefix for `foo/bar.md` becomes the prefix `foo_md_bar_`. 14 | # The trailing `_` exists as a separator to the internal reference label. 15 | # 16 | filename != FILENAME { 17 | filename = FILENAME 18 | # strip path 19 | "pwd -P" | getline path 20 | path = path "/" 21 | prefix = substr(filename, 1, length(path)) == path \ 22 | ? substr(filename, length(path) + 1) \ 23 | : filename 24 | # put the filename suffix behind the path but in front of 25 | # the file name 26 | sufx = prefix 27 | sub(/.*\./, "", sufx) # get suffix 28 | sub(/\.[^.]*$/, "", prefix) # strip suffix from filename 29 | sub(/[^\/]*$/, sufx "_&_", prefix) # insert suffix 30 | # sanitise characters 31 | gsub(/[^_a-zA-Z0-9]+/, "_", prefix) 32 | } 33 | 34 | # 35 | # Substitute github references with doxygen references. 36 | # 37 | /\(#[_a-z0-9-]*\)/ { 38 | gsub(/\[[^]]*\]\(#/, "&" prefix) 39 | } 40 | 41 | # 42 | # If this line is underlining a heading, add a label to the previous 43 | # line. 44 | # 45 | line && (/^===*$/ || /^---*$/) { 46 | id = tolower(line) 47 | gsub(/[^- \ta-z0-9]/, "", id) # remove undesired characters 48 | gsub(/[ \t]/, "-", id) # replace white space with - 49 | line = line " {#" prefix id "}" 50 | } 51 | 52 | # 53 | # Doxygen up to at least 1.8.16 chokes on successive - in labels. 54 | # 55 | line ~ "#" prefix { 56 | cnt = split(line, a, "#" prefix) 57 | line = a[1] 58 | for (i = 2; i <= cnt; ++i) { 59 | while (a[i] ~ /^[-a-z0-9]*--[-a-z0-9]*/) { 60 | sub(/--+/, "-", a[i]) 61 | } 62 | line = line "#" prefix a[i] 63 | } 64 | } 65 | 66 | # 67 | # Print the previous line, after a label might have been added. 68 | # 69 | {print line} 70 | 71 | # 72 | # Remember this line when scanning the next one for underlining. 73 | # 74 | {line = $0} 75 | 76 | # 77 | # Print the last line. 78 | # 79 | END { print } 80 | -------------------------------------------------------------------------------- /src/constants.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines a collection of constants. 3 | * 4 | * @file 5 | */ 6 | 7 | #include "types.hpp" 8 | 9 | #ifndef _POWERDXX_CONSTANTS_HPP_ 10 | #define _POWERDXX_CONSTANTS_HPP_ 11 | 12 | /** 13 | * A collection of constants. 14 | */ 15 | namespace constants { 16 | 17 | /* 18 | * Sysctl symbolic MIB representations. 19 | */ 20 | 21 | /** 22 | * The MIB name for per-CPU time statistics. 23 | */ 24 | char const * const CP_TIMES = "kern.cp_times"; 25 | 26 | /** 27 | * The MIB name for the AC line state. 28 | */ 29 | char const * const ACLINE = "hw.acpi.acline"; 30 | 31 | /** 32 | * The MIB name for CPU frequencies. 33 | */ 34 | char const * const FREQ = "dev.cpu.%d.freq"; 35 | 36 | /** 37 | * The MIB name for CPU frequency levels. 38 | */ 39 | char const * const FREQ_LEVELS = "dev.cpu.%d.freq_levels"; 40 | 41 | /** 42 | * The MIB name for CPU temperatures. 43 | */ 44 | char const * const TEMPERATURE = "dev.cpu.%d.temperature"; 45 | 46 | /** 47 | * An array of maximum temperature sources. 48 | */ 49 | char const * const TJMAX_SOURCES[]{ 50 | "dev.cpu.%d.coretemp.tjmax" 51 | }; 52 | 53 | /** 54 | * The MIB name for the CPU frequency drivers. 55 | */ 56 | char const * const FREQ_DRIVER = "dev.cpufreq.%d.freq_driver"; 57 | 58 | /** 59 | * A list of driver prefixes, that are known not to allow manual 60 | * frequency control. 61 | */ 62 | char const * const FREQ_DRIVER_BLACKLIST[]{ 63 | "hwpstate_" 64 | }; 65 | 66 | /* 67 | * Default values. 68 | */ 69 | 70 | /** 71 | * Default maximum clock frequency value. 72 | */ 73 | types::mhz_t const FREQ_DEFAULT_MAX{1000000}; 74 | 75 | /** 76 | * Default minimum clock frequency value. 77 | */ 78 | types::mhz_t const FREQ_DEFAULT_MIN{0}; 79 | 80 | /** 81 | * Clock frequency representing an uninitialised value. 82 | */ 83 | types::mhz_t const FREQ_UNSET{1000001}; 84 | 85 | /** 86 | * The default pidfile name of powerd. 87 | */ 88 | char const * const POWERD_PIDFILE = "/var/run/powerd.pid"; 89 | 90 | /** 91 | * The load target for adaptive mode, equals 50% load. 92 | */ 93 | types::cptime_t const ADP{512}; 94 | 95 | /** 96 | * The load target for hiadaptive mode, equals 37.5% load. 97 | */ 98 | types::cptime_t const HADP{384}; 99 | 100 | /** 101 | * The default temperautre offset between high and critical temperature. 102 | */ 103 | types::decikelvin_t const HITEMP_OFFSET{100}; 104 | 105 | } /* namespace constants */ 106 | 107 | #endif /* _POWERDXX_CONSTANTS_HPP_ */ 108 | -------------------------------------------------------------------------------- /src/sys/pidfile.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements safer c++ wrappers for the pidfile_*() interface. 3 | * 4 | * Requires linking with -lutil. 5 | * 6 | * @file 7 | */ 8 | 9 | #ifndef _POWERDXX_SYS_PIDFILE_HPP_ 10 | #define _POWERDXX_SYS_PIDFILE_HPP_ 11 | 12 | #include "error.hpp" /* sys::sc_error */ 13 | 14 | #include /* pidfile_*() */ 15 | 16 | namespace sys { 17 | 18 | /** 19 | * This namespace contains safer c++ wrappers for the pidfile_*() interface. 20 | * 21 | * The class Pidfile implements the RAII pattern for holding a pidfile. 22 | */ 23 | namespace pid { 24 | 25 | /** 26 | * The domain error type. 27 | */ 28 | struct error {}; 29 | 30 | /** 31 | * A wrapper around the pidfile_* family of commands implementing the 32 | * RAII pattern. 33 | */ 34 | class Pidfile final { 35 | private: 36 | /** 37 | * In case of failure to acquire the lock, the PID of the other 38 | * process holding it is stored here. 39 | */ 40 | pid_t otherpid; 41 | 42 | /** 43 | * Pointer to the pidfile state data structure. 44 | * 45 | * Thus is allocated by pidfile_open() and assumedly freed by 46 | * pidfile_remove(). 47 | */ 48 | pidfh * pfh; 49 | 50 | public: 51 | /** 52 | * Attempts to open the pidfile. 53 | * 54 | * @param pfname,mode 55 | * Arguments to pidfile_open() 56 | * @throws pid_t 57 | * Throws the PID of the other process already holding 58 | * the requested pidfile 59 | * @throws sys::sc_error 60 | * Throws with the errno of pidfile_open() 61 | */ 62 | Pidfile(char const * const pfname, mode_t const mode) : 63 | otherpid{0}, pfh{pidfile_open(pfname, mode, &this->otherpid)} { 64 | if (this->pfh == nullptr) { 65 | switch (errno) { 66 | case EEXIST: 67 | throw this->otherpid; 68 | default: 69 | throw sc_error{errno}; 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Removes the pidfile. 76 | */ 77 | ~Pidfile() { 78 | pidfile_remove(this->pfh); 79 | } 80 | 81 | /** 82 | * Returns the PID of the other process holding the lock. 83 | */ 84 | pid_t other() { return this->otherpid; } 85 | 86 | /** 87 | * Write PID to the file, should be called after daemon(). 88 | * 89 | * @throws sys::sc_error 90 | * Throws with the errno of pidfile_write() 91 | */ 92 | void write() { 93 | if (pidfile_write(this->pfh) == -1) { 94 | throw sc_error{errno}; 95 | } 96 | } 97 | }; 98 | 99 | } /* namespace pid */ 100 | 101 | } /* namespace sys */ 102 | 103 | #endif /* _POWERDXX_SYS_PIDFILE_HPP_ */ 104 | -------------------------------------------------------------------------------- /src/utility.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements generally useful functions not intended for inlining. 3 | * 4 | * @file 5 | */ 6 | 7 | #include "utility.hpp" 8 | 9 | using namespace utility::literals; 10 | 11 | /** 12 | * File local scope. 13 | */ 14 | namespace { 15 | 16 | /** 17 | * Append a string literal to a sanitised string. 18 | * 19 | * Updates the meta data along with the string. 20 | * 21 | * @tparam SizeV 22 | * The string literal size, including the terminator 23 | * @param lhs 24 | * The sanitised string to update 25 | * @param rhs 26 | * The string literal to append 27 | */ 28 | template 29 | constexpr utility::Sanitised & 30 | operator +=(utility::Sanitised & lhs, char const (& rhs)[SizeV]) { 31 | lhs.text += rhs; 32 | lhs.width += (SizeV - 1); 33 | return lhs; 34 | } 35 | 36 | } /* namespace */ 37 | 38 | utility::Sanitised utility::sanitise(std::string_view const & str) { 39 | Sanitised result{}; 40 | auto & text = result.text; 41 | auto & width = result.width; 42 | auto const end = cend(str); 43 | for (auto it = cbegin(str); it != end; ++it) { 44 | /* utf-8 multi-byte */ 45 | if (std::size_t bytes = 0; 46 | ((*it & 0xe0) == 0xc0 && (bytes = 2)) || /* 2-byte */ 47 | ((*it & 0xf0) == 0xe0 && (bytes = 3)) || /* 3-byte */ 48 | ((*it & 0xf8) == 0xf0 && (bytes = 4))) { /* 4-byte */ 49 | text += *it; 50 | for (std::size_t i = 1; i < bytes; ++i) { 51 | if (it + 1 != end && (it[1] & 0xc0) == 0x80) { 52 | text += *(++it); 53 | } 54 | } 55 | ++width; 56 | continue; 57 | } 58 | 59 | /* printf control characters */ 60 | switch (*it) { 61 | case '\a': 62 | result += "\\a"; 63 | continue; 64 | case '\b': 65 | result += "\\b"; 66 | continue; 67 | case '\f': 68 | result += "\\f"; 69 | continue; 70 | case '\n': 71 | result += "\\n"; 72 | continue; 73 | case '\r': 74 | result += "\\r"; 75 | continue; 76 | case '\t': 77 | result += "\\t"; 78 | continue; 79 | case '\v': 80 | result += "\\v"; 81 | continue; 82 | case '\\': 83 | result += "\\\\"; 84 | continue; 85 | } 86 | /* other control characters and invalid code points */ 87 | if (*it < ' ') { 88 | auto const escaped = "\\%o"_fmt(*it & 0xff); 89 | text += escaped; 90 | width += escaped.size(); 91 | continue; 92 | } 93 | /* regular characters */ 94 | text += *it; 95 | ++width; 96 | } 97 | return result; 98 | } 99 | 100 | utility::Underlined 101 | utility::highlight(std::string_view const & str, std::size_t const offs, 102 | std::size_t const len) { 103 | /* 104 | * Sanitise the string in 3 stages to get a separate count 105 | * of visible characters for each stage. 106 | */ 107 | /* before the underlining offset */ 108 | auto [text, width] = sanitise(str.substr(0, offs)); 109 | Underlined result{std::move(text)}; 110 | result.line.insert(0, width, ' '); 111 | 112 | /* the underlined section */ 113 | result.line += '^'; 114 | if (offs < str.size()) { 115 | auto [text, width] = sanitise(str.substr(offs, len)); 116 | result.text += text; 117 | result.line.insert(result.line.size(), width - 1, '~'); 118 | } 119 | 120 | /* behind the underlined section */ 121 | if (offs + len < str.size()) { 122 | auto [text, width] = sanitise(str.substr(offs + len)); 123 | result.text += text; 124 | } 125 | 126 | return result; 127 | } 128 | -------------------------------------------------------------------------------- /src/errors.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Common error handling code. 3 | * 4 | * @file 5 | */ 6 | 7 | #include "utility.hpp" 8 | 9 | #ifndef _POWERDXX_ERRORS_HPP_ 10 | #define _POWERDXX_ERRORS_HPP_ 11 | 12 | /** 13 | * Common error handling types and functions. 14 | */ 15 | namespace errors { 16 | 17 | using namespace std::literals::string_literals; 18 | 19 | /** 20 | * Exit codes. 21 | */ 22 | enum class Exit : int { 23 | OK, /**< Regular termination */ 24 | ECLARG, /**< Unexpected command line argument */ 25 | EOUTOFRANGE, /**< A user provided value is out of range */ 26 | ELOAD, /**< The provided value is not a valid load */ 27 | EFREQ, /**< The provided value is not a valid frequency */ 28 | EMODE, /**< The provided value is not a valid mode */ 29 | EIVAL, /**< The provided value is not a valid interval */ 30 | ESAMPLES, /**< The provided value is not a valid sample count */ 31 | ESYSCTL, /**< A sysctl operation failed */ 32 | ENOFREQ, /**< System does not support changing core frequencies */ 33 | ECONFLICT, /**< Another frequency daemon instance is running */ 34 | EPID, /**< A pidfile could not be created */ 35 | EFORBIDDEN, /**< Insufficient privileges to change sysctl */ 36 | EDAEMON, /**< Unable to detach from terminal */ 37 | EWOPEN, /**< Could not open file for writing */ 38 | ESIGNAL, /**< Failed to install signal handler */ 39 | ERANGEFMT, /**< A user provided range is missing the separator */ 40 | ETEMPERATURE, /**< The provided value is not a valid temperature */ 41 | EEXCEPT, /**< Untreated exception */ 42 | EFILE, /**< Not a valid file name */ 43 | EEXEC, /**< Command execution failed */ 44 | EDRIVER, /**< Frequency driver does not allow manual control */ 45 | ESYSCTLNAME, /**< User provided sysctl contains invalid characters */ 46 | EFORMATFIELD, /**< Formatting string contains unexpected field */ 47 | LENGTH /**< Enum length */ 48 | }; 49 | 50 | /** 51 | * Printable strings for exit codes. 52 | */ 53 | const char * const ExitStr[]{ 54 | "OK", "ECLARG", "EOUTOFRANGE", "ELOAD", "EFREQ", "EMODE", "EIVAL", 55 | "ESAMPLES", "ESYSCTL", "ENOFREQ", "ECONFLICT", "EPID", "EFORBIDDEN", 56 | "EDAEMON", "EWOPEN", "ESIGNAL", "ERANGEFMT", "ETEMPERATURE", 57 | "EEXCEPT", "EFILE", "EEXEC", "EDRIVER", "ESYSCTLNAME", "EFORMATFIELD" 58 | }; 59 | 60 | static_assert(size_t{utility::to_value(Exit::LENGTH)} == utility::countof(ExitStr), 61 | "Every Exit code must have a string representation"); 62 | 63 | /** 64 | * Exceptions bundle an exit code, errno value and message. 65 | */ 66 | struct Exception { 67 | /** 68 | * The code to exit with. 69 | */ 70 | Exit exitcode; 71 | 72 | /** 73 | * The errno value at the time of creation. 74 | */ 75 | int err; 76 | 77 | /** 78 | * An error message. 79 | */ 80 | std::string msg; 81 | }; 82 | 83 | /** 84 | * Throws an Exception instance with the given message. 85 | * 86 | * @param exitcode 87 | * The exit code to return on termination 88 | * @param err 89 | * The errno value at the time the exception was created 90 | * @param msg 91 | * The message to show 92 | */ 93 | [[noreturn]] inline void 94 | fail(Exit const exitcode, int const err, std::string const & msg) { 95 | throw Exception{exitcode, err, "("s + 96 | ExitStr[utility::to_value(exitcode)] + 97 | ") " + msg}; 98 | } 99 | 100 | } /* namespace errors */ 101 | 102 | #endif /* _POWERDXX_ERRORS_HPP_ */ 103 | -------------------------------------------------------------------------------- /doxy/mantohtml.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | # 3 | # Takes a terminal formatted manual page and generates an identical 4 | # looking HTML
 block.
  5 | #
  6 | # This script only supports UTF-8 encoding.
  7 | #
  8 | 
  9 | BEGIN {
 10 | 	# The backspace character is used to combine characters
 11 | 	BS = "\x08"
 12 | 
 13 | 	# prepare requested escapes
 14 | 	n = u8chars(escape, ESCAPE)
 15 | 	for (i = 1; i <= n; ++i) {
 16 | 		SUB[escape[i]] = "\\" escape[i]
 17 | 	}
 18 | 
 19 | 	# special HTML character substitutions
 20 | 	SUB["&"] = "&"
 21 | 	SUB["<"] = "<"
 22 | 	SUB[">"] = ">"
 23 | 
 24 | 	# formatting flags
 25 | 	FMT_REGULAR    = 0
 26 | 	FMT_BOLD       = 1
 27 | 	FMT_UNDERLINE  = 2
 28 | 
 29 | 	# formatting selectors
 30 | 	SELECT["_"] = FMT_UNDERLINE
 31 | 
 32 | 	# opening tags
 33 | 	OPEN[FMT_BOLD]                 = ""
 34 | 	OPEN[FMT_UNDERLINE]            = ""
 35 | 	OPEN[FMT_BOLD + FMT_UNDERLINE] = ""
 36 | 
 37 | 	# closing tags
 38 | 	CLOSE[FMT_BOLD]                 = ""
 39 | 	CLOSE[FMT_UNDERLINE]            = ""
 40 | 	CLOSE[FMT_BOLD + FMT_UNDERLINE] = ""
 41 | 
 42 | 	print "
"
 43 | }
 44 | 
 45 | END {
 46 | 	print "
" 47 | } 48 | 49 | function u8error(msg) 50 | { 51 | printf("utf-8 error:%d:%d:%d: %s\n\n%s\n% " p "s\n", NR, p, i, msg, str, "^") > "/dev/stderr" 52 | } 53 | 54 | function u8chars(uchars, str, 55 | n, chars, p, mbyte, i) 56 | { 57 | # split characters 58 | delete chars 59 | n = split(str, chars, "") 60 | 61 | delete uchars 62 | p = 1 # index of the next unicode character 63 | mbyte = 0 # expected multi-byte character bytes 64 | for (i = 1; i <= n; ++i) { 65 | # no leading bits, 1 byte character 66 | if (mbyte) 67 | { 68 | if (chars[i] < "\x80" || chars[i] >= "\xc0") { 69 | u8error("unexected byte in multibyte character") 70 | mbyte = 0 71 | continue 72 | } 73 | uchars[p] = uchars[p] chars[i] 74 | p += !--mbyte 75 | } 76 | # regular 1 byte characters 77 | else if (chars[i] < "\x80") { 78 | uchars[p++] = chars[i] 79 | } 80 | # 2 leading bits, merge 2 bytes 81 | else if (chars[i] >= "\xc0" && chars[i] < "\xe0") { 82 | uchars[p] = chars[i] 83 | mbyte = 1 84 | } 85 | # 3 leading bits, merge 3 bytes 86 | else if (chars[i] >= "\xe0" && chars[i] < "\xf0") { 87 | uchars[p] = chars[i] 88 | mbyte = 2 89 | } 90 | # 4 byte characters 91 | else if (chars[i] >= "\xf0" && chars[i] < "\xf8") { 92 | uchars[p] = chars[i] 93 | mbyte = 3 94 | } 95 | # error 96 | else 97 | { 98 | u8error("byte is not a character") 99 | } 100 | } 101 | return p - 1 102 | } 103 | 104 | { 105 | # 106 | # merge utf-8 multibyte chars 107 | # 108 | delete uchars 109 | n = u8chars(uchars, $0) 110 | 111 | # 112 | # detect per character formatting 113 | # 114 | delete format 115 | delete ochars 116 | 117 | olen = 0 118 | for (i = 1; i <= n; ++i) { 119 | ++olen 120 | # check all formatting characters 121 | for (; uchars[i + 1] == BS; i += 2) { 122 | format[olen] = or(format[olen], SELECT[uchars[i]]) 123 | # bold is any match in the chain of characters, 124 | # so check the current one against all the 125 | # following ones 126 | for (p = i + 2; uchars[p - 1] == BS; p += 2) { 127 | format[olen] = or(format[olen], uchars[i] == uchars[p] ? FMT_BOLD : FMT_REGULAR) 128 | } 129 | } 130 | # add character 131 | ochars[olen] = uchars[i] 132 | } 133 | 134 | # 135 | # replace special characters 136 | # 137 | for (i = 1; i <= olen; ++i) { 138 | ochars[i] in SUB && ochars[i] = SUB[ochars[i]] 139 | } 140 | 141 | # 142 | # output with HTML formatting 143 | # 144 | $0 = "" 145 | 146 | last = FMT_REGULAR 147 | for (i = 1; i <= olen; ++i) { 148 | if (format[i] != last) { 149 | $0 = $0 CLOSE[last] OPEN[format[i]] 150 | } 151 | $0 = $0 ochars[i] 152 | last = format[i] 153 | } 154 | $0 = $0 CLOSE[last] 155 | 156 | print 157 | } 158 | 159 | -------------------------------------------------------------------------------- /src/sys/env.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements zero-cost abstractions for the getenv(3) facilities. 3 | * 4 | * @file 5 | */ 6 | 7 | #ifndef _POWERDXX_SYS_ENV_HPP_ 8 | #define _POWERDXX_SYS_ENV_HPP_ 9 | 10 | #include "error.hpp" /* sys::sc_error */ 11 | 12 | #include /* getenv(), setenv() etc. */ 13 | 14 | namespace sys { 15 | 16 | /** 17 | * Provides wrappers around the getenv() family of functions. 18 | */ 19 | namespace env { 20 | 21 | /** 22 | * The domain error type. 23 | */ 24 | struct error {}; 25 | 26 | /** 27 | * A reference type refering to an environment variable. 28 | * 29 | * To avoid issues with the lifetime of the name string this is not 30 | * copy constructible or assignable. 31 | */ 32 | class Var { 33 | private: 34 | /** 35 | * A pointer to the variable name. 36 | */ 37 | char const * const name; 38 | 39 | public: 40 | /** 41 | * Construct an environment variable reference. 42 | * 43 | * @tparam Size 44 | * The size of the name buffer 45 | * @param name 46 | * The name of the environment variable 47 | */ 48 | template 49 | Var(char const (& name)[Size]) : name{name} {} 50 | 51 | /** 52 | * Do not permit copy construction. 53 | */ 54 | Var(Var const &) = delete; 55 | 56 | /** 57 | * Do not permit copy assignment. 58 | */ 59 | Var & operator =(Var const &) = delete; 60 | 61 | /** 62 | * Retrieve the value of the environment variable. 63 | * 64 | * @return 65 | * A pointer to the character array with the variable value 66 | * @retval nullptr 67 | * The variable does not exist 68 | */ 69 | operator char const *() const { 70 | return getenv(this->name); 71 | } 72 | 73 | /** 74 | * Assign a new value to the environment variable. 75 | * 76 | * Deletes the variable if nullptr is assigned. 77 | * 78 | * @param assign 79 | * The new value 80 | * @return 81 | * A self-reference 82 | * @throws sc_error{EINVAL} 83 | * Invalid variable name 84 | * @throws sc_error{ENOMEM} 85 | * Failed to allocate memory when updating the environment 86 | */ 87 | Var & operator =(char const * const assign) { 88 | auto const result = 89 | assign 90 | ? setenv(this->name, assign, 1) 91 | : unsetenv(this->name); 92 | if (0 != result) { 93 | throw sc_error{errno}; 94 | } 95 | return *this; 96 | } 97 | 98 | /** 99 | * Explicitly deletes the environment variable. 100 | * 101 | * @return 102 | * A self-reference 103 | * @throws sc_error{EINVAL} 104 | * Invalid variable name 105 | * @throws sc_error{ENOMEM} 106 | * Failed to allocate memory when updating the environment 107 | */ 108 | Var & erase() { 109 | return *this = nullptr; 110 | } 111 | 112 | /** 113 | * Explicitly retrieve the value as a character array. 114 | * 115 | * @return 116 | * A pointer to the character array with the variable value 117 | * @retval nullptr 118 | * The variable does not exist 119 | */ 120 | char const * c_str() const { 121 | return *this; 122 | } 123 | }; 124 | 125 | /** 126 | * A singleton class providing access to environment variables. 127 | */ 128 | struct Vars { 129 | /** 130 | * Access environment variable by name. 131 | * 132 | * @tparam T 133 | * The name argument type 134 | * @param name 135 | * The name of the variable by reference 136 | */ 137 | template 138 | Var const operator [](T const & name) const { 139 | return {name}; 140 | } 141 | 142 | /** 143 | * Access environment variable by name. 144 | * 145 | * @tparam T 146 | * The name argument type 147 | * @param name 148 | * The name of the variable by reference 149 | */ 150 | template 151 | Var operator [](T const & name) { 152 | return {name}; 153 | } 154 | } vars; /**< Singleton providing access to environment variables. */ 155 | 156 | } /* namespace env */ 157 | 158 | } /* namespace sys */ 159 | 160 | #endif /* _POWERDXX_SYS_ENV_HPP_ */ 161 | -------------------------------------------------------------------------------- /src/Cycle.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements timing::Cycle, a cyclic sleep functor. 3 | * 4 | * @file 5 | */ 6 | 7 | #ifndef _POWERDXX_TIMING_CYCLE_HPP_ 8 | #define _POWERDXX_TIMING_CYCLE_HPP_ 9 | 10 | #include /* std::chrono::steady_clock::now() */ 11 | #include /* usleep() */ 12 | 13 | /** 14 | * Namespace for time management related functionality. 15 | */ 16 | namespace timing { 17 | 18 | /** 19 | * Implements an interruptible cyclic sleeping functor. 20 | * 21 | * Cyclic sleeping means that instead of having a fixed sleeping 22 | * time, each sleep is timed to meet a fixed wakeup time. I.e. the 23 | * waking rhythm does not drift with changing system loads. 24 | * 25 | * The canonical way to do this in C++ is like this: 26 | * 27 | * ~~~ c++ 28 | * #include 29 | * #include 30 | * 31 | * int main() { 32 | * std::chrono::milliseconds const ival{500}; 33 | * auto time = std::chrono::steady_clock::now(); 34 | * while (…something…) { 35 | * std::this_thread::sleep_until(time += ival); 36 | * …do stuff… 37 | * } 38 | * return 0; 39 | * } 40 | * ~~~ 41 | * 42 | * The issue is that you might want to install a signal handler to 43 | * guarantee stack unwinding and sleep_until() will resume its wait 44 | * after the signal handler completes. 45 | * 46 | * The Cycle class offers you an interruptible sleep: 47 | * 48 | * ~~~ c++ 49 | * #include "Cycle.hpp" 50 | * #include 51 | * …signal handlers… 52 | * 53 | * int main() { 54 | * std::chrono::milliseconds const ival{500}; 55 | * …setup some signal handlers… 56 | * timing::Cycle sleep; 57 | * while (…something… && sleep(ival)) { 58 | * …do stuff… 59 | * } 60 | * return 0; 61 | * } 62 | * ~~~ 63 | * 64 | * In the example the while loop is terminated if the `sleep()` is 65 | * interrupted by a signal. Optionally the sleep cycle can be resumed: 66 | * 67 | * ~~~ 68 | * timing::Cycle sleep; 69 | * while (…something…) { 70 | * if (!sleep(ival)) { 71 | * …interrupted… 72 | * while (!sleep()); 73 | * } 74 | * …do stuff… 75 | * } 76 | * ~~~ 77 | * 78 | * Note there was a design decision between providing a cycle time 79 | * to the constructor or providing it every cycle. The latter was 80 | * chosen so the cycle time can be adjusted. 81 | */ 82 | class Cycle { 83 | private: 84 | /** 85 | * Use steady_clock, avoid time jumps. 86 | */ 87 | using clock = std::chrono::steady_clock; 88 | 89 | /** 90 | * Shorthand for microseconds. 91 | */ 92 | using us = std::chrono::microseconds; 93 | 94 | /** 95 | * The current time clock 96 | */ 97 | std::chrono::time_point clk = clock::now(); 98 | 99 | public: 100 | /** 101 | * Completes an interrupted sleep cycle. 102 | * 103 | * I.e. if the last sleep cycle was 500 ms and the sleep 104 | * was interrupted 300 ms into the cycle, this would sleep 105 | * for the remaining 200 ms unless interrupted. 106 | * 107 | * @retval true 108 | * Sleep completed uninterrupted 109 | * @retval false 110 | * Sleep was interrupted 111 | */ 112 | bool operator ()() const { 113 | auto const remainingTime{ 114 | std::chrono::duration_cast(this->clk - clock::now()) 115 | }; 116 | auto const sleepDuration = remainingTime.count(); 117 | return sleepDuration <= 0 || 0 == usleep(sleepDuration); 118 | } 119 | 120 | /** 121 | * Sleep for the time required to complete the given cycle 122 | * time. 123 | * 124 | * I.e. if the time since the last sleep cycle was 12 ms and 125 | * the given cycleTime was 500 ms, the actual sleeping time 126 | * would be 488 ms. 127 | * 128 | * @tparam DurTraits 129 | * The traits of the duration type 130 | * @param cycleTime 131 | * The duration of the cycle to complete 132 | * @retval true 133 | * Command completed uninterrupted 134 | * @retval false 135 | * Command was interrupted 136 | */ 137 | template 138 | bool operator ()(std::chrono::duration const & cycleTime) { 139 | this->clk += cycleTime; 140 | return (*this)(); 141 | } 142 | 143 | }; 144 | 145 | } /* namespace timing */ 146 | 147 | #endif /* _POWERDXX_TIMING_CYCLE_HPP_ */ 148 | -------------------------------------------------------------------------------- /man/loadrec.1: -------------------------------------------------------------------------------- 1 | .Dd 4 February, 2019 2 | .Dt loadrec 1 3 | .Os 4 | .Sh NAME 5 | .Nm loadrec 6 | .Nd CPU load recorder 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Fl h 10 | .Nm 11 | .Op Fl v 12 | .Op Fl d Ar ival 13 | .Op Fl p Ar ival 14 | .Op Fl o Ar file 15 | .Sh DESCRIPTION 16 | The 17 | .Nm 18 | command performs a recording of the current load. The purpose is to 19 | reproduce this load to test different 20 | .Xr powerd 8 21 | and 22 | .Xr powerd++ 8 23 | configurations under identical load conditions using 24 | .Xr loadplay 1 . 25 | .Ss ARGUMENTS 26 | The following argument types can be given: 27 | .Bl -tag -width indent 28 | .It Ar ival 29 | A time interval can be given in seconds or milliseconds. 30 | .D1 Li s , Li ms 31 | An interval without a unit is treated as milliseconds. 32 | .It Ar file 33 | A file name. 34 | .El 35 | .Ss OPTIONS 36 | The following options are supported: 37 | .Bl -tag -width indent 38 | .It Fl h , -help 39 | Show usage and exit. 40 | .It Fl v , -verbose 41 | Be verbose and produce initial diagnostics on 42 | .Pa stderr . 43 | .It Fl d , -duration Ar ival 44 | The duration of the recording session, defaults to 30 seconds. 45 | .It Fl p , -poll Ar ival 46 | The polling interval to take load samples at, defaults to 25 milliseconds. 47 | .It Fl o , -output Ar file 48 | The output file to write the load to. 49 | .El 50 | .Sh USAGE NOTES 51 | To create reproducible results set a fixed CPU frequency below the 52 | threshold at which the turbo mode is activated. E.g. an 53 | .Nm Intel(R) Core(TM) i7-4500U CPU 54 | supports the following frequency settings: 55 | .Bd -literal -offset 4m 56 | > sysctl dev.cpu.0.freq_levels 57 | dev.cpu.0.freq_levels: 2401/15000 2400/15000 2300/14088 2200/13340 2000/11888 1900/11184 1800/10495 1700/9680 1500/8372 1400/7738 1300/7119 1200/6511 1100/5789 900/4643 800/4090 768/3550 58 | .Ed 59 | .Pp 60 | Supposedly the first mode, which is off by 1 MHz, invokes the turbo 61 | mode. However all modes down to 1800 MHz actually invoke the turbo 62 | mode for this model. The only way to determine this is by benchmarking 63 | the steppings to find out that there is a huge performance step between 64 | 1700 and 1800 MHz and that all the modes above 1700 MHz show the exact 65 | same performance (given similar thermal conditions). 66 | .Pp 67 | So in order to produce a usable measurement for this CPU the clock 68 | needs to be set to 1700 MHz or lower (higher is better to be able 69 | to record a wider range of loads): 70 | .Bd -literal -offset 4m 71 | # service powerd++ stop 72 | Stopping powerdxx. 73 | Waiting for PIDS: 63574. 74 | # powerd++ -M1700 75 | .Ed 76 | .Pp 77 | Run 78 | .Nm 79 | for a brief time to test it: 80 | .Bd -literal -offset 4m 81 | > loadrec -d.25s 82 | usr.app.powerdxx.loadrec.features=1 83 | hw.machine=amd64 84 | hw.model=Intel(R) Core(TM) i7-4500U CPU @ 1.80GHz 85 | hw.ncpu=4 86 | hw.acpi.acline=1 87 | dev.cpu.0.freq=768 88 | dev.cpu.0.freq_levels=2401/15000 2400/15000 2300/14088 2200/13340 2000/11888 1900/11184 1800/10495 1700/9680 1500/8372 1400/7738 1300/7119 1200/6511 1100/5789 900/4643 800/4090 768/3550 89 | 0 768 768 768 768 728001 0 278439 54957 10215972 753315 0 245117 7838 10270972 767662 0 241991 37110 10230545 772745 0 235729 10307 10258490 90 | 25 768 768 768 768 0 0 0 0 3 2 0 0 0 1 0 0 0 0 3 0 0 0 0 4 91 | 25 768 768 768 768 0 0 0 0 3 1 0 0 0 2 0 0 0 0 3 1 0 0 0 2 92 | 25 768 768 768 768 0 0 1 0 2 1 0 2 0 0 0 0 0 0 3 0 0 1 0 2 93 | 25 768 768 768 768 3 0 0 0 1 1 0 2 0 1 1 0 3 0 0 2 0 2 0 0 94 | 25 768 768 768 768 0 0 0 0 3 0 0 1 0 2 0 0 0 0 3 3 0 0 0 0 95 | 25 768 768 768 768 0 0 0 0 3 0 0 0 0 3 0 0 0 0 3 2 0 1 0 0 96 | 25 768 768 768 768 0 0 0 0 3 0 0 0 0 3 0 0 0 0 3 2 0 1 0 0 97 | 25 768 768 768 768 2 0 0 0 1 1 0 1 0 1 0 0 2 0 1 2 0 1 0 0 98 | 25 768 768 768 768 0 0 0 0 4 1 0 2 0 1 0 0 0 0 4 0 0 1 0 2 99 | 25 768 768 768 768 0 0 0 0 3 2 0 1 0 0 0 0 0 0 3 0 0 0 0 4 100 | .Ed 101 | .Pp 102 | Printing the load creates significant load itself, so for the actual 103 | measurement the output should be written to a file. Create your workload 104 | and start your measurement: 105 | .Bd -literal -offset 4m 106 | > loadrec -o video-session.load 107 | .Ed 108 | .Pp 109 | On the example setup 110 | .Nm 111 | produces a load of 0.001 (i.e. 0.1%), so its effect on the measurement is negligible. 112 | .Sh SEE ALSO 113 | .Xr cpufreq 4 , Xr loadplay 1 , Xr powerd 8 , Xr powerd++ 8 , Xr sysctl 8 114 | .Sh AUTHORS 115 | Implementation and manual by 116 | .An Dominic Fandrey Aq Mt kami@freebsd.org 117 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FLAGS= -std=${STD} -Wall -Werror -pedantic 2 | STD= c++17 3 | DBGFLAGS= -O0 -g -DEBUG 4 | PFLAGS= -fstack-protector -fsanitize=undefined -fsanitize-undefined-trap-on-error \ 5 | -fsanitize=address 6 | PROFILEFLAGS= -pg 7 | TESTBUILDS= clang++13 clang++12 clang++11 g++11 g++10 8 | 9 | PREFIX?= /usr/local 10 | DOCSDIR?= ${PREFIX}/share/doc/powerdxx 11 | 12 | BINCPPS= src/powerd++.cpp src/loadrec.cpp src/loadplay.cpp 13 | SOCPPS= src/libloadplay.cpp 14 | SRCFILES!= cd ${.CURDIR} && find src/ -type f 15 | HPPS= ${SRCFILES:M*.hpp} 16 | CPPS= ${SRCFILES:M*.cpp} 17 | TARGETS= ${BINCPPS:T:.cpp=} ${SOCPPS:T:.cpp=.so} 18 | CLEAN= *.o *.pch ${TARGETS} 19 | 20 | PKGVERSION= ${.CURDIR:T:C/[^-]*-//:M[0-9]*.[0-9]*.[0-9]*} 21 | GITVERSION.sh= git describe 2>&- || : 22 | GITRELEASE= ${GITRELEASE.sh:sh} 23 | GITCOMMITS= ${GITCOMMITS.sh:sh} 24 | GITVERSION= ${GITVERSION.sh:sh} 25 | VERSIONLIST= ${GITVERSION} ${PKGVERSION} unknown 26 | VERSION= ${VERSIONLIST:[1]} 27 | 28 | INFO= VERSION GITVERSION GITHASH PKGVERSION TARGETS \ 29 | CXX CXXFLAGS CXXVERSION UNAME_A 30 | GITHASH.sh= git log -1 --pretty=%H 2>&- || : 31 | GITHASH= ${GITHASH.sh:sh} 32 | CXXVERSION.sh= ${CXX} --version 33 | CXXVERSION= ${CXXVERSION.sh:sh} 34 | UNAME_A.sh= uname -a 35 | UNAME_A= ${UNAME_A.sh:sh} 36 | 37 | # Build 38 | all: ${TARGETS} 39 | 40 | # Create .depend 41 | .depend: ${SRCFILES} 42 | cd ${.CURDIR} && env MKDEP_CPP_OPTS="-MM -std=${STD}" mkdep ${CPPS} 43 | 44 | .if !make(.depend) 45 | TMP!= cd ${.CURDIR} && ${MAKE} .depend 46 | # Usually .depend is read implicitly after parsing the Makefile, 47 | # but it's needed before that to generate the testbuild/* targets. 48 | .include "${.CURDIR}/.depend" 49 | .endif 50 | 51 | # Building/Linking 52 | # 53 | # | Flag | Targets | Why | 54 | # |-----------|-------------------|----------------------------------------| 55 | # | -lutil | powerd++ | Required for pidfile_open() etc. | 56 | # | -lpthread | libloadplay.so | Uses std::thread | 57 | 58 | CXXFLAGS.libloadplay.o= -fPIC 59 | CXXFLAGS.libloadplay.so= -lpthread -shared 60 | CXXFLAGS.powerd++ = -lutil 61 | 62 | ${TARGETS:M*.so}: mk-binary ${.TARGET:.so=.o} 63 | ${TARGETS:N*.so}: mk-binary ${.TARGET}.o clas.o utility.o 64 | 65 | mk-binary: .USE 66 | ${CXX} ${CXXFLAGS} ${.ALLSRC} -o ${.TARGET} 67 | 68 | info:: 69 | @echo -n '${INFO:@v@${v}="${${v}}"${.newline}@}' 70 | 71 | # Combinable build targets 72 | .ifmake(debug) 73 | CXXFLAGS= ${DBGFLAGS} 74 | .endif 75 | CXXFLAGS+= ${FLAGS} 76 | .ifmake(paranoid) 77 | CXXFLAGS+= ${PFLAGS} 78 | .endif 79 | .ifmake(profile) 80 | CXXFLAGS+= ${PROFILEFLAGS} 81 | .endif 82 | 83 | debug paranoid profile: ${.TARGETS:Ndebug:Nparanoid:Nprofile:S/^$/all/W} 84 | 85 | # Final addition to CXXFLAGS, target specific flags 86 | CXXFLAGS+= ${CXXFLAGS.${.TARGET}} 87 | 88 | # Build headers to verify they are consistent 89 | headers: ${HPPS:T:=.pch} 90 | .for h in ${HPPS} 91 | ${h:T:=.pch}: ${h} 92 | ${CXX} ${CXXFLAGS} -c ${.ALLSRC} -o ${.TARGET} 93 | .endfor 94 | 95 | # Install 96 | install: ${TARGETS} 97 | 98 | install deinstall: pkg/${.TARGET:C,.*/,,}.sh pkg/files 99 | @${.CURDIR}/pkg/${.TARGET:C,.*/,,}.sh < ${.CURDIR}/pkg/files \ 100 | DESTDIR="${DESTDIR}" PREFIX="${PREFIX}" DOCSDIR="${DOCSDIR}" \ 101 | CURDIR="${.CURDIR}" OBJDIR="${.OBJDIR}" 102 | 103 | # Clean 104 | clean: 105 | rm -f ${CLEAN} 106 | 107 | # Test release build and install 108 | RTNAME= ${.CURDIR:T:S,-*,,}-${VERSION} 109 | RTMAKE= env GIT_DIR=.nogit ${MAKE} -C${RTNAME} MAKEOBJDIR= 110 | releasetest:: 111 | @rm -rf ${RTNAME} 112 | @git clone ${.CURDIR} ${RTNAME} 113 | ${RTMAKE} info 114 | ${RTMAKE} 115 | ${RTMAKE} install DESTDIR=root 116 | @rm -rf ${RTNAME} 117 | 118 | # Test-build with supported compilers 119 | # 120 | # Provides the following targets: 121 | # 122 | # | Target | Description | 123 | # |------------------------|------------------------------------------| 124 | # | testbuild | Builds the default target for all builds | 125 | # | testbuild/TARGET | Builds TARGET for all builds | 126 | # | testbuild/BUILD | Builds the default target for BUILD | 127 | # | testbuild/BUILD/TARGET | Build TARGET for BUILD | 128 | # 129 | # Valid builds are listed in ${TESTBUILDS}. 130 | # Valid targets are all targets occurring before this block. 131 | TBTARGETS:= ${.ALLTARGETS:N*/*:N$$*} 132 | 133 | # Build default target 134 | ${TESTBUILDS:S,^,testbuild/,}:: 135 | @mkdir -p "${.TARGET}" 136 | @echo [${.TARGET}]: ${MAKE} ${MAKEFLAGS:N.*} 137 | @(cd "${.CURDIR}" && \ 138 | ${MAKE} MAKEOBJDIR="${.OBJDIR}/${.TARGET}" CXX="${.TARGET:T}") 139 | @rmdir -p "${.TARGET}" 2> /dev/null ||: 140 | 141 | # Build default target for all 142 | testbuild: ${TESTBUILDS:S,^,testbuild/,} 143 | 144 | # Build specific target 145 | .for target in ${TBTARGETS} 146 | ${TESTBUILDS:S,^,testbuild/,:S,$,/${target},}:: 147 | @mkdir -p "${.TARGET:H}" 148 | @echo [${.TARGET:H}]: ${MAKE} ${.TARGET:T} ${MAKEFLAGS:N.*} 149 | @(cd "${.CURDIR}" && \ 150 | ${MAKE} MAKEOBJDIR="${.OBJDIR}/${.TARGET:H}" CXX="${.TARGET:H:T}" \ 151 | ${.TARGET:T}) 152 | @rmdir -p "${.TARGET:H}" 2> /dev/null ||: 153 | .endfor 154 | 155 | # Build specific target for all 156 | ${TBTARGETS:S,^,testbuild/,}: ${TESTBUILDS:S,^,testbuild/,:S,$,/${.TARGET:T},} 157 | 158 | # Alias all testbuild/* targets to tb/ 159 | ${.ALLTARGETS:Mtestbuild/*:S,testbuild/,tb/,}: ${.TARGET:S,tb/,testbuild/,} 160 | tb: testbuild 161 | 162 | # Documentation 163 | doc:: 164 | rm -rf ${.TARGET}/* 165 | cd "${.CURDIR}" && (cat doxy/doxygen.conf; \ 166 | echo PROJECT_NUMBER='"${VERSION}"') | \ 167 | env PREFIX="${PREFIX}" doxygen - 168 | 169 | doc/latex/refman.pdf: doc 170 | cd "${.CURDIR}" && cd "${.TARGET:H}" && ${MAKE} 171 | 172 | gh-pages: doc doc/latex/refman.pdf 173 | rm -rf ${.TARGET}/* 174 | cp -R ${.CURDIR}/doc/html/* ${.CURDIR}/doc/latex/refman.pdf ${.TARGET}/ 175 | -------------------------------------------------------------------------------- /src/loadplay.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements loadplay, a bootstrapping tool for libloadplay. 3 | * 4 | * @file 5 | */ 6 | 7 | #include "Options.hpp" 8 | 9 | #include "errors.hpp" 10 | #include "utility.hpp" 11 | 12 | #include "sys/env.hpp" 13 | #include "sys/io.hpp" 14 | 15 | #include /* execvp() */ 16 | 17 | /** 18 | * File local scope. 19 | */ 20 | namespace { 21 | 22 | using nih::Parameter; 23 | using nih::Options; 24 | 25 | using errors::Exit; 26 | using errors::Exception; 27 | using errors::fail; 28 | 29 | namespace io = sys::io; 30 | 31 | using utility::to_value; 32 | using namespace utility::literals; 33 | 34 | using namespace std::literals::string_literals; 35 | 36 | /** 37 | * An enum for command line parsing. 38 | */ 39 | enum class OE { 40 | USAGE, /**< Print help */ 41 | FILE_IN, /**< Set input file instead of stdin */ 42 | FILE_OUT, /**< Set output file instead of stdout */ 43 | CMD, /**< The command to execute */ 44 | OPT_NOOPT = CMD, /**< Obligatory */ 45 | OPT_UNKNOWN, /**< Obligatory */ 46 | OPT_DASH, /**< Obligatory */ 47 | OPT_LDASH, /**< Obligatory */ 48 | OPT_DONE /**< Obligatory */ 49 | }; 50 | 51 | /** 52 | * The short usage string. 53 | */ 54 | char const * const USAGE = "[-h] [-i file] [-o file] command [...]"; 55 | 56 | /** 57 | * Definitions of command line parameters. 58 | */ 59 | Parameter const PARAMETERS[]{ 60 | {OE::USAGE, 'h', "help", "", "Show usage and exit"}, 61 | {OE::FILE_IN, 'i', "input", "file", "Input file (load recording)"}, 62 | {OE::FILE_OUT, 'o', "output", "file", "Output file (replay stats)"}, 63 | {OE::CMD, 0 , "", "command,[...]", "The command to execute"}, 64 | }; 65 | 66 | /** 67 | * Performs very rudimentary file name argument checks. 68 | * 69 | * - Fail on empty path 70 | * - Return nullptr on '-' 71 | * 72 | * @param path 73 | * The file path to check 74 | * @return 75 | * The given path or nullptr if the given path is '-' 76 | */ 77 | char const * filename(char const * const path) { 78 | if (!path || !path[0]) { 79 | fail(Exit::EFILE, 0, "empty or missing string for filename"); 80 | } 81 | if ("-"s == path) { 82 | return nullptr; 83 | } 84 | return path; 85 | } 86 | 87 | /** 88 | * Executes the given command, substituting this process. 89 | * 90 | * This function is a wrapper around execvp(3) and does not return. 91 | * 92 | * @param file 93 | * The command to execute, looked up in PATH if no path is provided 94 | * @param argv 95 | * The command line arguments of the command 96 | * @throws errors::Exception{Exit::EEXEC} 97 | */ 98 | [[noreturn]] void execute(char const * const file, char * const argv[]) { 99 | if (!file || !file[0]) { 100 | fail(Exit::EEXEC, 0, "failed to execute empty command"); 101 | } 102 | execvp(file, argv); 103 | /* ^^ must not return */ 104 | fail(Exit::EEXEC, errno, 105 | "failed to execute %s: %s"_fmt(file, strerror(errno))); 106 | } 107 | 108 | /** 109 | * If running from an explicit path add the path to the library search path. 110 | * 111 | * This function facilitates calling `loadplay` directly from the build 112 | * directory for testing and allows it to pick up `libloadplay.so` from 113 | * the same directory. 114 | * 115 | * @param argc,argv 116 | * The command line arguments provided to loadplay 117 | * @pre argc >= 2 118 | * @warning 119 | * This function changes the contents of argv[0] 120 | */ 121 | void set_library_path(int const argc, char * const argv[]) { 122 | assert(argc >= 2); 123 | /* search argv[0] for a / from right to left */ 124 | auto argp = argv[1] - 1; 125 | while (--argp >= argv[0] && *argp != '/'); 126 | 127 | /* replace the / with a cstring terminator and set LD_LIBRARY_PATH 128 | * to the resulting path in argv[0] */ 129 | if (argp >= argv[0]) { 130 | *argp = 0; 131 | sys::env::vars["LD_LIBRARY_PATH"] = argv[0]; 132 | return; 133 | } 134 | } 135 | 136 | } /* namespace */ 137 | 138 | /** 139 | * Parse command line arguments and execute the given command. 140 | * 141 | * @param argc,argv 142 | * The command line arguments 143 | * @return 144 | * An exit code 145 | * @see Exit 146 | */ 147 | int main(int argc, char * argv[]) try { 148 | auto & env = sys::env::vars; 149 | 150 | auto getopt = Options{argc, argv, USAGE, PARAMETERS}; 151 | 152 | try { 153 | while (true) switch (getopt()) { 154 | case OE::USAGE: 155 | io::ferr.printf("%s", getopt.usage().c_str()); 156 | throw Exception{Exit::OK, 0, ""}; 157 | case OE::FILE_IN: 158 | env["LOADPLAY_IN"] = filename(getopt[1]); 159 | break; 160 | case OE::FILE_OUT: 161 | env["LOADPLAY_OUT"] = filename(getopt[1]); 162 | break; 163 | case OE::CMD: 164 | env["LD_PRELOAD"] = "libloadplay.so"; 165 | assert(getopt.offset() < argc && 166 | "only OPT_DONE may violate this constraint"); 167 | set_library_path(argc, argv); 168 | /* forward the remainder of the arguments */ 169 | execute(getopt[0], argv + getopt.offset()); 170 | break; 171 | case OE::OPT_UNKNOWN: 172 | case OE::OPT_DASH: 173 | case OE::OPT_LDASH: 174 | fail(Exit::ECLARG, 0, 175 | "unexpected command line argument: "s + getopt[0]); 176 | break; 177 | case OE::OPT_DONE: 178 | fail(Exit::ECLARG, 0, "command expected"); 179 | break; 180 | } 181 | } catch (Exception & e) { 182 | switch (getopt) { 183 | case OE::USAGE: 184 | break; 185 | case OE::FILE_IN: 186 | case OE::FILE_OUT: 187 | e.msg += "\n\n"s += getopt.show(1); 188 | break; 189 | case OE::CMD: 190 | e.msg += "\n\n"s += getopt.show(0, 0); 191 | break; 192 | case OE::OPT_UNKNOWN: 193 | case OE::OPT_DASH: 194 | case OE::OPT_LDASH: 195 | case OE::OPT_DONE: 196 | e.msg += "\n\n"s += getopt.show(0); 197 | break; 198 | } 199 | throw; 200 | } 201 | assert(!"must never be reached"); 202 | } catch (Exception & e) { 203 | if (e.msg != "") { 204 | io::ferr.printf("loadplay: %s\n", e.msg.c_str()); 205 | } 206 | return to_value(e.exitcode); 207 | } catch (...) { 208 | io::ferr.print("loadplay: untreated failure\n"); 209 | return to_value(Exit::EEXCEPT); 210 | } 211 | -------------------------------------------------------------------------------- /src/clas.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides functions to process command line arguments. 3 | * 4 | * @file 5 | */ 6 | 7 | #ifndef _POWERDXX_CLAS_HPP_ 8 | #define _POWERDXX_CLAS_HPP_ 9 | 10 | #include "types.hpp" 11 | #include "errors.hpp" 12 | #include "utility.hpp" 13 | 14 | #include /* std::string */ 15 | #include /* std::pair() */ 16 | 17 | /** 18 | * A collection of functions to process command line arguments. 19 | */ 20 | namespace clas { 21 | 22 | using namespace std::literals::string_literals; 23 | 24 | /** 25 | * Convert string to load in the range [0, 1024]. 26 | * 27 | * The given string must have the following format: 28 | * 29 | * \verbatim 30 | * load = , [ "%" ]; 31 | * \endverbatim 32 | * 33 | * The input value must be in the range [0.0, 1.0] or [0%, 100%]. 34 | * 35 | * @param str 36 | * A string encoded load 37 | * @retval [0, 1024] 38 | * The load given by str 39 | * @retval > 1024 40 | * The given string is not a load 41 | */ 42 | types::cptime_t load(char const * const str); 43 | 44 | /** 45 | * Convert string to frequency in MHz. 46 | * 47 | * The given string must have the following format: 48 | * 49 | * \verbatim 50 | * freq = , [ "hz" | "khz" | "mhz" | "ghz" | "thz" ]; 51 | * \endverbatim 52 | * 53 | * For compatibility with powerd MHz are assumed, if no unit string is given. 54 | * 55 | * The resulting frequency must be in the range [0Hz, 1THz]. 56 | * 57 | * @param str 58 | * A string encoded frequency 59 | * @return 60 | * The frequency given by str 61 | */ 62 | types::mhz_t freq(char const * const str); 63 | 64 | /** 65 | * Convert string to time interval in milliseconds. 66 | * 67 | * The given string must have the following format: 68 | * 69 | * \verbatim 70 | * ival = , [ "s" | "ms" ]; 71 | * \endverbatim 72 | * 73 | * For compatibility with powerd scalar values are assumed to represent 74 | * milliseconds. 75 | * 76 | * @param str 77 | * A string encoded time interval 78 | * @return 79 | * The interval in milliseconds 80 | */ 81 | types::ms ival(char const * const str); 82 | 83 | /** 84 | * A string encoded number of samples. 85 | * 86 | * The string is expected to contain a scalar integer. 87 | * 88 | * @param str 89 | * The string containing the number of samples 90 | * @return 91 | * The number of samples 92 | */ 93 | size_t samples(char const * const str); 94 | 95 | /** 96 | * Convert string to temperature in dK. 97 | * 98 | * The given string must have the following format: 99 | * 100 | * \verbatim 101 | * temperature = , [ "C" | "K" | "F" | "R" ]; 102 | * \endverbatim 103 | * 104 | * In absence of a unit °C is assumed. 105 | * 106 | * @param str 107 | * A string encoded temperature 108 | * @return 109 | * The temperature given by str 110 | */ 111 | types::decikelvin_t temperature(char const * const str); 112 | 113 | /** 114 | * Converts dK into °C for display purposes. 115 | * 116 | * @param val 117 | * A temperature in dK 118 | * @return 119 | * The temperature in °C 120 | */ 121 | inline int celsius(types::decikelvin_t const val) { 122 | auto const valc = val - 2731; 123 | return (valc + (valc >= 0) * 5 - (valc < 0) * 5) / 10; 124 | } 125 | 126 | /** 127 | * Takes a string encoded range of values and returns them. 128 | * 129 | * A range has the format `from:to`. 130 | * 131 | * @tparam T 132 | * The return type of the conversion function 133 | * @param func 134 | * The function that converts the values from the string 135 | * @param str 136 | * The string containing the range 137 | * @return 138 | * A pair with the `from` and `to` values 139 | */ 140 | template 141 | std::pair range(T (& func)(char const * const), char const * const str) { 142 | std::pair result; 143 | std::string first{str}; 144 | 145 | if (first == "") { 146 | /* give func() an opportunity to fail */ 147 | func(str); 148 | errors::fail(errors::Exit::ERANGEFMT, 0, "range missing"); 149 | } 150 | 151 | auto const sep = first.find(':'); 152 | if (sep == std::string::npos) { 153 | errors::fail(errors::Exit::ERANGEFMT, 0, 154 | "missing colon separator in range: "s += 155 | utility::sanitise(str)); 156 | } 157 | first.erase(sep); 158 | result.first = func(first.c_str()); 159 | result.second = func(str + sep + 1); 160 | return result; 161 | } 162 | 163 | /** 164 | * Verify that the given string only contains characters allowed in 165 | * sysctl names. 166 | * 167 | * The currently permitted characters are: `[0-9A-Za-z%._-]` 168 | * 169 | * @throws errors::Exit::ESYSCTLNAME 170 | * For empty or invalid strings 171 | * @return 172 | * The given string 173 | */ 174 | char const * sysctlname(char const * const str); 175 | 176 | /** 177 | * Sanitise user-provided formatting strings. 178 | * 179 | * Ensure that the given string contains no more than the given formatting 180 | * fields in the given order. 181 | * 182 | * This only passes plain data format fields, no flags, field width 183 | * or precision are allowed. 184 | * 185 | * @throws errors::Exit::EFORMATFIELD 186 | * For unexpected formatting fields 187 | * @param fmt 188 | * The formatting string to sanitise 189 | * @param fields 190 | * A set of characters representing a printf-style formatting 191 | * @return 192 | * The given string 193 | */ 194 | template 195 | char const * formatfields(char const * const fmt, CharTs const ... fields) { 196 | using namespace utility::literals; 197 | using utility::highlight; 198 | using errors::fail; 199 | using errors::Exit; 200 | 201 | auto it = fmt; 202 | for (char const expect : {fields ..., '%'}) { 203 | for (; it && *it; ++it) { 204 | if (it[0] == '%' && it[1] == '%') { 205 | ++it; 206 | continue; 207 | } 208 | if (it[0] == '%' && it[1] == expect) { 209 | ++it; 210 | break; 211 | } 212 | if (it[0] == '%') { 213 | auto const hl = highlight(fmt, it - fmt, 2); 214 | fail(Exit::EFORMATFIELD, 0, 215 | "unexpected formatting field: "s + 216 | "\n\t" + hl.text + "\n\t" + hl.line); 217 | } 218 | } 219 | } 220 | return fmt; 221 | } 222 | 223 | } /* namespace clas */ 224 | 225 | #endif /* _POWERDXX_CLAS_HPP_ */ 226 | -------------------------------------------------------------------------------- /tools/playfilter.movingavg.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | # 3 | # Applies a moving average. 4 | # 5 | # The user is expected to set the following parameters via `-v var=value` 6 | # command line arguments: 7 | # 8 | # | Variable | Description | 9 | # |-----------|----------------------------------------------------------| 10 | # | `COLUMNS` | A `FS` separated list of column numbers | 11 | # | `PRE` | The number of time slices to use before the sample point | 12 | # | `POST` | The number of time slices to use behind the sample point | 13 | # 14 | # Algorithm 15 | # --------- 16 | # 17 | # The moving average algorithm takes the mean over a set of buffered 18 | # values, the user provided PRE and POST values determine the amount 19 | # of samples used. 20 | # 21 | # The following global variables control the algorithm and maintain 22 | # its state: 23 | # 24 | # | Variable | Type | Description | 25 | # |------------|----------------|--------------------------------------| 26 | # | `PRE` | int | The target pre-sample buffer size | 27 | # | `POST` | int | The target post-sample buffer size | 28 | # | `SIZE` | int | The target buffer size (PRE + POST) | 29 | # | `SUM` | int[co] | The sums of buffered input columns | 30 | # | `SUM_TO` | double | Factor for conversion to fix-point | 31 | # | `SUM_FROM` | double | Factor for conversion from fix-point | 32 | # | `BUF_IN` | double[li, co] | Buffered inputs | 33 | # | `BUF_OUT` | double[li, co] | Buffered outputs | 34 | # | `BUF_SIZE` | int | The actual buffer size | 35 | # 36 | # ### Computation of the Moving Average 37 | # 38 | # The `SIZE = PRE + POST` value represents the desired buffer size. 39 | # The actual buffer size is kept in `BUF_SIZE`. At the beginning of 40 | # the file `BUF_SIZE` grows with every line read until it has reached 41 | # `SIZE`. 42 | # 43 | # For each input column an output buffer is kept, untouched columns 44 | # are put straight into the output buffer `BUF_OUT`. This buffer 45 | # is kept around for the last `BUF_SIZE` lines of processed input. 46 | # Filtered columns are first stored in the `BUF_IN` buffer. The per 47 | # column `SUM` values represent the sum of all `BUF_IN` values in 48 | # a fix-point representation. 49 | # 50 | # The mean is derived from the `SUM` for each line of output, once 51 | # `BUF_SIZE` has reached `SIZE` the oldest value in `BUF_IN` is subtracted 52 | # from the `SUM` and deleted with each additional line of input. The 53 | # fix-point representation of `SUM` ensures that the sum does not 54 | # accumulate drift through floating point rounding. 55 | # 56 | # ### Output 57 | # 58 | # How the mean is output depends on the values of `PRE` and `POST`. 59 | # If only `PRE` is set every line of input causes an immediate output 60 | # of the updated mean. If `POST` is set the output is delayed so 61 | # *future values* become part of the mean. 62 | # 63 | # If `POST` is set the remaining lines of output have to be produced 64 | # after reading the last input line. The `POST` buffer effect is 65 | # reduced with every line of output until the last line, which only 66 | # is made up of PRE buffer values. 67 | # 68 | 69 | # 70 | # Setup global constants. 71 | # 72 | BEGIN { 73 | # extract columns 74 | split(COLUMNS, columns) 75 | for (i in columns) { SUM[columns[i]] = 0 } 76 | delete columns 77 | 78 | # sampling buffer properties 79 | PRE = int(PRE) # pre sample buffer size 80 | POST = int(POST) # post sample buffer size 81 | SIZE = PRE + POST # buffer size 82 | if (!SIZE) { 83 | print "error: movingavg: PRE + POST buffer must contain at least 1 value" > "/dev/stderr" 84 | exit 1 85 | } 86 | 87 | # use 20 fix-point fraction bits to calculate the buffer sum 88 | SUM_TO = 1048576 89 | SUM_FROM = 1. / SUM_TO 90 | } 91 | 92 | # 93 | # Retrieve/Update the buffer sum for the given column. 94 | # 95 | # This function is the single point of access to the SUM buffer, it 96 | # enforces integer arithmetic with a fixed number of fraction bits 97 | # to avoid floating point drift. 98 | # 99 | # @param co 100 | # The column to retrieve/update 101 | # @param offset 102 | # The change to the sum (optional) 103 | # @param SUM 104 | # Global per column storage array 105 | # @param SUM_TO,SUM_FROM 106 | # To and from fixed point conversion factors 107 | # @return 108 | # The current buffer sum for the given column 109 | # 110 | function sum(co, offset) { 111 | return (SUM[co] += int(offset * SUM_TO + .5)) * SUM_FROM 112 | } 113 | 114 | # 115 | # Print a buffered line, and trim the output buffer. 116 | # 117 | # Overwrites the input fields. 118 | # 119 | # @param NR,POST 120 | # The current line and the output line offset 121 | # @param BUF_OUT 122 | # The output buffer to output a line from and trim 123 | # 124 | function printbuf(_li, _co) { 125 | # Fetch columns from output buffer 126 | _li = NR - POST 127 | for (_co = 1; _co <= NF; ++_co) { 128 | $_co = BUF_OUT[_li, _co] 129 | delete BUF_OUT[_li, _co] 130 | } 131 | print 132 | } 133 | 134 | # 135 | # Update the buffer size until the target is reached. 136 | # 137 | NR <= SIZE { BUF_SIZE = NR } 138 | 139 | # 140 | # Update buffers. 141 | # 142 | # - Forward unaffected columns to the output buffer 143 | # - Update BUF_IN, SUM and BUF_OUT for managed columns 144 | # - Trim obsolete input buffer 145 | # 146 | { 147 | for (co = 1; co <= NF; ++co) { 148 | if (co in SUM) { 149 | # remove buffered input moving out of the 150 | # buffering window 151 | sum(co, -BUF_IN[NR - SIZE, co]) 152 | delete BUF_IN[NR - SIZE, co] 153 | 154 | # add latest value to the input buffer 155 | BUF_IN[NR, co] = $co 156 | # add latest value to the buffer sum and 157 | # update the output buffer 158 | BUF_OUT[NR - POST, co] = sum(co, +$co) / BUF_SIZE 159 | } else { 160 | # forward column to the output buffer 161 | BUF_OUT[NR, co] = $co 162 | } 163 | } 164 | } 165 | 166 | # 167 | # For every line of input, produce a line of output, starting when 168 | # the POST buffer is full. 169 | # 170 | NR > POST { printbuf() } 171 | 172 | # 173 | # Flush the remaining post buffer. 174 | # 175 | END { 176 | for (o = 1; o <= POST; ++o) { 177 | ++NR 178 | --BUF_SIZE 179 | for (co in SUM) { 180 | # remove buffered input moving out of the 181 | # buffering window and update the output buffer 182 | BUF_OUT[NR - POST, co] = sum(co, -BUF_IN[NR - SIZE, co]) / BUF_SIZE 183 | delete BUF_IN[NR - SIZE, co] 184 | } 185 | printbuf() 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /tools/playfilter.horiz.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | # 3 | # Add a new column based on the values of the matched columns. 4 | # 5 | # The user is expected to set the following parameters via `-v var=value` 6 | # command line arguments: 7 | # 8 | # | Variable | Description | 9 | # |-----------|-----------------------------------------| 10 | # | `COLUMNS` | A `FS` separated list of column numbers | 11 | # | `ALGO` | An aggregating algorithm | 12 | # 13 | # The `ALGO` variable should be set to one of: 14 | # 15 | # | Algorithm | Function | 16 | # |-----------|-------------------------------------------| 17 | # | max | The maximum value of the selected columns | 18 | # | min | The minimum value of the selected columns | 19 | # | sum | The sum of the selected columns | 20 | # | avg | The mean over the selected columns | 21 | # 22 | 23 | # 24 | # Return the unit suffix of a column title. 25 | # 26 | # @param str 27 | # A column title 28 | # @return 29 | # The unit suffix of the given title 30 | # 31 | function getunit(str, _i) { 32 | _i = match(str, RE_UNIT) 33 | return _i ? substr(str, _i) : "" 34 | } 35 | 36 | # 37 | # Return the name in a column title without the unit suffix. 38 | # 39 | # @param str 40 | # A column title 41 | # @return 42 | # The column name without the unit suffix 43 | # 44 | function getname(str, _i) { 45 | _i = match(str, RE_UNIT) 46 | return _i ? substr(str, 1, _i - 1) : str 47 | } 48 | 49 | # 50 | # Read an indexed character from the front or back of a string. 51 | # 52 | # Characters are indexed starting from 1 for the first character 53 | # and -1 for the last character. 54 | # 55 | # @param str 56 | # The string to get a character from 57 | # @param i 58 | # The character index 59 | # @return 60 | # The character at the given index 61 | # 62 | function getc(str, i) { 63 | if (i > 0) { 64 | return substr(str, i, 1) 65 | } 66 | return substr(str, length(str) + i + 1, 1) 67 | } 68 | 69 | # 70 | # Extract a common prefix from the given strings. 71 | # 72 | # @param s0,s1 73 | # The strings to get a common prefix from 74 | # @return 75 | # All front characters the strings have in common 76 | # 77 | function getprefix(s0, s1, _res, _len, _i, _c) { 78 | # prefix cannot be longer than the smaller string 79 | _len = length(s0) < length(s1) ? length(s0) : length(s1) 80 | _res = "" 81 | # accumulate prefix as long as characters match 82 | for (_i = 1; _i <= _len && (_c = getc(s0, _i)) == getc(s1, _i); ++_i) { 83 | # append newest character 84 | _res = _res _c 85 | } 86 | return _res 87 | } 88 | 89 | # 90 | # Extract a common suffix from the given strings. 91 | # 92 | # @param s0,s1 93 | # The strings to get a common suffix from 94 | # @return 95 | # All back characters the strings have in common 96 | # 97 | function getsuffix(s0, s1, _res, _len, _i, _c) { 98 | # suffix cannot be longer than the smaller string 99 | _len = length(s0) < length(s1) ? length(s0) : length(s1) 100 | _res = "" 101 | # accumulate suffix as long as characters match 102 | for (_i = 1; _i <= _len && (_c = getc(s0, -_i)) == getc(s1, -_i); ++_i) { 103 | # prepend newest character 104 | _res = _c _res 105 | } 106 | return _res 107 | } 108 | 109 | # 110 | # Trim a prefix and suffix from a given string 111 | # 112 | # Does not verify that the prefix/suffix match the given string. 113 | # 114 | # @param str 115 | # The string to extract the middle slice from 116 | # @param prefix,suffix 117 | # The head/tail to cut off the given string 118 | # @return 119 | # The trimmed string 120 | # 121 | function getslice(str, prefix, suffix) { 122 | return substr(str, length(prefix) + 1, 123 | length(str) - length(prefix) - length(suffix)) 124 | } 125 | 126 | # 127 | # Setup global constants. 128 | # 129 | BEGIN { 130 | # extract columns 131 | split(COLUMNS, columns) 132 | for (i in columns) { COLS[columns[i]] = 0 } 133 | delete columns 134 | 135 | # regex matching the unit at the end of a column title 136 | RE_UNIT = "\\[[^[]*\\]$" 137 | } 138 | 139 | # 140 | # Create a new column. 141 | # 142 | { $++NF } 143 | 144 | # 145 | # Add a column title for the new column. 146 | # 147 | # Setup the following variables and assemble them into a new column title: 148 | # 149 | # | Variable | Description | 150 | # |----------|------------------------------------------------| 151 | # | `UNIT` | The unit suffix at the end of the column title | 152 | # | `PREFIX` | A common prefix of column titles | 153 | # | `SUFFIX` | A common suffix of column titles | 154 | # | `NAME` | A comma separated list of title fragments | 155 | # | `TITLE` | The title of the new column | 156 | # 157 | # E.g. given the columns `cpu.0.rec.load[MHz]` and `cpu.1.rec.load[MHz]` 158 | # and the algorithm `ALGO=sum` would result in: 159 | # 160 | # | Variable | Value | 161 | # |----------|--------------------------------| 162 | # | `UNIT` | `[MHz]` | 163 | # | `PREFIX` | `cpu.` | 164 | # | `SUFFIX` | `.rec.load` | 165 | # | `NAME` | `0,1` | 166 | # | `TITLE` | `sum(cpu.{0,1}.rec.load)[MHz]` | 167 | # 168 | NR == 1 { 169 | # initialise, UNIT, PREFIX and SUFFIX from the first column 170 | for (co in COLS) { 171 | UNIT = getunit($co) 172 | SUFFIX = PREFIX = getname($co) 173 | break 174 | } 175 | # aggregate common PREFIX and SUFFIX, check UNIT 176 | for (co in COLS) { 177 | unit = getunit($co) 178 | name = getname($co) 179 | PREFIX = getprefix(PREFIX, name) 180 | SUFFIX = getsuffix(SUFFIX, name) 181 | if (unit != UNIT) { 182 | print "error: h" ALGO " unit mismatch" > "/dev/stderr" 183 | print "hint: " unit " != " UNIT > "/dev/stderr" 184 | exit 1 185 | } 186 | } 187 | # assemble name slices in column order 188 | NAMES = "" 189 | for (co = 1; co < NF; ++co) { 190 | if (co in COLS) { 191 | NAMES = (NAMES ? NAMES "," : "") \ 192 | getslice($co, PREFIX, SUFFIX UNIT) 193 | } 194 | } 195 | # assign title 196 | $NF = TITLE = ALGO "(" PREFIX "{" NAMES "}" SUFFIX ")" UNIT 197 | } 198 | 199 | # 200 | # Assign the maximum of the selected columns. 201 | # 202 | NR > 1 && ALGO == "max" { 203 | max = 0 204 | for (co in COLS) { 205 | max = (max >= $co ? max : $co) 206 | } 207 | $NF = max 208 | } 209 | 210 | # 211 | # Assign the minimum of the selected columns. 212 | # 213 | NR > 1 && ALGO == "min" { 214 | for (co in COLS) { 215 | min = $co 216 | break 217 | } 218 | for (co in COLS) { 219 | min = (min <= $co ? min : $co) 220 | } 221 | $NF = min 222 | } 223 | 224 | # 225 | # Assign the sum of the selected columns. 226 | # 227 | NR > 1 && ALGO == "sum" { 228 | sum = 0 229 | for (co in COLS) { 230 | sum += $co 231 | } 232 | $NF = sum 233 | } 234 | 235 | # 236 | # Assign the mean of the selected columns. 237 | # 238 | NR > 1 && ALGO == "avg" { 239 | sum = 0 240 | cnt = 0 241 | for (co in COLS) { 242 | ++cnt 243 | sum += $co 244 | } 245 | $NF = sum / cnt 246 | } 247 | 248 | # Print the current row. 249 | 1 250 | -------------------------------------------------------------------------------- /man/loadplay.1: -------------------------------------------------------------------------------- 1 | .Dd Mar 5, 2020 2 | .Dt loadplay 1 3 | .Os 4 | .Sh NAME 5 | .Nm loadplay 6 | .Nd CPU load player 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Fl h 10 | .Nm 11 | .Op Fl i Ar file 12 | .Op Fl o Ar file 13 | .Ar command Op ... 14 | .Sh DESCRIPTION 15 | The 16 | .Nm 17 | command replays a load recording created with 18 | .Xr loadrec 1 . 19 | The 20 | .Ar command 21 | can either be 22 | .Xr powerd 8 23 | or 24 | .Xr powerd++ 8 , 25 | compatibility with other tools has not been tested. 26 | .Ss OPTIONS 27 | The following options are supported: 28 | .Bl -tag -width indent 29 | .It Fl h , -help 30 | Show usage and exit. 31 | .It Fl i , -input Ar file 32 | Read load recording from 33 | .Ar file 34 | instead of 35 | .Pa stdin . 36 | .It Fl o , -output Ar file 37 | Output statistics to 38 | .Ar file 39 | instead of 40 | .Pa stdout . 41 | .El 42 | .Sh USAGE NOTES 43 | The 44 | .Nm 45 | command injects the 46 | .Lb libloadplay.so 47 | into 48 | .Ar command . 49 | This library simulates the load from the input and outputs load statistics. 50 | .Ss OUTPUT 51 | The first line of output contains column headings, columns are separated 52 | by a single space. 53 | .Pp 54 | The Following columns are present, columns containing 55 | .Ic %d 56 | occur for each core simulated: 57 | .Bl -tag -width indent 58 | .It Ic time[s] 59 | The simulation progress in 0.001 second resolution. 60 | .It Ic cpu.%d.rec.freq[MHz] 61 | The recorded clock frequency, sampled at the end of the frame. 62 | .It Ic cpu.%d.rec.load[MHz] 63 | The recorded load in 0.1 MHz resolution. 64 | .It Ic cpu.%d.run.freq[MHz] 65 | The simulated clock frequency set by the host process, sampled at 66 | the end of the frame. 67 | .It Ic cpu.%d.run.load[MHz] 68 | The simulated load in 0.1 MHz resolution. 69 | .El 70 | .Pp 71 | .Ss SAMPLING 72 | There is one sample for each recorded line. The duration of each frame 73 | depends on the recording, which defaults to 25\ ms. 74 | At this sample rate loads are dominated by noise, so a gliding average 75 | should be applied to any load columns for further use, such as plotting. 76 | .Sh IMPLEMENTATION NOTES 77 | The injected 78 | .Pa libloadplay.so 79 | works by intercepting system function calls and substituting the host 80 | environment with the recording. To achieve this the following function 81 | calls are intercepted: 82 | .Bl -bullet 83 | .It 84 | .Xr sysctl 3 , Xr sysctlnametomib 3 , Xr sysctlbyname 3 85 | .It 86 | .Xr daemon 3 87 | .It 88 | .Xr geteuid 2 89 | .It 90 | .Xr pidfile_open 3 , Xr pidfile_write 3 , Xr pidfile_close 3 , 91 | .Xr pidfile_remove 3 , Xr pidfile_fileno 3 92 | .El 93 | .Ss INITIALISATION 94 | The 95 | .Nm sysctl 96 | family of functions is backed by a table that is initialised from 97 | the header of the load recording. If the heading is incomplete the 98 | setup routines print a message on 99 | .Pa stderr . 100 | All the following intercepted function calls will return failure, 101 | ensuring that the host process is unable to operate and terminates. 102 | .Pp 103 | Like 104 | .Xr powerd++ 8 105 | and 106 | .Xr loadrec 1 107 | .Nm 108 | is core agnostic. Meaning that any core may have a 109 | .Ic .freq 110 | and 111 | .Ic .freq_levels 112 | sysctl handle. Due to this flexibility load recordings may in part 113 | or wholly be fabricated to test artificial loads or systems and features 114 | that do not yet exist. E.g. it is possible to offer a 115 | .Ic .freq 116 | handle for each core or fabricate new 117 | .Ic .freq_levels . 118 | .Ss SIMULATION 119 | If setup succeeds a simulation thread is started that reads the remaining 120 | input lines, simulates the load and updates the 121 | .Nm kern.cp_times 122 | entry in the thread safe sysctl table. For each frame a line of output 123 | with load statistics is produced. 124 | .Pp 125 | Interaction with the host process happens solely through the sysctl 126 | table. The simulation reads the recorded loads and the current core 127 | frequencies to update 128 | .Nm kern.cp_times . 129 | The host process reads this data and adjusts the clock frequencies, 130 | which in turn affects the next frame. 131 | .Ss FINALISATION 132 | After reading the last line of input the simulation thread sends a 133 | .Nm SIGINT 134 | to the process to cause it to terminate. 135 | .Sh ENVIRONMENT 136 | .Bl -tag -width indent 137 | .It Ev LOADPLAY_IN 138 | If set the file named is used for input instead of 139 | .Pa stdin . 140 | This only affects the input of 141 | .Nm , 142 | the host process is not affected. 143 | .It Ev LOADPLAY_OUT 144 | If set the file named is used for output instead of 145 | .Pa stdout . 146 | This only affects the output of 147 | .Nm , 148 | the host process is not affected. 149 | .It Ev LD_PRELOAD 150 | Used to inject the 151 | .Lb libloadplay.so 152 | into the host process. 153 | .It Ev LD_LIBRARY_PATH 154 | Is set to the same path 155 | .Nm 156 | was called through. Remains untouched if the path does not contain a 157 | .Sq / 158 | character. 159 | .Pp 160 | I.e. calling 161 | .Dq Pa obj/loadplay 162 | will set 163 | .Dq Ev LD_LIBRARY_PATH=obj , 164 | calling 165 | .Dq Pa loadplay 166 | will not. 167 | .Pp 168 | This behaviour facilitates running test builds of 169 | .Nm 170 | and the 171 | .Lb libloadplay.so 172 | without performing an install. 173 | .El 174 | .Sh FILES 175 | .Bl -tag -width indent 176 | .It Pa %%PREFIX%%/lib/libloadplay.so 177 | A library injected into 178 | .Ar command 179 | via the 180 | .Ev LD_PRELOAD 181 | environment variable. 182 | .El 183 | .Sh EXAMPLES 184 | Play a load recording with 185 | .Nm : 186 | .Bd -literal -offset 4m 187 | > loadplay -i loads/freq_tracking.load powerd++ 188 | time[s] cpu.0.rec.freq[MHz] cpu.0.rec.load[MHz] cpu.0.run.freq[MHz] cpu.0.run.load[MHz] cpu.1.rec.freq[MHz] cpu.1.rec.load[MHz] cpu.1.run.freq[MHz] cpu.1.run.load[MHz] cpu.2.rec.freq[MHz] cpu.2.rec.load[MHz] cpu.2.run.freq[MHz] cpu.2.run.load[MHz] cpu.3.rec.freq[MHz] cpu.3.rec.load[MHz] cpu.3.run.freq[MHz] cpu.3.run.load[MHz] 189 | 0.025 1700 1700.0 1700 1700.0 1700 0.0 1700 0.0 1700 1700.0 1700 1700.0 1700 850.0 1700 850.0 190 | 0.050 1700 1700.0 1700 1700.0 1700 1700.0 1700 1700.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 191 | 0.075 1700 566.7 1700 566.6 1700 1700.0 1700 1700.0 1700 0.0 1700 0.0 1700 566.7 1700 566.6 192 | 0.100 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 193 | 0.125 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 194 | 0.150 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 195 | 0.175 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 196 | 0.200 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 197 | 0.225 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 198 | 0.250 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 199 | 0.275 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 1700 0.0 200 | .Ed 201 | .Pp 202 | Capture load and 203 | .Nm 204 | output simultaneously into two different files: 205 | .Bd -literal -offset 4m 206 | > loadplay -i loads/freq_tracking.load -o load.csv powerd++ -f > load.out 207 | .Ed 208 | .Pp 209 | Capture and display 210 | .Nm 211 | output: 212 | .Bd -literal -offset 4m 213 | > loadplay -i loads/freq_tracking.load -o load.csv powerd++ -f | tee load.out 214 | power: online, load: 527 MHz, cpu0.freq: 1700 MHz, wanted: 1405 MHz 215 | power: online, load: 459 MHz, cpu0.freq: 1400 MHz, wanted: 1224 MHz 216 | power: online, load: 502 MHz, cpu0.freq: 1200 MHz, wanted: 1338 MHz 217 | power: online, load: 548 MHz, cpu0.freq: 1300 MHz, wanted: 1461 MHz 218 | power: online, load: 704 MHz, cpu0.freq: 1500 MHz, wanted: 1877 MHz 219 | power: online, load: 750 MHz, cpu0.freq: 1900 MHz, wanted: 2000 MHz 220 | power: online, load: 805 MHz, cpu0.freq: 2000 MHz, wanted: 2146 MHz 221 | power: online, load: 772 MHz, cpu0.freq: 2200 MHz, wanted: 2058 MHz 222 | power: online, load: 574 MHz, cpu0.freq: 2000 MHz, wanted: 1530 MHz 223 | power: online, load: 515 MHz, cpu0.freq: 1500 MHz, wanted: 1373 MHz 224 | .Ed 225 | .Sh SEE ALSO 226 | .Xr loadrec 1 , Xr powerd 8 , Xr powerd++ 8 , Xr rtld 1 , Xr signal 3 , 227 | .Xr tee 1 228 | .Sh AUTHORS 229 | Implementation and manual by 230 | .An Dominic Fandrey Aq Mt kami@freebsd.org 231 | -------------------------------------------------------------------------------- /src/clas.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements functions to process command line arguments. 3 | * 4 | * @file 5 | */ 6 | 7 | #include "clas.hpp" 8 | 9 | #include /* strtod(), strtol() */ 10 | 11 | #include /* std::tolower() */ 12 | 13 | /** 14 | * File local scope. 15 | */ 16 | namespace { 17 | /** 18 | * Command line argument units. 19 | * 20 | * These units are supported for command line arguments, for SCALAR 21 | * arguments the behaviour of powerd is to be imitated. 22 | */ 23 | enum class Unit : size_t { 24 | SCALAR, /**< Values without a unit */ 25 | PERCENT, /**< % */ 26 | SECOND, /**< s */ 27 | MILLISECOND, /**< ms */ 28 | HZ, /**< hz */ 29 | KHZ, /**< khz */ 30 | MHZ, /**< mhz */ 31 | GHZ, /**< ghz */ 32 | THZ, /**< thz */ 33 | CELSIUS, /**< C */ 34 | KELVIN, /**< K */ 35 | FAHRENHEIT, /**< F */ 36 | RANKINE, /**< R */ 37 | UNKNOWN /**< Unknown unit */ 38 | }; 39 | 40 | /** 41 | * The unit strings on the command line, for the respective Unit instances. 42 | */ 43 | char const * const UnitStr[]{ 44 | "", "%", "s", "ms", "hz", "khz", "mhz", "ghz", "thz", "C", "K", "F", "R" 45 | }; 46 | 47 | /** 48 | * Determine the unit of a string encoded value. 49 | */ 50 | struct Value { 51 | /** 52 | * The magnitude of the value. 53 | */ 54 | double value; 55 | 56 | /** 57 | * The unit of the value. 58 | */ 59 | Unit unit; 60 | 61 | /** 62 | * Implicitly cast to the magnitude. 63 | * 64 | * @return 65 | * The magnitude of the value 66 | */ 67 | operator double() const { return this->value; } 68 | 69 | /** 70 | * Implicitly cast to the unit. 71 | * 72 | * @return 73 | * The unit of the value 74 | */ 75 | operator Unit() const { return this->unit; } 76 | 77 | /** 78 | * Add offset to the magnitude. 79 | * 80 | * @param off 81 | * The offset value 82 | * @return 83 | * A self reference 84 | */ 85 | Value & operator +=(double const off) { 86 | return this->value += off, *this; 87 | } 88 | 89 | /** 90 | * Subtract offset from the magnitude. 91 | * 92 | * @param off 93 | * The offset value 94 | * @return 95 | * A self reference 96 | */ 97 | Value & operator -=(double const off) { 98 | return this->value -= off, *this; 99 | } 100 | 101 | /** 102 | * Scale magnitude by the given factor. 103 | * 104 | * @param fact 105 | * The factor to scale the magnitude by 106 | * @return 107 | * A self reference 108 | */ 109 | Value & operator *=(double const fact) { 110 | return this->value *= fact, *this; 111 | } 112 | 113 | /** 114 | * Divide the magnitude by the given divisor. 115 | * 116 | * @param div 117 | * The divisor to divide the magnitude by 118 | * @return 119 | * A self reference 120 | */ 121 | Value & operator /=(double const div) { 122 | return this->value /= div, *this; 123 | } 124 | 125 | /** 126 | * Construct value from a null terminated character array. 127 | * 128 | * @param valp 129 | * A pointer to the value portion of the array 130 | * @param unitp 131 | * Set by the constructor to point behind the magnitude 132 | */ 133 | Value(char const * const valp, char * unitp = nullptr) : 134 | value{std::strtod(valp, &unitp)}, unit{Unit::UNKNOWN} { 135 | for (size_t unit = 0; 136 | unitp && unit < utility::countof(UnitStr); ++unit) { 137 | auto unitstr = UnitStr[unit]; 138 | for (size_t i = 0; 139 | std::tolower(unitp[i]) == std::tolower(unitstr[i]); 140 | ++i) { 141 | if (!unitstr[i]) { 142 | this->unit = static_cast(unit); 143 | return; 144 | } 145 | } 146 | } 147 | } 148 | 149 | }; 150 | 151 | } /* namespace */ 152 | 153 | types::cptime_t clas::load(char const * const str) { 154 | if (!str || !*str) { 155 | errors::fail(errors::Exit::ELOAD, 0, 156 | "load target value missing"); 157 | } 158 | 159 | auto value = Value{str}; 160 | switch (value) { 161 | case Unit::SCALAR: 162 | if (value > 1. || value < 0) { 163 | errors::fail(errors::Exit::EOUTOFRANGE, 0, 164 | "load targets must be in the range [0.0, 1.0]"); 165 | } 166 | /* convert load to [0, 1024] range */ 167 | value *= 1024; 168 | return value < 1 ? 1 : value; 169 | case Unit::PERCENT: 170 | if (value > 100. || value < 0) { 171 | errors::fail(errors::Exit::EOUTOFRANGE, 0, 172 | "load targets must be in the range [0%, 100%]"); 173 | } 174 | /* convert load to [0, 1024] range */ 175 | value *= 10.24; 176 | return value < 1 ? 1 : value; 177 | default: 178 | break; 179 | } 180 | errors::fail(errors::Exit::ELOAD, 0, "load target not recognised"); 181 | } 182 | 183 | types::mhz_t clas::freq(char const * const str) { 184 | if (!str || !*str) { 185 | errors::fail(errors::Exit::EFREQ, 0, 186 | "frequency value missing"); 187 | } 188 | 189 | auto value = Value{str}; 190 | switch (value) { 191 | case Unit::HZ: 192 | value /= 1000000.; 193 | break; 194 | case Unit::KHZ: 195 | value /= 1000.; 196 | break; 197 | case Unit::SCALAR: /* for compatibilty with powerd */ 198 | case Unit::MHZ: 199 | break; 200 | case Unit::GHZ: 201 | value *= 1000.; 202 | break; 203 | case Unit::THZ: 204 | value *= 1000000.; 205 | break; 206 | default: 207 | errors::fail(errors::Exit::EFREQ, 0, 208 | "frequency value not recognised"); 209 | } 210 | if (value > 1000000. || value < 0) { 211 | errors::fail(errors::Exit::EOUTOFRANGE, 0, 212 | "target frequency must be in the range [0Hz, 1THz]"); 213 | } 214 | return types::mhz_t(value); 215 | } 216 | 217 | types::ms clas::ival(char const * const str) { 218 | if (!str || !*str) { 219 | errors::fail(errors::Exit::EIVAL, 0, "interval value missing"); 220 | } 221 | 222 | auto value = Value{str}; 223 | if (value < 0) { 224 | errors::fail(errors::Exit::EOUTOFRANGE, 0, 225 | "interval must be positive"); 226 | } 227 | switch (value) { 228 | case Unit::SECOND: 229 | return types::ms{static_cast(value * 1000.)}; 230 | case Unit::SCALAR: /* for powerd compatibility */ 231 | case Unit::MILLISECOND: 232 | return types::ms{static_cast(value)}; 233 | default: 234 | break; 235 | } 236 | errors::fail(errors::Exit::EIVAL, 0, 237 | "interval not recognised"); 238 | } 239 | 240 | size_t clas::samples(char const * const str) { 241 | if (!str || !*str) { 242 | errors::fail(errors::Exit::ESAMPLES, 0, 243 | "sample count value missing"); 244 | } 245 | 246 | auto value = Value{str}; 247 | if (value != Unit::SCALAR) { 248 | errors::fail(errors::Exit::ESAMPLES, 0, 249 | "sample count must be a scalar integer"); 250 | } 251 | if (value != static_cast(value)) { 252 | errors::fail(errors::Exit::EOUTOFRANGE, 0, 253 | "sample count must be an integer"); 254 | } 255 | if (value < 1 || value > 1000) { 256 | errors::fail(errors::Exit::EOUTOFRANGE, 0, 257 | "sample count must be in the range [1, 1000]"); 258 | } 259 | return value; 260 | } 261 | 262 | types::decikelvin_t clas::temperature(char const * const str) { 263 | if (!str || !*str) { 264 | errors::fail(errors::Exit::ETEMPERATURE, 0, 265 | "temperature value missing"); 266 | } 267 | 268 | auto value = Value{str}; 269 | switch (value) { 270 | case Unit::SCALAR: 271 | case Unit::CELSIUS: 272 | value += 273.15; 273 | case Unit::KELVIN: 274 | break; 275 | case Unit::FAHRENHEIT: 276 | value += 459.67; 277 | case Unit::RANKINE: 278 | value *= 5. / 9.; 279 | break; 280 | default: 281 | errors::fail(errors::Exit::ETEMPERATURE, 0, 282 | "temperature value not recognised"); 283 | } 284 | if (value < 0) { 285 | errors::fail(errors::Exit::EOUTOFRANGE, 0, 286 | "temperature must be above absolute zero (-273.15 C): "s + str); 287 | } 288 | return value *= 10; 289 | } 290 | 291 | char const * clas::sysctlname(char const * const str) { 292 | using namespace utility::literals; 293 | using utility::highlight; 294 | using errors::fail; 295 | using errors::Exit; 296 | 297 | if (!str || !*str) { 298 | fail(Exit::ESYSCTLNAME, 0, "sysctl name missing"); 299 | } 300 | 301 | for (auto it = str; it && *it; ++it) { 302 | /* pass permitted characters */ 303 | if ((*it >= '0' && *it <= '9') || 304 | (*it >= 'A' && *it <= 'Z') || 305 | (*it >= 'a' && *it <= 'z') || 306 | *it == '.' || 307 | *it == '_' || 308 | *it == '-' || 309 | *it == '%') { 310 | continue; 311 | } 312 | auto const hl = highlight(str, it - str); 313 | /* utf-8 multy-byte fragments */ 314 | if ((*it & 0xc0) == 0x80) { 315 | fail(Exit::ESYSCTLNAME, 0, 316 | "multi-byte (utf-8) character fragment embedded in sysctl name:"s + 317 | "\n\t" + hl.text + "\n\t" + hl.line); 318 | } 319 | /* utf-8 multi-byte heads */ 320 | if ((*it & 0xe0) == 0xc0 || /* 2-byte */ 321 | (*it & 0xf0) == 0xe0 || /* 3-byte */ 322 | (*it & 0xf8) == 0xf0) { /* 4-byte */ 323 | fail(Exit::ESYSCTLNAME, 0, 324 | "multi-byte (utf-8) character embedded in sysctl name:"s + 325 | "\n\t" + hl.text + "\n\t" + hl.line); 326 | } 327 | /* non-ascii and non-utf-8 code points */ 328 | if (*it < 0) { 329 | fail(Exit::ESYSCTLNAME, 0, 330 | "invalid code point embedded in sysctl name:"s + 331 | "\n\t" + hl.text + "\n\t" + hl.line); 332 | } 333 | /* ascii control characters */ 334 | if (*it < ' ' || *it == 0x7f) { 335 | fail(Exit::ESYSCTLNAME, 0, 336 | "control character embedded in sysctl name:"s + 337 | "\n\t" + hl.text + "\n\t" + hl.line); 338 | } 339 | /* regular forbidden character */ 340 | fail(Exit::ESYSCTLNAME, 0, 341 | "forbidden character in sysctl name:"s + 342 | "\n\t" + hl.text + "\n\t" + hl.line); 343 | } 344 | return str; 345 | } 346 | -------------------------------------------------------------------------------- /src/loadrec.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements a load recorder, useful for simulating loads to test 3 | * CPU clock daemons and settings. 4 | * 5 | * @file 6 | */ 7 | 8 | #include "Options.hpp" 9 | 10 | #include "types.hpp" 11 | #include "constants.hpp" 12 | #include "errors.hpp" 13 | #include "utility.hpp" 14 | #include "clas.hpp" 15 | #include "version.hpp" 16 | 17 | #include "sys/io.hpp" 18 | #include "sys/sysctl.hpp" 19 | 20 | #include /* std::chrono::steady_clock::now() */ 21 | #include /* std::this_thread::sleep_until() */ 22 | #include /* std::unique_ptr */ 23 | 24 | #include /* CPUSTATES */ 25 | 26 | /** 27 | * File local scope. 28 | */ 29 | namespace { 30 | 31 | using nih::Parameter; 32 | using nih::Options; 33 | 34 | using constants::ACLINE; 35 | using constants::FREQ; 36 | using constants::FREQ_LEVELS; 37 | using constants::FREQ_DRIVER; 38 | using constants::CP_TIMES; 39 | 40 | using types::ms; 41 | using types::coreid_t; 42 | using types::cptime_t; 43 | using types::mhz_t; 44 | 45 | using errors::Exit; 46 | using errors::Exception; 47 | using errors::fail; 48 | 49 | using utility::to_value; 50 | using utility::sprintf_safe; 51 | using namespace std::literals::string_literals; 52 | 53 | using clas::ival; 54 | 55 | using sys::ctl::Sysctl; 56 | using sys::ctl::Once; 57 | using sys::ctl::SysctlSync; 58 | using sys::ctl::SysctlOnce; 59 | 60 | namespace io = sys::io; 61 | 62 | /** 63 | * Output file type alias. 64 | * 65 | * @tparam Ownership 66 | * The io::ownership type of the file 67 | */ 68 | template using ofile = io::file; 69 | 70 | using version::LOADREC_FEATURES; 71 | using version::flag_t; 72 | using namespace version::literals; 73 | 74 | /** 75 | * The set of supported features. 76 | * 77 | * This value is stored in load recordings to allow loadplay to correctly 78 | * interpret the data. 79 | */ 80 | constexpr flag_t const FEATURES{ 81 | 1_FREQ_TRACKING 82 | }; 83 | 84 | /** 85 | * The global state. 86 | */ 87 | struct { 88 | bool verbose{false}; /**< Verbosity flag. */ 89 | ms duration{30000}; /**< Recording duration in ms. */ 90 | ms interval{25}; /**< Recording sample interval in ms. */ 91 | 92 | /** 93 | * The output stream either io::fout (stdout) or a file. 94 | */ 95 | ofile fout = io::fout; 96 | 97 | /** 98 | * The user provided output file name. 99 | */ 100 | char const * outfilename{nullptr}; 101 | 102 | /** 103 | * The number of CPU cores/threads. 104 | */ 105 | SysctlOnce const ncpu{1U, {CTL_HW, HW_NCPU}}; 106 | } g; 107 | 108 | /** 109 | * An enum for command line parsing. 110 | */ 111 | enum class OE { 112 | USAGE, /**< Print help */ 113 | IVAL_DURATION, /**< Set the duration of the recording */ 114 | IVAL_POLL, /**< Set polling interval */ 115 | FILE_OUTPUT, /**< Set output file */ 116 | FILE_PID, /**< Set PID file */ 117 | FLAG_VERBOSE, /**< Verbose output on stderr */ 118 | OPT_UNKNOWN, /**< Obligatory */ 119 | OPT_NOOPT, /**< Obligatory */ 120 | OPT_DASH, /**< Obligatory */ 121 | OPT_LDASH, /**< Obligatory */ 122 | OPT_DONE /**< Obligatory */ 123 | }; 124 | 125 | /** 126 | * The short usage string. 127 | */ 128 | char const * const USAGE = "[-hv] [-d ival] [-p ival] [-o file]"; 129 | 130 | /** 131 | * Definitions of command line parameters. 132 | */ 133 | Parameter const PARAMETERS[]{ 134 | {OE::USAGE, 'h', "help", "", "Show usage and exit"}, 135 | {OE::FLAG_VERBOSE, 'v', "verbose", "", "Be verbose"}, 136 | {OE::IVAL_DURATION, 'd', "duration", "ival", "The duration of the recording"}, 137 | {OE::IVAL_POLL, 'p', "poll", "ival", "The polling interval"}, 138 | {OE::FILE_OUTPUT, 'o', "output", "file", "Output to file"}, 139 | {OE::FILE_PID, 'P', "pid", "file", "Ignored"}, 140 | }; 141 | 142 | /** 143 | * Outputs the given printf style message on stderr if g.verbose is set. 144 | * 145 | * @tparam MsgTs 146 | * The message argument types 147 | * @param msg 148 | * The message to output 149 | */ 150 | template 151 | inline void verbose(MsgTs &&... msg) { 152 | if (g.verbose) { 153 | io::ferr.print("loadrec: "); 154 | io::ferr.printf(std::forward(msg)...); 155 | } 156 | } 157 | 158 | /** 159 | * Set up output to the given file. 160 | */ 161 | void init() { 162 | if (g.outfilename) { 163 | static ofile outfile{g.outfilename, "wb"}; 164 | if (!outfile) { 165 | fail(Exit::EWOPEN, errno, 166 | "could not open file for writing: "s + g.outfilename); 167 | } 168 | g.fout = outfile; 169 | } 170 | } 171 | 172 | /** 173 | * Parse command line arguments. 174 | * 175 | * @param argc,argv 176 | * The command line arguments 177 | */ 178 | void read_args(int const argc, char const * const argv[]) { 179 | auto getopt = Options{argc, argv, USAGE, PARAMETERS}; 180 | 181 | try { 182 | while (true) switch (getopt()) { 183 | case OE::USAGE: 184 | io::ferr.printf("%s", getopt.usage().c_str()); 185 | throw Exception{Exit::OK, 0, ""}; 186 | case OE::FLAG_VERBOSE: 187 | g.verbose = true; 188 | break; 189 | case OE::IVAL_DURATION: 190 | g.duration = ival(getopt[1]); 191 | break; 192 | case OE::IVAL_POLL: 193 | g.interval = ival(getopt[1]); 194 | break; 195 | case OE::FILE_OUTPUT: 196 | g.outfilename = getopt[1]; 197 | break; 198 | case OE::FILE_PID: 199 | break; 200 | case OE::OPT_UNKNOWN: 201 | case OE::OPT_NOOPT: 202 | case OE::OPT_DASH: 203 | case OE::OPT_LDASH: 204 | fail(Exit::ECLARG, 0, 205 | "unexpected command line argument: "s + getopt[0]); 206 | case OE::OPT_DONE: 207 | return; 208 | } 209 | } catch (Exception & e) { 210 | switch (getopt) { 211 | case OE::USAGE: 212 | break; 213 | case OE::FLAG_VERBOSE: 214 | e.msg += "\n\n"; 215 | e.msg += getopt.show(0); 216 | break; 217 | case OE::IVAL_DURATION: 218 | case OE::IVAL_POLL: 219 | case OE::FILE_OUTPUT: 220 | case OE::FILE_PID: 221 | e.msg += "\n\n"; 222 | e.msg += getopt.show(1); 223 | break; 224 | case OE::OPT_UNKNOWN: 225 | case OE::OPT_NOOPT: 226 | case OE::OPT_DASH: 227 | case OE::OPT_LDASH: 228 | e.msg += "\n\n"; 229 | e.msg += getopt.show(0); 230 | break; 231 | case OE::OPT_DONE: 232 | return; 233 | } 234 | throw; 235 | } 236 | } 237 | 238 | /** 239 | * Print the sysctls 240 | */ 241 | void print_sysctls() { 242 | Sysctl hw_acpi_acline; 243 | try { 244 | hw_acpi_acline = {ACLINE}; 245 | } catch (sys::sc_error) { 246 | verbose("cannot read %s\n", ACLINE); 247 | } 248 | g.fout.printf("%s=%ld\n" 249 | "hw.machine=%s\n" 250 | "hw.model=%s\n" 251 | "hw.ncpu=%d\n" 252 | "%s=%d\n", 253 | LOADREC_FEATURES, FEATURES, 254 | Sysctl{CTL_HW, HW_MACHINE}.get().get(), 255 | Sysctl{CTL_HW, HW_MODEL}.get().get(), 256 | g.ncpu, 257 | ACLINE, Once{1U, hw_acpi_acline}); 258 | 259 | for (coreid_t i = 0; i < g.ncpu; ++i) { 260 | char mibname[40]; 261 | sprintf_safe(mibname, FREQ, i); 262 | try { 263 | Sysctl ctl{mibname}; 264 | g.fout.printf("%s=%d\n", mibname, Once{0, ctl}); 265 | } catch (sys::sc_error e) { 266 | verbose("cannot access sysctl: %s\n", mibname); 267 | if (i == 0) { 268 | fail(Exit::ENOFREQ, e, 269 | "at least the first CPU core must report its clock frequency"); 270 | } 271 | continue; 272 | } 273 | for (auto const mibbasename : {FREQ_LEVELS, FREQ_DRIVER}) { 274 | sprintf_safe(mibname, mibbasename, i); 275 | try { 276 | Sysctl ctl{mibname}; 277 | g.fout.printf("%s=%s\n", mibname, ctl.get().get()); 278 | } catch (sys::sc_error) { 279 | verbose("cannot access sysctl: %s\n", mibname); 280 | } 281 | } 282 | } 283 | } 284 | 285 | /** 286 | * Report the load frames. 287 | * 288 | * This prints the time in ms since the last frame and the cp_times 289 | * growth as a space separated list. 290 | */ 291 | void run() try { 292 | /* 293 | * Setup cptimes buffer for two samples. 294 | */ 295 | Sysctl const cp_times_ctl{CP_TIMES}; 296 | 297 | auto const columns = cp_times_ctl.size() / sizeof(cptime_t); 298 | auto cp_times = std::unique_ptr( 299 | new cptime_t[2 * columns]{}); 300 | 301 | /* 302 | * Setup clock frequency sources for each core. 303 | */ 304 | coreid_t const cores = columns / CPUSTATES; 305 | auto corefreqs = std::unique_ptr[]>( 306 | new SysctlSync[cores]{}); 307 | 308 | for (coreid_t i = 0; i < cores; ++i) { 309 | char mibname[40]; 310 | sprintf_safe(mibname, FREQ, i); 311 | try { 312 | corefreqs[i] = Sysctl{mibname}; 313 | } catch (sys::sc_error e) { 314 | if (i == 0) { 315 | fail(Exit::ENOFREQ, e, 316 | "at least the first CPU core must report its clock frequency"); 317 | } 318 | /* Fall back to previous clock provider. */ 319 | corefreqs[i] = corefreqs[i - 1]; 320 | } 321 | } 322 | 323 | /* 324 | * Record freq and cptimes. 325 | */ 326 | auto time = std::chrono::steady_clock::now(); 327 | auto last = time; 328 | auto const stop = time + g.duration; 329 | size_t sample = 0; 330 | /* Takes a sample and prints it, avoids duplicating code 331 | * behind the loop. */ 332 | auto const takeAndPrintSample = [&]() { 333 | cp_times_ctl.get(&cp_times[sample * columns], 334 | sizeof(cptime_t) * columns); 335 | g.fout.printf("%lld", std::chrono::duration_cast(time - last).count()); 336 | for (coreid_t i = 0; i < cores; ++i) { 337 | g.fout.printf(" %u", static_cast(corefreqs[i])); 338 | } 339 | for (size_t i = 0; i < columns; ++i) { 340 | g.fout.printf(" %lu", cp_times[sample * columns + i] - 341 | cp_times[((sample + 1) % 2) * columns + i]); 342 | } 343 | g.fout.putc('\n'); 344 | }; 345 | while (time < stop) { 346 | takeAndPrintSample(); 347 | sample = (sample + 1) % 2; 348 | last = time; 349 | std::this_thread::sleep_until(time += g.interval); 350 | } 351 | takeAndPrintSample(); 352 | g.fout.flush(); 353 | } catch (sys::sc_error e) { 354 | fail(Exit::ESYSCTL, e, "failed to access sysctl: "s + CP_TIMES); 355 | } 356 | 357 | } /* namespace */ 358 | 359 | 360 | /** 361 | * Main routine, setup and execute daemon, print errors. 362 | * 363 | * @param argc,argv 364 | * The command line arguments 365 | * @return 366 | * An exit code 367 | * @see Exit 368 | */ 369 | int main(int argc, char * argv[]) try { 370 | read_args(argc, argv); 371 | init(); 372 | print_sysctls(); 373 | run(); 374 | return to_value(Exit::OK); 375 | } catch (Exception & e) { 376 | if (e.msg != "") { 377 | io::ferr.printf("loadrec: %s\n", e.msg.c_str()); 378 | } 379 | return to_value(e.exitcode); 380 | } catch (sys::sc_error e) { 381 | io::ferr.printf("loadrec: untreated sysctl failure: %s\n", e.c_str()); 382 | return to_value(Exit::EEXCEPT); 383 | } catch (...) { 384 | io::ferr.print("loadrec: untreated failure\n"); 385 | return to_value(Exit::EEXCEPT); 386 | } 387 | -------------------------------------------------------------------------------- /man/powerd++.8: -------------------------------------------------------------------------------- 1 | .Dd Mar 3, 2020 2 | .Dt powerd++ 8 3 | .Os 4 | .Sh NAME 5 | .Nm powerd++ 6 | .Nd CPU clock speed daemon 7 | .Sh SYNOPSIS 8 | .Nm 9 | .Fl h 10 | .Nm 11 | .Op Fl vfN 12 | .Op Fl a Ar mode 13 | .Op Fl b Ar mode 14 | .Op Fl n Ar mode 15 | .Op Fl m Ar freq 16 | .Op Fl M Ar freq 17 | .Op Fl F Ar freq:freq 18 | .Op Fl A Ar freq:freq 19 | .Op Fl B Ar freq:freq 20 | .Op Fl H Ar temp:temp 21 | .Op Fl t Ar sysctl 22 | .Op Fl p Ar ival 23 | .Op Fl s Ar cnt 24 | .Op Fl P Ar file 25 | .Sh DESCRIPTION 26 | The 27 | .Nm 28 | daemon monitors the system load and adjusts the CPU clock speed accordingly. 29 | It is a drop-in replacement for 30 | .Xr powerd 8 31 | and supports two modes of operation, a load feedback control loop or fixed 32 | frequency operation. 33 | .Ss ARGUMENTS 34 | The following argument types can be given: 35 | .Bl -tag -width indent 36 | .It Ar mode 37 | The mode is either a 38 | .Ar load 39 | target or a fixed 40 | .Ar freq . 41 | The 42 | .Xr powerd 8 43 | modes are interpreted as follows: 44 | .Bl -tag -nested -width indent -compact 45 | .It Li maximum , Li max 46 | Use the highest clock frequency. 47 | .It Li minimum , Li min 48 | Use the lowest clock frequency. 49 | .It Li adaptive , Li adp 50 | A target load of 0.5 (50%). 51 | .It Li hiadaptive , Li hadp 52 | A target load of 0.375 (37.5%). 53 | .El 54 | .Pp 55 | If a scalar number is given, it is interpreted as a load. 56 | .It Ar load 57 | A load is either a fraction in the range [0.0, 1.0] or a percentage in the 58 | range [0%, 100%]. 59 | .It Ar freq 60 | A clock frequency consists of a number and a frequency unit. 61 | .D1 Li Hz , Li KHz , Li MHz , Li GHz , Li THz 62 | The unit is not case sensitive, if omitted 63 | .Li MHz 64 | are assumed for compatibility with 65 | .Xr powerd 8 . 66 | .It Ar temp 67 | A temperature consisting of a number and a temperature unit. Supported 68 | units are: 69 | .D1 Li C , Li K , Li F , Li R 70 | These units stand for deg. Celsius, Kelvin, deg. Fahrenheit and 71 | deg. Rankine. A value without a unit is treated as deg. Celsius. 72 | .It Ar sysctl 73 | The name of a 74 | .Xr sysctl 3 , 75 | may consists of the characters 76 | .Bq 0-9A-Za-z%._- . 77 | Characters preceded by 78 | .Sq % 79 | are considered formatting fields. Allowed formatting fields are specific 80 | to a particular sysctl. Unexpected formatting fields are rejected. 81 | In order to produce a literal 82 | .Sq % , 83 | .Sq %% 84 | should be used. 85 | .It Ar ival 86 | A time interval can be given in seconds or milliseconds. 87 | .D1 Li s , Li ms 88 | An interval without a unit is treated as milliseconds. 89 | .It Ar cnt 90 | A positive integer. 91 | .It Ar file 92 | A file name. 93 | .El 94 | .Ss OPTIONS 95 | The following options are supported: 96 | .Bl -tag -width indent 97 | .It Fl h , -help 98 | Show usage and exit 99 | .It Fl v , -verbose 100 | Be verbose and produce initial diagnostics on 101 | .Pa stderr . 102 | .It Fl f , -foreground 103 | Stay in foreground, produce an event log on 104 | .Pa stdout . 105 | .It Fl N , -idle-nice 106 | Treat nice time as idle. 107 | .Pp 108 | This option exists for 109 | .Xr powerd 8 110 | compatibility, but note that most heavy workloads such as compiling 111 | software mostly consist of nice time. Users considering this flag 112 | may be better served with running at a fixed low frequency: 113 | .Dl Nm Fl b Ar min 114 | .It Fl a , -ac Ar mode 115 | Mode to use while the AC power line is connected (default 116 | .Li hadp ) . 117 | .It Fl b , -batt Ar mode 118 | Mode to use while battery powered (default 119 | .Li adp ) . 120 | .It Fl n , -unknown Ar mode 121 | Mode to use while the power line state is unknown (default 122 | .Li hadp ) . 123 | .It Fl m , -min Ar freq 124 | The lowest CPU clock frequency to use (default 0Hz). 125 | .It Fl M , -max Ar freq 126 | The highest CPU clock frequency to use (default 1THz). 127 | .It Fl -min-ac Ar freq 128 | The lowest CPU clock frequency to use on AC power. 129 | .It Fl -max-ac Ar freq 130 | The highest CPU clock frequency to use on AC power. 131 | .It Fl -min-batt Ar freq 132 | The lowest CPU clock frequency to use on battery power. 133 | .It Fl -max-batt Ar freq 134 | The highest CPU clock frequency to use on battery power. 135 | .It Fl F , -freq-range Ar freq:freq 136 | A pair of frequency values representing the minimum and maximum CPU 137 | clock frequency. 138 | .It Fl A , -freq-range-ac Ar freq:freq 139 | A pair of frequency values representing the minimum and maximum CPU 140 | clock frequency on AC power. 141 | .It Fl B , -freq-range-batt Ar freq:freq 142 | A pair of frequency values representing the minimum and maximum CPU 143 | clock frequency on battery power. 144 | .It Fl H , -hitemp-range Ar temp:temp 145 | Set the high to critical temperature range, enables temperature based 146 | throttling. 147 | .It Fl t , -temperature Ar sysctl 148 | Set the temperature source sysctl name. May contain a single 149 | .Sq %d 150 | to insert the core ID. 151 | .It Fl p , -poll Ar ival 152 | The polling interval that is used to take load samples and update the 153 | CPU clock (default 0.5s). 154 | .It Fl s , -samples Ar cnt 155 | The number of load samples to use to calculate the current load. 156 | The default is 4. 157 | .It Fl P , -pid Ar file 158 | Use an alternative pidfile, the default is 159 | .Pa /var/run/powerd.pid . 160 | The default ensures that 161 | .Xr powerd 8 162 | and 163 | .Nm 164 | are not run simultaneously. 165 | .It Fl i , r Ar load 166 | Legacy arguments from 167 | .Xr powerd 8 168 | not applicable to 169 | .Nm 170 | and thus ignored. 171 | .El 172 | .Sh SERVICE 173 | The 174 | .Nm 175 | daemon can be run as an 176 | .Xr rc 8 177 | service. Add the following line to 178 | .Xr rc.conf 5 : 179 | .Dl powerdxx_enable="YES" 180 | Command line arguments can be set via 181 | .Va powerdxx_flags . 182 | .Sh TOOLS 183 | The 184 | .Xr loadrec 1 185 | and 186 | .Xr loadplay 1 187 | tools offer the possibility to record system loads and replay them. 188 | .Sh IMPLEMENTATION NOTES 189 | This section describes the operation of 190 | .Nm . 191 | .Pp 192 | Both 193 | .Xr powerd 8 194 | and 195 | .Nm 196 | have in common, that they work by polling 197 | .Li kern.cp_times 198 | via 199 | .Xr sysctl 3 , 200 | which is an array of the accumulated loads of every core. By subtracting the 201 | last 202 | .Li cp_times 203 | sample the loads over the polling interval can be determined. This information 204 | is used to set a new CPU clock frequency by updating 205 | .Li dev.cpu.0.freq . 206 | .Ss Initialisation 207 | After parsing command line arguments 208 | .Nm 209 | assigns a clock frequency controller to every core. I.e. cores are 210 | grouped by a common 211 | .Li dev.cpu.%d.freq 212 | handle that controls the clock for all of them. Due to limitations of 213 | .Xr cpufreq 4 214 | .Li dev.cpu.0.freq 215 | is the controlling handle for all cores, even across multiple CPUs. However 216 | .Nm 217 | is not built with that assumption and per CPU, core or thread controls will 218 | work as soon as the hardware and kernel support them. 219 | .Pp 220 | In the next initialisation stage the available frequencies for every core 221 | group are determined to set appropriate lower and upper boundaries. This 222 | is a purely cosmetic measure and used to avoid unnecessary frequency 223 | updates. The controlling algorithm does not require this information, so 224 | failure to do so will only be reported (non-fatally) in verbose mode. 225 | .Pp 226 | Unless the 227 | .Fl H 228 | option is given, the initialisation checks for a critical temperature 229 | source. If one is found temperature throttling is implicitly turned 230 | on, causing throttling to start 10 deg. Celsius below the critical 231 | temperature. 232 | .Pp 233 | So far the 234 | .Xr sysctl 3 235 | .Li dev.cpu.%d.coretemp.tjmax 236 | is the only supported critical temperature source. 237 | .Ss Detaching From the Terminal 238 | After the initialisation phase 239 | .Nm 240 | prepares to detach from the terminal. The first step is to acquire a lock 241 | on the pidfile. Afterwards all the frequencies are read and written as 242 | a last opportunity to fail. After detaching from the terminal the pidfile 243 | is written and the daemon goes into frequency controlling operation until 244 | killed by a signal. 245 | .Ss Load Control Loop 246 | The original 247 | .Xr powerd 8 248 | uses a hysteresis to control the CPU frequency. I.e. it determines the load 249 | over all cores since taking the last sample (the summary load during the last 250 | polling interval) and uses a lower and an upper load boundary to decide 251 | whether it should update the frequency or not. 252 | .Pp 253 | .Nm 254 | has some core differences. It can take more than two samples (four by 255 | default), this makes it more robust against small spikes in load, while 256 | retaining much of its ability to quickly react to sudden surges in load. 257 | Changing the number of samples does not change the runtime cost of running 258 | .Nm . 259 | .Pp 260 | Instead of taking the sum of all loads, the highest load within the core 261 | group is used to decide the next frequency target. Like with 262 | .Xr powerd 8 263 | this means, that high load on a single core will cause an increase in the 264 | clock frequency. Unlike 265 | .Xr powerd 8 266 | it also means that moderate load over all cores allows a decrease of the 267 | clock frequency. 268 | .Pp 269 | The 270 | .Nm 271 | daemon steers the clock frequency to match a load target, e.g. if there was 272 | a 25% load at 2 GHz and the load target was 50%, the frequency would be set 273 | to 1 GHz. 274 | .Ss Temperature Based Throttling 275 | If temperature based throttling is active and the temperature is above 276 | the high temperature boundary (the critical temperature minus 10 277 | deg. Celsius by default), the core clock is limited to a value below 278 | the permitted maximum. The limit depends on the remaining distance 279 | to the critical temperature. 280 | .Pp 281 | Thermal throttling ignores user-defined frequency limits, i.e. when using 282 | .Fl F , B , A 283 | or 284 | .Fl m 285 | to prevent the clock from going unreasonably low, sufficient thermal 286 | load may cause 287 | .Nm 288 | to select a clock frequency below the user provided minimum. 289 | .Ss Termination and Signals 290 | The signals 291 | .Li HUP 292 | and 293 | .Li TERM 294 | cause an orderly shutdown of 295 | .Nm . 296 | An orderly shutdown means the pidfile is removed and the clock frequencies 297 | are restored to their original values. 298 | .Sh FILES 299 | .Bl -tag -width indent 300 | .It Pa /var/run/powerd.pid 301 | Common pidfile with 302 | .Xr powerd 8 . 303 | .It Pa %%PREFIX%%/etc/rc.d/powerdxx 304 | Service file, enable in 305 | .Xr rc.conf 5 . 306 | .El 307 | .Sh EXAMPLES 308 | Run in foreground, minimum clock frequency 800 MHz: 309 | .Dl powerd++ -fm800 310 | .Pp 311 | Report configuration before detaching into the background: 312 | .Dl powerd++ -v 313 | .Pp 314 | Target 75% load on battery power and run at 2.4 GHz on AC power: 315 | .Dl powerd++ -b .75 -a 2.4ghz 316 | .Pp 317 | Target 25% load on AC power: 318 | .Dl powerd++ -a 25% 319 | .Pp 320 | Use the same load sampling 321 | .Xr powerd 8 322 | does: 323 | .Dl powerd++ -s1 -p.25s 324 | .Pp 325 | Limit CPU clock frequencies to a range from 800 MHz to 1.8 GHz: 326 | .Dl powerd++ -F800:1.8ghz 327 | .Sh DIAGNOSTICS 328 | The 329 | .Nm 330 | daemon exits 0 on receiving an 331 | .Li INT 332 | or 333 | .Li TERM 334 | signal, and >0 if an error occurs. 335 | .Sh COMPATIBILITY 336 | So far 337 | .Nm 338 | requires ACPI to detect the current power line state. 339 | .Sh SEE ALSO 340 | .Xr cpufreq 4 , Xr powerd 8 , Xr loadrec 1 , Xr loadplay 1 341 | .Sh AUTHORS 342 | Implementation and manual by 343 | .An Dominic Fandrey Aq Mt kami@freebsd.org 344 | .Sh CAVEATS 345 | Unlike 346 | .Xr powerd 8 , 347 | .Nm 348 | refuses to run if the frequency control driver is known not to allow 349 | user control of the CPU frequency (e.g. 350 | .Xr hwpstate_intel 4 ). 351 | -------------------------------------------------------------------------------- /tools/playfilter: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Provide a set of filters post-process loadplay(1) output. 4 | # 5 | # Every filter step is spawned as a process in a pipeline so long 6 | # filter lists scale well across multiple CPU cores. 7 | # 8 | # @see README.md#playfilter 9 | # 10 | 11 | set -f 12 | set -o pipefail 13 | 14 | # 15 | # Interpreters. 16 | # 17 | readonly AWK=${AWK:-/usr/bin/awk} 18 | readonly CAT=${CAT:-/bin/cat} 19 | 20 | # 21 | # Exit/error numbers. 22 | # 23 | readonly ERRORS=' 24 | OK 25 | E_USAGE 26 | E_READ_CONCAT 27 | E_READ_COLUMNS 28 | E_FILTER_UNKNOWN 29 | E_FILTER_ORDER 30 | E_FILTER_NOMATCH 31 | E_FILTER_MOVINGAVG_RANGE 32 | E_FILTER_HORIZ_UNIT 33 | E_FILTER_CLONE_COLLISION 34 | E_FILTER_STYLE_UNKNOWN 35 | ' 36 | 37 | errno=-1 38 | for error in ${ERRORS}; do 39 | readonly ${error}=$((errno += 1)) 40 | done 41 | 42 | # 43 | # Output the indices of all matching arguments against a glob pattern. 44 | # 45 | # @param 1 46 | # The glob pattern to match 47 | # @param @ 48 | # The arguments to match against, indices are counted from 1 49 | # 50 | selectp() { 51 | local expr i 52 | expr="${1}" 53 | shift 54 | i=0 55 | while [ $((i += 1)) -le $# ]; do 56 | eval "case \"\${${i}%\\[*\\]}\" in 57 | ${expr}) 58 | echo ${i} 59 | ;; 60 | esac" 61 | done 62 | } 63 | 64 | # 65 | # Return a list of indices for arguments matching a glob pattern. 66 | # 67 | # @param &1 68 | # The result variable name 69 | # @param 2 70 | # The glob pattern to match 71 | # @param @ 72 | # The arguments to match against, indices are counted from 1 73 | # @param FILTER 74 | # The filter to attribute an error to 75 | # @param E_FILTER_NOMATCH 76 | # Exit value if no argument was matched 77 | # 78 | select() { 79 | join "${1}" $(shift; selectp "$@") 80 | if eval "[ -z \"\${$1}\" ]"; then 81 | echo "error: ${FILTER}: no column match for: ${2}" >&2 82 | exit ${E_FILTER_NOMATCH} 83 | fi 84 | } 85 | 86 | # 87 | # Join the list of arguments with the first character in IFS. 88 | # 89 | # @param &1 90 | # The result variable name 91 | # @param @ 92 | # The list of arguments to join 93 | # @param IFS 94 | # The first character is used to join the arguments 95 | # 96 | join() { 97 | setvar "${1}" "$(shift; echo "$*")" 98 | } 99 | 100 | # 101 | # Apply a moving average filter to selected columns. 102 | # 103 | # Replace each sample with the mean of a set of buffered samples. 104 | # 105 | # @param 1 106 | # The glob pattern to select columns 107 | # @param 2 108 | # The number of time slices before the sample point 109 | # @param 3 110 | # The (optional) number of time slices after the sample point 111 | # @param E_FILTER_MOVINGAVG_RANGE 112 | # Exit value if the buffer range is unset or 0 113 | # @see playfilter.movingavg.awk 114 | # For a detailed description of the algorithm 115 | # 116 | filter_movingavg() { 117 | local head columns 118 | read -r head 119 | select columns "${1}" ${head} 120 | echo "${head}" 121 | if ! ${AWK} -f "${0}.movingavg.awk" \ 122 | -vCOLUMNS="${columns}" -vPRE="${2}" -vPOST="${3}"; then 123 | exit ${E_FILTER_MOVINGAVG_RANGE} 124 | fi 125 | } 126 | 127 | # 128 | # Display only one in a given number of samples. 129 | # 130 | # @param 1 131 | # The number of samples from which to output the last one 132 | # 133 | filter_subsample() { 134 | ${AWK} -vWRAP="${1}" 'NR % int(WRAP) == 1' 135 | } 136 | 137 | # 138 | # Output selected columns only. 139 | # 140 | # @param 1 141 | # The glob pattern to select columns 142 | # 143 | filter_cut() { 144 | local head columns 145 | read -r head 146 | select columns "${1}" ${head} 147 | IFS=, join columns $(printf '$%d ' ${columns}) 148 | (echo "${head}"; ${CAT}) | ${AWK} "{ print(${columns}) }" 149 | } 150 | 151 | # 152 | # Accumulate a new column from a set of columns. 153 | # 154 | # @param 1 155 | # The glob pattern to select columns 156 | # @param FILTER 157 | # The accumulation algorithm, a leading `h` is discarded 158 | # @param E_FILTER_HORIZ_UNIT 159 | # Exit value if the selected columns do not have the same unit 160 | # @see playfilter.horiz.awk 161 | # For the set of supported algorithms 162 | # 163 | filter_horiz() { 164 | local head columns 165 | read -r head 166 | select columns "${1}" ${head} 167 | if ! (echo "${head}"; ${CAT}) | \ 168 | ${AWK} -f "${0}.horiz.awk" \ 169 | -vALGO="${FILTER#h}" -vCOLUMNS="${columns}"; then 170 | exit ${E_FILTER_HORIZ_UNIT} 171 | fi 172 | } 173 | 174 | # 175 | # Patch continuous columns of subsequent files and increment columns. 176 | # 177 | # E.g. if three input files contain a time column with the value 178 | # range `(0; 30] s`, the resulting output would contain one column 179 | # with the concatenated ranges `(0; 30](0; 30](0; 30] s`. 180 | # Running the time column through this filter results in an output 181 | # range `(0; 90] s`. 182 | # 183 | # The other use case for this filter is to accumulate constant increments, 184 | # similar to the time column in load recordings (instead of replays). 185 | # 186 | # @param 1 187 | # The columns to patch 188 | # 189 | filter_patch() { 190 | local head columns 191 | read -r head 192 | select columns "${1}" ${head} 193 | columns="$(printf 'COLUMNS[%d];' ${columns})" 194 | echo "${head}" 195 | ${AWK} " 196 | BEGIN { ${columns} } 197 | { 198 | for (c in COLUMNS) { 199 | if (PREV[c] >= \$c) { 200 | ADD[c] += PREV[c] 201 | } 202 | PREV[c] = \$c 203 | \$c += ADD[c] 204 | } 205 | print 206 | } 207 | " 208 | } 209 | 210 | # 211 | # Create duplicates of columns. 212 | # 213 | # @param 1 214 | # The columns to duplicate 215 | # @param 2 216 | # The number of clones 217 | # @param E_FILTER_CLONE_COLLISION 218 | # Exit value if a clone would have the same name as an existing 219 | # column 220 | # 221 | filter_clone() { 222 | local head columns 223 | read -r head 224 | select columns "${1}" ${head} 225 | columns="$(printf 'COLUMNS[%d];' ${columns})" 226 | (echo "${head}"; ${CAT}) | ${AWK} -vCLONES="${2}" " 227 | BEGIN { 228 | ${columns} 229 | CLONES = int(CLONES) 230 | } 231 | NR == 1 { 232 | for (co = 1; co <= NF; ++co) { 233 | ALL[\$co] 234 | } 235 | COLS = NF 236 | for (c = 1; c <= COLS; ++c) { 237 | if (!(c in COLUMNS)) { 238 | continue 239 | } 240 | tpl = \$c 241 | gsub(/%/, \"%%\", tpl) 242 | sub(/(\\[.*\\])?\$/, \".%d&\", tpl) 243 | for (clone = 0; clone < CLONES; ++clone) { 244 | name = sprintf(tpl, clone) 245 | if (name in ALL) { 246 | print \"error: ${FILTER}: column already exists: \" name > \"/dev/stderr\" 247 | exit ${E_FILTER_CLONE_COLLISION} 248 | } 249 | ALL[\$++NF = name] 250 | } 251 | } 252 | } 253 | NR > 1 { 254 | for (c = 1; c <= COLS; ++c) { 255 | if (!(c in COLUMNS)) { 256 | continue 257 | } 258 | for (clone = 0; clone < CLONES; ++clone) { 259 | \$++NF = \$c 260 | } 261 | } 262 | } 263 | 1 264 | " 265 | } 266 | 267 | # 268 | # Reformat columns with the given number of fraction decimals. 269 | # 270 | # @param 1 271 | # The columns to reformat 272 | # @param 2 273 | # The number of fraction decimals 274 | # 275 | filter_precision() { 276 | local head columns 277 | read -r head 278 | select columns "${1}" ${head} 279 | columns="$(printf 'COLUMNS[%d];' ${columns})" 280 | echo "${head}" 281 | ${AWK} -vPRECISION="${2}" " 282 | BEGIN { 283 | PRECISION += 0 284 | FMT = \"%.\" PRECISION \"f\" 285 | ${columns} 286 | } 287 | { 288 | for (c in COLUMNS) { 289 | \$c = sprintf(FMT, \$c) 290 | } 291 | } 292 | 1 293 | " 294 | } 295 | 296 | # 297 | # Format output columns. 298 | # 299 | # Supports two output formats: 300 | # 301 | # | Style | Format | 302 | # |-------|------------------------| 303 | # | `csv` | Comma Separated Values | 304 | # | `md` | Markdown | 305 | # 306 | # The `csv` formatting style puts headings in double quotes `"` and 307 | # separates fields by comma `,`. 308 | # 309 | # The `md` formatting style produces right aligned fixed with columns, 310 | # delimited by the pipe character `|`. The heading row is followed by 311 | # an additional row with dash `-` filled fields. 312 | # 313 | # @param 1 314 | # The formatting style to output 315 | # @param E_FILTER_STYLE_UNKNOWN 316 | # Exit value if the given formatting style is not supported 317 | # 318 | filter_style() { 319 | case "${1}" in 320 | [Cc][Ss][Vv]) 321 | ${AWK} -vOFS=, ' 322 | NR == 1 { 323 | for (c = 1; c <= NF; ++c) { 324 | $c = "\"" $c "\"" 325 | } 326 | } 327 | { 328 | $1 = $1 329 | print 330 | } 331 | ' 332 | ;; 333 | [Mm][Dd]) 334 | ${AWK} ' 335 | BEGIN { 336 | OFS="" 337 | ORS="|\n" 338 | } 339 | NR == 1 { 340 | for (c = 1; c <= NF; ++c) { 341 | LEN[c] = length($c) 342 | } 343 | } 344 | { 345 | for (c = 1; c <= NF; ++c) { 346 | $c = sprintf("| %" LEN[c] "s ", $c) 347 | } 348 | print 349 | } 350 | NR == 1 { 351 | for (c = 1; c <= NF; ++c) { 352 | gsub(/[^|]/, "-", $c) 353 | sub(/.$/, ":", $c) 354 | } 355 | print 356 | } 357 | ' 358 | ;; 359 | *) 360 | echo "error: style: unsupported formatting: ${1}" >&2 361 | exit ${E_FILTER_STYLE_UNKNOWN} 362 | ;; 363 | esac 364 | 365 | } 366 | 367 | # 368 | # Run a single filter. 369 | # 370 | # Breaks the filter arguments out of the CLI filter commands and calls 371 | # the respective filter. 372 | # 373 | # @param 1 374 | # The CLI filter command `FILTER=ARG,...` 375 | # @param FINAL 376 | # Expected to be a non-empty string for the last filter in 377 | # the chain 378 | # @param E_FILTER_ORDER 379 | # Exit value if filter ordering constraints were violated 380 | # @param E_FILTER_UNKNOWN 381 | # Exit value if the given filter is not known 382 | # 383 | runFilter() { 384 | local IFS 385 | IFS=, 386 | case "${1}" in 387 | cut=*|movingavg=*|subsample=*|patch=*|clone=*|precision=*) 388 | FILTER="${1%%=*}" IFS=$' \n\t' filter_${1%%=*} ${1#*=} 389 | ;; 390 | hmax=*|hmin=*|hsum=*|havg=*) 391 | FILTER="${1%%=*}" IFS=$' \n\t' filter_horiz ${1#*=} 392 | ;; 393 | style=*) 394 | if [ -n "${FINAL}" ]; then 395 | FILTER="${1%%=*}" IFS=$' \n\t' filter_${1%%=*} ${1#*=} 396 | else 397 | echo "error: ${1%%=*}: must be the final filter" >&2 398 | exit ${E_FILTER_ORDER} 399 | fi 400 | ;; 401 | *) 402 | echo "error: not a valid filter: ${1}" >&2 403 | exit ${E_FILTER_UNKNOWN} 404 | ;; 405 | esac 406 | } 407 | 408 | # 409 | # Recursively pipeline all the filters. 410 | # 411 | # @param @ 412 | # The CLI arguments 413 | # 414 | runFilterPipeline() { 415 | if [ -n "${2}" -a -z "${2##*=*}" ]; then 416 | FINAL= runFilter "${1}" | (shift; runFilterPipeline "$@") 417 | return 418 | fi 419 | FINAL=1 runFilter "${1}" 420 | } 421 | 422 | # 423 | # Setup the filter pipeline. 424 | # 425 | # Print usage if no arguments are given, otherwise run the filter 426 | # pipeline. 427 | # 428 | # @param @ 429 | # The CLI arguments 430 | # @param E_USAGE 431 | # Exit value if no arguments are given 432 | # 433 | runFilters() { 434 | if [ $# -eq 0 ]; then 435 | local errno error 436 | echo "usage: ${0} [ filters... ] [--] [ files... ]" 437 | printf "%8s %s\n" "Exit no." "Exit symbol" 438 | errno=-1 439 | for error in ${ERRORS}; do 440 | printf " %6d %s\n" $((errno += 1)) "${error}" 441 | done 442 | exit ${E_USAGE} 443 | fi 444 | if [ -z "${1##*=*}" ]; then 445 | runFilterPipeline "$@" 446 | return 447 | fi 448 | ${CAT} 449 | } 450 | 451 | # 452 | # Read input files. 453 | # 454 | # Takes all CLI arguments and executes an input filter. Starting from 455 | # the first argument that does not look like a filter, all arguments 456 | # are forwarded to an awk based input filter. 457 | # 458 | # The filter list can be terminated explicitly using the `--` argument. 459 | # 460 | # @param @ 461 | # All CLI arguments 462 | # @param E_READ_COLUMNS 463 | # Exit value if the number of columns changes 464 | # @param E_READ_CONCAT 465 | # Exit value if a subsequent file has a column heading mismatch 466 | # with the first file 467 | # 468 | readFiles() { 469 | [ $# -eq 0 ] && return 470 | while [ -z "${1##*=*}" ] && shift; do :; done 471 | [ "${1}" == "--" ] && shift 472 | ${AWK} " 473 | NR == 1 { 474 | for (c = 1; c <= NF; ++c) { 475 | HEADER[c] = \$c 476 | } 477 | HEADER_NF = NF 478 | } 479 | NF != HEADER_NF { 480 | print \"error: \" FILENAME \":\" FNR \": mismatching number of columns\" > \"/dev/stderr\" 481 | exit ${E_READ_COLUMNS} 482 | } 483 | FNR != NR && FNR == 1 { 484 | for (c in HEADER) { 485 | if (\$c != HEADER[c]) { 486 | print \"error: \" FILENAME \":\" FNR \": column name mismatch\" > \"/dev/stderr\" 487 | print \"hint: \" \$c \" != \" HEADER[c] > \"/dev/stderr\" 488 | exit ${E_READ_CONCAT} 489 | } 490 | } 491 | next 492 | } 493 | 1 494 | " "$@" 495 | } 496 | 497 | # execute the filter pipeline 498 | readFiles "$@" | runFilters "$@" 499 | -------------------------------------------------------------------------------- /src/utility.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements generally useful functions. 3 | * 4 | * @file 5 | */ 6 | 7 | #include /* std::underlying_type */ 8 | #include /* std::from_chars() */ 9 | #include /* std::isspace() */ 10 | #include 11 | 12 | #include /* snprintf() */ 13 | 14 | #ifndef _POWERDXX_UTILITY_HPP_ 15 | #define _POWERDXX_UTILITY_HPP_ 16 | 17 | /** 18 | * A collection of generally useful functions. 19 | */ 20 | namespace utility { 21 | 22 | /** 23 | * Like sizeof(), but it returns the number of elements an array consists 24 | * of instead of the number of bytes. 25 | * 26 | * @tparam T,Count 27 | * The type and number of array elements 28 | * @return 29 | * The number of array entries 30 | */ 31 | template 32 | constexpr size_t countof(T (&)[Count]) { return Count; } 33 | 34 | /** 35 | * This is a safeguard against accidentally using sprintf(). 36 | * 37 | * Using it triggers a static_assert(), preventing compilation. 38 | * 39 | * @tparam Args 40 | * Catch all arguments 41 | */ 42 | template 43 | inline void sprintf(Args...) { 44 | /* Assert depends on Args so it can only be determined if 45 | * the function is actually instantiated. */ 46 | static_assert(sizeof...(Args) && false, 47 | "Use of sprintf() is unsafe, use sprintf_safe() instead"); 48 | } 49 | 50 | /** 51 | * A wrapper around snprintf() that automatically pulls in the 52 | * destination buffer size. 53 | * 54 | * @tparam Size 55 | * The destination buffer size 56 | * @tparam Args 57 | * The types of the arguments 58 | * @param dst 59 | * A reference to the destination buffer 60 | * @param format 61 | * A printf style formatting string 62 | * @param args 63 | * The printf arguments 64 | * @return 65 | * The number of characters in the resulting string, regardless of the 66 | * available space 67 | */ 68 | template 69 | inline int sprintf_safe(char (& dst)[Size], char const * const format, 70 | Args const... args) { 71 | return snprintf(dst, Size, format, args...); 72 | } 73 | 74 | /** 75 | * Casts an enum to its underlying value. 76 | * 77 | * @tparam ET,VT 78 | * The enum and value type 79 | * @param op 80 | * The operand to convert 81 | * @return 82 | * The integer representation of the operand 83 | */ 84 | template ::type> 85 | constexpr VT to_value(ET const op) { 86 | return static_cast(op); 87 | } 88 | 89 | /** 90 | * A formatting wrapper around string literals. 91 | * 92 | * Overloads operator (), which treats the string as a printf formatting 93 | * string, the arguments represent the data to format. 94 | * 95 | * In combination with the literal _fmt, it can be used like this: 96 | * 97 | * ~~~ c++ 98 | * std::cout << "%-15.15s %#018p\n"_fmt("Address:", this); 99 | * ~~~ 100 | * 101 | * @tparam BufSize 102 | * The buffer size for formatting, resulting strings cannot 103 | * grow beyond `BufSize - 1` 104 | */ 105 | template 106 | class Formatter { 107 | private: 108 | /** 109 | * Pointer to the string literal. 110 | */ 111 | char const * const fmt; 112 | 113 | public: 114 | /** 115 | * Construct from string literal. 116 | */ 117 | constexpr Formatter(char const * const fmt) : fmt{fmt} {} 118 | 119 | /** 120 | * Returns a formatted string. 121 | * 122 | * @tparam ArgTs 123 | * Variadic argument types 124 | * @param args 125 | * Variadic arguments 126 | * @return 127 | * An std::string formatted according to fmt 128 | */ 129 | template 130 | std::string operator ()(ArgTs const &... args) const { 131 | char buf[BufSize]; 132 | auto count = sprintf_safe(buf, this->fmt, args...); 133 | if (count < 0) { 134 | /* encoding error */ 135 | return {}; 136 | } else if (static_cast(count) >= BufSize) { 137 | /* does not fit into buffer */ 138 | return {buf, BufSize - 1}; 139 | } 140 | return {buf, static_cast(count)}; 141 | } 142 | }; 143 | 144 | /** 145 | * Contains literal operators. 146 | */ 147 | namespace literals { 148 | /** 149 | * Literal to convert a string literal to a Formatter instance. 150 | * 151 | * @param fmt 152 | * A printf style format string 153 | * @return 154 | * A Formatter instance 155 | */ 156 | constexpr Formatter<16384> operator "" _fmt(char const * const fmt, size_t const) { 157 | return {fmt}; 158 | } 159 | } /* namespace literals */ 160 | 161 | /** 162 | * A simple value container only allowing += and copy assignment. 163 | * 164 | * @tparam T 165 | * The value type 166 | */ 167 | template 168 | class Sum { 169 | private: 170 | /** 171 | * The sum of values accumulated. 172 | */ 173 | T value; 174 | 175 | public: 176 | /** 177 | * Construct from an initial value. 178 | * 179 | * @param value 180 | * The initial value 181 | */ 182 | explicit constexpr Sum(T const & value) : value{value} {} 183 | 184 | /** 185 | * Default construct. 186 | */ 187 | constexpr Sum() : Sum{0} {} 188 | 189 | /** 190 | * Returns the current sum of values. 191 | * 192 | * @return 193 | * The sum of values by const reference 194 | */ 195 | constexpr operator T const &() const { 196 | return this->value; 197 | } 198 | 199 | /** 200 | * Add a value to the sum. 201 | * 202 | * @param value 203 | * The value to add to the current sum 204 | * @return 205 | * A self reference 206 | */ 207 | constexpr Sum & operator +=(T const & value) { 208 | this->value += value; 209 | return *this; 210 | } 211 | }; 212 | 213 | /** 214 | * A simple value container that provides the minimum of assigned values. 215 | * 216 | * @tparam T 217 | * The value type 218 | */ 219 | template 220 | class Min { 221 | private: 222 | /** 223 | * The minimum of the assigned values. 224 | */ 225 | T value; 226 | 227 | public: 228 | /** 229 | * Construct from an initial value. 230 | * 231 | * @param value 232 | * The initial value 233 | */ 234 | explicit constexpr Min(T const & value) : value{value} {} 235 | 236 | /** 237 | * Returns the current minimum. 238 | * 239 | * @return 240 | * The minimum by const reference 241 | */ 242 | constexpr operator T const &() const { 243 | return this->value; 244 | } 245 | 246 | /** 247 | * Assign a new value, if it is less than the current value. 248 | * 249 | * @param value 250 | * The value to assign 251 | * @return 252 | * A self reference 253 | */ 254 | constexpr Min & operator =(T const & value) { 255 | this->value = this->value <= value ? this->value : value; 256 | return *this; 257 | } 258 | }; 259 | 260 | /** 261 | * A simple value container that provides the maximum of assigned values. 262 | * 263 | * @tparam T 264 | * The value type 265 | */ 266 | template 267 | class Max { 268 | private: 269 | /** 270 | * The maximum of the assigned values. 271 | */ 272 | T value; 273 | 274 | public: 275 | /** 276 | * Construct from an initial value. 277 | * 278 | * @param value 279 | * The initial value 280 | */ 281 | explicit constexpr Max(T const & value) : value{value} {} 282 | 283 | /** 284 | * Returns the current maximum. 285 | * 286 | * @return 287 | * The maximum by const reference 288 | */ 289 | constexpr operator T const &() const { 290 | return this->value; 291 | } 292 | 293 | /** 294 | * Assign a new value, if it is greater than the current value. 295 | * 296 | * @param value 297 | * The value to assign 298 | * @return 299 | * A self reference 300 | */ 301 | constexpr Max & operator =(T const & value) { 302 | this->value = this->value >= value ? this->value : value; 303 | return *this; 304 | } 305 | }; 306 | 307 | /** 308 | * A functor for reading numerical values from a string or character 309 | * array. 310 | */ 311 | struct FromChars { 312 | /** 313 | * The next character to read. 314 | */ 315 | char const * it; 316 | 317 | /** 318 | * The first character of the same array that may not be read, 319 | * this should usually point to a terminating zero or behind 320 | * a buffer. 321 | */ 322 | char const * const end; 323 | 324 | /** 325 | * Retrieve an integral or floating point value from the array. 326 | * 327 | * The operation may fail for multiple reasons: 328 | * 329 | * - No more characters left to read, in that case the functor 330 | * will equal false 331 | * - The given characters do not represent a valid value, in 332 | * that case the functor will equal true 333 | * 334 | * @tparam T 335 | * The value type to retrieve 336 | * @param dst 337 | * The lvalue to assign to 338 | * @retval true 339 | * The numerical value was successfully read from the array 340 | * @retval false 341 | * The numerical value could not be read from the array 342 | */ 343 | template 344 | [[nodiscard]] bool operator ()(T & dst) { 345 | if (!this->it) { 346 | return false; 347 | } 348 | auto [p, ec] = std::from_chars(this->it, this->end, dst); 349 | if (this->it == p) { 350 | return false; 351 | } 352 | for (; p != this->end && std::isspace(*p); ++p); 353 | this->it = p; 354 | return true; 355 | } 356 | 357 | /** 358 | * Check if unread characters remain. 359 | * 360 | * @retval false 361 | * All characters have been read 362 | * @retval true 363 | * Characters remain to be read 364 | */ 365 | operator bool() const { 366 | return this->it && this->end != this->it && *this->it; 367 | } 368 | 369 | /** 370 | * Range based constructor. 371 | * 372 | * @param start,end 373 | * The character array range 374 | */ 375 | FromChars(char const * const start, char const * const end) : 376 | it{start}, end{end} { 377 | for (; this->it != end && std::isspace(*this->it); ++this->it); 378 | } 379 | 380 | /** 381 | * Construct from a character array. 382 | * 383 | * @tparam CountV 384 | * The number of characters 385 | * @param str 386 | * The character array to parse from 387 | * @param terminator 388 | * Indicates whether the character array has a terminating 389 | * null character. 390 | */ 391 | template 392 | FromChars(char const (& str)[CountV], bool const terminator = true) : 393 | FromChars{str, str + CountV - terminator} {} 394 | 395 | /** 396 | * Construct functor from a string. 397 | * 398 | * Note that changing the string during the lifetime of the 399 | * functor may silently invalidate the functor's state and 400 | * thus invoke undefined behaviour. 401 | * 402 | * @param str 403 | * The string to parse from 404 | */ 405 | FromChars(std::string const & str) : 406 | FromChars{str.data(), str.data() + str.size()} {} 407 | }; 408 | 409 | /** 410 | * Bundles a string with printf-style escapes along with the number 411 | * of printable characters. 412 | */ 413 | struct Sanitised { 414 | /** 415 | * The text with printf-style escapes. 416 | */ 417 | std::string text; 418 | 419 | /** 420 | * The number of visible characters in the text. 421 | * 422 | * I.e. multi-byte characters count a single characters, escaped 423 | * characters count as the number of characters from their 424 | * visual representation. 425 | */ 426 | std::size_t width; 427 | 428 | /** 429 | * Implicit conversion to std::string. 430 | */ 431 | operator std::string const &() const { 432 | return text; 433 | } 434 | }; 435 | 436 | /** 437 | * Produces a sanitised string that is safe to display. 438 | * 439 | * Escapes control and invalid characters with printf(3) style escapes. 440 | * 441 | * @param str 442 | * The string to escape 443 | * @return 444 | * The sanitised string with meta information 445 | */ 446 | Sanitised sanitise(std::string_view const & str); 447 | 448 | /** 449 | * A line of text and an underlining line. 450 | * 451 | * The text and the line are kept in a separate string to ease indenting 452 | * them. 453 | */ 454 | struct Underlined { 455 | /** 456 | * The text with printf-style escapes. 457 | */ 458 | std::string text; 459 | 460 | /** 461 | * Aligned underlining characters `^~~~`. 462 | */ 463 | std::string line; 464 | 465 | /** 466 | * Implicit conversion to std::string. 467 | * 468 | * Convenient if indentation is not required. 469 | */ 470 | operator std::string() const { 471 | return this->text + '\n' + this->line; 472 | } 473 | }; 474 | 475 | /** 476 | * Underline the given number of characters. 477 | * 478 | * The given length and offset use byte-addressing, the resulting 479 | * text is sanitised for printing, which may affect the actual number 480 | * of underlining characters: 481 | * 482 | * - Control characters, multi-byte character fragments and invalid 483 | * code points are substituted by printf-style escapes 484 | * - Multi-byte characters are underlined with a single character 485 | * 486 | * Double width characters are not supported (i.e. the resulting underline 487 | * is too short). 488 | * 489 | * The underlining string is only as long as it needs to be, i.e. 490 | * it is not right-padded with white space. 491 | * 492 | * @param str 493 | * The string to sanitise and underline 494 | * @param offs,len 495 | * The byte-offset and length of the underline 496 | * @return 497 | * The sanitised text and the underline 498 | */ 499 | Underlined highlight(std::string_view const & str, std::size_t const offs, 500 | std::size_t const len = 1); 501 | 502 | } /* namespace utility */ 503 | 504 | #endif /* _POWERDXX_UTILITY_HPP_ */ 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | /\ __ 3 | __ __ ___ ___ __ _____ ___ ___/ /_/ /_ 4 | /_//_// \/ \/ // / _ \/ __\/ /_ _/__ 5 | __ __ / // / // / / ___/ / / // / /_/_/ /_ 6 | /_//_// ___/\___/\_/_/\___/_/ \___/ /_ _/ 7 | / / /_/ 8 | \/ multi-core CPU clock daemon for FreeBSD® 9 | ``` 10 | 11 | The powerd++ daemon is a drop-in replacement for FreeBSD's native 12 | powerd. Its purpose is to reduce the energy consumption of CPUs for 13 | the following benefits: 14 | 15 | - Avoid unnecessary fan noise from portable devices 16 | - Improve the battery runtime of portable devices 17 | - Improve hardware lifetime by reducing thermal stress 18 | - Energy conservation 19 | 20 | [code]: https://github.com/lonkamikaze/powerdxx 21 | [releases]: https://github.com/lonkamikaze/powerdxx/releases 22 | [issues]: https://github.com/lonkamikaze/powerdxx/issues 23 | [HTML]: https://lonkamikaze.github.io/powerdxx 24 | [PDF]: https://lonkamikaze.github.io/powerdxx/refman.pdf 25 | 26 | **Contents** 27 | 28 | 1. [Using powerd++](#using-powerd) 29 | 1. [Packages](#packages) 30 | 2. [Running powerd++](#running-powerd) 31 | 3. [Manuals](#manuals) 32 | 4. [Tuning](#tuning) 33 | 5. [Reporting Issues / Requesting Features](#reporting-issues--requesting-features) 34 | 2. [Building/Installing](#buildinginstalling) 35 | 1. [Building](#building) 36 | 2. [Installing](#installing) 37 | 3. [Documentation](#documentation) 38 | 3. [Development](#development) 39 | 1. [Design](#design) 40 | 2. [License](#license) 41 | 42 | Using powerd++ 43 | ============== 44 | 45 | Powerd++ offers the following features: 46 | 47 | - Load target based clock frequency control 48 | - Tunable sampling with moving average filter 49 | - Load recording and replay tooling for benchmarking, tuning and 50 | reporting issues 51 | - Command line compatibility with [powerd(8)] 52 | - Temperature based throttling 53 | - Expressive command line arguments with units, ranges and argument 54 | chaining 55 | - Helpful error messages 56 | - Comprehensive manual pages 57 | 58 | [powerd(8)]: https://www.freebsd.org/cgi/man.cgi?query=powerd 59 | 60 | Packages 61 | -------- 62 | 63 | The [FreeBSD] port is `sysutils/powerdxx`, the package name `powerdxx`. 64 | 65 | [FreeBSD]: https://www.freebsd.org/ 66 | 67 | Running powerd++ 68 | ---------------- 69 | 70 | It is not intended to run powerd++ simultaneously with powerd. 71 | To prevent this powerd++ uses the same default pidfile as powerd: 72 | 73 | ``` 74 | # service powerdxx onestart 75 | Starting powerdxx. 76 | powerd++: (ECONFLICT) a power daemon is already running under PID: 59866 77 | /usr/local/etc/rc.d/powerdxx: WARNING: failed to start powerdxx 78 | ``` 79 | 80 | So if powerd is already setup, it first needs to be disabled: 81 | 82 | ``` 83 | # service powerd stop 84 | Stopping powerd. 85 | Waiting for PIDS: 50127. 86 | # service powerd disable 87 | powerd disabled in /etc/rc.conf 88 | ``` 89 | 90 | Afterwards powerd++ can be enabled: 91 | 92 | ``` 93 | # service powerdxx enable 94 | powerdxx enabled in /etc/rc.conf 95 | # service powerdxx start 96 | Starting powerdxx. 97 | ``` 98 | 99 | Manuals 100 | ------- 101 | 102 | Comprehensive manual pages exist for powerd++ and its accompanying 103 | tools loadrec and loadplay: 104 | 105 | ``` 106 | > man powerd++ loadrec loadplay 107 | ``` 108 | 109 | The current version of the manual pages may be read directly from 110 | the repository: 111 | 112 | ``` 113 | > man man/* 114 | ``` 115 | 116 | The manual pages as of the last release can also be 117 | [read online](https://lonkamikaze.github.io/powerdxx/pages.html). 118 | 119 | Tuning 120 | ------ 121 | 122 | Three parameters affect the responsiveness of powerd++: 123 | 124 | - The load target (refer to `-a`, `-b` and `-n`) 125 | - The polling interval (refer to `-p`) 126 | - The sample count (refer to `-s`) 127 | 128 | The key to tuning powerd++ is the `-f` flag, which keeps powerd++ 129 | in foreground and causes it to report its activity. 130 | This allows directly observing the effects of a parameter set. 131 | 132 | Observing the defaults in action may be a good start: 133 | 134 | ``` 135 | # powerd++ -f 136 | power: online, load: 693 MHz, 42 C, cpu.0.freq: 2401 MHz, wanted: 1848 MHz 137 | power: online, load: 475 MHz, 43 C, cpu.0.freq: 1800 MHz, wanted: 1266 MHz 138 | power: online, load: 271 MHz, 43 C, cpu.0.freq: 1300 MHz, wanted: 722 MHz 139 | power: online, load: 64 MHz, 43 C, cpu.0.freq: 768 MHz, wanted: 170 MHz 140 | power: online, load: 55 MHz, 42 C, cpu.0.freq: 768 MHz, wanted: 146 MHz 141 | power: online, load: 57 MHz, 42 C, cpu.0.freq: 768 MHz, wanted: 152 MHz 142 | power: online, load: 60 MHz, 44 C, cpu.0.freq: 768 MHz, wanted: 160 MHz 143 | power: online, load: 67 MHz, 42 C, cpu.0.freq: 768 MHz, wanted: 178 MHz 144 | ... 145 | ``` 146 | 147 | Note, the immediate high load is due to the load buffer being filled 148 | under the assumption that the past load fits the current clock frequency 149 | when powerd++ starts. 150 | 151 | Reporting Issues / Requesting Features 152 | -------------------------------------- 153 | 154 | Please report issues and feature requests on [GitHub][issues] or 155 | to . 156 | 157 | ### Build Issues 158 | 159 | In case of a build issue, please report the build output as well 160 | as the output of `make info`: 161 | 162 | ``` 163 | > make info 164 | VERSION="0.4.3+c8" 165 | GITVERSION="0.4.3+c8" 166 | GITHASH="8431d86abe7479a4c0a040c19551ff3fa2454ea1" 167 | PKGVERSION="" 168 | TARGETS="powerd++ loadrec loadplay libloadplay.so" 169 | CXX="ccache c++" 170 | CXXFLAGS="-O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic" 171 | CXXVERSION="FreeBSD clang version 8.0.1 (tags/RELEASE_801/final 366581) (based on LLVM 8.0.1) Target 172 | : x86_64-unknown-freebsd12.1 Thread model: posix InstalledDir: /usr/bin" 173 | UNAME_A="FreeBSD AprilRyan.norad 12.1-STABLE FreeBSD 12.1-STABLE #1 ea071b9cb32(stable/12)-dirty: Mo 174 | n Oct 28 23:37:31 CET 2019 root@AprilRyan.norad:/usr/obj/S403/amd64/usr/src/amd64.amd64/sys/S403 175 | amd64" 176 | ``` 177 | 178 | ### Performance Issues 179 | 180 | If powerd++ behaves in some unexpected or undesired manner, please 181 | mention all the command line flags (e.g. from `/etc/rc.conf` 182 | `powerdxx_flags`) and provide a load recording: 183 | 184 | ``` 185 | > loadrec -o myissue.load 186 | ``` 187 | 188 | The default recording duration is 30 s. Do not omit the `-o` parameter, 189 | printing the output on the terminal may create significant load and 190 | impact the recorded load significantly. 191 | 192 | Before submitting the report, try to reproduce the behaviour using 193 | the recorded load: 194 | 195 | ``` 196 | > loadplay -i myissue.load -o /dev/null powerd++ -f 197 | power: online, load: 224 MHz, cpu.0.freq: 768 MHz, wanted: 597 MHz 198 | power: online, load: 155 MHz, cpu.0.freq: 768 MHz, wanted: 413 MHz 199 | power: online, load: 85 MHz, cpu.0.freq: 768 MHz, wanted: 226 MHz 200 | power: online, load: 29 MHz, cpu.0.freq: 768 MHz, wanted: 77 MHz 201 | power: online, load: 23 MHz, cpu.0.freq: 768 MHz, wanted: 61 MHz 202 | ... 203 | ``` 204 | 205 | Building/Installing 206 | =================== 207 | 208 | The `Makefile` offers a set of targets, it is written for FreeBSD's 209 | [make(1)](https://www.freebsd.org/cgi/man.cgi?query=make): 210 | 211 | | Target | Description | 212 | |-------------|-------------------------------------------------------| 213 | | all | Build everything | 214 | | info | Print the build configuration | 215 | | debug | Build with `CXXFLAGS=-O0 -g -DEBUG` | 216 | | paranoid | Turn on undefined behaviour canaries | 217 | | install | Install tools and manuals | 218 | | deinstall | Deinstall tools and manuals | 219 | | clean | Clear build directory `obj/` | 220 | | releasetest | Attempt a build and install from a gitless repo clone | 221 | | testbuild | Test build with a set of compilers | 222 | | tb | Alias for testbuild | 223 | | doc | Build HTML documentation | 224 | | gh-pages | Build and publish HTML and PDF documentation | 225 | 226 | Building 227 | -------- 228 | 229 | The `all` target is the default target that is called implicitly if 230 | make is run without arguments: 231 | 232 | ``` 233 | > make 234 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -c src/powerd++.cpp -o powerd++.o 235 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -c src/clas.cpp -o clas.o 236 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -c src/utility.cpp -o utility.o 237 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -lutil powerd++.o clas.o utility.o -o powerd++ 238 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -c src/loadrec.cpp -o loadrec.o 239 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic loadrec.o clas.o utility.o -o loadrec 240 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -c src/loadplay.cpp -o loadplay.o 241 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic loadplay.o clas.o utility.o -o loadplay 242 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -fPIC -c src/libloadplay.cpp -o libloadplay.o 243 | c++ -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -lpthread -shared libloadplay.o -o libloadplay.so 244 | > 245 | ``` 246 | 247 | The `debug` and `paranoid` flags perform the same build as the `all` 248 | target, but with different/additional `CXXFLAGS`. The `debug` and 249 | `paranoid` targets can be combined. 250 | 251 | ### `make testbuild` / `make tb` 252 | 253 | The `testbuild` target builds all supported test builds, the list 254 | of builds can be queried from the `TESTBUILDS` make variable: 255 | 256 | ``` 257 | > make -VTESTBUILDS 258 | clang++90 clang++80 clang++70 g++9 259 | ``` 260 | 261 | A specific test build may be selected by appending it to the `testbuild` target: 262 | 263 | ``` 264 | > make tb/g++9 265 | [testbuild/g++9]: make 266 | g++9 -O2 -pipe -march=haswell -std=c++17 -Wall -Werror -pedantic -c ../src/powerd++.cpp -o powerd++.o 267 | ... 268 | ``` 269 | 270 | Instead of creating the default target any non-documentation target 271 | may be appended to the `testbuild` target: 272 | 273 | ``` 274 | > make tb/g++9/clean 275 | [testbuild/g++9]: make clean 276 | rm -f *.o powerd++ loadrec loadplay libloadplay.so 277 | ``` 278 | 279 | In order to run a specific target on all test builds, the build can 280 | be omitted from the target: 281 | 282 | ``` 283 | > make tb/clean 284 | [testbuild/clang++90]: make clean 285 | rm -f *.o powerd++ loadrec loadplay libloadplay.so 286 | [testbuild/clang++80]: make clean 287 | rm -f *.o powerd++ loadrec loadplay libloadplay.so 288 | [testbuild/clang++70]: make clean 289 | rm -f *.o powerd++ loadrec loadplay libloadplay.so 290 | [testbuild/g++9]: make clean 291 | rm -f *.o powerd++ loadrec loadplay libloadplay.so 292 | ``` 293 | 294 | Installing 295 | ---------- 296 | 297 | The installer installs the tools and manual pages according to a recipe 298 | in `pkg/files`. The following variables can be passed to `make install` 299 | or `make deinstall` to affect the install destination: 300 | 301 | | Variable | Default | 302 | |-----------|--------------------------------| 303 | | `DESTDIR` | | 304 | | `PREFIX` | `/usr/local` | 305 | | `DOCSDIR` | `${PREFIX}/share/doc/powerdxx` | 306 | 307 | `DESTDIR` can be used to install powerd++ into a chroot or jail, e.g. 308 | to put it into the staging area when building a package using the 309 | FreeBSD ports. Unlike `PREFIX` and `DOCSDIR` it does not affect the 310 | installed files themselves. 311 | 312 | Documentation 313 | ------------- 314 | 315 | Building the documentation requires `doxygen` 1.8.15 or later, building 316 | the PDF version of the documentation requires `xelatex` as provided 317 | by the tex-xetex package. 318 | 319 | The `doc` target populates `doc/html` and `doc/latex`, to create the 320 | PDF documentation `doc/latex/refman.pdf` must be built. 321 | 322 | The `gh-pages` target builds the HTML and PDF documentation and drops 323 | it into the `gh-pages` submodule for publishing on [github.io][HTML]. 324 | 325 | Development 326 | =========== 327 | 328 | The following table provides an overview of repository contents: 329 | 330 | | File/Folder | Contents | 331 | |---------------|--------------------------------------------| 332 | | `doc/` | Output directory for doxygen documentation | 333 | | `doxy/` | Doxygen configuration and filter scripts | 334 | | `gh-pages/` | Submodule for publishing the documentation | 335 | | `man/` | Manual pages written using mdoc(7) markup | 336 | | `obj/` | Build output | 337 | | `pkg/` | Installer scripts and instructions | 338 | | `loads/` | Load recordings useful for testing | 339 | | `src/` | C++ source files | 340 | | `src/sys/` | C++ wrappers for common C interfaces | 341 | | `powerd++.rc` | Init script / service description | 342 | | `LICENSE.md` | ISC license | 343 | | `Makefile` | Build instructions | 344 | | `README.md` | Project overview | 345 | 346 | Design 347 | ------ 348 | 349 | The life cycle of the powerd++ process goes through three stages: 350 | 351 | 1. Command line argument parsing 352 | 2. Initialisation and optionally printing the detected/configured parameters 353 | 3. Clock frequency control 354 | 355 | The first stage is designed to maximise usability by providing both, 356 | the compact short option syntax (e.g. `-vfbhadp`) as well as the more 357 | self-descriptive long option syntax 358 | (e.g. `--verbose --foreground --batt hiadaptive`). 359 | 360 | The second stage is designed to trigger all known error conditions 361 | in order to fail before calling daemon(3) at the start of the third 362 | stage. Both the first and second stage are meant to provide specific, 363 | helpful error messages. 364 | 365 | The third stage tracks the CPU load and performs clock frequency 366 | control. It is designed to provide its functionality with as little 367 | runtime as possible. This is achieved by: 368 | 369 | - Using integer arithmetic only 370 | - Minimising branching 371 | 372 | The latter is achieved by using function templates to roll out possible 373 | runtime state combinations as multiple functions. A single, central 374 | switch/case selects the correct function each cycle. This basically 375 | rolls out multiple code paths through a single function into multiple 376 | functions with a single code path. 377 | 378 | The trade-off made is for runtime over code size. With every bit 379 | of state rolled out like this the number of functions that need to 380 | be generated doubles, thus this approach is limited to the few bits 381 | of state that control the most expensive functionality, e.g. the 382 | foreground mode. 383 | 384 | License 385 | ------- 386 | 387 | This project is published under the [ISC license](LICENSE.md). 388 | -------------------------------------------------------------------------------- /src/sys/sysctl.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Implements safer c++ wrappers for the sysctl() interface. 3 | * 4 | * @file 5 | */ 6 | 7 | #ifndef _POWERDXX_SYS_SYSCTL_HPP_ 8 | #define _POWERDXX_SYS_SYSCTL_HPP_ 9 | 10 | #include "error.hpp" /* sys::sc_error */ 11 | 12 | #include /* std::unique_ptr */ 13 | 14 | #include /* assert() */ 15 | 16 | #include /* sysctl() */ 17 | #include /* sysctl() */ 18 | 19 | namespace sys { 20 | 21 | /** 22 | * This namespace contains safer c++ wrappers for the sysctl() interface. 23 | * 24 | * The template class Sysctl represents a sysctl address and offers 25 | * handles to retrieve or set the stored value. 26 | * 27 | * The template class Sync represents a sysctl value that is read and 28 | * written synchronously. 29 | * 30 | * The template class Once represents a read once value. 31 | */ 32 | namespace ctl { 33 | 34 | /** 35 | * The domain error type. 36 | */ 37 | struct error {}; 38 | 39 | /** 40 | * Management Information Base identifier type (see sysctl(3)). 41 | */ 42 | typedef int mib_t; 43 | 44 | /** 45 | * A wrapper around the sysctl() function. 46 | * 47 | * All it does is throw an exception if sysctl() fails. 48 | * 49 | * @param name,namelen 50 | * The MIB buffer and its length 51 | * @param oldp,oldlenp 52 | * Pointers to the return buffer and its length 53 | * @param newp,newlen 54 | * A pointer to the buffer with the new value and the buffer length 55 | * @throws sys::sc_error 56 | * Throws if sysctl() fails for any reason 57 | */ 58 | inline void sysctl_raw(mib_t const * name, u_int const namelen, 59 | void * const oldp, size_t * const oldlenp, 60 | void const * const newp, size_t const newlen) { 61 | if (sysctl(name, namelen, oldp, oldlenp, newp, newlen) == -1) { 62 | throw sc_error{errno}; 63 | } 64 | } 65 | 66 | /** 67 | * Returns a sysctl() value to a buffer. 68 | * 69 | * @tparam MibDepth 70 | * The length of the MIB buffer 71 | * @param mib 72 | * The MIB buffer 73 | * @param oldp,oldlen 74 | * A pointers to the return buffer and a reference to its length 75 | * @throws sys::sc_error 76 | * Throws if sysctl() fails for any reason 77 | */ 78 | template 79 | void sysctl_get(mib_t const (& mib)[MibDepth], void * const oldp, size_t & oldlen) { 80 | sysctl_raw(mib, MibDepth, oldp, &oldlen, nullptr, 0); 81 | } 82 | 83 | /** 84 | * Sets a sysctl() value. 85 | * 86 | * @tparam MibDepth 87 | * The length of the MIB buffer 88 | * @param mib 89 | * The MIB buffer 90 | * @param newp,newlen 91 | * A pointer to the buffer with the new value and the buffer length 92 | * @throws sys::sc_error 93 | * Throws if sysctl() fails for any reason 94 | */ 95 | template 96 | void sysctl_set(mib_t const (& mib)[MibDepth], void const * const newp, 97 | size_t const newlen) { 98 | sysctl_raw(mib, MibDepth, nullptr, nullptr, newp, newlen); 99 | } 100 | 101 | /** 102 | * Represents a sysctl MIB address. 103 | * 104 | * It offers set() and get() methods to access these sysctls. 105 | * 106 | * There are two ways of initialising a Sysctl instance, by symbolic 107 | * name or by directly using the MIB address. The latter one only 108 | * makes sense for sysctls with a fixed address, known at compile 109 | * time, e.g. `Sysctl<2>{CTL_HW, HW_NCPU}` for "hw.ncpu". Check 110 | * `/usr/include/sys/sysctl.h` for predefined MIBs. 111 | * 112 | * For all other sysctls, symbolic names must be used. E.g. 113 | * `Sysctl<0>{"dev.cpu.0.freq"}`. Creating a Sysctl from a symbolic 114 | * name may throw. 115 | * 116 | * Suitable deduction guides usually allow omitting the template arguments, 117 | * i.e. Sysctl{CTL_HW, HW_NCPU} and Sysctl{"dev.cpu.0.freq"} implicitly 118 | * use the correct template argument. 119 | * 120 | * @tparam MibDepth 121 | * The MIB level, e.g. "hw.ncpu" is two levels deep 122 | */ 123 | template 124 | class Sysctl { 125 | private: 126 | /** 127 | * Stores the MIB address. 128 | */ 129 | mib_t mib[MibDepth]; 130 | 131 | public: 132 | /** 133 | * Initialise the MIB address directly. 134 | * 135 | * Some important sysctl values have a fixed address that 136 | * can be initialised at compile time with a noexcept guarantee. 137 | * 138 | * Spliting the MIB address into head and tail makes sure 139 | * that `Sysctl(char *)` does not match the template and is 140 | * instead implicitly cast to invoke `Sysctl(char const *)`. 141 | * 142 | * @tparam Tail 143 | * The types of the trailing MIB address values (must 144 | * be mib_t) 145 | * @param head,tail 146 | * The mib 147 | */ 148 | template 149 | constexpr Sysctl(mib_t const head, Tail const... tail) noexcept : 150 | mib{head, tail...} { 151 | static_assert(MibDepth == sizeof...(Tail) + 1, 152 | "MIB depth mismatch"); 153 | } 154 | 155 | /** 156 | * The size of the sysctl. 157 | * 158 | * @return 159 | * The size in characters 160 | */ 161 | size_t size() const 162 | { 163 | size_t len = 0; 164 | sysctl_get(this->mib, nullptr, len); 165 | return len; 166 | } 167 | 168 | /** 169 | * Update the given buffer with a value retrieved from the 170 | * sysctl. 171 | * 172 | * @param buf,bufsize 173 | * The target buffer and its size 174 | * @throws sys::sc_error 175 | * Throws if value retrieval fails or is incomplete, 176 | * e.g. because the value does not fit into the target 177 | * buffer 178 | */ 179 | void get(void * const buf, size_t const bufsize) const { 180 | auto len = bufsize; 181 | sysctl_get(this->mib, buf, len); 182 | } 183 | 184 | /** 185 | * Update the given value with a value retreived from the 186 | * sysctl. 187 | * 188 | * @tparam T 189 | * The type store the sysctl value in 190 | * @param value 191 | * A reference to the target value 192 | * @throws sys::sc_error 193 | * Throws if value retrieval fails or is incomplete, 194 | * e.g. because the value does not fit into the target 195 | * type 196 | */ 197 | template 198 | void get(T & value) const { 199 | get(&value, sizeof(T)); 200 | } 201 | 202 | /** 203 | * Retrieve an array from the sysctl address. 204 | * 205 | * This is useful to retrieve variable length sysctls, like 206 | * characer strings. 207 | * 208 | * @tparam T 209 | * The type stored in the array 210 | * @return 211 | * And array of T with the right length to store the 212 | * whole sysctl value 213 | * @throws sys::sc_error 214 | * May throw if the size of the sysctl increases after 215 | * the length was queried 216 | */ 217 | template 218 | std::unique_ptr get() const { 219 | auto const len = size(); 220 | auto result = std::unique_ptr(new T[len / sizeof(T)]); 221 | get(result.get(), len); 222 | return result; 223 | } 224 | 225 | /** 226 | * Update the the sysctl value with the given buffer. 227 | * 228 | * @param buf,bufsize 229 | * The source buffer 230 | * @throws sys::sc_error 231 | * If the source buffer cannot be stored in the sysctl 232 | */ 233 | void set(void const * const buf, size_t const bufsize) { 234 | sysctl_set(this->mib, buf, bufsize); 235 | } 236 | 237 | /** 238 | * Update the the sysctl value with the given value. 239 | * 240 | * @tparam T 241 | * The value type 242 | * @param value 243 | * The value to set the sysctl to 244 | */ 245 | template 246 | void set(T const & value) { 247 | set(&value, sizeof(T)); 248 | } 249 | }; 250 | 251 | /** 252 | * This is a specialisation of Sysctl for sysctls using symbolic names. 253 | * 254 | * A Sysctl instance created with the default constructor is unitialised, 255 | * initialisation can be deferred to a later moment by using copy assignment. 256 | * This can be used to create globals but construct them inline where 257 | * exceptions can be handled. 258 | */ 259 | template <> 260 | class Sysctl<0> { 261 | private: 262 | /** 263 | * Stores the MIB address. 264 | */ 265 | mib_t mib[CTL_MAXNAME]; 266 | 267 | /** 268 | * The MIB depth. 269 | */ 270 | size_t depth; 271 | 272 | public: 273 | /** 274 | * The default constructor. 275 | * 276 | * This is available to defer initialisation to a later moment. 277 | */ 278 | constexpr Sysctl() : mib{}, depth{0} {} 279 | 280 | /** 281 | * Initialise the MIB address from a character string. 282 | * 283 | * @param name 284 | * The symbolic name of the sysctl 285 | * @throws sys::sc_error 286 | * May throw an exception if the addressed sysct does 287 | * not exist or if the address is too long to store 288 | */ 289 | Sysctl(char const * const name) : depth{CTL_MAXNAME} { 290 | if (sysctlnametomib(name, this->mib, &this->depth) == -1) { 291 | throw sc_error{errno}; 292 | } 293 | assert(this->depth <= CTL_MAXNAME && "MIB depth exceeds limit"); 294 | } 295 | 296 | /** 297 | * @copydoc Sysctl::size() const 298 | */ 299 | size_t size() const 300 | { 301 | size_t len = 0; 302 | sysctl_raw(this->mib, this->depth, nullptr, &len, nullptr, 0); 303 | return len; 304 | } 305 | 306 | /** 307 | * @copydoc Sysctl::get(void * const, size_t const) const 308 | */ 309 | void get(void * const buf, size_t const bufsize) const { 310 | auto len = bufsize; 311 | sysctl_raw(this->mib, this->depth, buf, &len, nullptr, 0); 312 | } 313 | 314 | /** 315 | * @copydoc Sysctl::get(T &) const 316 | */ 317 | template 318 | void get(T & value) const { 319 | get(&value, sizeof(T)); 320 | } 321 | 322 | /** 323 | * @copydoc Sysctl::get() const 324 | */ 325 | template 326 | std::unique_ptr get() const { 327 | size_t const len = size(); 328 | auto result = std::unique_ptr(new T[len / sizeof(T)]); 329 | get(result.get(), len); 330 | return result; 331 | } 332 | 333 | /** 334 | * @copydoc Sysctl::set(void const * const, size_t const) 335 | */ 336 | void set(void const * const buf, size_t const bufsize) { 337 | sysctl_raw(this->mib, this->depth, nullptr, nullptr, buf, bufsize); 338 | } 339 | 340 | /** 341 | * @copydoc Sysctl::set(T const &) 342 | */ 343 | template 344 | void set(T const & value) { 345 | set(&value, sizeof(T)); 346 | } 347 | }; 348 | 349 | /** 350 | * Create a Sysctl from a set of predefined MIBs. 351 | * 352 | * @tparam Args 353 | * List of argument types, should all be mib_t 354 | */ 355 | template 356 | Sysctl(mib_t const, ArgTs const ...) -> Sysctl<(1 + sizeof...(ArgTs))>; 357 | 358 | /** 359 | * Create a Sysctl<0> by name. 360 | */ 361 | Sysctl(char const * const) -> Sysctl<0>; 362 | 363 | /** 364 | * Default construct a Sysctl<0>. 365 | */ 366 | Sysctl() -> Sysctl<0>; 367 | 368 | /** 369 | * This is a wrapper around Sysctl that allows semantically transparent 370 | * use of a sysctl. 371 | * 372 | * ~~~ c++ 373 | * Sync> sndUnit{{"hw.snd.default_unit"}}; 374 | * if (sndUnit != 3) { // read from sysctl 375 | * sndUnit = 3; // assign to sysctl 376 | * } 377 | * ~~~ 378 | * 379 | * Note that both assignment and read access (implemented through 380 | * type casting to T) may throw an exception. 381 | * 382 | * @tparam T 383 | * The type to represent the sysctl as 384 | * @tparam SysctlT 385 | * The Sysctl type 386 | */ 387 | template 388 | class Sync { 389 | private: 390 | /** 391 | * A sysctl to represent. 392 | */ 393 | SysctlT sysctl; 394 | 395 | public: 396 | /** 397 | * The default constructor. 398 | * 399 | * This is available to defer initialisation to a later moment. 400 | * This might be useful when initialising global or static 401 | * instances by a character string repesented name. 402 | */ 403 | constexpr Sync() {} 404 | 405 | /** 406 | * The constructor copies the given Sysctl instance. 407 | * 408 | * @param sysctl 409 | * The Sysctl instance to represent 410 | */ 411 | constexpr Sync(SysctlT const & sysctl) noexcept : sysctl{sysctl} {} 412 | 413 | /** 414 | * Transparently assiges values of type T to the represented 415 | * Sysctl instance. 416 | * 417 | * @param value 418 | * The value to assign 419 | * @return 420 | * A self reference 421 | */ 422 | Sync & operator =(T const & value) { 423 | this->sysctl.set(value); 424 | return *this; 425 | } 426 | 427 | /** 428 | * Implicitly cast to the represented type. 429 | * 430 | * @return 431 | * Returns the value from the sysctl 432 | */ 433 | operator T () const { 434 | T value; 435 | this->sysctl.get(value); 436 | return value; 437 | } 438 | }; 439 | 440 | /** 441 | * A convenience alias around Sync. 442 | * 443 | * ~~~ c++ 444 | * // Sync> sndUnit{{"hw.snd.default_unit"}}; 445 | * SysctlSync sndUnit{{"hw.snd.default_unit"}}; 446 | * if (sndUnit != 3) { // read from sysctl 447 | * sndUnit = 3; // assign to sysctl 448 | * } 449 | * ~~~ 450 | * 451 | * @tparam T 452 | * The type to represent the sysctl as 453 | * @tparam MibDepth 454 | * The MIB depth, provide only for compile time initialisation 455 | */ 456 | template 457 | using SysctlSync = Sync>; 458 | 459 | /** 460 | * A read once representation of a Sysctl. 461 | * 462 | * This reads a sysctl once upon construction and always returns that 463 | * value. It does not support assignment. 464 | * 465 | * This class is intended for sysctls that are not expected to change, 466 | * such as hw.ncpu. A special property of this class is that the 467 | * constructor does not throw and takes a default value in case reading 468 | * the sysctl fails. 469 | * 470 | * ~~~ c++ 471 | * // Read number of CPU cores, assume 1 on failure: 472 | * Once> ncpu{1, {CTL_HW, HW_NCPU}}; 473 | * // Equivalent: 474 | * int hw_ncpu; 475 | * try { 476 | * Sysctl<2>{CTL_HW, HW_NCPU}.get(hw_ncpu); 477 | * } catch (sys::sc_error) { 478 | * hw_ncpu = 1; 479 | * } 480 | * ~~~ 481 | * 482 | * @tparam T 483 | * The type to represent the sysctl as 484 | * @tparam SysctlT 485 | * The Sysctl type 486 | */ 487 | template 488 | class Once { 489 | private: 490 | /** 491 | * The sysctl value read upon construction. 492 | */ 493 | T value; 494 | 495 | public: 496 | /** 497 | * The constructor tries to read and store the requested sysctl. 498 | * 499 | * If reading the requested sysctl fails for any reason, 500 | * the given value is stored instead. 501 | * 502 | * @param value 503 | * The fallback value 504 | * @param sysctl 505 | * The sysctl to represent 506 | */ 507 | Once(T const & value, SysctlT const & sysctl) noexcept { 508 | try { 509 | sysctl.get(this->value); 510 | } catch (sc_error) { 511 | this->value = value; 512 | } 513 | } 514 | 515 | /** 516 | * Return a const reference to the value. 517 | * 518 | * @return 519 | * A const reference to the value 520 | */ 521 | operator T const &() const { 522 | return this->value; 523 | } 524 | }; 525 | 526 | /** 527 | * A convenience alias around Once. 528 | * 529 | * ~~~ c++ 530 | * // Once> ncpu{0, {CTL_HW, HW_NCPU}}; 531 | * SysctlOnce ncpu{1, {CTL_HW, HW_NCPU}}; 532 | * ~~~ 533 | * 534 | * @tparam T 535 | * The type to represent the sysctl as 536 | * @tparam MibDepth 537 | * The maximum allowed MIB depth 538 | */ 539 | template 540 | using SysctlOnce = Once>; 541 | 542 | } /* namespace ctl */ 543 | } /* namespace sys */ 544 | 545 | #endif /* _POWERDXX_SYS_SYSCTL_HPP_ */ 546 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | TOOLS 2 | ===== 3 | 4 | playdiff 5 | -------- 6 | 7 | Computes metrics of the deviations between two `loadplay(1)` generated 8 | outputs. 9 | 10 | ``` 11 | usage: tools/playdiff file1 file2 ... 12 | ``` 13 | 14 | The output of `loadplay(1)` is not reproducible. Due to differences 15 | in timing between each run there are slight variations in the load 16 | that a powerd samples. This makes it difficult to tell whether a second 17 | run with a different parameter set or a different powerd version 18 | exhibits different behaviour, which is important for regression testing. 19 | 20 | The most intuitive way of dealing with this is plotting a graph. The 21 | `playdiff` tool instead provides metrics to make the same judgement. 22 | 23 | ### Metrics 24 | 25 | The `playdiff` tool integrates the deviations and absolute deviations 26 | between two `loadplay` outputs over time. These values are used to 27 | present four metrics per column of `loadplay` output: 28 | 29 | - Integral over Deviations (ID) 30 | - Mean Deviation (MD) 31 | - Integral over Absolute Deviations (IAD) 32 | - Mean Absolute Deviation (MAD) 33 | 34 | ### Interpreting the Data 35 | 36 | The integrals and means provide the same information, but the magnitude 37 | of the means is independent of the duration of the load replay, thus 38 | the means make it easier to interpret the data. 39 | 40 | The following excerpt of a real dataset, shows the IAD looks high, 41 | the MAD is a much better presentation. An average CPU frequency deviation 42 | of 34 MHz is noteworthy, but not indicative of a fundamental difference. 43 | 44 | A look at the MAD column of the `run.load` row shows that `loadplay` 45 | presented different load data to the powerd between runs. The `rec.load` 46 | row confirms that both runs are based on the same recording. However 47 | the ID column shows that the accumulated deviation over the entire run 48 | is less than 0.05 MHz. This is indicative of an aliasing effect that 49 | implies there was a small time offset between both runs, apart from 50 | that performance of the powerd was the same. 51 | 52 | ``` 53 | --- a/load.play 54 | +++ b/load.play 55 | ID MD IAD MAD 56 | time[s] 0.0 0.0 0.0 0.0 57 | cpu.0.rec.freq[MHz] 0.0 0.0 0.0 0.0 58 | cpu.0.rec.load[MHz] 0.0 0.0 0.0 0.0 59 | cpu.0.run.freq[MHz] -94.0 -3.1 1016.0 33.9 60 | cpu.0.run.load[MHz] 0.0 0.0 160.0 5.3 61 | ``` 62 | 63 | playfilter 64 | ---------- 65 | 66 | Post-process loadplay(1) output. 67 | 68 | ``` 69 | usage: tools/playfilter [ filters... ] [--] [ files... ] 70 | ``` 71 | 72 | Takes an optional list of filters and an optional list of files. The 73 | first argument not matching the syntax for a filter is treated as 74 | a file. Alternatively the `--` argument can be provided to mark the 75 | end of the list of filters. This allows providing file names that 76 | look like filters. 77 | 78 | The syntax for a filter is `FILTER=ARG[,...]`. Individual filters 79 | are described in the [Filters](#filters) subsection. 80 | 81 | ### Files 82 | 83 | If no file names are given, `stdin` is used as the input. Otherwise 84 | the given files are concatenated. 85 | Each line of input is expected to contain a fixed number of fields 86 | separated by white space. The first line of each file is referred 87 | to as the header and expected to contain the column names. 88 | 89 | Subsequent headers are discarded if they match the first file's header. 90 | A mismatch is treated as an error. 91 | 92 | ### Filters 93 | 94 | The following filters are supported. 95 | 96 | | Filter | Arguments | Describe | 97 | |-----------|-----------------|---------------------------------------------| 98 | | cut | glob | Remove unmatched columns | 99 | | movingavg | glob pre [post] | Apply a moving average (mean) | 100 | | subsample | n | Only output every nth sample | 101 | | patch | glob | Patch concatenated x column | 102 | | clone | glob n | Clone matched columns n times | 103 | | hmax | glob | Add column with the max of matched columns | 104 | | hmin | glob | Add column with the min of matched columns | 105 | | hsum | glob | Add column with the sum of matched columns | 106 | | havg | glob | Add column with the mean of matched columns | 107 | | precision | glob digits | Set a fixed amount of fraction digits | 108 | | style | format | Format output (must be the last filter) | 109 | 110 | #### Selecting Columns 111 | 112 | The `glob` argument of a filter is used to select the columns to apply 113 | a filter to. The pattern should match the names of the columns without 114 | the unit, an optional square bracket enclosure at the end of a column 115 | name. 116 | 117 | Note the that the horizontal filters `hmax`, `hmin`, `hsum` and `havg` 118 | require that all matched columns have the same unit. 119 | 120 | #### Pretty Printing 121 | 122 | The following filters can be used to customise output: 123 | 124 | - `cut=GLOB` 125 | - `precision=GLOB,DIGITS` 126 | - `style=FORMAT` 127 | 128 | The `cut` filter selects a subset of columns to output: 129 | 130 | ``` 131 | # obj/loadplay -i loads/freq_tracking.load -o replay.csv obj/powerd++ 132 | # tools/playfilter cut='time|cpu.3.*' -- replay.csv 133 | time[s] cpu.3.rec.freq[MHz] cpu.3.rec.load[MHz] cpu.3.run.freq[MHz] cpu.3.run.load[MHz] 134 | 0.025 1700 850.0 1700 850.0 135 | 0.050 1700 0.0 1700 0.0 136 | 0.075 1700 566.7 1700 566.7 137 | 0.100 1700 0.0 1700 0.0 138 | ... 139 | ``` 140 | 141 | The `precision` filter sets a fixed number of fraction digits for 142 | the matched columns: 143 | 144 | ``` 145 | # tools/playfilter cut='time|cpu.3.*' precision='*.load',3 -- replay.csv 146 | time[s] cpu.3.rec.freq[MHz] cpu.3.rec.load[MHz] cpu.3.run.freq[MHz] cpu.3.run.load[MHz] 147 | 0.025 1700 850.000 1700 850.000 148 | 0.050 1700 0.000 1700 0.000 149 | 0.075 1700 566.700 1700 566.700 150 | 0.100 1700 0.000 1700 0.000 151 | ... 152 | ``` 153 | 154 | The `style` filter is only allowed as the last filter in the pipeline, 155 | because it produces output that is not valid filter input. It formats 156 | the output for different applications, the supported styles are: 157 | 158 | - `CSV`: Fields are separated by a `,` and column names are quoted using `"` 159 | - `MD`: The output is formatted as a markdown table 160 | 161 | ``` 162 | # tools/playfilter cut='time|cpu.3.*' precision='*.load',3 style=md -- replay.csv 163 | | time[s] | cpu.3.rec.freq[MHz] | cpu.3.rec.load[MHz] | cpu.3.run.freq[MHz] | cpu.3.run.load[MHz] | 164 | |--------:|--------------------:|--------------------:|--------------------:|--------------------:| 165 | | 0.025 | 1700 | 850.000 | 1700 | 850.000 | 166 | | 0.050 | 1700 | 0.000 | 1700 | 0.000 | 167 | | 0.075 | 1700 | 566.700 | 1700 | 566.700 | 168 | | 0.100 | 1700 | 0.000 | 1700 | 0.000 | 169 | ... 170 | ``` 171 | 172 | #### Subsampling 173 | 174 | The following filters can be used for subsampling: 175 | 176 | - `subsample=N` 177 | - `movingavg=GLOB,PRE[,POST]` 178 | 179 | If only a subset of the available lines is required, the `subsample` 180 | filter can be used: 181 | 182 | ``` 183 | # tools/playfilter cut='time|cpu.3.*' subsample=4 precision='*.load',3 style=md -- replay.csv 184 | | time[s] | cpu.3.rec.freq[MHz] | cpu.3.rec.load[MHz] | cpu.3.run.freq[MHz] | cpu.3.run.load[MHz] | 185 | |--------:|--------------------:|--------------------:|--------------------:|--------------------:| 186 | | 0.100 | 1700 | 0.000 | 1700 | 0.000 | 187 | | 0.200 | 1700 | 0.000 | 1700 | 0.000 | 188 | | 0.300 | 1700 | 0.000 | 1700 | 0.000 | 189 | | 0.400 | 1700 | 0.000 | 1700 | 0.000 | 190 | ... 191 | ``` 192 | 193 | The above example uses every fourth sample, however that means the 194 | information of the other 3 samples is not used. This can be avoided 195 | by applying a low-pass filter: 196 | 197 | ``` 198 | # tools/playfilter cut='time|cpu.3.*' movingavg='cpu*',4 subsample=4 precision='*.load',3 style=md -- replay.csv 199 | | time[s] | cpu.3.rec.freq[MHz] | cpu.3.rec.load[MHz] | cpu.3.run.freq[MHz] | cpu.3.run.load[MHz] | 200 | |--------:|--------------------:|--------------------:|--------------------:|--------------------:| 201 | | 0.100 | 1700 | 354.175 | 1700 | 354.175 | 202 | | 0.200 | 1700 | 0.000 | 1700 | 0.000 | 203 | | 0.300 | 1700 | 0.000 | 1700 | 0.000 | 204 | | 0.400 | 1700 | 0.000 | 1700 | 0.000 | 205 | ... 206 | ``` 207 | 208 | The above example uses a four sample pre-filter, i.e. every sample 209 | contains the mean value of the last four samples. Synchronised to 210 | the subsampling interval this results in the reported sample containing 211 | the mean of the original samples without overlap. For this example 212 | the `0.100 s` sample contains the mean of the original `0.025 s`, 213 | `0.050 s`, `0.075 s` and `0.100 s` samples. 214 | 215 | #### Imitating `powerd(8)` Sampling 216 | 217 | The default sample time of `powerd(8)` is `0.250 s`: 218 | 219 | ``` 220 | # tools/playfilter cut='time|cpu.0.*.load' movingavg='cpu.*',10 subsample=10 precision='*.load',3 style=md -- replay.csv 221 | | time[s] | cpu.0.rec.load[MHz] | cpu.0.run.load[MHz] | 222 | |--------:|--------------------:|--------------------:| 223 | | 0.250 | 396.670 | 396.670 | 224 | | 0.500 | 170.000 | 170.000 | 225 | | 0.750 | 0.000 | 0.000 | 226 | | 1.000 | 405.000 | 405.000 | 227 | ... 228 | ``` 229 | 230 | However `powerd(8)` uses the sum of the load of all cores. This can 231 | be achieved using one of the horizontal family of filters: 232 | 233 | - `hmax=GLOB` (horizontal maximum) 234 | - `hmin=GLOB` (horizontal minimum) 235 | - `hsum=GLOB` (horizontal sum) 236 | - `havg=GLOB` (horizontal mean) 237 | 238 | This set of filters creates a new column by aggregating data from 239 | the matched columns: 240 | 241 | ``` 242 | # tools/playfilter movingavg='cpu.*',10 subsample=10 hsum='*.run.load' hsum='*.rec.load' cut='time|sum*' precision='sum*',3 style=md -- replay.csv 243 | | time[s] | sum(cpu.{0,1,2,3}.run.load)[MHz] | sum(cpu.{0,1,2,3}.rec.load)[MHz] | 244 | |--------:|---------------------------------:|---------------------------------:| 245 | | 0.250 | 1048.340 | 1048.340 | 246 | | 0.500 | 212.500 | 212.500 | 247 | | 0.750 | 0.000 | 0.000 | 248 | | 1.000 | 2115.000 | 2115.000 | 249 | ... 250 | ``` 251 | 252 | Note there are separate filter steps for the `run.load` and `rec.load` 253 | columns to create two separate sums. 254 | 255 | #### Imitating `powerd++(8)` Sampling and Filtering 256 | 257 | The default sample rate of `powerd++(8)` is `0.5 s` and instead of 258 | the sum it uses the maximum. On top of it, it uses the mean of the 259 | last 4 sampled maxima: 260 | 261 | ``` 262 | # tools/playfilter movingavg='cpu.*',20 subsample=20 hmax='*.run.load' hmax='*.rec.load' movingavg='max*',4 cut='time|max*' precision='max*',3 style=md -- replay.csv 263 | | time[s] | max(cpu.{0,1,2,3}.run.load)[MHz] | max(cpu.{0,1,2,3}.rec.load)[MHz] | 264 | |--------:|---------------------------------:|---------------------------------:| 265 | | 0.500 | 283.335 | 283.335 | 266 | | 1.000 | 294.168 | 294.168 | 267 | | 1.500 | 446.112 | 449.445 | 268 | | 2.000 | 525.521 | 526.771 | 269 | ... 270 | ``` 271 | 272 | #### Side by Side Filter Comparisons 273 | 274 | Columns can be reproduced, so different filters can be applied to 275 | the same data: 276 | 277 | - `clone=GLOB,N` 278 | 279 | This can be used to compare the effects of different filters: 280 | 281 | ``` 282 | # tools/playfilter cut='time|cpu.0.rec.load' clone='*.load',2 movingavg='*.load.0',80 movingavg='*.load.1',40,40 precision='cpu.*',3 style=md -- replay.csv 283 | | time[s] | cpu.0.rec.load[MHz] | cpu.0.rec.load.0[MHz] | cpu.0.rec.load.1[MHz] | 284 | |--------:|--------------------:|----------------------:|----------------------:| 285 | | 0.025 | 1700.000 | 1700.000 | 236.993 | 286 | | 0.050 | 1700.000 | 1700.000 | 259.921 | 287 | | 0.075 | 566.700 | 1322.230 | 281.784 | 288 | | 0.100 | 0.000 | 991.675 | 302.652 | 289 | ... 290 | ``` 291 | 292 | The column `cpu.0.rec.load` contains the original data, `cpu.0.rec.load.0` 293 | applies a `2 s` moving average. The `cpu.0.rec.load.1` column contains 294 | a symmetric `2 s` moving average (i.e. `1 s` pre and `1 s` post), 295 | which is the best in hindsight representation of a filtered value. 296 | 297 | Plotting these illustrates that this produces the same curve with a 298 | `1 s` offset. This illustrates how a `2 s` moving average causes `1 s` 299 | of latency reacting to load events like spikes and drops. 300 | 301 | #### Serialising Multiple Replays 302 | 303 | It is possible to concatenate multiple replays, but it usually requires 304 | patching the `time` column: 305 | 306 | - `patch=GLOB` 307 | 308 | Without patching, the time column jumps back down when transitioning 309 | from one file to the next: 310 | 311 | ``` 312 | # tools/playfilter movingavg='*.run.load',20 subsample=20 hmax='*.run.load' cut='time|max*|cpu.0.run.freq' movingavg='max*',4 precision=time,3 precision='max*',1 style=md -- replay.csv replay.csv 313 | | time[s] | cpu.0.run.freq[MHz] | max(cpu.{0,1,2,3}.run.load)[MHz] | 314 | |--------:|--------------------:|---------------------------------:| 315 | | 0.500 | 1700 | 283.3 | 316 | | 1.000 | 1400 | 294.2 | 317 | | 1.500 | 1200 | 446.1 | 318 | | 2.000 | 1300 | 525.5 | 319 | ... 320 | | 28.500 | 1800 | 732.8 | 321 | | 29.000 | 2000 | 665.3 | 322 | | 29.500 | 1900 | 690.1 | 323 | | 30.000 | 1900 | 810.0 | 324 | | 0.500 | 1700 | 593.3 | 325 | | 1.000 | 1400 | 650.8 | 326 | | 1.500 | 1200 | 525.8 | 327 | | 2.000 | 1300 | 525.5 | 328 | ... 329 | | 28.500 | 1800 | 732.8 | 330 | | 29.000 | 2000 | 665.3 | 331 | | 29.500 | 1900 | 690.1 | 332 | | 30.000 | 1900 | 810.0 | 333 | ``` 334 | 335 | The `patch` filter uses the previous value as an offset for following values 336 | if the new value is less than or equal to the previous one: 337 | 338 | ``` 339 | # tools/playfilter patch=time movingavg='*.run.load',20 subsample=20 hmax='*.run.load' cut='time|max*|cpu.0.run.freq' movingavg='max*',4 precision=time,3 precision='max*',1 style=md -- replay.csv replay.csv 340 | | time[s] | cpu.0.run.freq[MHz] | max(cpu.{0,1,2,3}.run.load)[MHz] | 341 | |--------:|--------------------:|---------------------------------:| 342 | | 0.500 | 1700 | 283.3 | 343 | | 1.000 | 1400 | 294.2 | 344 | | 1.500 | 1200 | 446.1 | 345 | | 2.000 | 1300 | 525.5 | 346 | ... 347 | | 28.500 | 1800 | 732.8 | 348 | | 29.000 | 2000 | 665.3 | 349 | | 29.500 | 1900 | 690.1 | 350 | | 30.000 | 1900 | 810.0 | 351 | | 30.500 | 1700 | 593.3 | 352 | | 31.000 | 1400 | 650.8 | 353 | | 31.500 | 1200 | 525.8 | 354 | | 32.000 | 1300 | 525.5 | 355 | ... 356 | | 58.500 | 1800 | 732.8 | 357 | | 59.000 | 2000 | 665.3 | 358 | | 59.500 | 1900 | 690.1 | 359 | | 60.000 | 1900 | 810.0 | 360 | ``` 361 | -------------------------------------------------------------------------------- /src/Options.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides the nih::Options functor template, a substitute for `getopt(3)`. 3 | * 4 | * The `getopt(3)` interface takes the command line arguments as `char * const` 5 | * instead of `char const *`. I.e. it reserves the right to mutate the 6 | * provided arguments, which it actually does. 7 | * 8 | * The nih::Options functor is not a drop in substitute, but tries 9 | * to be easily adoptable and does not change the data entrusted to it. 10 | * 11 | * To use the options an enum or enum class is required, e.g.: 12 | * 13 | * ~~~~{.cpp} 14 | * enum class MyOptions { 15 | * USAGE, FILE_IN, FILE_OUT, FLAG_VERBOSE, 16 | * OPT_UNKNOWN, OPT_NOOPT, OPT_DASH, OPT_LDASH, OPT_DONE 17 | * }; 18 | * ~~~~ 19 | * 20 | * The options prefixed with `OPT_` are obligatory. Their meaning is 21 | * documented in nih::enum_has_members<>. Their presence is validated 22 | * at compile time. 23 | * 24 | * The enum values are returned when matching the next argument to 25 | * a parameter. In order to do that a usage string and a list of 26 | * parameter definitions are required: 27 | * 28 | * ~~~~{.cpp} 29 | * static char const * const USAGE = "[-hv] [-i file] [-o file] [command ...]"; 30 | * 31 | * static nih::Parameter const PARAMETERS[]{ 32 | * {MyOptions::USAGE, 'h', "help", "", "Show this help"}, 33 | * {MyOptions::USAGE, 0, "usage", "", ""}, 34 | * {MyOptions::FILE_IN, 'i', "in", "file", "Input file"}, 35 | * {MyOptions::FILE_OUT, 'o', "out", "file", "Output file"}, 36 | * {MyOptions::FLAG_VERBOSE, 'v', "verbose", "", "Verbose output"} 37 | * }; 38 | * ~~~~ 39 | * 40 | * Each entry in the array defines a parameter consisting of the following: 41 | * 42 | * | Field | Meaning | 43 | * |----------|---------------------------------------------------| 44 | * | `option` | The option symbol (enum value) | 45 | * | `sparam` | An optional parameter character (short parameter) | 46 | * | `lparam` | An optional long parameter string | 47 | * | `args` | A comma separated list of parameter arguments | 48 | * | `usage` | A descriptive string | 49 | * 50 | * Multiple parameters may be mapped to a single option (e.g. `--help` 51 | * and `--usage`). Parameters without arguments are called flags. It 52 | * is possible to map parameters with different numbers of arguments 53 | * to a single option, but this is arguably semantically confusing 54 | * and should not be done. 55 | * 56 | * Multiple flags' parameter characters can be concatenated in an argument. 57 | * A parameter with arguments' character can appear at the end of a 58 | * character chain. The first argument to the parameter may be concatenated 59 | * as well. E.g. `-v -i file`, `-vi file` and `-vifile` are all equivalent. 60 | * Parameters' string representations always stand alone, they can 61 | * neither be combined with each other nor with parameter characters. 62 | * E.g. `--verbose --in file` is the equivalent parameter string 63 | * representation. 64 | * 65 | * The usage string and the parameter usage strings are used to assemble 66 | * the string provided by the nih::Options<>::usage() method. 67 | * 68 | * The parameter definitions must be passed to nih::Options constructor 69 | * to create the functor: 70 | * 71 | * ~~~~{.cpp} 72 | * #include 73 | * ... 74 | * 75 | * int main(int argc, char * argv[]) { 76 | * char const * infile = "-"; 77 | * char const * outfile = "-"; 78 | * bool verbose = false; 79 | * 80 | * auto getopt = nih::Options{argc, argv, USAGE, PARAMETERS}; 81 | * while (true) switch (getopt()) { // get new option/argument 82 | * case MyOptions::USAGE: 83 | * std::cerr << getopt.usage(); // show usage 84 | * return 0; 85 | * case MyOptions::FILE_IN: 86 | * infile = getopt[1]; // get first argument 87 | * break; 88 | * case MyOptions::FILE_OUT: 89 | * outfile = getopt[1]; // get first argument 90 | * break; 91 | * case MyOptions::FLAG_VERBOSE: 92 | * verbose = true; 93 | * break; 94 | * case MyOptions::OPT_UNKNOWN: 95 | * case MyOptions::OPT_NOOPT: 96 | * case MyOptions::OPT_DASH: 97 | * case MyOptions::OPT_LDASH: 98 | * std::cerr << "Unexpected command line argument: " 99 | * << getopt[0] << '\n'; // output option/argument 100 | * return 1; 101 | * case MyOptions::OPT_DONE: 102 | * return do_something(infile, outfile, verbose); 103 | * } 104 | * return 0; 105 | * } 106 | * ~~~~ 107 | * 108 | * Every call of the functor moves on to the next parameter or argument. 109 | * For non-option arguments it returns `OPT_NOOPT`. 110 | * 111 | * The `getopt[1]` calls return the first argument following the option. It 112 | * is possible to retrieve more arguments than were defined in the options 113 | * definition. The `[]` opterator always returns a valid, terminated string 114 | * (provided the command line arguments are valid, terminated strings). So 115 | * it is always safe to dereference the pointer, even when reading beyond the 116 | * end of command line arguments. 117 | * 118 | * The `getopt[0]` calls return the command line argument that contains the 119 | * selected option. So in the `FILE_IN` case it could be any of `-i`, `--in`, 120 | * `-vi`, `-ifile` or `-vifile`. This is useful for the `OPT_UNKNOWN` 121 | * and `OPT_NOOPT` cases. The `getopt[1]` call on the other hand would 122 | * return `file` regardless of argument chaining. 123 | * 124 | * @file 125 | */ 126 | 127 | #ifndef _POWERDXX_NIH_OPTIONS_HPP_ 128 | #define _POWERDXX_NIH_OPTIONS_HPP_ 129 | 130 | #include "utility.hpp" /* utility::literals */ 131 | 132 | #include /* size_t */ 133 | #include /* std::true_type, std::void_t */ 134 | #include /* assert() */ 135 | 136 | /** 137 | * Not invented here namespace, for code that substitutes already commonly 138 | * available functionality. 139 | */ 140 | namespace nih { 141 | 142 | /** 143 | * Tests whether the given enum provides all the required definitions. 144 | * 145 | * The Options<> template expects the provided enum to provide the 146 | * following members: 147 | * 148 | * | Member | Description | 149 | * |-------------|--------------------------------------------------------| 150 | * | OPT_UNKNOWN | An undefined option (long or short) was encountered | 151 | * | OPT_NOOPT | The encountered command line argument is not an option | 152 | * | OPT_DASH | A single dash "-" was encountered | 153 | * | OPT_LDASH | Double dashes "--" were encountered | 154 | * | OPT_DONE | All command line arguments have been processed | 155 | * 156 | * @tparam OptionT 157 | * An enum or enum class representing the available options 158 | */ 159 | template 160 | struct enum_has_members : /** @cond */ std::false_type {}; 161 | 162 | template 163 | struct enum_has_members> : 168 | /** @endcond */ std::true_type {}; 169 | 170 | /** 171 | * Container for an option definition. 172 | * 173 | * Aliases can be defined by creating definitions with the same option 174 | * member. 175 | * 176 | * The lparam, args and usage members have to be 0 terminated, using string 177 | * literals is safe. 178 | * 179 | * @tparam OptionT 180 | * An enum or enum class representing the available options 181 | */ 182 | template 183 | struct Parameter { 184 | /** 185 | * The enum value to return for this option. 186 | */ 187 | OptionT option; 188 | 189 | /** 190 | * The short version of this parameter. 191 | * 192 | * Set to 0 if no short parameter is available. 193 | */ 194 | char sparam; 195 | 196 | /** 197 | * The long version of this parameter. 198 | * 199 | * Set to nullptr or "" if no long parameter is available. 200 | */ 201 | char const * lparam; 202 | 203 | /** 204 | * A comma separated list of arguments. 205 | * 206 | * Set to nullptr or "" if no argument is available. 207 | */ 208 | char const * args; 209 | 210 | /** 211 | * A usage string. 212 | */ 213 | char const * usage; 214 | }; 215 | 216 | /** 217 | * Retrieves the count of arguments in an option definition. 218 | * 219 | * @tparam OptionT 220 | * An enum or enum class representing the available options 221 | * @param def 222 | * The parameter definition 223 | * @return 224 | * The number of arguments specified in the given definition 225 | */ 226 | template 227 | size_t argCount(Parameter const & def) { 228 | if (!def.args || !def.args[0]) { return 0; } 229 | size_t argc = 1; 230 | for (char const * pch = def.args; *pch; ++pch) { 231 | argc += (*pch == ',' ? 1 : 0); 232 | } 233 | return argc; 234 | } 235 | 236 | /** 237 | * An instance of this class offers operators to retrieve command line 238 | * options and arguments. 239 | * 240 | * Check the `operator ()` and `operator []` for use. 241 | * 242 | * @tparam OptionT 243 | * An enum or enum class matching the requirements set by 244 | * enum_has_members 245 | * @tparam DefCount 246 | * The number of option definitions 247 | */ 248 | template 249 | class Options { 250 | static_assert(enum_has_members::value, 251 | "The enum must have the members OPT_UNKNOWN, OPT_NOOPT, OPT_DASH, OPT_LDASH and OPT_DONE"); 252 | private: 253 | /** 254 | * The number of command line arguments. 255 | */ 256 | int const argc; 257 | 258 | /** 259 | * The command line arguments. 260 | */ 261 | char const * const * const argv; 262 | 263 | /** 264 | * A string literal for the usage() output. 265 | */ 266 | char const * const usageStr; 267 | 268 | /** 269 | * A reference to the option definitions. 270 | */ 271 | Parameter const (& defs)[DefCount]; 272 | 273 | /** 274 | * The option definition to use for unknown options. 275 | */ 276 | Parameter const opt_unknown{ 277 | OptionT::OPT_UNKNOWN, 0, nullptr, nullptr, nullptr 278 | }; 279 | 280 | /** 281 | * The option definition to use for non-options. 282 | */ 283 | Parameter const opt_noopt{ 284 | OptionT::OPT_NOOPT, 0, nullptr, nullptr, nullptr 285 | }; 286 | 287 | /** 288 | * The option definition to use for a single dash. 289 | */ 290 | Parameter const opt_dash{ 291 | OptionT::OPT_DASH, 0, nullptr, nullptr, nullptr 292 | }; 293 | 294 | /** 295 | * The option definition to use for a single double-dash. 296 | */ 297 | Parameter const opt_ldash{ 298 | OptionT::OPT_LDASH, 0, nullptr, nullptr, nullptr 299 | }; 300 | 301 | /** 302 | * The index of the command line argument containing the current 303 | * option. 304 | */ 305 | int argi; 306 | 307 | /** 308 | * Points to the current short option character. 309 | */ 310 | char const * argp; 311 | 312 | /** 313 | * Points to the current option definition. 314 | */ 315 | Parameter const * current; 316 | 317 | /** 318 | * The argument index to show if no argument is supplied 319 | * to show(). 320 | * 321 | * This is initially 0 for each new argument and updated 322 | * by use of the subscript operator. 323 | * 324 | * This is for error handling convenience and not considered 325 | * part of the state. 326 | */ 327 | int mutable showi; 328 | 329 | /** 330 | * Returns a pointer to the file name portion of the given string. 331 | * 332 | * @param file 333 | * The string containing the path to the file 334 | * @return 335 | * A pointer to the file name portion of the path 336 | */ 337 | static char const * removePath(char const * const file) { 338 | auto result = file; 339 | for (auto ptr = file; *ptr; ++ptr) { 340 | if (*ptr == '/' || *ptr == '\\') { 341 | result = ptr + 1; 342 | } 343 | } 344 | return result; 345 | } 346 | 347 | /** 348 | * Returns true if the given strings match. 349 | * 350 | * @param lstr,rstr 351 | * Two 0 terminated strings 352 | * @retval true 353 | * The given strings match 354 | * @retval false 355 | * The strings do not match 356 | */ 357 | static bool match(char const * const lstr, char const * const rstr) { 358 | size_t i = 0; 359 | for (; lstr[i] && rstr[i]; ++i) { 360 | if (lstr[i] != rstr[i]) { return false; } 361 | } 362 | return lstr[i] == rstr[i]; 363 | } 364 | 365 | /** 366 | * Returns true if the given string starts with the given prefix. 367 | * 368 | * @param str,prefix 369 | * Two 0 terminated strings 370 | * @retval true 371 | * The string starts with the prefix 372 | * @retval false 373 | * The string does not start with the prefix 374 | */ 375 | static bool bmatch(char const * const str, 376 | char const * const prefix) { 377 | size_t i = 0; 378 | for (; str[i] && prefix[i]; ++i) { 379 | if (str[i] != prefix[i]) { return false; } 380 | } 381 | return !prefix[i]; 382 | } 383 | 384 | /** 385 | * Finds the short option matching the given character. 386 | * 387 | * @param ch 388 | * The short option to find 389 | * @return 390 | * An option definition by reference 391 | */ 392 | Parameter const & get(char const ch) { 393 | for (auto const & def : this->defs) { 394 | if (def.sparam == ch) { 395 | return def; 396 | } 397 | } 398 | return this->opt_unknown; 399 | } 400 | 401 | /** 402 | * Finds the long option matching the given string. 403 | * 404 | * @param str 405 | * The long option to find 406 | * @return 407 | * An option definition by reference 408 | */ 409 | Parameter const & get(char const * const str) { 410 | for (auto const & def : this->defs) { 411 | if (match(str, def.lparam)) { 412 | return def; 413 | } 414 | } 415 | return this->opt_unknown; 416 | } 417 | 418 | public: 419 | /** 420 | * Construct an options functor. 421 | * 422 | * @param argc,argv 423 | * The command line arguments 424 | * @param usage 425 | * A usage string following "usage: progname " 426 | * @param defs 427 | * An array of parameter definitions 428 | */ 429 | Options(int const argc, char const * const * const argv, 430 | char const * const usage, 431 | Parameter const (& defs)[DefCount]) : 432 | argc{argc}, argv{argv}, usageStr{usage}, defs{defs}, 433 | argi{0}, argp{nullptr}, current{nullptr}, showi{0} {} 434 | 435 | /** 436 | * Updates the internal state by parsing the next option. 437 | * 438 | * When reaching the end of the argument list, the internal 439 | * state is reset, so a successive call will restart the 440 | * argument parsing. 441 | * 442 | * @return 443 | * A self-reference 444 | */ 445 | Options & operator ()() { 446 | /* reset what to show() */ 447 | this->showi = 0; 448 | 449 | /* 450 | * point argi and argp to the appropriate places 451 | */ 452 | if (this->current) { 453 | /* this is not the first call */ 454 | if (this->argp && this->argp[0] && this->argp[1]) { 455 | /* argp is set and does not point to the end 456 | * of an argument */ 457 | if (argCount(*this->current) == 0) { 458 | /* proceed to the next short option */ 459 | ++this->argp; 460 | } else { 461 | /* the chained characters were an 462 | * option argument */ 463 | this->argp = nullptr; 464 | this->argi += argCount(*this->current); 465 | } 466 | } else { 467 | /* point forward for the option stand alone 468 | * case */ 469 | this->argp = nullptr; 470 | this->argi += argCount(*this->current) + 1; 471 | } 472 | } else { 473 | /* no current state, start with the first argument */ 474 | this->argi = 1; 475 | this->argp = nullptr; 476 | } 477 | 478 | /* 479 | * match the current option 480 | */ 481 | /* ran out of options */ 482 | if (this->argi >= this->argc) { 483 | /* reset state */ 484 | this->current = nullptr; 485 | return *this; 486 | } 487 | /* continue short option chain */ 488 | if (this->argp) { 489 | this->current = &get(this->argp[0]); 490 | return *this; 491 | } 492 | /* long option */ 493 | if (bmatch(this->argv[this->argi], "--")) { 494 | if (!this->argv[this->argi][2]) { 495 | this->current = &this->opt_ldash; 496 | return *this; 497 | } 498 | this->current = &get(this->argv[this->argi] + 2); 499 | return *this; 500 | } 501 | /* short option */ 502 | if (this->argv[this->argi][0] == '-') { 503 | if (!this->argv[this->argi][1]) { 504 | this->current = &this->opt_dash; 505 | return *this; 506 | } 507 | this->argp = this->argv[this->argi] + 1; 508 | this->current = &get(this->argp[0]); 509 | return *this; 510 | } 511 | /* not an option */ 512 | this->current = &this->opt_noopt; 513 | return *this; 514 | } 515 | 516 | /** 517 | * Implicitly cast to the current option. 518 | * 519 | * @return 520 | * An OptionT member representing the current option 521 | * @retval OPT_UNKNOWN 522 | * An option that was not in the list of option definitions 523 | * was encountered 524 | * @retval OPT_NOOPT 525 | * An argument that is not an option was encountered 526 | * @retval OPT_DASH 527 | * A lone dash "-" was encountered 528 | * @retval OPT_LDASH 529 | * A lone long dash "--" was encountered 530 | * @retval OPT_DONE 531 | * All arguments have been processed, or argument processing 532 | * has not yet started 533 | */ 534 | operator OptionT() const { 535 | return this->current 536 | ? this->current->option 537 | : OptionT::OPT_DONE; 538 | } 539 | 540 | /** 541 | * Retrieve arguments to the current option. 542 | * 543 | * The string containing the current option is returned with i = 0, 544 | * the arguments following the option with greater values of i. 545 | * 546 | * When no more arguments are left the empty string is returned. 547 | * 548 | * @param i 549 | * The index of the argument to retrieve 550 | * @return 551 | * The option or one of its arguments 552 | */ 553 | char const * operator [](int const i) const { 554 | /* show() the latest requested argument */ 555 | this->showi = i; 556 | 557 | /* argument is in the same string as option */ 558 | if (this->argp && this->argp[0] && this->argp[1] && i > 0) { 559 | if (this->argi + i - 1 >= this->argc) { 560 | return ""; 561 | } 562 | if (i == 1) { 563 | return this->argp + 1; 564 | } 565 | return this->argv[this->argi + i - 1]; 566 | } 567 | /* read in front of arguments */ 568 | if (this->argi + i < 0) { 569 | return ""; 570 | } 571 | /* argument is in the string following the option */ 572 | if (this->argi + i < this->argc) { 573 | return this->argv[this->argi + i]; 574 | } 575 | /* read beyond end of arguments */ 576 | return ""; 577 | } 578 | 579 | /** 580 | * Returns a string for usage output, created from the option 581 | * definitions. 582 | * 583 | * @return 584 | * A usage string for printing on the CLI 585 | */ 586 | std::string usage() const { 587 | using namespace utility::literals; 588 | auto result = "usage: %s %s\n\n"_fmt 589 | (removePath(this->argv[0]), this->usageStr); 590 | 591 | /* get column sizes */ 592 | size_t lparam_max = 0; /* length of the longest option */ 593 | size_t args_max = 0; /* length of the longest argument */ 594 | for (auto const & def : this->defs) { 595 | if (def.lparam) { 596 | auto const len = std::string{def.lparam}.size(); 597 | lparam_max = 598 | lparam_max >= len ? lparam_max : len; 599 | } 600 | auto const len = std::string{def.args}.size(); 601 | args_max = args_max >= len ? args_max : len; 602 | } 603 | /* print columns */ 604 | for (auto const & def : this->defs) { 605 | /* get option */ 606 | auto const sparam = def.sparam ? def.sparam : ' '; 607 | auto const sdash = def.sparam ? '-' : ' '; 608 | auto const lparam = def.lparam ? def.lparam : ""; 609 | auto const ldash = lparam[0] ? '-' : ' '; 610 | auto const sep = def.sparam && lparam[0] ? ',' : ' '; 611 | result += " %c%c%c %c%c%-*s "_fmt 612 | (sdash, sparam, sep, 613 | ldash, ldash, lparam_max, lparam); 614 | /* get arguments */ 615 | auto len = args_max; 616 | for (auto it = def.args; it && *it; ++it, --len) { 617 | result += *it == ',' ? ' ' : *it; 618 | } 619 | result += "%-*s %s\n"_fmt(len, "", def.usage); 620 | } 621 | return result; 622 | } 623 | 624 | /** 625 | * Provide a string containing the entire command line, with the 626 | * indexed argument highlighted. 627 | * 628 | * The current implementation highlights arguments by underlining 629 | * them with `^~~~`. 630 | * 631 | * @param i 632 | * The argument index, like operator [] 633 | * @param n 634 | * The number of arguments to highlight, highlights all 635 | * remaining arguments if n <= 0 636 | * @return 637 | * A string formatted to highlight the given argument 638 | */ 639 | utility::Underlined show(int const i, int const n = 1) const { 640 | using utility::highlight; 641 | using std::string; 642 | 643 | /* select a whole argument */ 644 | char const * const select = (*this)[i]; 645 | /* if the current option (i == 0) is requested, pick 646 | * up the pointer to the current short option character */ 647 | char const * const argp = i == 0 ? this->argp : nullptr; 648 | string cmd; /* command and arguments string */ 649 | ptrdiff_t offset = -1; /* the underline byte-offset */ 650 | ptrdiff_t length = 1; /* the underline byte-length */ 651 | int remaining = 0; /* #args left to underline */ 652 | /* build cmd string */ 653 | for (int p = 0; p < this->argc; ++p) { 654 | /* build each argument character wise */ 655 | for (auto it = this->argv[p]; *it; ++it) { 656 | /* underline short option */ 657 | if (argp && it == argp) { 658 | remaining = n > 0 ? n : -1; 659 | it[1] && --remaining; 660 | offset = cmd.size(); 661 | } 662 | /* underline long option / argument */ 663 | if (!argp && it == select) { 664 | remaining = n > 0 ? n : -1; 665 | offset = cmd.size(); 666 | } 667 | /* regular character */ 668 | cmd += *it; 669 | } 670 | /* update the underline length */ 671 | remaining && (length = cmd.size() - offset); 672 | remaining > 0 && --remaining; 673 | /* space separate arguments */ 674 | cmd += ' '; 675 | } 676 | /* the selected argument must be behind the last argument, 677 | * e.g. because the last parameter is missing an argument 678 | * or the state is OPT_DONE */ 679 | offset >= 0 || i < 0 || (offset = cmd.size()); 680 | return highlight(cmd, offset, length); 681 | } 682 | 683 | /** 684 | * Highlight the last recently accessed argument. 685 | + 686 | * @return 687 | * A string with the last recently accessed argument underlined 688 | * @see show(int const, int const = 1) 689 | */ 690 | std::string show() const { 691 | return show(this->showi); 692 | } 693 | 694 | /** 695 | * Returns the argument offset of the current parameter/argument. 696 | * 697 | * @warning 698 | * This may return a value >= argc if the current state is 699 | * OptionT::OPT_DONE 700 | * @return 701 | * The current argument index 702 | */ 703 | int offset() const { 704 | return this->argi; 705 | } 706 | }; 707 | 708 | } /* namespace nih */ 709 | 710 | #endif /* _POWERDXX_NIH_OPTIONS_HPP_ */ 711 | --------------------------------------------------------------------------------