diff --git a/backend/pom.xml b/backend/pom.xml
index c2d503f..e9ad7fa 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -26,6 +26,11 @@
quarkus-junit5
test
+
+ com.zaxxer
+ nuprocess
+ 2.0.6
+
diff --git a/backend/src/main/java/net/laprun/sustainability/power/Security.java b/backend/src/main/java/net/laprun/sustainability/power/Security.java
index 7bbabed..5fcf7a6 100644
--- a/backend/src/main/java/net/laprun/sustainability/power/Security.java
+++ b/backend/src/main/java/net/laprun/sustainability/power/Security.java
@@ -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;
}
}
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java
index fabd0d7..820a204 100644
--- a/backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java
@@ -1,10 +1,8 @@
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;
@@ -12,17 +10,14 @@
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")) {
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/FileMacOSPowermetricsSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/FileMacOSPowermetricsSensor.java
index 90ee33d..57c8d07 100644
--- a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/FileMacOSPowermetricsSensor.java
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/FileMacOSPowermetricsSensor.java
@@ -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());
}
}
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/GrowableBuffer.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/GrowableBuffer.java
new file mode 100644
index 0000000..58ccedf
--- /dev/null
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/GrowableBuffer.java
@@ -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();
+ }
+}
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/JavaProcessWrapper.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/JavaProcessWrapper.java
new file mode 100644
index 0000000..e1d5cd5
--- /dev/null
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/JavaProcessWrapper.java
@@ -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;
+ }
+
+}
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java
index 45652b9..36cd601 100644
--- a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java
@@ -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 {
@@ -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()];
@@ -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);
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/NuProcessWrapper.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/NuProcessWrapper.java
new file mode 100644
index 0000000..2d03fc2
--- /dev/null
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/NuProcessWrapper.java
@@ -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");
+ }
+}
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/PowermetricsProcessHandler.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/PowermetricsProcessHandler.java
new file mode 100644
index 0000000..22ddc89
--- /dev/null
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/PowermetricsProcessHandler.java
@@ -0,0 +1,95 @@
+package net.laprun.sustainability.power.sensors.macos.powermetrics;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import com.zaxxer.nuprocess.NuAbstractProcessHandler;
+import com.zaxxer.nuprocess.NuProcess;
+
+public class PowermetricsProcessHandler extends NuAbstractProcessHandler {
+ private String errorMsg;
+ private NuProcess process;
+ private final String[] command;
+ private final GrowableBuffer stdOutBuffer = new GrowableBuffer();
+ private final CompletableFuture output = new CompletableFuture<>();
+
+ public PowermetricsProcessHandler(String... command) {
+ if (command == null || command.length == 0) {
+ throw new IllegalArgumentException("No powermetrics options specified");
+ }
+ final var additionalArgsCardinality = 3;
+ final var args = new String[command.length + additionalArgsCardinality];
+ args[0] = "sudo";
+ args[1] = "powermetrics";
+ args[2] = "--samplers";
+ System.arraycopy(command, 0, args, additionalArgsCardinality, command.length);
+ this.command = args;
+ }
+
+ public String[] comand() {
+ return command;
+ }
+
+ @Override
+ public void onPreStart(NuProcess nuProcess) {
+ this.process = nuProcess;
+ }
+
+ @Override
+ public void onExit(int statusCode) {
+ if (Integer.MIN_VALUE == statusCode) {
+ throw new IllegalArgumentException("Unknown command " + Arrays.toString(command));
+ }
+ if (statusCode != 0) {
+ throw new RuntimeException("Couldn't execute command " + Arrays.toString(command)
+ + ". Error code: " + statusCode + ", message: " + errorMsg);
+ }
+ }
+
+ public void stop() {
+ if (process.isRunning()) {
+ process.destroy(false);
+ try {
+ process.waitFor(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ process.destroy(true);
+ }
+ }
+ }
+
+ @Override
+ public void onStdout(ByteBuffer buffer, boolean closed) {
+ if (buffer.hasRemaining()) {
+ stdOutBuffer.put(buffer);
+ }
+
+ if (closed) {
+ output.complete(new ByteArrayInputStream(stdOutBuffer.array()));
+ }
+ }
+
+ @Override
+ public void onStderr(ByteBuffer buffer, boolean closed) {
+ if (!closed) {
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ errorMsg = new String(bytes);
+ }
+ super.onStderr(buffer, closed);
+ }
+
+ public Future getInputStream() {
+ return output;
+ }
+
+ public boolean isRunning() {
+ return process != null && process.isRunning();
+ }
+}
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java
index 4d84d02..1811946 100644
--- a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java
@@ -2,60 +2,34 @@
import java.io.InputStream;
-import net.laprun.sustainability.power.Security;
-
public class ProcessMacOSPowermetricsSensor extends MacOSPowermetricsSensor {
- private final Security security;
- private Process powermetrics;
-
- public ProcessMacOSPowermetricsSensor(Security security) {
- this.security = security;
+ private final ProcessWrapper processWrapper = new NuProcessWrapper();
+ public ProcessMacOSPowermetricsSensor() {
// extract metadata
try {
- final var exec = security.execPowermetrics("cpu_power", "-i", "10", "-n", "1");
-
- // 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);
- }
- }
-
- initMetadata(exec.getInputStream());
+ initMetadata(processWrapper.streamForMetadata());
} catch (Exception e) {
throw new RuntimeException("Couldn't extract sensor metadata", e);
}
}
public void start(long frequency) throws Exception {
- if (!isStarted()) {
- // 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
- frequency = Math.min(0, frequency - 50);
- final var freq = Long.toString(frequency);
- powermetrics = security.execPowermetrics("cpu_power,tasks", "--show-process-samp-norm", "--show-process-gpu", "-i",
- freq);
- }
+ processWrapper.start(frequency);
}
@Override
public boolean isStarted() {
- return powermetrics != null && powermetrics.isAlive();
+ return processWrapper.isRunning();
}
@Override
protected InputStream getInputStream() {
- return powermetrics.getInputStream();
+ return processWrapper.streamForMeasure();
}
@Override
public void stop() {
- powermetrics.destroy();
+ processWrapper.stop();
}
}
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessRecord.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessRecord.java
new file mode 100644
index 0000000..d888cab
--- /dev/null
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessRecord.java
@@ -0,0 +1,98 @@
+package net.laprun.sustainability.power.sensors.macos.powermetrics;
+
+import net.laprun.sustainability.power.sensors.RegisteredPID;
+
+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';
+ }
+}
diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessWrapper.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessWrapper.java
new file mode 100644
index 0000000..8d9be78
--- /dev/null
+++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessWrapper.java
@@ -0,0 +1,15 @@
+package net.laprun.sustainability.power.sensors.macos.powermetrics;
+
+import java.io.InputStream;
+
+public interface ProcessWrapper {
+ InputStream streamForMetadata();
+
+ void start(long periodInMilliSeconds);
+
+ void stop();
+
+ boolean isRunning();
+
+ InputStream streamForMeasure();
+}
diff --git a/server/src/main/java/net/laprun/sustainability/power/PowerMeasurer.java b/server/src/main/java/net/laprun/sustainability/power/PowerMeasurer.java
index 9bc5834..3a8888d 100644
--- a/server/src/main/java/net/laprun/sustainability/power/PowerMeasurer.java
+++ b/server/src/main/java/net/laprun/sustainability/power/PowerMeasurer.java
@@ -40,6 +40,7 @@ private RegisteredPID track(long pid) throws Exception {
sensor.start(samplingPeriod.toMillis());
periodicSensorCheck = Multi.createFrom().ticks()
.every(samplingPeriod)
+ .log()
.map(sensor::update)
.broadcast()
.withCancellationAfterLastSubscriberDeparture()