CPU Load is a library that reports CPU load of the system, of a specific process by its ID, or a specific application by its name (meaning every one of its processes running when a sample is taken).
The library is written in Ada, and also provides a C interface so it can be used from any language with a C FFI (C, C++, Java, Python, Rust, etc.).
Detailed documentation (including user and reference guides) is available at: https://joular.github.io/cpuload/.
| What is measured | OS | Method |
|---|---|---|
| The whole system | Linux | The cpu line of /proc/stat |
| The whole system | macOS | host_statistics, macOS' CPU counters |
| The whole system | Windows | GetSystemTimes |
| The whole system | FreeBSD | kern.cp_time through sysctl |
| One process, by its number | Linux | utime + stime of /proc/<pid>/stat |
| One process, by its number | macOS | proc_pidinfo, the user and system time of the process |
| One process, by its number | Windows | OpenProcess + GetProcessTimes |
| One process, by its number | FreeBSD | kern.proc through sysctl, the CPU time of the process |
| An application, every process of it | Linux | /proc scanned, each process named by /proc/<pid>/exe |
| An application, every process of it | macOS | proc_listpids, each process named by proc_pidpath |
| An application, every process of it | Windows | EnumProcesses + QueryFullProcessImageNameW |
| An application, every process of it | FreeBSD | kern.proc listed, each process named by kern.proc.pathname |
Every OS matches the program with the process that actually runs, so firefox finds every process of Firefox, its content processes included. On macOS that is the program inside the bundle, so firefox finds the application inside Firefox.app, and also every program inside Firefox.app: for example, its content processes run plugin-container, from a helper bundle inside it. On Windows a trailing .exe is ignored, so firefox also finds firefox.exe.
On Linux, a process whose /proc/<pid>/exe cannot be read (such as a kernel thread, which runs no program directly, or another user's process) falls back on /proc/<pid>/comm. This file have the given name of the process (with a max size of 15 character).
macOS is supported on Apple Silicon and Intel Macs, with the library built for the chip it runs on. An x86_64 build running under Rosetta on an Apple Silicon Mac reads process times about 40 times too low, so process and application loads come out close to 0%.
FreeBSD is supported on 64-bit systems only. A process whose program cannot be named (a kernel process, which runs none, or a process whose program was replaced while it runs) falls back on its command name, which the kernel cuts to 19 characters.
Every counter in a Sample is in microseconds, on every system. Both loads run from 0.0 to 1.0 and are a share of the whole machine, not of one core: a process using all of one core of an eight core machine reads 0.125, not 1.0.
A negative load means it could not be read at all: not running (a process that has ended but is not yet cleaned up by the system counts as not running), or not allowed to get the information needed. That is not the same as 0.0, which means no CPU time used at all.
For an application, a process that could not be read is left out of the sum, so the figure is short by what it used. The answer is negative only when some of the application's processes are running and none of them would say anything at all. An application that is not running reads 0.0. A process of the application that ends between two samples takes all of its time out of the second one, so that stretch reads low, or 0.0.
The library keeps no state. Linked statically into an Ada program, it can be called from any number of tasks. The shared library must be called from one thread or task at a time, whatever the language: it carries its own Ada runtime without tasking, which has a single working stack for the whole process. A sample is a plain record either way, so it can be taken in one thread and compared in another.
With Alire:
alr buildOr directly with GNAT:
gprbuild -P cpuload.gprOn FreeBSD, build with GNAT 15 or newer: pkg install gprbuild gnat15, then add /usr/local/gnat15/bin to PATH. GNAT 12, which pkg install gprbuild uses, ignores the pragma that keeps the shared library away from the signal handlers of the program loading it. Alire takes the GNAT in PATH there too, and refuses an older one.
The build produces a static library by default, and detects the system automatically: Linux, macOS, Windows and FreeBSD are each recognised from the target gprbuild reports, so nothing has to be passed. -XPJ_OS still says which system to build for (linux, macos, windows or freebsd) when it is not the one of the machine building it. A library built for another system reads no counters at all and reports 0% for everything.
For other library types (shared, etc.), set -XCPULOAD_LIBRARY_TYPE:
gprbuild -P cpuload.gpr -XCPULOAD_LIBRARY_TYPE=relocatablerelocatable builds the shared library (libcpuload.so / .dll / .dylib) that carries the C interface and is standalone: it starts itself up when loaded. On Linux, FreeBSD and Windows it is encapsulated as well, carrying the Ada runtime with it, so it is one self-contained file. On macOS it cannot be encapsulated, so the Ada runtime is a separate file: the library records the folder of the runtime of the compiler that built it, and loads it from there with nothing to set (no DYLD_LIBRARY_PATH, so it also works under sudo and from the system's Python).
with Ada.Text_IO; use Ada.Text_IO;
with CPU_Load; use CPU_Load;
procedure Measure is
-- The first sample, which the first reading below is measured against
Before : Sample := Take ("firefox");
After : Sample;
begin
for I in 1 .. 5 loop
delay 1.0;
After := Take ("firefox");
-- The same pair of samples gives both figures
Put_Line ("machine:" & Long_Float'Image (100.0 * System_Usage (Before, After)) & " %");
Put_Line ("firefox:" & Long_Float'Image (100.0 * Process_Usage (Before, After)) & " %");
-- This reading becomes the one the next is measured against
Before := After;
end loop;
end Measure;The whole interface is Sample, the three Take functions (the system alone, one Process_ID, or an application by name).
To follow several things at once, read the machine once and measure each of them against that one reading. Each is then measured over exactly the same period of time, and the machine's counters are read once instead of once per thing:
Machine := Take;
Ours := Take (Our_PID, Machine);
Theirs := Take ("firefox", Machine);A full example program is in example/src/example_cpu_load.adb. It follows the machine, itself, and an application named on the command line, once per second until stopped with Ctrl+C:
gprbuild -P example/example.gpr -p
./example/example_cpu_load firefoxA number after the name stops it after that many readings ("" follows no application), which is what the CI runs: ./example/example_cpu_load "" 3.
The counters need no particular rights on any system. When the machine reads nothing at all, the example says what to look into on the system it was built for.
With Alire, add the library to your project with alr with cpuload.
The C declarations are in include/cpuload.h. Build the relocatable library, then:
#include "cpuload.h"
cpuload_sample before, after;
cpuload_take_app("firefox", &before);
sleep(1);
cpuload_take_app("firefox", &after);
printf("machine: %.2f%%\n", 100.0 * cpuload_system_usage(&before, &after));
printf("firefox: %.2f%%\n", 100.0 * cpuload_process_usage(&before, &after));cpuload_take_pid_with and cpuload_take_app_with are the same as cpuload_take_pid and cpuload_take_app, but measure against a machine sample already taken instead of taking another one:
cpuload_sample machine, mine, theirs;
cpuload_take_system(&machine);
cpuload_take_pid_with(getpid(), &machine, &mine);
cpuload_take_app_with("firefox", &machine, &theirs);A full example program is in example/c/main.c. Like the Ada one, it follows the machine, itself, and an application named on the command line, once per second until stopped with Ctrl+C. It comes with a Makefile that builds the shared library and the program:
make -C example/c run APP=firefoxOn FreeBSD, the Makefile needs GNU make: run gmake instead of make.
To build it by hand instead, from the root of the repository, first compile the library:
gprbuild -P cpuload.gpr -XCPULOAD_LIBRARY_TYPE=relocatableThen compile the C program:
gcc example/c/main.c -Iinclude -Llib/relocatable -lcpuload -Wl,-rpath,"$PWD/lib/relocatable" -o example/c/example_c-I is the folder holding cpuload.h, -L and -l the library to link with, and -rpath the folder where the program looks for the library when it runs. Without -rpath, the program still compiles but stops on start with a "library not loaded" error, unless you set LD_LIBRARY_PATH yourself. macOS works the same way. Windows has no -rpath: put a copy of the DLL next to the program instead (which is what the Makefile does).
From Python, the same interface through ctypes:
import ctypes, time
class Sample(ctypes.Structure):
_fields_ = [("busy", ctypes.c_int64), ("total", ctypes.c_int64), ("used", ctypes.c_int64)]
lib = ctypes.CDLL("lib/relocatable/libcpuload.so") # libcpuload.dylib on macOS, libcpuload.dll on Windows
lib.cpuload_system_usage.restype = ctypes.c_double
lib.cpuload_process_usage.restype = ctypes.c_double
lib.cpuload_version.restype = ctypes.c_char_p
before, after = Sample(), Sample()
lib.cpuload_take_app(b"firefox", ctypes.byref(before))
time.sleep(1)
lib.cpuload_take_app(b"firefox", ctypes.byref(after))
print("machine:", 100.0 * lib.cpuload_system_usage(ctypes.byref(before), ctypes.byref(after)), "%")
print("firefox:", 100.0 * lib.cpuload_process_usage(ctypes.byref(before), ctypes.byref(after)), "%")cpuload_take_pid_with and cpuload_take_app_with are there as well, taking the machine sample as their middle argument. example/python/main.py uses them, and declares the types of every function it calls, which ctypes needs to keep the double values whole on the way back.
Note that Python puts its own Ctrl+C handler back after loading the library if you want to stop a reading loop that way: the Ada runtime installs its own while it starts up.
Java (through FFM or JNA), Rust (through libloading or FFI declarations), and every other language with a C FFI work the same way.
The package spec src/cpu_load.ads and its body src/cpu_load.adb are shared by every OS. They hold the Take and usage functions, and what an application is: every process running its program, their times added up.
Each OS has its own body of src/cpu_load-platform.ads, which has four functions about the machine:
Measure_System: the machine's own CPU countersUsed_By_PID: the CPU time one process has usedRuns: whether a process runs the program an application is named byFor_Each_Process: every process running
One body per OS lives in src/linux, src/macos, src/windows and src/freebsd, and cpuload.gpr picks the folder for the OS being built from the PJ_OS symbol.
To support a new OS, write a body of CPU_Load.Platform for it and add its folder there.
CPU Load is licensed under the GNU Lesser General Public License 3 license only (LGPL-3.0-only).
Copyright ยฉ 2026, Adel Noureddine. All rights reserved. This program and the accompanying materials are made available under the terms of the GNU Lesser General Public License v3.0 (LGPL-3.0-only) which accompanies this distribution.
Author: Prof. Adel Noureddine
