Skip to content

Commit b823d80

Browse files
committed
feat: isolate process management code into ProcessWrapper
This allows for different implementations to be tested.
1 parent 371677b commit b823d80

File tree

8 files changed

+217
-45
lines changed

8 files changed

+217
-45
lines changed

backend/src/main/java/net/laprun/sustainability/power/Security.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,31 @@ public Process sudo(String... cmd) throws IOException {
7070
throw new IllegalArgumentException("No command specified to run with sudo");
7171
}
7272

73+
final Process exec;
7374
if (!isRunningAsAdministrator()) {
7475
final var args = new String[cmd.length + 2];
7576
args[0] = "sudo";
7677
args[1] = "-S";
7778
System.arraycopy(cmd, 0, args, 2, cmd.length);
7879
final var runWithSudo = new ProcessBuilder(args);
79-
return ProcessBuilder.startPipeline(List.of(new ProcessBuilder("echo", pwd), runWithSudo)).getLast();
80+
exec = ProcessBuilder.startPipeline(List.of(new ProcessBuilder("echo", pwd), runWithSudo)).getLast();
8081
} else {
81-
return new ProcessBuilder().command(cmd).start();
82+
exec = new ProcessBuilder().command(cmd).start();
8283
}
84+
85+
// if the process is already dead, get the error
86+
if (!exec.isAlive()) {
87+
final var exitValue = exec.exitValue();
88+
if (exitValue != 0) {
89+
final String errorMsg;
90+
try (final var error = exec.errorReader()) {
91+
errorMsg = error.readLine();
92+
}
93+
throw new RuntimeException(
94+
"Couldn't execute powermetrics. Error code: " + exitValue + ", message: " + errorMsg);
95+
}
96+
}
97+
98+
return exec;
8399
}
84100
}

backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensorProducer.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
package net.laprun.sustainability.power.sensors;
22

33
import jakarta.enterprise.inject.Produces;
4-
import jakarta.inject.Inject;
54
import jakarta.inject.Singleton;
65

7-
import net.laprun.sustainability.power.Security;
86
import net.laprun.sustainability.power.sensors.linux.rapl.IntelRAPLSensor;
97
import net.laprun.sustainability.power.sensors.macos.powermetrics.ProcessMacOSPowermetricsSensor;
108

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

15-
@Inject
16-
Security security;
17-
1813
@Produces
1914
public PowerSensor sensor() {
20-
return determinePowerSensor(security);
15+
return determinePowerSensor();
2116
}
2217

