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
5 changes: 5 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>nuprocess</artifactId>
<version>2.0.6</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,31 @@ public Process sudo(String... cmd) throws IOException {
throw new IllegalArgumentException("No command specified to run with sudo");
}

final Process exec;
if (!isRunningAsAdministrator()) {
final var args = new String[cmd.length + 2];
args[0] = "sudo";
args[1] = "-S";
System.arraycopy(cmd, 0, args, 2, cmd.length);
final var runWithSudo = new ProcessBuilder(args);
return ProcessBuilder.startPipeline(List.of(new ProcessBuilder("echo", pwd), runWithSudo)).getLast();
exec = ProcessBuilder.startPipeline(List.of(new ProcessBuilder("echo", pwd), runWithSudo)).getLast();
} else {
return new ProcessBuilder().command(cmd).start();
exec = new ProcessBuilder().command(cmd).start();
}

// if the process is already dead, get the error
if (!exec.isAlive()) {
final var exitValue = exec.exitValue();
if (exitValue != 0) {
final String errorMsg;
try (final var error = exec.errorReader()) {
errorMsg = error.readLine();
}
throw new RuntimeException(
"Couldn't execute powermetrics. Error code: " + exitValue + ", message: " + errorMsg);
}
}

return exec;
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
package net.laprun.sustainability.power.sensors;

import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import net.laprun.sustainability.power.Security;
import net.laprun.sustainability.power.sensors.linux.rapl.IntelRAPLSensor;
import net.laprun.sustainability.power.sensors.macos.powermetrics.ProcessMacOSPowermetricsSensor;

