Skip to content

Commit 2ee9ae2

Browse files
committed
refactor: extract powermetrics output parsing logic
1 parent 402e522 commit 2ee9ae2

File tree

2 files changed

+200
-171
lines changed

2 files changed

+200
-171
lines changed

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

Lines changed: 3 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
package net.laprun.sustainability.power.sensors.macos.powermetrics;
22

3-
import java.io.BufferedReader;
4-
import java.io.IOException;
53
import java.io.InputStream;
6-
import java.io.InputStreamReader;
7-
import java.util.ArrayList;
8-
import java.util.Arrays;
9-
import java.util.HashMap;
10-
import java.util.HashSet;
11-
import java.util.Map;
124

135
import io.quarkus.logging.Log;
146
import net.laprun.sustainability.power.SensorMetadata;
15-
import net.laprun.sustainability.power.measures.NoDurationSensorMeasure;
16-
import net.laprun.sustainability.power.measures.PartialSensorMeasure;
177
import net.laprun.sustainability.power.sensors.AbstractPowerSensor;
188
import net.laprun.sustainability.power.sensors.Measures;
199
import net.laprun.sustainability.power.sensors.PowerSensor;
@@ -50,10 +40,6 @@ public abstract class MacOSPowermetricsSensor extends AbstractPowerSensor {
5040
* The extracted CPU share component name, this represents the process' share of the measured power consumption
5141
*/
5242
public static final String CPU_SHARE = "cpuShare";
53-
private static final String DURATION_SUFFIX = "ms elapsed) ***";
54-
private static final int DURATION_SUFFIX_LENGTH = DURATION_SUFFIX.length();
55-
private static final String TASKS_SECTION_MARKER = "*** Running tasks ***";
56-
private static final String CPU_USAGE_SECTION_MARKER = "**** Processor usage ****";
5743

5844
private CPU cpu;
5945
private long lastCalled;
@@ -64,41 +50,7 @@ public boolean supportsProcessAttribution() {
6450
}
6551

6652
void initMetadata(InputStream inputStream) {
67-
try (BufferedReader input = new BufferedReader(new InputStreamReader(inputStream))) {
68-
String line;
69-
var components = new ArrayList<SensorMetadata.ComponentMetadata>();
70-
while ((line = input.readLine()) != null) {
71-
if (cpu == null) {
72-
// if we reached the OS line while cpu is still null, we're looking at an Apple Silicon CPU
73-
if (line.startsWith("OS ")) {
74-
cpu = new AppleSiliconCPU();
75-
} else if (line.startsWith("EFI ")) {
76-
cpu = new IntelCPU();
77-
}
78-
79-
if (cpu != null && cpu.doneAfterComponentsInitialization(components)) {
80-
break;
81-
}
82-
} else {
83-
// skip empty / header lines
84-
if (line.isEmpty() || line.startsWith("*")) {
85-
continue;
86-
}
87-
88-
cpu.addComponentIfFound(line, components);
89-
}
90-
}
91-
92-
if (cpu == null) {
93-
throw new IllegalStateException("Couldn't determine CPU family from powermetrics output");
94-
}
95-
96-
final var metadata = new SensorMetadata(components,
97-
"macOS powermetrics derived information, see https://firefox-source-docs.mozilla.org/performance/powermetrics.html");
98-
cpu.setMetadata(metadata);
99-
} catch (IOException e) {
100-
throw new RuntimeException(e);
101-
}
53+
cpu = PowerMetricsParser.initCPU(inputStream);
10254
}
10355

10456
@Override
@@ -114,137 +66,17 @@ protected void doStart() {
11466
}
11567
}
11668

