From 79c5a03dfd4de312d799d4574f6d902231494050 Mon Sep 17 00:00:00 2001 From: Adel Noureddine Date: Thu, 25 Jun 2026 15:08:03 +0200 Subject: [PATCH 1/2] Add direct RAPL measurement on Linux --- README.md | 18 +- .../agent/source/LinuxRaplPowerSource.java | 183 ++++++++++++ .../agent/source/PowerSource.java | 8 +- .../agent/source/PowerSourceFactory.java | 1 + .../source/LinuxRaplPowerSourceTest.java | 261 ++++++++++++++++++ .../agent/source/PowerSourceFactoryTest.java | 13 + 6 files changed, 476 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/noureddine/joular/joularcodejava/agent/source/LinuxRaplPowerSource.java create mode 100644 src/test/java/org/noureddine/joular/joularcodejava/agent/source/LinuxRaplPowerSourceTest.java diff --git a/README.md b/README.md index 2465a74..ab08827 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,11 @@ This project is part of [Joular Code](https://github.com/joular/joularcode), and - Monitor power consumption and energy of each method and execution branch at runtime - Works as a Java agent — no source code instrumentation or modification needed - Samples the JVM stack at high frequency (default: every 10 ms) and attributes energy every second -- Supports three power data source backends from [Joular Core](https://github.com/joular/joularcore): +- Supports four power data source backends: - Shared memory ring buffer (IPC) — lowest latency, recommended - CSV file — file-based polling - HTTP endpoint — remote or containerized setups + - Linux RAPL powercap PKG domain — direct hardware counter source - Generates CSV files with per-method and per-branch power (Watts) and energy (Joules) - Produces two output sets: one for all methods (including JDK internals), one filtered to your application packages - Configurable method filtering by package/class prefix to focus energy data on your code @@ -38,7 +39,7 @@ Joular Code - Java runs as a Java instrumentation agent alongside your applicati - Java 21 or later - Apache Maven 3.6 or later -- [Joular Core](https://github.com/joular/joularcore) running on the same machine +- [Joular Core](https://github.com/joular/joularcore) for `ringbuffer`, `csv`, or `http` power sources, or Linux powercap/RAPL access for `rapl` ### Build @@ -60,7 +61,7 @@ The JAR bundles all dependencies (including JNA) via the Maven Shade plugin, so ## :bulb: Usage -Joular Code - Java attaches to your Java application as a Java agent. You must have [Joular Core](https://github.com/joular/joularcore) running before starting your application. +Joular Code - Java attaches to your Java application as a Java agent. Start [Joular Core](https://github.com/joular/joularcore) first when using `ringbuffer`, `csv`, or `http`; on Linux, `rapl` can read CPU package power directly from the powercap interface. ### Basic usage @@ -94,7 +95,7 @@ All configuration is done via a `joularcodejava.properties` file. Joular Code - | Property | Default | Description | |---|---|---| -| `power-source-type` | `ringbuffer` | Power data backend: `ringbuffer`, `csv`, or `http` | +| `power-source-type` | `ringbuffer` | Power data backend: `ringbuffer`, `csv`, `http`, or `rapl` | | `joular-core-ringbuffer-path` | OS-dependent | Path to the Joular Core ring buffer (see below) | | `joular-core-csv-path` | `joularcore-data.csv` | Path to the Joular Core CSV output file | | `joular-core-http-url` | `http://localhost:8080/data` | URL of the Joular Core HTTP endpoint | @@ -136,6 +137,14 @@ power-source-type=http joular-core-http-url=http://localhost:8080/data ``` +#### Linux RAPL powercap + +Reads CPU package power directly from the Linux powercap RAPL PKG domain at `/sys/class/powercap/intel-rapl/intel-rapl:0`. This source is Linux-only, uses `energy_uj` and `max_energy_range_uj`, and supports the PKG `package-0` domain only. + +```properties +power-source-type=rapl +``` + ### Method filtering The `methods-filtering-prefix` property controls which methods appear in the `methods-power-app.csv` output. Provide one or more comma-separated package or class name prefixes: @@ -187,6 +196,7 @@ timestamp,branch,power_watts,energy_joules,interval_seconds - **"Joular Core ring buffer appears stale"** (`WARNING` log): Joular Core has stopped advancing the ring buffer, so confirm that the Joular Core process is still running and still writing data. Until that resumes, Joular Code - Java gives a power value of `0.0`. - **"Could not read power data from CSV: ..."** (`WARNING` log): the configured `joular-core-csv-path` does not exist. Check that Joular Core is running in CSV export mode and writing to the same path. The warning is logged only once until the file becomes available again. - **HTTP mode returns 0.0 power**: the HTTP endpoint must return a JSON object containing a `"cpu_power": ` key directly on the top-level object (not nested). Keys like `"last_cpu_power"` or `"cpu_power_limit"` are ignored. +- **RAPL mode fails at startup**: `power-source-type=rapl` is Linux-only and requires readable `/sys/class/powercap/intel-rapl/intel-rapl:0/name`, `energy_uj`, and `max_energy_range_uj` files for the `package-0` domain. Depending on the system, this may require elevated permissions. - **No rows in `methods-power-app.csv`**: if `methods-filtering-prefix` is set, the configured prefix may not match any fully-qualified method name in your application. If it is unset, all observed methods are eligible for both output files. ## :information_source: Notes diff --git a/src/main/java/org/noureddine/joular/joularcodejava/agent/source/LinuxRaplPowerSource.java b/src/main/java/org/noureddine/joular/joularcodejava/agent/source/LinuxRaplPowerSource.java new file mode 100644 index 0000000..09737fa --- /dev/null +++ b/src/main/java/org/noureddine/joular/joularcodejava/agent/source/LinuxRaplPowerSource.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 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, and is available at + * https://www.gnu.org/licenses/lgpl-3.0.en.html + * + * Author : Adel Noureddine + */ + +package org.noureddine.joular.joularcodejava.agent.source; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Locale; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LinuxRaplPowerSource implements PowerSource { + + private static final Logger logger = Logger.getLogger(LinuxRaplPowerSource.class.getName()); + private static final String DEFAULT_PKG_PATH = "/sys/class/powercap/intel-rapl/intel-rapl:0"; + private static final String EXPECTED_PKG_NAME = "package-0"; + private static final double MICROJOULES_PER_JOULE = 1_000_000.0; + + private final Path packagePath; + private final String osName; + private final Clock clock; + + private Path energyPath; + private double maxEnergyRangeJoules; + private double lastEnergyJoules; + private Instant lastReadAt; + private double lastKnownPower = 0.0; + private boolean initialized = false; + + public LinuxRaplPowerSource() { + this(System.getProperty("os.name", "")); + } + + private LinuxRaplPowerSource(String osName) { + this(defaultPackagePath(osName), osName, Clock.systemUTC()); + } + + LinuxRaplPowerSource(Path packagePath, String osName, Clock clock) { + this.packagePath = Objects.requireNonNull(packagePath, "packagePath"); + this.osName = osName == null ? "" : osName; + this.clock = Objects.requireNonNull(clock, "clock"); + } + + @Override + public void initialize() throws Exception { + if (!isLinux()) { + throw new IllegalStateException("Linux RAPL power source is only supported on Linux."); + } + + Path namePath = packagePath.resolve("name"); + Path maxEnergyRangePath = packagePath.resolve("max_energy_range_uj"); + energyPath = packagePath.resolve("energy_uj"); + + requireReadableFile(namePath, "RAPL package name"); + requireReadableFile(energyPath, "RAPL package energy"); + requireReadableFile(maxEnergyRangePath, "RAPL package max energy range"); + + String packageName = Files.readString(namePath).trim(); + if (!EXPECTED_PKG_NAME.equals(packageName)) { + throw new IOException("Unsupported RAPL package domain '" + packageName + + "' at " + packagePath + "; expected " + EXPECTED_PKG_NAME + "."); + } + + maxEnergyRangeJoules = readJoules(maxEnergyRangePath); + if (!Double.isFinite(maxEnergyRangeJoules) || maxEnergyRangeJoules <= 0) { + throw new IOException("Invalid RAPL max energy range: " + maxEnergyRangeJoules + " J."); + } + + lastEnergyJoules = readEnergyJoules(); + lastReadAt = clock.instant(); + lastKnownPower = 0.0; + initialized = true; + + logger.log(Level.INFO, () -> "Initialized Linux RAPL PKG source: " + packagePath); + } + + @Override + public double getCurrentPower() { + if (!initialized) { + return 0.0; + } + + try { + double currentEnergyJoules = readEnergyJoules(); + Instant currentReadAt = clock.instant(); + double elapsedSeconds = elapsedSeconds(lastReadAt, currentReadAt); + if (elapsedSeconds <= 0 || !Double.isFinite(elapsedSeconds)) { + return lastKnownPower; + } + + double energyDelta = currentEnergyJoules >= lastEnergyJoules + ? currentEnergyJoules - lastEnergyJoules + : currentEnergyJoules - lastEnergyJoules + maxEnergyRangeJoules; + if (!Double.isFinite(energyDelta) || energyDelta < 0) { + return lastKnownPower; + } + + double power = energyDelta / elapsedSeconds; + if (!Double.isFinite(power) || power < 0) { + return lastKnownPower; + } + + lastEnergyJoules = currentEnergyJoules; + lastReadAt = currentReadAt; + lastKnownPower = power; + return power; + } catch (Exception e) { + logger.log(Level.FINE, "Could not read Linux RAPL power data", e); + return lastKnownPower; + } + } + + @Override + public void close() { + initialized = false; + energyPath = null; + maxEnergyRangeJoules = 0.0; + lastEnergyJoules = 0.0; + lastReadAt = null; + lastKnownPower = 0.0; + } + + private boolean isLinux() { + return isLinux(osName); + } + + private static boolean isLinux(String osName) { + return osName != null && osName.toLowerCase(Locale.ROOT).contains("linux"); + } + + private static Path defaultPackagePath(String osName) { + if (isLinux(osName)) { + return Path.of(DEFAULT_PKG_PATH); + } + return Path.of("."); + } + + private double readEnergyJoules() throws IOException { + double energyJoules = readJoules(energyPath); + if (!Double.isFinite(energyJoules) || energyJoules < 0 || energyJoules > maxEnergyRangeJoules) { + throw new IOException("Invalid RAPL energy reading: " + energyJoules + " J."); + } + return energyJoules; + } + + private static double readJoules(Path path) throws IOException { + String text = Files.readString(path).trim(); + double microjoules; + try { + microjoules = Double.parseDouble(text); + } catch (NumberFormatException e) { + throw new IOException("Could not parse RAPL value from " + path + ": " + text, e); + } + return microjoules / MICROJOULES_PER_JOULE; + } + + private static void requireReadableFile(Path path, String description) throws IOException { + if (!Files.isRegularFile(path) || !Files.isReadable(path)) { + throw new IOException(description + " file is not readable: " + path); + } + } + + private static double elapsedSeconds(Instant start, Instant end) { + if (start == null || end == null) { + return 0.0; + } + Duration elapsed = Duration.between(start, end); + return elapsed.toNanos() / 1_000_000_000.0; + } +} diff --git a/src/main/java/org/noureddine/joular/joularcodejava/agent/source/PowerSource.java b/src/main/java/org/noureddine/joular/joularcodejava/agent/source/PowerSource.java index 9ebc5c7..e4c6bc1 100644 --- a/src/main/java/org/noureddine/joular/joularcodejava/agent/source/PowerSource.java +++ b/src/main/java/org/noureddine/joular/joularcodejava/agent/source/PowerSource.java @@ -12,15 +12,15 @@ package org.noureddine.joular.joularcodejava.agent.source; /** - * Interface for different power data sources from Joular Core. + * Interface for different power data sources. * *

All implementations must follow the same failure semantics: *