Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Linux Intel support using RAPL #9

Merged
merged 3 commits into from
Nov 1, 2023
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
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,37 @@

[![Version](https://img.shields.io/maven-central/v/io.quarkiverse.power/quarkus-power?logo=apache-maven&style=flat-square)](https://search.maven.org/artifact/io.quarkiverse.power/quarkus-power)

## Basic usage
This extension is an experiment to measure and display the power consumption of your application as it runs in Dev mode.
Only Linux/amd64 and macOS (amd64/apple silicon) are supported at the moment. See below for platform-specific
requirements.

## Requirements

NOTE: Currently only works on macOS. The power monitoring is performed using the bundled `powermetrics` tool, which
requires `sudo` access. For convenience and security, it's recommended you add your user to the `sudoers` file, giving
it passwordless access to `/usr/bin/powermetrics` (and possibly, only that).
### macOS

This extension is an experiment to measure and display the power consumption of your application as it runs in Dev mode.
The power monitoring is performed using the bundled `powermetrics` tool, which requires `sudo` access. For convenience
and security, it's recommended you add your user to the `sudoers` file, giving it passwordless access
to `/usr/bin/powermetrics` (and possibly, only that).

### Linux

The extension makes use of the RAPL information accessible under the `/sys/class/powercap/intel-rapl` directory. For
security purposes, some of that information is only readable by root, in particular the values that we need to get the
power consumption, currently:

- /sys/class/powercap/intel-rapl/intel-rapl:1/energy_uj (if available)
- /sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj
- /sys/class/powercap/intel-rapl/intel-rapl:0/intel-rapl:0:2/energy_uj

For the extension to work properly, these files need to be readable by the current user, which you can accomplish by
running `sudo chmod +r` on these files. Note that this change will only persist until the next restart.

## Usage

To use the extension:

1. Clone this repository locally
2. Build the code using `mvn install`
2. Build the code using `mvn install -DskipTests` (tests currently only work on macOS)
3. Add the extension to the application which energy consumption you wish to measure. Since the extension is not yet
released, you will need to add it manually as a dependency to your application:
```xml
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkiverse.power.deployment;

import io.quarkiverse.power.deployment.devui.commands.PowerCommands;
import io.quarkiverse.power.runtime.PowerSensorProducer;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand All @@ -19,6 +20,7 @@ FeatureBuildItem feature() {
@BuildStep(onlyIf = IsDevelopment.class)
void addConsoleCommands(BuildProducer<ConsoleCommandBuildItem> commands) {
// register dev console commands
commands.produce(new ConsoleCommandBuildItem(new PowerCommands()));
final var producer = new PowerSensorProducer();
commands.produce(new ConsoleCommandBuildItem(new PowerCommands(producer.sensor())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
import org.aesh.command.*;
import org.aesh.command.invocation.CommandInvocation;

import io.quarkiverse.power.runtime.PowerSensor;

@GroupCommandDefinition(name = "power", description = "Power consumption commands", generateHelp = true)
@SuppressWarnings("rawtypes")
public class PowerCommands implements GroupCommand {
private final PowerSensor<?> sensor;

public PowerCommands() {
public PowerCommands(PowerSensor<?> sensor) {
this.sensor = sensor;
}

@Override
public List<Command> getCommands() {
return List.of(new StartCommand(), new StopCommand());
return List.of(new StartCommand(sensor), new StopCommand(sensor));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@
import org.aesh.command.option.Option;

import io.quarkiverse.power.runtime.PowerSensor;
import io.quarkiverse.power.runtime.sensors.macos.powermetrics.MacOSPowermetricsSensor;
import io.quarkus.deployment.console.QuarkusCommand;

@CommandDefinition(name = "start", description = "Starts measuring power consumption of the current application")
@SuppressWarnings("rawtypes")
public class StartCommand extends QuarkusCommand {

// @Inject
PowerSensor sensor = MacOSPowermetricsSensor.instance;
private final PowerSensor<?> sensor;

@Option(name = "stopAfter", shortName = 's', description = "Automatically stop the measures after the specified duration in seconds", defaultValue = "-1")
private long duration;

@Option(name = "frequency", shortName = 'f', description = "The frequency at which measurements should be taken, in milliseconds", defaultValue = "1000")
private long frequency;

public StartCommand(PowerSensor<?> sensor) {
this.sensor = sensor;
}

@Override
public CommandResult doExecute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
import org.aesh.command.invocation.CommandInvocation;

import io.quarkiverse.power.runtime.PowerSensor;
import io.quarkiverse.power.runtime.sensors.macos.powermetrics.MacOSPowermetricsSensor;
import io.quarkus.deployment.console.QuarkusCommand;

@CommandDefinition(name = "stop", description = "Stops power measurement and outputs accumulated power since measures were started")
@SuppressWarnings("rawtypes")
public class StopCommand extends QuarkusCommand {

// @Inject
PowerSensor sensor = MacOSPowermetricsSensor.instance;
private final PowerSensor sensor;

public StopCommand(PowerSensor<?> sensor) {
this.sensor = sensor;
}

@Override
public CommandResult doExecute(CommandInvocation commandInvocation) throws CommandException, InterruptedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;

import io.quarkiverse.power.runtime.sensors.linux.rapl.IntelRAPLSensor;
import io.quarkiverse.power.runtime.sensors.macos.powermetrics.MacOSPowermetricsSensor;

@Singleton
public class PowerSensorProducer {
@Produces
public PowerSensor sensor() {
return new MacOSPowermetricsSensor();
public PowerSensor<?> sensor() {
final var originalOSName = System.getProperty("os.name");
String osName = originalOSName.toLowerCase();

if (osName.contains("mac os x")) {
return MacOSPowermetricsSensor.instance;
}

if (!osName.contains("linux")) {
throw new RuntimeException("Unsupported platform: " + originalOSName);
}
return IntelRAPLSensor.instance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.quarkiverse.power.runtime.sensors.linux.rapl;

import java.util.Optional;

import io.quarkiverse.power.runtime.PowerSensor;

public class IntelRAPLMeasure implements PowerSensor.IncrementableMeasure {
private final long initial;
private final long startedAt = System.currentTimeMillis();
private int samplesNb;

private long cpu;

public IntelRAPLMeasure(long initial) {
this.initial = initial;
}

@Override
public double cpu() {
return ((double) cpu / measureDuration()) / 1_000;
}

@Override
public Optional<Double> gpu() {
return Optional.empty();
}

@Override
public Optional<Double> byKey(String key) {
return Optional.empty();
}

@Override
public double total() {
return cpu();
}

@Override
public int numberOfSamples() {
return samplesNb;
}

public void incrementSamples() {
samplesNb++;
}

@Override
public long measureDuration() {
return System.currentTimeMillis() - startedAt;
}

@Override
public void addCPU(double v) {
cpu += ((long) v - initial);
System.out.println("cpu = " + cpu);
}

@Override
public void addGPU(double v) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package io.quarkiverse.power.runtime.sensors.linux.rapl;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import io.quarkiverse.power.runtime.PowerSensor;

public class IntelRAPLSensor implements PowerSensor<IntelRAPLMeasure> {

public static final IntelRAPLSensor instance = new IntelRAPLSensor();

private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> scheduled;

private final List<Path> raplFiles = new ArrayList<>(3);

private IntelRAPLMeasure accumulatedPower;
private boolean running;

public IntelRAPLSensor() {
// if we total system energy is not available, read package and DRAM if possible
// todo: extract more granular information
// todo: check Intel doc
if (!checkAvailablity("/sys/class/powercap/intel-rapl/intel-rapl:1/energy_uj")) {
checkAvailablity("/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj");
checkAvailablity("/sys/class/powercap/intel-rapl/intel-rapl:0/intel-rapl:0:2/energy_uj");
}

if (raplFiles.isEmpty()) {
throw new RuntimeException("Failed to get RAPL energy readings, probably due to lack of read access ");
}
}

private boolean checkAvailablity(String raplFileAsString) {
final var raplFile = Path.of(raplFileAsString);
if (Files.exists(raplFile) && Files.isReadable(raplFile)) {
raplFiles.add(raplFile);
return true;
}
return false;
}

@Override
public void start(long duration, long frequency, Writer out) throws IOException, Exception {
if (!running) {
accumulatedPower = new IntelRAPLMeasure(extractPowerMeasure());
running = true;

if (duration > 0) {
executor.schedule(() -> stop(out), duration, TimeUnit.SECONDS);
}

scheduled = executor.scheduleAtFixedRate(
this::accumulatePower,
0, frequency,
TimeUnit.MILLISECONDS);
}
}

private void stop(Writer out) {
stop();
outputConsumptionSinceStarted(out);
}

private void accumulatePower() {
accumulatedPower.addCPU(extractPowerMeasure());
accumulatedPower.incrementSamples();
}

private long extractPowerMeasure() {
long energyData = 0;
for (final Path raplFile : raplFiles) {
try {
energyData += Long.parseLong(Files.readString(raplFile).trim());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return energyData;
}

@Override
public IntelRAPLMeasure stop() {
if (running) {
scheduled.cancel(true);
}
running = false;
return accumulatedPower;
}

@Override
public void outputConsumptionSinceStarted(Writer out) {
out = out == null ? System.out::println : out;
out.println("Consumed " + accumulatedPower.total() + " mW over " + (accumulatedPower.measureDuration() / 1000)
+ " seconds (" + accumulatedPower.numberOfSamples() + " samples)");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class MacOSPowermetricsSensor implements PowerSensor<AppleSiliconMeasure>
private ScheduledFuture<?> powermetricsSchedule;

private boolean running;
private AppleSiliconMeasure accumulatedPower = new AppleSiliconMeasure();
private AppleSiliconMeasure accumulatedPower;
public static PowerSensor<AppleSiliconMeasure> instance = new MacOSPowermetricsSensor();
private final static String pid = " " + ProcessHandle.current().pid() + " ";
private double accumulatedCPUShareDiff;
Expand Down