117-
private static class Section {
118-
boolean done;
119-
}
120-
12169
Measures extractPowerMeasure(InputStream powerMeasureInput, long lastUpdateEpoch, long newUpdateEpoch) {
12270
if (Log.isDebugEnabled()) {
12371
final var start = System.currentTimeMillis();
12472
Log.debugf("powermetrics measure extraction last called %dms ago", (start - lastCalled));
12573
lastCalled = start;
12674
}
127-
final long startMs = lastUpdateEpoch;
128-
try {
129-
// Should not be closed since it closes the process
130-
BufferedReader input = new BufferedReader(new InputStreamReader(powerMeasureInput));
131-
String line;
132-
133-
double totalSampledCPU = -1;
134-
double totalSampledGPU = -1;
135-
// copy the pids so that we can remove them as soon as we've processed them
136-
final var pidsToProcess = new HashSet<>(measures.trackedPIDs());
137-
// remove total system "pid"
138-
pidsToProcess.remove(RegisteredPID.SYSTEM_TOTAL_REGISTERED_PID);
139-
// start measure
140-
final var pidMeasures = new HashMap<RegisteredPID, ProcessRecord>(measures.numberOfTrackedPIDs());
141-
final var metadata = metadata();
142-
final var powerComponents = new HashMap<String, Number>(metadata.componentCardinality());
143-
var duration = -1L;
144-
Section processes = null;
145-
Section cpuSection = null;
146-
while ((line = input.readLine()) != null) {
147-
if (line.isEmpty()) {
148-
continue;
149-
}
150-
151-
if (line.charAt(0) == '*') {
152-
// check if we have the sample duration
153-
if (duration == -1 && line.endsWith(DURATION_SUFFIX)) {
154-
final var startLookingIndex = line.length() - DURATION_SUFFIX_LENGTH;
155-
final var lastOpenParenIndex = line.lastIndexOf('(', startLookingIndex);
156-
if (lastOpenParenIndex > 0) {
157-
duration = Math.round(Float.parseFloat(line.substring(lastOpenParenIndex + 1, startLookingIndex)));
158-
}
159-
continue;
160-
}
161-
162-
// check for the beginning of tasks section
163-
if (processes == null && line.equals(TASKS_SECTION_MARKER)) {
164-
// make flag null to indicate it needs to be set to true on the next iteration, to avoid processing the marker line for processes
165-
processes = new Section();
166-
continue;
167-
}
168-
169-
if (cpuSection == null && line.equals(CPU_USAGE_SECTION_MARKER)) {
170-
cpuSection = new Section();
171-
continue;
172-
}
173-
174-
continue;
175-
}
176-
177-
// first, look for process line detailing share
178-
if (processes != null && !processes.done && !pidsToProcess.isEmpty()) {
179-
if (line.startsWith("ALL_TASKS")) {
180-
processes.done = true; // we reached the end of the process section
181-
} else {
182-
for (RegisteredPID pid : pidsToProcess) {
183-
if (line.contains(pid.stringForMatching())) {
184-
pidMeasures.put(pid, new ProcessRecord(line));
185-
pidsToProcess.remove(pid);
186-
break;
187-
}
188-
}
189-
}
190-
}
191-
192-
// then skip all lines until we get the totals
193-
if (totalSampledCPU < 0 && line.startsWith("ALL_TASKS")) {
194-
final var totals = new ProcessRecord(line);
195-
// compute ratio
196-
totalSampledCPU = totals.cpu;
197-
totalSampledGPU = totals.gpu > 0 ? totals.gpu : 0;
198-
if (!pidsToProcess.isEmpty()) {
199-
Log.warnf("Couldn't find processes: %s",
200-
Arrays.toString(pidsToProcess.stream().map(RegisteredPID::pid).toArray(Long[]::new)));
201-
}
202-
}
203-
204-
// we need an exit condition to break out of the loop, otherwise we'll just keep looping forever since there are always new lines since the process is periodical
205-
// fixme: perhaps we should relaunch the process on each update loop instead of keeping it running? Not sure which is more efficient
206-
if (cpuSection != null && !cpuSection.done && cpu.doneExtractingPowerComponents(line, powerComponents)) {
207-
break;
208-
}
209-
}
210-
211-
double finalTotalSampledGPU = totalSampledGPU;
212-
double finalTotalSampledCPU = totalSampledCPU;
213-
final var endMs = newUpdateEpoch;
214-
final var durationMs = duration;
215-
216-
// handle total system measure separately
217-
final var systemTotalMeasure = getSystemTotalMeasure(metadata, powerComponents);
218-
recordMeasure(RegisteredPID.SYSTEM_TOTAL_REGISTERED_PID, systemTotalMeasure, startMs, endMs, durationMs);
219-
220-
pidMeasures.forEach((pid, record) -> {
221-
final var attributedMeasure = record.asAttributedMeasure(metadata, powerComponents, finalTotalSampledCPU,
222-
finalTotalSampledGPU);
223-
recordMeasure(pid, attributedMeasure, startMs, endMs, durationMs);
224-
});
225-
} catch (Exception exception) {
226-
throw new RuntimeException(exception);
227-
}
75+
PowerMetricsParser.extractPowerMeasure(powerMeasureInput, measures, lastUpdateEpoch, newUpdateEpoch,
76+
measures.trackedPIDs(), metadata(), cpu);
22877
return measures;
22978
}
23079

231-
private void recordMeasure(RegisteredPID pid, double[] components, long startMs, long endMs, long duration) {
232-
measures.record(pid, duration > 0 ? new PartialSensorMeasure(components, startMs, endMs, duration)
233-
: new NoDurationSensorMeasure(components, startMs, endMs));
234-
}
235-
236-
private static double[] getSystemTotalMeasure(SensorMetadata metadata, Map<String, Number> powerComponents) {
237-
final var measure = new double[metadata.componentCardinality()];
238-
metadata.components().forEach((name, cm) -> {
239-
final var index = cm.index();
240-
final var value = CPU_SHARE.equals(name) ? 1.0
241-
: powerComponents.getOrDefault(name, 0).doubleValue();
242-
measure[index] = value;
243-
});
244-
245-
return measure;
246-
}
247-
24880
@Override
24981
protected Measures doUpdate(long lastUpdateEpoch, long newUpdateStartEpoch) {
25082
return extractPowerMeasure(getInputStream(), lastUpdateEpoch, newUpdateStartEpoch);

0 commit comments

Comments
 (0)