Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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": <number>` 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>All implementations must follow the same failure semantics:
* <ul>
* <li>Before any successful read, {@link #getCurrentPower()} returns {@code 0.0}.</li>
* <li>On a successful read, the returned value is cached as the "last known power".</li>
* <li>On transient errors (network blip, mid-write torn reads, parse failures, missing
* row), implementations return the last known power so the agent smooths over momentary
* producer unavailability.</li>
* row, temporarily unreadable device data), implementations return the last known power
* so the agent smooths over momentary producer unavailability.</li>
* <li>Implementations must reject {@code NaN}, infinite, and negative values returned by
* the underlying source — these are treated as transient errors.</li>
* <li>Returned values are CPU power in Watts and must be {@code &gt;= 0} and finite.</li>
Expand All @@ -46,4 +46,4 @@ public interface PowerSource {
* was never called or threw.
*/
void close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static PowerSource getPowerSource(AgentProperties properties) {
case "csv" -> new JoularCoreCSVSource(properties.getJoularCoreCsvPath());
case "http" -> new JoularCoreHttpSource(properties.getJoularCoreHttpUrl());
case "ringbuffer" -> new JoularCoreRingBufferSource(properties.getRingBufferPath());
case "rapl" -> new LinuxRaplPowerSource();
default -> {
logger.log(Level.SEVERE, () -> "Unknown power source type: " + type);
yield null;
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/joularcodejava.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# csv (read power from CSV file)
# http (read from HTTP endpoint/websockets)
# ringbuffer (read from shared memory ring buffer, IPC)
# rapl (read from RAPL on Linux directly, without Joular Core)
# Default if left empty: ringbuffer
power-source-type=

Expand Down
Loading
Loading