23-
public static PowerSensor determinePowerSensor(Security security) {
18+
public static PowerSensor determinePowerSensor() {
2419
if (OS_NAME.contains("mac os x")) {
25-
return new ProcessMacOSPowermetricsSensor(security);
20+
return new ProcessMacOSPowermetricsSensor();
2621
}
2722

2823
if (!OS_NAME.contains("linux")) {

backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/FileMacOSPowermetricsSensor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public void start(long samplingFrequencyInMillis) {
3939
@Override
4040
public void stop() {
4141
started = false;
42+
// need to defer reading metadata until we know the file has been populated
4243
initMetadata(getInputStream());
4344
}
4445
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package net.laprun.sustainability.power.sensors.macos.powermetrics;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
6+
import io.quarkus.logging.Log;
7+
8+
public class JavaProcessWrapper implements ProcessWrapper {
9+
private Process powermetrics;
10+
11+
@Override
12+
public InputStream streamForMetadata() {
13+
final Process exec;
14+
try {
15+
exec = execPowermetrics("cpu_power", "-i", "10", "-n", "1");
16+
exec.waitFor();
17+
} catch (IOException | InterruptedException e) {
18+
throw new RuntimeException(e);
19+
}
20+
21+
return exec.getInputStream();
22+
}
23+
24+
@Override
25+
public void start(long periodInMilliSeconds) {
26+
if (!isRunning()) {
27+
// 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
28+
periodInMilliSeconds = periodInMilliSeconds > 100 ? periodInMilliSeconds - 50 : periodInMilliSeconds;
29+
final var freq = Long.toString(periodInMilliSeconds);
30+
try {
31+
powermetrics = execPowermetrics("cpu_power,tasks", "--show-process-samp-norm", "--show-process-gpu", "-i",
32+
freq);
33+
} catch (IOException e) {
34+
throw new RuntimeException(e);
35+
}
36+
}
37+
38+
}
39+
40+
@Override
41+
public void stop() {
42+
powermetrics.destroy();
43+
}
44+
45+
@Override
46+
public boolean isRunning() {
47+
return powermetrics != null && powermetrics.isAlive();
48+
}
49+
50+
@Override
51+
public InputStream streamForMeasure() {
52+
return powermetrics.getInputStream();
53+
}
54+
55+
public Process execPowermetrics(String... options) throws IOException {
56+
if (options == null || options.length == 0) {
57+
throw new IllegalArgumentException("No powermetrics options specified");
58+
}
59+
final var additionalArgsCardinality = 3;
60+
final var args = new String[options.length + additionalArgsCardinality];
61+
args[0] = "sudo";
62+
args[1] = "powermetrics";
63+
args[2] = "--samplers";
64+
System.arraycopy(options, 0, args, additionalArgsCardinality, options.length);
65+
final var start = System.currentTimeMillis();
66+
final Process exec = new ProcessBuilder().command(args).start();
67+
Log.info("Starting process took " + (System.currentTimeMillis() - start) + "ms");
68+
// if the process is already dead, get the error
69+
if (!exec.isAlive()) {
70+
final var exitValue = exec.exitValue();
71+
if (exitValue != 0) {
72+
final String errorMsg;
73+
try (final var error = exec.errorReader()) {
74+
errorMsg = error.readLine();
75+
}
76+
throw new RuntimeException(
77+
"Couldn't execute powermetrics. Error code: " + exitValue + ", message: " + errorMsg);
78+
}
79+
}
80+
return exec;
81+
}
82+
83+
}

backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/MacOSPowermetricsSensor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Measures extractPowerMeasure(InputStream powerMeasureInput, Long tick) {
158158
final var hasGPU = totalSampledGPU != 0;
159159
double finalTotalSampledGPU = totalSampledGPU;
160160
double finalTotalSampledCPU = totalSampledCPU;
161+
final var endMs = System.currentTimeMillis();
161162
pidMeasures.forEach((pid, record) -> {
162163
final var cpuShare = record.cpu / finalTotalSampledCPU;
163164
final var measure = new double[metadata.componentCardinality()];
@@ -178,8 +179,7 @@ Measures extractPowerMeasure(InputStream powerMeasureInput, Long tick) {
178179
measure[index] = value;
179180
}
180181
});
181-
182-
measures.record(pid, new SensorMeasure(measure, start, System.currentTimeMillis()));
182+
measures.record(pid, new SensorMeasure(measure, start, endMs));
183183
});
184184
} catch (Exception exception) {
185185
throw new RuntimeException(exception);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package net.laprun.sustainability.power.sensors.macos.powermetrics;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.InputStream;
5+
import java.nio.ByteBuffer;
6+
import java.util.Arrays;
7+
import java.util.concurrent.TimeUnit;
8+
9+
import com.zaxxer.nuprocess.NuAbstractProcessHandler;
10+
import com.zaxxer.nuprocess.NuProcess;
11+
12+
public class PowermetricsProcessHandler extends NuAbstractProcessHandler {
13+
private String errorMsg;
14+
private NuProcess process;
15+
private final String[] command;
16+
private ByteArrayInputStream bais;
17+
18+
public PowermetricsProcessHandler(String... command) {
19+
if (command == null || command.length == 0) {
20+
throw new IllegalArgumentException("No powermetrics options specified");
21+
}
22+
final var additionalArgsCardinality = 3;
23+
final var args = new String[command.length + additionalArgsCardinality];
24+
args[0] = "sudo";
25+
args[1] = "powermetrics";
26+
args[2] = "--samplers";
27+
System.arraycopy(command, 0, args, additionalArgsCardinality, command.length);
28+
this.command = args;
29+
}
30+
31+
public String[] comand() {
32+
return command;
33+
}
34+
35+
@Override
36+
public void onPreStart(NuProcess nuProcess) {
37+
this.process = nuProcess;
38+
}
39+
40+
@Override
41+
public void onExit(int statusCode) {
42+
if (Integer.MIN_VALUE == statusCode) {
43+
throw new IllegalArgumentException("Unknown command " + Arrays.toString(command));
44+
}
45+
if (statusCode != 0) {
46+
throw new RuntimeException("Couldn't execute command " + Arrays.toString(command)
47+
+ ". Error code: " + statusCode + ", message: " + errorMsg);
48+
}
49+
}
50+
51+
public void stop() {
52+
if (process.isRunning()) {
53+
process.destroy(false);
54+
try {
55+
process.waitFor(5, TimeUnit.SECONDS);
56+
} catch (InterruptedException e) {
57+
throw new RuntimeException(e);
58+
} finally {
59+
process.destroy(true);
60+
}
61+
}
62+
}
63+
64+
@Override
65+
public void onStdout(ByteBuffer buffer, boolean closed) {
66+
if (!closed) {
67+
bais = new ByteArrayInputStream(buffer.array());
68+
}
69+
}
70+
71+
@Override
72+
public void onStderr(ByteBuffer buffer, boolean closed) {
73+
if (!closed) {
74+
byte[] bytes = new byte[buffer.remaining()];
75+
buffer.get(bytes);
76+
errorMsg = new String(bytes);
77+
}
78+
super.onStderr(buffer, closed);
79+
}
80+
81+
public InputStream getInputStream() {
82+
return bais;
83+
}
84+
85+
public boolean isRunning() {
86+
return process != null && process.isRunning();
87+
}
88+
}

backend/src/main/java/net/laprun/sustainability/power/sensors/macos/powermetrics/ProcessMacOSPowermetricsSensor.java

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,34 @@
22

33
import java.io.InputStream;
44

5-
import net.laprun.sustainability.power.Security;
6-
75
public class ProcessMacOSPowermetricsSensor extends MacOSPowermetricsSensor {
8-
private final Security security;
9-
private Process powermetrics;
10-
11-
public ProcessMacOSPowermetricsSensor(Security security) {
12-
this.security = security;
6+
private final ProcessWrapper processWrapper = new JavaProcessWrapper();
137

8+
public ProcessMacOSPowermetricsSensor() {
149
// extract metadata
1510
try {
16-
final var exec = security.execPowermetrics("cpu_power", "-i", "10", "-n", "1");
17-
18-
// if the process is already dead, get the error
19-
if (!exec.isAlive()) {
20-
final var exitValue = exec.exitValue();
21-
if (exitValue != 0) {
22-
final String errorMsg;
23-
try (final var error = exec.errorReader()) {
24-
errorMsg = error.readLine();
25-
}
26-
throw new RuntimeException(
27-
"Couldn't execute powermetrics. Error code: " + exitValue + ", message: " + errorMsg);
28-
}
29-
}
30-
31-
initMetadata(exec.getInputStream());
11+
initMetadata(processWrapper.streamForMetadata());
3212
} catch (Exception e) {
3313
throw new RuntimeException("Couldn't extract sensor metadata", e);
3414
}
3515
}
3616

3717
public void start(long frequency) throws Exception {
38-
if (!isStarted()) {
39-
// 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
40-
frequency = Math.min(0, frequency - 50);
41-
final var freq = Long.toString(frequency);
42-
powermetrics = security.execPowermetrics("cpu_power,tasks", "--show-process-samp-norm", "--show-process-gpu", "-i",
43-
freq);
44-
}
18+
processWrapper.start(frequency);
4519
}
4620

4721
@Override
4822
public boolean isStarted() {
49-
return powermetrics != null && powermetrics.isAlive();
23+
return processWrapper.isRunning();
5024
}
5125

5226
@Override
5327
protected InputStream getInputStream() {
54-
return powermetrics.getInputStream();
28+
return processWrapper.streamForMeasure();
5529
}
5630

5731
@Override
5832
public void stop() {
59-
powermetrics.destroy();
33+
processWrapper.stop();
6034
}
6135
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package net.laprun.sustainability.power.sensors.macos.powermetrics;
2+
3+
import java.io.InputStream;
4+
5+
public interface ProcessWrapper {
6+
InputStream streamForMetadata();
7+
8+
void start(long periodInMilliSeconds);
9+
10+
void stop();
11+
12+
boolean isRunning();
13+
14+
InputStream streamForMeasure();
15+
}

0 commit comments

Comments
 (0)