Overview
Joular Core is an Ada library that measures the energy or power consumption of hardware components. It detects automatically what the machine offers (which CPU, which GPU, and how to read them), and gives one simple interface: open, read, close.
Joular Core is part of the Joular project.
The official website is: https://www.noureddine.org/research/joular/joularcore.
It compiles to native code with minimal overhead, and also provides a C interface, so it can be used from any language with a C FFI (C, C++, Java, Python, Rust, etc.).
Joular Core is the library PowerJoular uses to measure the hardware.
Key Features
- Measure the CPU and the GPU on Linux, Windows, macOS and FreeBSD
- Measure Intel and AMD processors through RAPL, on Linux, Windows and FreeBSD
- Measure Apple Silicon and Intel Macs through powermetrics
- Measure Raspberry Pi and Asus Tinker Board through our research-based regression power models
- Measure Nvidia, AMD and Apple Silicon GPUs
- Detect automatically the hardware and how to read it, with nothing to configure
- Report a source that is not there or cannot be read as not available, while the other sources keep working
- Static library for Ada programs, and a shared library with a C interface for other languages (one self-contained file on Linux, Windows and FreeBSD)
License
Joular Core is licensed under the GNU LGPL 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 only (LGPL-3.0-only).
Author: Adel Noureddine
Supported Platforms
Joular Core runs on Linux, Windows, macOS and FreeBSD, on PCs, servers, Macs and single-board computers. The tables below summarise what is supported on each platform and architecture.
Operating Systems and Architectures
CPU
| OS / Architecture | x86_64 | x86 | Apple Silicon | arm | aarch64 |
|---|---|---|---|---|---|
| Linux (PC / servers) | ✓ | ✓ | |||
| Windows | ✓ | ||||
| macOS | ✓ | ✓ | |||
| FreeBSD | ✓ | ||||
| SBC (Raspberry Pi, Asus Tinker Board) | ✓ | ✓ |
GPU
| OS / Architecture | Nvidia | AMD | Apple GPU |
|---|---|---|---|
| Linux (PC / servers) | ✓ | ✓ | |
| Windows | ✓ | ✓ | |
| macOS | ✓ | ||
| FreeBSD | ✓ | ||
| SBC (Raspberry Pi) |
Platform Details
| Platform | OS | Power source | Reports |
|---|---|---|---|
| Linux PC / Server | Linux | RAPL through powercap sysfs, Nvidia through NVML, AMD through amdgpu hwmon sysfs | Energy (CPU), power (GPU) |
| Windows PC / Server | Windows | RAPL through the Energy Meter Interface, PawnIO or Hubblo’s driver, Nvidia through NVML, AMD through ADLX | Energy (CPU), power (GPU) |
| macOS (Apple Silicon) | macOS | powermetrics (CPU and GPU) | Power |
| macOS (Intel) | macOS | powermetrics (CPU only) | Power |
| Raspberry Pi | Linux | Regression power models | Power |
| Asus Tinker Board (S) | Linux | Regression power models | Power |
| FreeBSD PC / Server | FreeBSD | RAPL through the cpuctl driver, Nvidia through NVML | Energy (CPU), power (GPU) |
Energy is the joules consumed since the previous reading, and power is the watts being drawn. See Reading the Measurements for the details.
Supported Single-Board Computers
Joular Core includes power models for the following devices. Every revision of each model is supported, though the power model was trained on one particular revision (two for the 4 B, rev 1.1 and rev 1.2), on which the accuracy is at its best.
Raspberry Pi:
- Zero W, 1 B, 1 B+, 2 B, 3 B, 3 B+ (model measured on a 32-bit OS, also used on a 64-bit one)
- 4 B (32-bit and 64-bit OS)
- 400, 5 B (64-bit OS only)
Asus Tinker Board (S)
CPU Power Monitoring Details
Linux (x86 / x86_64)
CPU energy is read from the RAPL package counter exposed through the powercap sysfs interface (/sys/class/powercap/intel-rapl:N), for Intel and AMD processors. Joular Core reads one package, the first one whose name begins with package, so on a server with several sockets only the first socket is measured. On recent kernels, the counter is only readable by root (see Installation). If it cannot be read, the CPU is reported as not available.
Windows
CPU energy is read from the same RAPL package counter, through one of three approaches, tried in this order:
- The Energy Meter Interface (EMI), built into Windows 11. Nothing to install, and no administrative rights needed.
- The RAPL registers through the PawnIO driver, which needs administrative rights.
- The RAPL registers through Hubblo’s RAPL driver, which does not.
The first one that answers is kept. Setting the JOULARCORE_WINDOWS_RAPL environment variable to emi, pawnio or hubblo picks one instead (see Library Interface).
macOS
CPU power, and GPU power on Apple Silicon, are read from Apple’s powermetrics tool, which ships with macOS. It covers both Apple Silicon and Intel Macs, and only runs as root. On Intel Macs, the CPU power is the whole chip (cores, integrated graphics and system agent), and there is no GPU power.
Raspberry Pi and SBC
Power is calculated from CPU utilization using polynomial regression models that were measured against each supported board at various load levels. The board is detected from /proc/device-tree/model, and the CPU utilization is read from /proc/stat. No special permissions are needed. On a board with no power model, the CPU is reported as not available.
FreeBSD (x86_64)
CPU energy is read from the same RAPL package counter, from the registers of the first processor, through the cpuctl(4) driver (/dev/cpuctl0), for Intel and AMD processors. The driver is a module to load first, and its device is only readable by root and the kmem group (see Installation). If it cannot be read, the CPU is reported as not available.
Virtual Machines
Inside a virtual machine, the hardware counters are usually not reachable, so the CPU is reported as not available. PowerJoular can read the power of a virtual machine from a file the host writes.
GPU Power Monitoring Details
Nvidia (Linux, Windows and FreeBSD)
GPU power is read through NVML, the library installed with the Nvidia driver, and loaded by Joular Core when the GPU is opened. The first card listed is the one read. If the driver is not installed or that card does not report its power, the GPU is reported as not available. On Linux and Windows, Nvidia is tried first, then AMD, and only one GPU is read: on a machine with both, the Nvidia card is the one measured.
AMD (Linux)
GPU power is read from the hwmon sysfs of the amdgpu kernel driver (/sys/class/hwmon), with nothing to install. The first amdgpu sensor with a readable power file is the one read.
AMD (Windows)
GPU power is read through ADLX, the library installed with the AMD driver. The first card listed is the one read.
Apple Silicon
GPU power comes from the same powermetrics sample as the CPU power.
SBC
GPU monitoring is not supported on single-board computers. The GPU is reported as not available.
Installation
Joular Core is added to the program that uses it rather than installed on its own. It can be taken from Alire, or compiled from source.
With Alire
In an Ada project managed by Alire, add the library with:
alr with joularcore
Alire fetches the library and builds it along with your program. Then add with Joular_Core; to your code (see Quick Usage).
Alire offers Joular Core on Linux, Windows, macOS and FreeBSD. On FreeBSD, it builds with the GNAT in PATH, which has to be GNAT 15 or newer (see Compilation).
From Source
Clone the GitHub repository and build it with GNAT and GPRBuild, or with Alire. The build gives:
- a static library (
libjoularcore.a), to link into an Ada program, which is the default - a shared library (
libjoularcore.soon Linux and FreeBSD,libjoularcore.dllon Windows,libjoularcore.dylibon macOS), which carries the C interface for programs in C, C++, Java, Python, Rust, etc.
On Linux, Windows and FreeBSD, the shared library carries the Ada runtime too, so it is the only file to ship with your program (on Linux and FreeBSD, libjoularcore.so.0, the name programs look for). On macOS, the Ada runtime stays a file of its own, which the library loads from the folder of the compiler that built it.
See Compilation for the build commands and options.
Platform-specific Requirements
Joular Core is a library, so the privileges below are needed for the program using it.
Linux (PC / servers)
CPU energy is read via the RAPL powercap sysfs interface. On Linux kernel 5.10 and newer, the RAPL energy_uj files are only readable by root. You have two options:
- Run your program with
sudo - Grant read access to the RAPL files for your user (see this GitHub issue for instructions)
GPU monitoring needs the Nvidia driver for Nvidia cards (NVML is installed with it), and the amdgpu kernel driver for AMD cards. Neither needs special privileges.
Windows
CPU energy is read in one of three ways, depending on what the machine offers:
- The Energy Meter Interface is the one used by default and checked first. It needs no installation and no elevated access, and works only on Windows 11.
- PawnIO is the main RAPL driver used (after EMI): it is maintained and properly signed, and its installer is all that is needed, as Joular Core carries the modules it loads. It needs elevated access, so run the program using the library from a terminal with administrative rights.
- Hubblo’s RAPL driver still works and is used when PawnIO does not answer (not installed, or the program is not run with administrative rights). It does not require elevated access, but its development has paused. The easiest way to install a signed version is through the Scaphandre installer.
GPU monitoring needs the Nvidia driver for Nvidia cards (NVML is installed with it), or the AMD driver for AMD cards (ADLX is installed with it).
macOS
No additional software is required. Power data is read via powermetrics, which ships with macOS. Because powermetrics only runs as the superuser, run your program with sudo. Without it, both the CPU and the GPU are reported as not available.
FreeBSD
CPU energy is read from the RAPL registers through the cpuctl(4) driver, which is a module not in the GENERIC kernel: load it with kldload cpuctl (or cpuctl_load="YES" in /boot/loader.conf), and run your program as root, or as a member of the kmem group (which /dev/cpuctl0 is part of).
GPU monitoring needs the Nvidia driver for Nvidia cards (NVML is installed with it), and no special privileges.
Raspberry Pi and SBC
No dependencies and no sudo required. Boards with no power model report the CPU as not available.
Quick Usage
Joular Core has one simple interface: open the sources, read them as often as you need, then close them.
Opendetects the hardware asked for (the CPU, the GPU, or both) and opens what is needed to read it.Readtakes one reading of every source that could be opened.Closecloses what was opened.
Each reading gives, for the CPU and for the GPU, whether the source is available, its value, and its unit: energy in joules consumed since the previous reading, or power in watts. See Reading the Measurements for what each one means.
Using from Ada
with Ada.Text_IO; use Ada.Text_IO;
with Joular_Core; use Joular_Core;
procedure Measure is
Measurements : Reading;
begin
Open; -- Detect and open every supported hardware source
for I in 1 .. 5 loop
delay 1.0;
Measurements := Read;
if Measurements (CPU).Available then
Put_Line ("CPU:" & Long_Float'Image (Measurements (CPU).Value)
& (if Measurements (CPU).Unit = Energy then " J" else " W"));
end if;
end loop;
Close;
end Measure;
With Alire, add the library to your project with alr with joularcore.
A full example program is in example/src/example_joular_core.adb. It reads once per second until stopped with Ctrl+C, which closes the sources cleanly. To build and run it:
gprbuild -P example/example.gpr
./example/example_joular_core
It takes two optional arguments, in any order: on Windows, emi, pawnio or hubblo to say how the RAPL counter is read (rather than trying them in turn), and a number of readings to do before stopping. A summary is printed at the end.
./example/example_joular_core emi 10
Using from C
The C declarations are in include/joularcore.h. Build the relocatable (shared) library (see Compilation), then:
#include <stdio.h>
#include "joularcore.h"
joularcore_reading reading;
joularcore_open(1, 1); /* measure the CPU and the GPU */
joularcore_read(&reading);
if (reading.cpu.available)
printf("CPU: %f %s\n", reading.cpu.value, reading.cpu.unit == 0 ? "J" : "W");
joularcore_close();
A full example program is in example/c/main.c. Like the Ada one, it reads 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
Using from Python
From Python, the same interface through ctypes:
import ctypes
class Measurement(ctypes.Structure):
_fields_ = [("value", ctypes.c_double), ("available", ctypes.c_int), ("unit", ctypes.c_int)]
class Reading(ctypes.Structure):
_fields_ = [("cpu", Measurement), ("gpu", Measurement)]
lib = ctypes.CDLL("lib/relocatable/libjoularcore.so")
lib.joularcore_read.argtypes = [ctypes.POINTER(Reading)]
lib.joularcore_version.restype = ctypes.c_char_p
lib.joularcore_open(1, 1) # measure the CPU and the GPU
r = Reading()
lib.joularcore_read(ctypes.byref(r))
if r.cpu.available:
print("CPU:", r.cpu.value, "J" if r.cpu.unit == 0 else "W")
lib.joularcore_close()
The library is libjoularcore.dylib on macOS and libjoularcore.dll on Windows.
A full example program is in example/python/main.py, with a Makefile that builds the shared library.
Other languages (C++, Java, Rust, etc.) use the same C interface. See Integration with Systems and Tools.
Compilation
Joular Core is written in Ada and is built with GPRBuild, or with Alire. A modern GNAT compiler is the only requirement.
On FreeBSD, GNAT 15 or newer is needed: pkg install gprbuild gnat15 brings GPRBuild and GNAT 15, whose folder /usr/local/gnat15/bin has to be added to PATH. GNAT 12, which pkg install gprbuild uses, crashes on the code reading cpuctl, and 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.
Default Build
With Alire:
alr build
Or directly with GNAT:
gprbuild -P joularcore.gpr
The build produces a static library by default, libjoularcore.a in lib/static/, for Ada programs.
Choosing the OS
The build detects the OS on its own to compile the appropriate version: Linux, Windows, macOS and FreeBSD are each recognised from the target GPRBuild reports.
-XPJ_OS overrides it when the version to build is not the one of the machine building it, with linux, windows, macos or freebsd:
gprbuild -P joularcore.gpr -XPJ_OS=windows
On Windows and FreeBSD, reading the RAPL registers through a driver needs the CPUID instruction of 64 bits x86 processors, to know whether they are the Intel or the AMD ones. Whether the processor is one is detected from the target too, and -XPJ_X86=True or -XPJ_X86=False overrides it. With False, only the Energy Meter Interface is used on Windows, and the CPU is not read on FreeBSD.
Library Types
For other library types, set -XJOULARCORE_LIBRARY_TYPE (when it is not set, the generic LIBRARY_TYPE is used if set, and static otherwise):
gprbuild -P joularcore.gpr -XJOULARCORE_LIBRARY_TYPE=relocatable
| Library type | Default | Description |
|---|---|---|
static | on | libjoularcore.a, linked into an Ada program. |
relocatable | off | The shared library (libjoularcore.so / .dll / .dylib) that carries the C interface, for programs in other languages. |
static-pic | off | A static library built position independent, to go inside someone else’s shared library. |
Each type is built in its own folder: lib/static/, lib/relocatable/ or lib/static-pic/.
The relocatable library is stand-alone (it starts itself up when loaded) and, on every OS but macOS, encapsulated: it carries the Ada runtime too, so it is one self-contained file. On Linux and FreeBSD, it is versioned as libjoularcore.so.0.
On macOS it cannot be encapsulated, so the Ada runtime stays a file of its own: 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 /usr/bin/java or the system’s Python).
Building the Examples
The Ada example uses the static library:
gprbuild -P example/example.gpr
./example/example_joular_core
The C example comes with a Makefile that builds the shared library and the program:
make -C example/c
On FreeBSD, the Makefiles need 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 joularcore.gpr -XJOULARCORE_LIBRARY_TYPE=relocatable
Then compile the C program:
gcc example/c/main.c -Iinclude -Llib/relocatable -ljoularcore -Wl,-rpath,"$PWD/lib/relocatable" -o example/c/example_c
-I is the folder holding joularcore.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 because it cannot find the library, unless you set LD_LIBRARY_PATH (Linux and FreeBSD) or DYLD_LIBRARY_PATH (macOS) yourself. Windows has no -rpath: put a copy of the DLL next to the program instead.
The Python example only needs the shared library, which its Makefile builds:
make -C example/python run
Library Interface
Joular Core has the same interface in Ada and in C: open, read, close, and the version of the library.
Ada
The whole interface is the Joular_Core package.
Types
| Type | Description |
|---|---|
Source | The hardware sources to measure: CPU or GPU. |
Source_List | An array of Boolean indexed by Source: which sources to measure (the ones set to True). |
All_Sources | A Source_List with every source set to True. |
Measurement_Unit | Energy (joules consumed since the previous reading) or Power (watts being drawn, when read or averaged since the previous reading). |
Measurement | One measurement of one source: Available (Boolean), Value (Long_Float, joules or watts) and Unit (Measurement_Unit). |
Reading | An array of Measurement indexed by Source: one measurement per source. |
Subprograms
| Subprogram | Description |
|---|---|
procedure Open (Sources : in Source_List := All_Sources) | Detect the hardware sources asked for, and open the files, drivers or processes needed to read them. A source that is not there, or cannot be read, is reported as not available by Read. Calling Open again closes what was open first. |
function Read return Reading | Take one reading of every source Open could open. A source that fails to answer reports a value of zero, with Available still True. |
procedure Close | Close what Open opened. |
function Version return String | The version of the library. |
For example, to measure the CPU only:
Open ((CPU => True, GPU => False));
C
The C declarations are in include/joularcore.h. Use them with the relocatable (shared) library, which starts itself up when loaded: no other initialization call is needed.
Types
typedef struct joularcore_measurement {
double value; /* energy or power value, see unit */
int available; /* 1 when the source was requested and opened, 0 otherwise */
int unit; /* 0 when value is energy in joules, 1 when it is power in watts */
} joularcore_measurement;
typedef struct joularcore_reading {
joularcore_measurement cpu;
joularcore_measurement gpu;
} joularcore_reading;
Functions
| Function | Description |
|---|---|
void joularcore_open(int cpu, int gpu) | Same as Open: a source is measured when its flag is not zero. |
void joularcore_read(joularcore_reading *out) | Same as Read: writes one reading of every source into *out. |
void joularcore_close(void) | Same as Close. |
const char *joularcore_version(void) | Same as Version. The string is owned by the library, so do not free it. |
Environment Variable
| Variable | Description |
|---|---|
JOULARCORE_WINDOWS_RAPL | On Windows, picks how the RAPL counter is read: emi, pawnio or hubblo. Any other value, or not setting it at all, tries the Energy Meter Interface first, then PawnIO, then Hubblo’s driver, and keeps the first that answers. |
When the one picked does not answer, the others are not tried and the CPU is reported as not available. Upper or lower case does not matter.
All three end up on the same package counter of the processor, so they report the same energy. Picking one is useful to test each of them on a machine carrying several, or to work around a broken one.
set JOULARCORE_WINDOWS_RAPL=emi
set JOULARCORE_WINDOWS_RAPL=pawnio
set JOULARCORE_WINDOWS_RAPL=hubblo
Constraints
- The library is not thread safe: call
Open,ReadandClosefrom a single thread (or task), as one monitoring loop is the intended use for the current version. - Energy counters (RAPL) wrap after a few minutes under load, so read at least once per minute to not miss a wrap. On Windows with the Energy Meter Interface, the wrap is handled by Windows.
- The first reading after
Opencounts the energy fromOpen.
Reading the Measurements
Each reading gives one measurement for the CPU and one for the GPU. A measurement says whether the source is available, its value, and its unit.
Energy and Power
- Some hardware reports energy: the joules consumed since the previous reading (RAPL on Linux, Windows and FreeBSD). The first reading counts from
Open. - Others report power: the watts being drawn when read (GPUs), or averaged since the previous reading (Raspberry Pi and Asus Tinker Board models).
- On macOS, the value is the average power since the previous reading: Joular Core asks
powermetricsfor a sample at each reading. Opening the sources waits for the first sample ofpowermetrics(up to about three seconds).
To get watts from an energy reading, divide it by the time elapsed since the previous reading. To get joules from a power reading, multiply it by that time. PowerJoular does the first on every cycle, dividing by the time the cycle actually took rather than assuming it was exactly one second.
Available, Not Available, and Zero
- A source that is not present, not supported, or not accessible is reported as not available. This is not an error: the other sources keep working.
- A source that was available but stops answering reports a value of zero, and is still available. For RAPL, the energy is not lost: the next reading that works also counts what was used during the one that failed.
- On macOS, if
powermetricsstops answering, its sources read zero, and it is started again around ten seconds later.
What Each Source Actually Measures
| Source | What the number covers |
|---|---|
| RAPL on Linux | One package of powercap, the first one whose name begins with package |
| RAPL on Windows | The package domain of the first socket |
| RAPL on FreeBSD | The package domain of the first processor, /dev/cpuctl0 |
| Raspberry Pi and Asus Tinker Board | A model-based estimate: a regression on CPU load, evaluated over the interval between two readings. It is not a reading of the board’s actual draw |
| Nvidia (NVML) | What the card reports for the whole GPU board. Depending on the architecture and driver, this is an average over about a second rather than an instant value |
| AMD on Linux (hwmon) | What the amdgpu driver reports (power1_average, or power1_input), which the kernel documents as the power used by the SoC, the GPU chip. On an APU that chip includes the CPU cores, so work done on the CPU raises what this library calls the GPU, and adding CPU and GPU together counts some of it twice |
| AMD on Windows (ADLX) | Whole GPU board power where the card offers it, and the graphics processor alone where it does not. Which of the two is chosen when the card is opened and does not change while the program runs, so a series of measurements always means one thing |
| macOS | Apple Silicon: the CPU and the GPU parts of the same chip, from the same sample, as powermetrics estimates them. Intel Macs: the whole chip (cores, integrated GPU and system agent), like the RAPL package on Linux and Windows |
Counter Wraps
RAPL counters wrap when they fill, and the library corrects that. The correction only works if less than half of what the counter holds was used between two readings, so read at least once per minute. How long the counter takes to fill depends on the energy unit of the processor.
A counter that goes back without looking like a wrap is taken as a reset (for example after the machine was suspended), and that reading gives zero.
The Energy Meter Interface (EMI) on Windows corrects for the wrap by itself, so Joular Core doesn’t need to do the correction.
Integration with Systems and Tools
Joular Core is designed to be embedded in other programs and tools. It only measures the hardware, and leaves to the program using it what to do with the measurements: show them, write them to files, attribute them to processes, etc.
For example, PowerJoular uses Joular Core to measure the CPU and the GPU, together with our CPU Load library to share the CPU power out to processes and applications, and exports the result to the terminal, CSV files and a shared memory ring buffer.
See Library Interface for the details of the interface. This page focuses on using it from different languages.
Ada Programs
Ada programs use the Joular_Core package directly, and link the static library. With Alire, add the library to your project with:
alr with joularcore
Without Alire, add the folder of the library to the project search path of GPRBuild, and add with "joularcore.gpr"; to your project file:
gprbuild -P your_project.gpr -aP../joularcore
C, C++ and Other Languages
Any language with a C FFI can use the shared library (libjoularcore.so on Linux and FreeBSD, libjoularcore.dll on Windows, libjoularcore.dylib on macOS) through the C interface in include/joularcore.h: C and C++ directly, Python through ctypes, Java through FFM or JNA, Rust through libloading or FFI declarations, etc.
The two structures of the C interface have the same layout on every target, with no padding: a double followed by two int for one measurement, and the CPU measurement followed by the GPU one for a reading.
On every OS but macOS, the shared library is one self-contained file, carrying the Ada runtime too, so it is the only file to ship with your program. On Windows, the program looks for the DLL next to it, so put a copy there.
On macOS, the library loads the Ada runtime from the folder of the compiler that built it (see Compilation), so it only runs on a Mac where that same compiler is installed in the same folder.
Python
The Quick Usage page shows how to call the library from Python with ctypes, and example/python/main.py is a full example.
The library leaves the signal handlers of the program loading it in place, so Ctrl+C raises KeyboardInterrupt in Python as usual.
Java
From Java, the library can be called through the Foreign Function and Memory API (FFM) or JNA. The shared library does not install any handler for the signals the JVM relies on (such as SIGSEGV and SIGBUS), so it does not get in the way of the JVM.
Threads
The library is not thread safe: call Open, Read and Close from a single thread, as one monitoring loop is the intended use for the current version. A program that needs the measurements in several threads can read them in one thread and share the values with the others.
How Joular Core Works
This page describes the internal architecture of Joular Core: how it detects the hardware, how it reads each source on each OS, and how to add new hardware or a new OS.
High-Level Architecture
The library has two sources, the CPU and the GPU, each measured by its own monitor:
Openasks each requested monitor to detect its hardware. The monitor tries, in order, every way it knows to read that hardware on this OS, and keeps the first one that answers. It also takes a first reading of cumulative counters (e.g. RAPL), so the next reading can report the energy consumed since then.Readasks each monitor that could be opened for one measurement: the energy consumed since the previous reading, or the power drawn, depending on what the hardware reports.Closecloses what each monitor opened (e.g. a driver on Windows, thepowermetricsprocess on macOS).
Open / Read / Close
│
┌───────────┴───────────┐
▼ ▼
CPU_Monitor GPU_Monitor
(one body per OS) (one body per OS)
│ │
├── RAPL (Linux) ├── NVML (Nvidia)
├── Board models (SBC) ├── hwmon sysfs (AMD, Linux)
├── RAPL (Windows) ├── ADLX (AMD, Windows)
├── RAPL (FreeBSD) └── powermetrics
└── powermetrics
Each source is opened, read and closed on its own: a source that fails while being opened is closed again and reported as not available, and a source that fails while being read reports zero. Neither stops the other source.
Linux
CPU. The CPU monitor tries RAPL first, then the power models of single-board computers.
RAPL is read from the powercap sysfs interface at /sys/class/powercap/intel-rapl:N. Joular Core looks for the first domain whose name begins with package (it is usually intel-rapl:0, but on some machines another domain such as psys comes first), and reads its energy_uj counter, in microjoules. The max_energy_range_uj file gives where the counter wraps. The counter reads zero without root on most systems, and the CPU is then reported as not available.
On Raspberry Pi and Asus Tinker Board, the board is detected from /proc/device-tree/model. The power is calculated from CPU utilization using polynomial regression models:
power = c₀ + c₁·u + c₂·u² + … + c₉·u⁹
where u is the CPU utilization from 0.0 to 1.0, measured from /proc/stat over the interval between two readings, and c₀…c₉ are the model coefficients measured for each board (models fitted at a lower degree have their remaining coefficients at zero). Some boards have separate models for 32-bit and 64-bit systems. The models come from our Joular Power Models Database.
GPU. The GPU monitor tries Nvidia cards through NVML first, then AMD cards through the hwmon sysfs of the amdgpu kernel driver. For AMD, Joular Core looks in /sys/class/hwmon for the first amdgpu sensor with a readable power file, and prefers power1_average (an average over a short period) to power1_input (an instant reading).
Windows
CPU. The same RAPL package counter of the first socket can be reached in three ways, tried in this order, and the first one that answers is kept:
- The Energy Meter Interface (EMI), built into Windows 11. The processor driver publishes the RAPL domains as channels of a meter, and Joular Core opens the meter publishing the package channel. A meter publishing something else (a board rail, a battery) is refused. Windows hands over a 64-bit counter already unwrapped, so there is no wrap to correct.
- The MSR registers through the PawnIO driver. The driver reads no register on its own: Joular Core loads into it a signed module saying which registers it lets through (one for Intel, one for AMD from family 17h onwards; older AMD processors are refused, and are left to Hubblo’s driver).
- The MSR registers through Hubblo’s RAPL driver, which only lets the RAPL registers through, so opening the device is enough.
When reading the registers directly, Joular Core asks the processor for its vendor with the CPUID instruction, to know which registers hold the energy unit and the package counter on Intel and AMD. Silvermont and Airmont Atoms, whose energy unit would be read wrongly, are refused.
GPU. The GPU monitor tries Nvidia cards through NVML first, then AMD cards through ADLX. ADLX gives the whole board power where the card offers it, and the graphics processor alone where it does not. Which of the two is used is chosen when the card is opened, and does not change afterwards.
macOS
The CPU, and the GPU on Apple Silicon, are read from Apple’s powermetrics tool (/usr/bin/powermetrics, given by its full path so that PATH cannot point at another program). One powermetrics process serves both sources: it is started by the first Open, and stopped by the last Close.
Each reading asks powermetrics for a sample (with the SIGINFO signal), so a reading is the average power since the previous one. powermetrics still takes a sample of its own every ten minutes (this is what makes it stop once the program using the library is gone), so readings should be closer than that. The CPU and the GPU are read one after the other, and readings within 0.1 seconds share one sample, so both values come from the same sample.
On Apple Silicon, powermetrics reports the CPU and the GPU each on a line. On Intel Macs, it reports the whole chip on one line, and no GPU.
powermetrics only runs as root, so Joular Core checks that before starting it. If it stops answering, its sources read zero, and it is started again on a reading about ten seconds later.
FreeBSD
CPU. The RAPL package counter of the first processor is read from its registers, through the cpuctl(4) driver (/dev/cpuctl0). As on Windows, Joular Core asks the processor for its vendor with the CPUID instruction, to know which registers hold the energy unit and the package counter on Intel and AMD, and refuses Silvermont and Airmont Atoms.
GPU. The GPU monitor reads Nvidia cards through NVML.
Nvidia GPUs on Linux, Windows and FreeBSD: NVML
NVML is the library installed with the Nvidia driver (libnvidia-ml.so.1 on Linux and FreeBSD, nvml.dll on Windows, in the system folder, or in the Nvidia folder of Program Files for older drivers). Joular Core loads it when the GPU is opened, rather than linking to it, so the library works the same on a machine with no Nvidia driver. It reads the power of the first card listed. The same applies to ADLX on Windows (amdadlx64.dll, or amdadlx32.dll for a 32 bits build).
Energy Counters
The RAPL counters of Linux, of Windows when read through a driver, and of FreeBSD, only count up, and wrap back to zero once full. Joular Core keeps the previous value of each counter, and turns two readings into the energy consumed between them:
- When the new value is lower, the counter wrapped, so the range it wraps at is added.
- A drop that does not look like a wrap (one that would mean more than half the range was used between two readings) is taken as a counter reset, for example after the machine was suspended, and gives zero.
- A counter that could not be read gives zero, and keeps the previous value, so the next reading that works covers the gap.
Only one wrap can be corrected between two readings, so read more often than the counter takes to fill half its range. Under load, this is a few minutes, hence the advice to read at least once per minute.
Code Layout
| Path | Purpose |
|---|---|
src/joular_core.ads | The Ada interface: Open, Read, Close, Version |
src/joular_core-c_api.ads | The C interface, matching include/joularcore.h |
src/joular_core-cpu_monitor.ads, src/joular_core-gpu_monitor.ads | The two monitors, with one body per OS |
src/joular_core-energy_counters.adb | The energy between two readings of a counter that wraps (RAPL on Linux, Windows and FreeBSD) |
src/joular_core-gpu_nvidia_nvml.adb | Nvidia GPUs through NVML, on Linux, Windows and FreeBSD |
src/joular_core-processor.adb | The CPUID vendor check, to read the RAPL registers on Windows and FreeBSD |
src/linux/ | RAPL through powercap, the power models of single-board computers, AMD GPUs through hwmon |
src/windows/ | RAPL through EMI, PawnIO and Hubblo’s driver, AMD GPUs through ADLX, loading a shared library and finding NVML, and the Win32 bindings they share |
src/macos/ | powermetrics, and the specs file recording where the shared library finds the Ada runtime |
src/freebsd/ | RAPL through cpuctl |
src/posix/ | What Linux, macOS and FreeBSD do the same way (loading a shared library) |
include/joularcore.h | The C declarations |
example/ | Example programs in Ada, C and Python |
tools/ | The PawnIO modules, and the script turning them into an Ada package |
Adding New Hardware or a New OS
Each hardware component is one package with three functions: Open (detect and open, returning False when the hardware is not there or cannot be read), Get_Power or Get_Energy (one reading, in watts or joules), and Close.
The code shared by every OS is in src, and the code of each OS is in its own folder, picked by joularcore.gpr from PJ_OS. Each OS folder has its own body of the two monitors, CPU_Monitor and GPU_Monitor, which lists the packages that can read that hardware on that OS, tries them in order and keeps the first one that answers.
- To support new hardware, write such a package in the folder of the OS it runs on (or in
srcif it is portable, like NVML), and add it to the monitor of that OS. - To support a new OS, add its folder with its two monitors, and add it to
PJ_OSin the project file.
Windows RAPL splits this further, as the same counter is reached in several ways. RAPL_Windows tries each way in order and keeps the first that answers: RAPL_EMI_Windows, which reads RAPL from the Energy Meter Interface, then RAPL_MSR_Windows, which reads the MSR registers with a driver. That second one keeps the vendor detection and the counter, and hands the reading of a single register to one of two interchangeable packages, MSR_PawnIO and MSR_Hubblo. Supporting another driver means writing another such package with Open, Read and Close, adding it to Driver_Kind in RAPL_MSR_Windows, and to the list tried in RAPL_Windows.Open.
Third Party Components
Joular Core carries two PawnIO modules, IntelMSR.bin and AMDFamily17.bin, taken byte for byte from release 0.2.11. The PawnIO driver checks their signature before running them, so they are shipped as they are and cannot be rebuilt here.
They are licensed under the GNU Lesser General Public License version 2.1 or later, copyright namazso and contributors. A copy of that license is in tools/pawnio/COPYING, next to the modules themselves.
They are turned into src/windows/joular_core-pawnio_modules.ads by tools/gen_pawnio_modules.py, which is also how that file is regenerated when a newer release is taken:
python3 tools/gen_pawnio_modules.py
The SHA-256 of each module is pinned in that script, which refuses to write anything when a module on disk is not the one it expects. Running it with --check writes nothing and only reports whether the modules, their digests and the committed package still agree.