@Singleton
public class PowerSensorProducer {
private static final String OS_NAME = System.getProperty("os.name").toLowerCase();

@Inject
Security security;

@Produces
public PowerSensor sensor() {
return determinePowerSensor(security);
return determinePowerSensor();
}

public static PowerSensor determinePowerSensor(Security security) {
public static PowerSensor determinePowerSensor() {
if (OS_NAME.contains("mac os x")) {
return new ProcessMacOSPowermetricsSensor(security);
return new ProcessMacOSPowermetricsSensor();
}

if (!OS_NAME.contains("linux")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void start(long samplingFrequencyInMillis) {
@Override
public void stop() {
started = false;
// need to defer reading metadata until we know the file has been populated
initMetadata(getInputStream());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.laprun.sustainability.power.sensors.macos.powermetrics;

import java.nio.ByteBuffer;

class GrowableBuffer {
private ByteBuffer buffer = ByteBuffer.allocate(20000);

public void put(ByteBuffer input) {
if (buffer.remaining() < input.remaining()) {
var newBuffer = ByteBuffer.allocate(buffer.capacity() + input.remaining());
buffer.flip();
newBuffer.put(buffer);
buffer = newBuffer;
} else {
buffer.put(input);
}
}

public byte[] array() {
return buffer.array();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package net.laprun.sustainability.power.sensors.macos.powermetrics;

import java.io.IOException;
import java.io.InputStream;

import io.quarkus.logging.Log;

public class JavaProcessWrapper implements ProcessWrapper {
private Process powermetrics;

@Override
public InputStream streamForMetadata() {
final Process exec;
try {
exec = execPowermetrics("cpu_power", "-i", "10", "-n", "1");
exec.waitFor();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}

return exec.getInputStream();
}

@Override
public void start(long periodInMilliSeconds) {
if (!isRunning()) {
// it takes some time for the external process in addition to the sampling time so adjust the sampling frequency to account for this so that at most one measure occurs during the sampling time window
periodInMilliSeconds = periodInMilliSeconds > 100 ? periodInMilliSeconds - 50 : periodInMilliSeconds;
final var freq = Long.toString(periodInMilliSeconds);
try {
powermetrics = execPowermetrics("cpu_power,tasks", "--show-process-samp-norm", "--show-process-gpu", "-i",
freq);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

}

@Override
public void stop() {
powermetrics.destroy();
}

@Override
public boolean isRunning() {
return powermetrics != null && powermetrics.isAlive();
}

@Override
public InputStream streamForMeasure() {
return powermetrics.getInputStream();
}

public Process execPowermetrics(String... options) throws IOException {
if (options == null || options.length == 0) {
throw new IllegalArgumentException("No powermetrics options specified");
}
final var additionalArgsCardinality = 3;
final var args = new String[options.length + additionalArgsCardinality];
args[0] = "sudo";
args[1] = "powermetrics";
args[2] = "--samplers";
System.arraycopy(options, 0, args, additionalArgsCardinality, options.length);
final var start = System.currentTimeMillis();
final Process exec = new ProcessBuilder().command(args).start();
Log.info("Starting process took " + (System.currentTimeMillis() - start) + "ms");
// if the process is already dead, get the error
if (!exec.isAlive()) {
final var exitValue = exec.exitValue();
if (exitValue != 0) {
final String errorMsg;
try (final var error = exec.errorReader()) {
errorMsg = error.readLine();
}
throw new RuntimeException(
"Couldn't execute powermetrics. Error code: " + exitValue + ", message: " + errorMsg);
}
}
return exec;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -97,102 +97,6 @@ public SensorMetadata metadata() {
return cpu.metadata();
}

private static class ProcessRecord {
final double cpu;
final double gpu;
final String pid;

public ProcessRecord(String line) throws IllegalArgumentException {
// Expected normal output:
//Name ID CPU ms/s samp ms/s User% Deadlines (<2 ms, 2-5 ms) Wakeups (Intr, Pkg idle) GPU ms/s
//iTerm2 1008 46.66 46.91 83.94 0.00 0.00 30.46 0.00 0.00
// Expected summary output:
//Name ID CPU ms/s samp ms/s [total] User% Deadlines/s [total] (<2 ms, 2-5 ms) Wakeups/s [total] (Intr, Pkg idle) Dead GPU ms/s
//WindowServer 406 493.74 493.96 [5165.88 ] 64.82 65.95 [690 ] 0.00 [0 ] 656.62 [6870 ] 4.21 [44 ] N 0.00

try {
// Trim leading/trailing whitespace
line = line.trim();

// Find first whitespace block after process name (marks start of ID)
int idStart = findFirstWhitespace(line);
if (idStart == -1) {
throw new IllegalArgumentException("Cannot find ID in line: " + line);
}

// Skip whitespace to get to ID
idStart = skipWhitespace(line, idStart);
int idEnd = findNextWhitespace(line, idStart);
pid = RegisteredPID.prepare(line.substring(idStart, idEnd));

// Skip CPU ms/s column (skip whitespace, then number, then whitespace)
int pos = skipWhitespace(line, idEnd);
pos = skipNumber(line, pos);

// Now at samp ms/s
pos = skipWhitespace(line, pos);
int sampStart = pos;
int sampEnd = skipNumber(line, sampStart);
cpu = Double.parseDouble(line.substring(sampStart, sampEnd));

// Skip to end and work backwards to find GPU ms/s
// The GPU value is the last numeric value on the line
int lastNumEnd = line.length();
while (lastNumEnd > 0 && Character.isWhitespace(line.charAt(lastNumEnd - 1))) {
lastNumEnd--;
}

int lastNumStart = lastNumEnd;
while (lastNumStart > 0 && isNumberChar(line.charAt(lastNumStart - 1))) {
lastNumStart--;
}

if (lastNumStart < lastNumEnd) {
gpu = Double.parseDouble(line.substring(lastNumStart, lastNumEnd));
} else {
throw new IllegalArgumentException("Cannot find GPU value in line: " + line);
}

} catch (Exception e) {
throw new IllegalArgumentException("Received line doesn't conform to expected format: " + line, e);
}
}

private static int findFirstWhitespace(String line) {
for (int i = 0; i < line.length(); i++) {
if (Character.isWhitespace(line.charAt(i))) {
return i;
}
}
return -1;
}

private static int skipWhitespace(String line, int pos) {
while (pos < line.length() && Character.isWhitespace(line.charAt(pos))) {
pos++;
}
return pos;
}

private static int findNextWhitespace(String line, int pos) {
while (pos < line.length() && !Character.isWhitespace(line.charAt(pos))) {
pos++;
}
return pos;
}

private static int skipNumber(String line, int pos) {
while (pos < line.length() && isNumberChar(line.charAt(pos))) {
pos++;
}
return pos;
}

private static boolean isNumberChar(char c) {
return Character.isDigit(c) || c == '.' || c == '-' || c == 'e' || c == 'E';
}
}

Measures extractPowerMeasure(InputStream powerMeasureInput, Long tick) {
final long start = System.currentTimeMillis();
try {
Expand Down Expand Up @@ -254,6 +158,7 @@ Measures extractPowerMeasure(InputStream powerMeasureInput, Long tick) {
final var hasGPU = totalSampledGPU != 0;
double finalTotalSampledGPU = totalSampledGPU;
double finalTotalSampledCPU = totalSampledCPU;
final var endMs = System.currentTimeMillis();
pidMeasures.forEach((pid, record) -> {
final var cpuShare = record.cpu / finalTotalSampledCPU;
final var measure = new double[metadata.componentCardinality()];
Expand All @@ -274,8 +179,7 @@ Measures extractPowerMeasure(InputStream powerMeasureInput, Long tick) {
measure[index] = value;
}
});

measures.record(pid, new SensorMeasure(measure, start, System.currentTimeMillis()));
measures.record(pid, new SensorMeasure(measure, start, endMs));
});
} catch (Exception exception) {
throw new RuntimeException(exception);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package net.laprun.sustainability.power.sensors.macos.powermetrics;

import java.io.InputStream;
import java.util.concurrent.ExecutionException;

import com.zaxxer.nuprocess.NuProcess;
import com.zaxxer.nuprocess.NuProcessBuilder;

public class NuProcessWrapper implements ProcessWrapper {
private final PowermetricsProcessHandler metadataHandler;
private PowermetricsProcessHandler measureHandler;
private String periodInMilliSecondsAsString;
private long periodInMilliSeconds;

public NuProcessWrapper() {
metadataHandler = new PowermetricsProcessHandler("cpu_power", "-i", "10", "-n", "1");
}

private NuProcess exec(PowermetricsProcessHandler handler) {
if (handler == null)
throw new IllegalArgumentException("Handler cannot be null");
return new NuProcessBuilder(handler, handler.comand()).start();
}

@Override
public InputStream streamForMetadata() {
exec(metadataHandler);
try {
return metadataHandler.getInputStream().get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}

@Override
public void start(long periodInMilliSeconds) {
// todo? check if asked period is the same as the current used one
this.periodInMilliSeconds = periodInMilliSeconds > 100 ? periodInMilliSeconds - 50 : periodInMilliSeconds;
this.periodInMilliSecondsAsString = Long.toString(this.periodInMilliSeconds);
}

@Override
public void stop() {
if (measureHandler != null) {
measureHandler.stop();
measureHandler = null;
}
}

@Override
public boolean isRunning() {
return measureHandler != null && measureHandler.isRunning();
}

@Override
public InputStream streamForMeasure() {
if (!isRunning()) {
measureHandler = new PowermetricsProcessHandler("cpu_power,tasks",
"--show-process-samp-norm", "--show-process-gpu", "-i",
periodInMilliSecondsAsString, "-n", "1");
exec(measureHandler);
try {
return measureHandler.getInputStream().get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
throw new IllegalStateException("Measure is still running");
}
}
Loading
Loading