From 88ec148eb8a887364b23a33005d502784792251d Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 17:18:04 +0100 Subject: [PATCH 01/13] refactor: record (for now) full CPU instead of always re-computing it --- .../power/sensors/cpu/PSExtractionStrategy.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java index 4b2c9f6d..c45f9620 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java @@ -14,6 +14,7 @@ public class PSExtractionStrategy implements ExtractionStrategy { public static final PSExtractionStrategy INSTANCE = new PSExtractionStrategy(); + private final int fullCPU = CpuCoreSensor.availableProcessors() * 100;// each core contributes 100% @Override public Map cpuSharesFor(Set pids) { @@ -92,8 +93,6 @@ private void extractCPUShare(String line, Map cpuShares) { } int fullCPU() { - final var fullCPU = CpuCoreSensor.availableProcessors() * 100; - Log.infof("'potential' full CPU: %d%%", fullCPU); - return fullCPU; // each core contributes 100% + return fullCPU; } } From e2668caa2a54f2e7bf81f4c0c8a0f3ac1b4f7573 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 17:18:47 +0100 Subject: [PATCH 02/13] fix: only clear map on error --- .../sustainability/power/sensors/cpu/PSExtractionStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java index c45f9620..5f8e8042 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java @@ -39,8 +39,8 @@ public void onStdout(ByteBuffer buffer, boolean closed) { public void onExit(int statusCode) { if (statusCode != 0) { Log.warnf("Failed to extract CPU shares for pids: %s", pids); + cpuShares.clear(); } - cpuShares.clear(); } }; new NuProcessBuilder(psHandler, psHandler.command()).run(); From b5dff70b0c3d60d0e288ee8637f3eb7e5655efea Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 17:19:08 +0100 Subject: [PATCH 03/13] refactor: improve logging, though destined to be removed --- .../sustainability/power/sensors/cpu/PSExtractionStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java index 5f8e8042..944edabc 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/cpu/PSExtractionStrategy.java @@ -77,8 +77,8 @@ private void extractCPUShare(String line, Map cpuShares) { var pid = line.substring(0, spaceIndex).trim(); var cpuPercentage = line.substring(spaceIndex + 1).trim(); - Log.infof("pid: %s / cpu: %s%%", pid, cpuPercentage); final var value = Double.parseDouble(cpuPercentage) / fullCPU(); + Log.infof("pid: %s -> cpu: %s/%d%% = %3.2f", pid, cpuPercentage, fullCPU(), value); if (value < 0) { Log.warnf("Invalid CPU share percentage: %s", cpuPercentage); return; From 3ab9fab74e6c8b75bef41014286358877fda7af6 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 17:25:46 +0100 Subject: [PATCH 04/13] feat: sensors can now "request" CPU sharing and enable it --- .../power/sensors/AbstractPowerSensor.java | 17 ++++++++++++++++- .../power/sensors/PowerSensor.java | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java index d79c5182..a1d4eaa5 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java @@ -3,6 +3,8 @@ import java.util.Map; import java.util.Set; +import org.eclipse.microprofile.config.inject.ConfigProperty; + import io.quarkus.logging.Log; import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.SensorUnit; @@ -11,7 +13,8 @@ public abstract class AbstractPowerSensor implements PowerSe protected final M measures; private long lastUpdateEpoch; private boolean started; - protected boolean cpuSharesEnabled = false; + @ConfigProperty(name = "net.laprun.sustainability.power.enable-cpu-share-sampling", defaultValue = "false") + protected boolean cpuSharesEnabled; private SensorMetadata metadata; public AbstractPowerSensor(M measures) { @@ -35,6 +38,18 @@ public SensorMetadata metadata() { abstract protected SensorMetadata nativeMetadata(); + @Override + public boolean wantsCPUShareSamplingEnabled() { + Log.infof("CPU Share sampling enabled: %b", cpuSharesEnabled); + return cpuSharesEnabled; + } + + @Override + public void enableCPUShareSampling(boolean enable) { + Log.infof("Enabling CPU Share sampling: %b", enable); + cpuSharesEnabled = enable; + } + @Override public RegisteredPID register(long pid) { Log.debugf("Registered pid: %d", pid); diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensor.java index cc8bec58..22ccdab1 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/PowerSensor.java @@ -22,6 +22,10 @@ default boolean supportsProcessAttribution() { return false; } + boolean wantsCPUShareSamplingEnabled(); + + void enableCPUShareSampling(boolean enable); + /** * Stops measuring power consumption */ From 774bb0149b43d5bf26fb176c9cc67b4c2348625e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 17:26:28 +0100 Subject: [PATCH 05/13] feat: record index of external CPU share component --- .../sustainability/power/sensors/AbstractPowerSensor.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java index a1d4eaa5..f2aa4f4e 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java @@ -16,6 +16,7 @@ public abstract class AbstractPowerSensor implements PowerSe @ConfigProperty(name = "net.laprun.sustainability.power.enable-cpu-share-sampling", defaultValue = "false") protected boolean cpuSharesEnabled; private SensorMetadata metadata; + private int externalCPUShareComponentIndex = -1; public AbstractPowerSensor(M measures) { this.measures = measures; @@ -31,6 +32,7 @@ public SensorMetadata metadata() { "CPU share estimate based on currently configured strategy used in CPUShare", false, SensorUnit.decimalPercentage) .build(); + externalCPUShareComponentIndex = metadata.metadataFor(EXTERNAL_CPU_SHARE_COMPONENT_NAME).index(); } } return metadata; @@ -50,6 +52,10 @@ public void enableCPUShareSampling(boolean enable) { cpuSharesEnabled = enable; } + protected int externalCPUShareComponentIndex() { + return externalCPUShareComponentIndex; + } + @Override public RegisteredPID register(long pid) { Log.debugf("Registered pid: %d", pid); From 24260439a6625ae473c52aaaaaef7e408597bc0b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 17:27:04 +0100 Subject: [PATCH 06/13] fix: typo --- .../net/laprun/sustainability/power/analysis/total/Totaler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java index ec19a7dc..03185a98 100644 --- a/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java +++ b/measure/src/main/java/net/laprun/sustainability/power/analysis/total/Totaler.java @@ -134,7 +134,7 @@ public SensorUnit expectedResultUnit() { public double computeTotalFrom(double[] measure) { if (measure.length < totalComponentIndices.length) { throw new IllegalArgumentException("Provided measure " + Arrays.toString(measure) + - " doesn't countain components for required total indices: " + Arrays.toString(totalComponentIndices)); + " doesn't contain components for required total indices: " + Arrays.toString(totalComponentIndices)); } checkValidated(); From 24c03e3c94928a0acfd9e8b7693db0328b16ca4b Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 17:33:05 +0100 Subject: [PATCH 07/13] fix: properly record raw values as expected by metadata --- .../sensors/linux/rapl/IntelRAPLSensor.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java index 1e8c29d6..dccb0880 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java @@ -18,7 +18,8 @@ */ public class IntelRAPLSensor extends AbstractPowerSensor { private final RAPLFile[] raplFiles; - private final SensorMetadata metadata; + private final int rawOffset; + private SensorMetadata nativeMetadata; private final long[] lastMeasuredSensorValues; /** @@ -62,7 +63,7 @@ private static SortedMap defaultRAPLFiles() { throw new RuntimeException("Failed to get RAPL energy readings, probably due to lack of read access "); raplFiles = files.values().toArray(new RAPLFile[0]); - final var rawOffset = files.size(); + rawOffset = files.size(); final var metadata = new ArrayList(rawOffset * 2); int fileNb = 0; for (String name : files.keySet()) { @@ -72,7 +73,7 @@ private static SortedMap defaultRAPLFiles() { name + " (raw micro Joule data)", false, µJ)); fileNb++; } - this.metadata = new SensorMetadata(metadata, + this.nativeMetadata = new SensorMetadata(metadata, "Linux RAPL derived information, see https://www.kernel.org/doc/html/latest/power/powercap/powercap.html"); lastMeasuredSensorValues = new long[raplFiles.length]; } @@ -128,13 +129,21 @@ static double computePowerInMilliWatts(long newMicroJoules, long prevMicroJoules @Override protected SensorMetadata nativeMetadata() { - return metadata; + try { + return nativeMetadata; + } finally { + // "forget" metadata once it's used in parent + nativeMetadata = null; + } } @Override protected Measures doUpdate(long lastUpdateEpoch, long newUpdateStartEpoch, Map cpuShares) { - final var measure = new double[raplFiles.length]; - readAndRecordSensor((value, index) -> measure[index] = computePowerInMilliWatts(index, value, newUpdateStartEpoch), + final var measure = new double[metadata().componentCardinality()]; + readAndRecordSensor((value, index) -> { + measure[index] = computePowerInMilliWatts(index, value, newUpdateStartEpoch); + measure[index + rawOffset] = value; + }, newUpdateStartEpoch); measures.singleMeasure(new SensorMeasure(measure, lastUpdateEpoch, newUpdateStartEpoch)); return measures; From e3462c26d57fcc56ac35cc49cc7af59558782635 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 17:33:54 +0100 Subject: [PATCH 08/13] fix: properly record cpu share for each process --- .../power/sensors/AbstractPowerSensor.java | 10 ++++++--- .../sensors/linux/rapl/IntelRAPLSensor.java | 20 ++++++++++++++--- .../powermetrics/MacOSPowermetricsSensor.java | 7 +----- .../power/sensors/test/TestPowerSensor.java | 2 +- .../linux/rapl/IntelRAPLSensorTest.java | 22 ++++++++++++++++++- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java index f2aa4f4e..83a68bb5 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java @@ -9,8 +9,8 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.SensorUnit; -public abstract class AbstractPowerSensor implements PowerSensor { - protected final M measures; +public abstract class AbstractPowerSensor implements PowerSensor { + protected final Measures measures; private long lastUpdateEpoch; private boolean started; @ConfigProperty(name = "net.laprun.sustainability.power.enable-cpu-share-sampling", defaultValue = "false") @@ -18,10 +18,14 @@ public abstract class AbstractPowerSensor implements PowerSe private SensorMetadata metadata; private int externalCPUShareComponentIndex = -1; - public AbstractPowerSensor(M measures) { + public AbstractPowerSensor(Measures measures) { this.measures = measures; } + public AbstractPowerSensor() { + this(new MapMeasures()); + } + @Override public SensorMetadata metadata() { if (metadata == null) { diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java index dccb0880..a0049971 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java @@ -16,7 +16,7 @@ /** * A sensor using Intel's RAPL accessed via Linux' powercap system. */ -public class IntelRAPLSensor extends AbstractPowerSensor { +public class IntelRAPLSensor extends AbstractPowerSensor { private final RAPLFile[] raplFiles; private final int rawOffset; private SensorMetadata nativeMetadata; @@ -58,7 +58,6 @@ private static SortedMap defaultRAPLFiles() { } IntelRAPLSensor(SortedMap files) { - super(new SingleMeasureMeasures()); if (files.isEmpty()) throw new RuntimeException("Failed to get RAPL energy readings, probably due to lack of read access "); @@ -145,7 +144,22 @@ protected Measures doUpdate(long lastUpdateEpoch, long newUpdateStartEpoch, Map< measure[index + rawOffset] = value; }, newUpdateStartEpoch); - measures.singleMeasure(new SensorMeasure(measure, lastUpdateEpoch, newUpdateStartEpoch)); + + int cpuShareIndex = externalCPUShareComponentIndex(); + boolean needMultipleMeasures = wantsCPUShareSamplingEnabled() && cpuShareIndex > 0 && cpuShares != null + && !cpuShares.isEmpty(); + final var single = new SensorMeasure(measure, lastUpdateEpoch, newUpdateStartEpoch); + measures.trackedPIDs().forEach(pid -> { + final SensorMeasure m; + if (needMultipleMeasures) { + measure[cpuShareIndex] = cpuShares.get(pid.pidAsString()); + m = new SensorMeasure(measure, lastUpdateEpoch, newUpdateStartEpoch); + } else { + m = single; + } + measures.record(pid, m); + }); + return measures; } 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 0bb23728..3e72f85b 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 @@ -14,7 +14,6 @@ import net.laprun.sustainability.power.SensorMeasure; import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.sensors.AbstractPowerSensor; -import net.laprun.sustainability.power.sensors.MapMeasures; import net.laprun.sustainability.power.sensors.Measures; import net.laprun.sustainability.power.sensors.PowerSensor; import net.laprun.sustainability.power.sensors.RegisteredPID; @@ -22,7 +21,7 @@ /** * A macOS powermetrics based {@link PowerSensor} implementation. */ -public abstract class MacOSPowermetricsSensor extends AbstractPowerSensor { +public abstract class MacOSPowermetricsSensor extends AbstractPowerSensor { /** * The Central Processing Unit component name */ @@ -57,10 +56,6 @@ public abstract class MacOSPowermetricsSensor extends AbstractPowerSensor { +public class TestPowerSensor extends AbstractPowerSensor { public static final String CPU = "cpu"; public static final SensorMetadata DEFAULT = new SensorMetadata( List.of(new SensorMetadata.ComponentMetadata(CPU, 0, "CPU", true, mW)), diff --git a/backend/src/test/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensorTest.java b/backend/src/test/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensorTest.java index b7692a35..0cf17881 100644 --- a/backend/src/test/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensorTest.java +++ b/backend/src/test/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensorTest.java @@ -104,11 +104,31 @@ void wattComputationShouldWork() throws Exception { final var pid = sensor.register(1234L); final var measures = sensor.update(1L, Map.of()); final var components = measures.getOrDefault(pid).components(); - assertEquals(1, components.length); + assertEquals(2, components.length); assertEquals(2, raplFile.callCount()); final var interval = raplFile.measureTimeFor(1) - raplFile.measureTimeFor(0); final var expected = (double) (raplFile.valueAt(1) - raplFile.valueAt(0)) / interval; assertEquals(expected, components[0]); + assertEquals(20000, components[1]); + } + + @Test + void shouldIncludeCPUShareIfRequested() throws Exception { + final var raplFile = new TestRAPLFile(10000L, 20000L, 30000L); + final var sensor = new TestIntelRAPLSensor(new TreeMap<>(Map.of("sensor", raplFile))); + sensor.enableCPUShareSampling(true); + sensor.start(500); + final var pid = sensor.register(1234L); + double cpuShare = 0.3; + final var measures = sensor.update(1L, Map.of("1234", cpuShare)); + final var components = measures.getOrDefault(pid).components(); + assertEquals(3, components.length); + assertEquals(2, raplFile.callCount()); + final var interval = raplFile.measureTimeFor(1) - raplFile.measureTimeFor(0); + final var expected = (double) (raplFile.valueAt(1) - raplFile.valueAt(0)) / interval; + assertEquals(expected, components[0]); + assertEquals(20000, components[1]); + assertEquals(cpuShare, components[2]); } private SensorMetadata loadMetadata(String... fileNames) { From 0e8ca81a5e47029664ef7c7e4da41c3677de8078 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 18:31:07 +0100 Subject: [PATCH 09/13] feat: only extract external CPU share if requested --- .../power/sensors/SamplingMeasurer.java | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/SamplingMeasurer.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/SamplingMeasurer.java index 2f2ba0d4..4751d1ff 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/SamplingMeasurer.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/SamplingMeasurer.java @@ -39,6 +39,10 @@ public class SamplingMeasurer { private Multi periodicSensorCheck; private final Map manuallyTrackedProcesses = new ConcurrentHashMap<>(); + public PowerSensor sensor() { + return sensor; + } + public Multi stream(String pid) throws Exception { final var parsedPID = validPIDOrFail(pid); return uncheckedStream(parsedPID); @@ -65,39 +69,45 @@ RegisteredPID track(long pid) throws Exception { if (!sensor.isStarted()) { sensor.start(samplingPeriod.toMillis()); - final var overSamplingFactor = 3; - final var cpuSharesMulti = Multi.createFrom().ticks() - // over sample but over a shorter period to ensure we have an average that covers most of the sampling period - .every(samplingPeriod.minus(50, ChronoUnit.MILLIS).dividedBy(overSamplingFactor)) - .runSubscriptionOn(Infrastructure.getDefaultExecutor()) - .map(tick -> CPUShare.cpuSharesFor(sensor.getRegisteredPIDs())) - .group() - .intoLists() - .of(overSamplingFactor) - .map(cpuShares -> { - // first convert list of mappings pid -> cpu to mapping pid -> list of cpu shares - Map> pidToRecordedCPUShares = new HashMap<>(); - cpuShares.forEach(cpuShare -> cpuShare.forEach( - (p, cpu) -> { - if (cpu != null && cpu > 0) { // drop null values to avoid skewing average even more - pidToRecordedCPUShares.computeIfAbsent(p, unused -> new ArrayList<>()).add(cpu); - } - })); - // then reduce each cpu shares list to their average - Map averages = new HashMap<>(pidToRecordedCPUShares.size()); - pidToRecordedCPUShares.forEach((p, values) -> averages.put(p, - values.stream().mapToDouble(Double::doubleValue).average().orElse(0))); - return averages; - }); - - final var samplingTicks = Multi.createFrom().ticks() - .every(samplingPeriod); - periodicSensorCheck = Multi.createBy() - .combining() - .streams(samplingTicks, cpuSharesMulti) - .asTuple() - .log() - .map(this::updateSensor) + final var samplingTicks = Multi.createFrom().ticks().every(samplingPeriod); + + if (sensor.wantsCPUShareSamplingEnabled()) { + final var overSamplingFactor = 3; + final var cpuSharesMulti = Multi.createFrom().ticks() + // over sample but over a shorter period to ensure we have an average that covers most of the sampling period + .every(samplingPeriod.minus(50, ChronoUnit.MILLIS).dividedBy(overSamplingFactor)) + .runSubscriptionOn(Infrastructure.getDefaultExecutor()) + .map(tick -> CPUShare.cpuSharesFor(sensor.getRegisteredPIDs())) + .group() + .intoLists() + .of(overSamplingFactor) + .map(cpuShares -> { + // first convert list of mappings pid -> cpu to mapping pid -> list of cpu shares + Map> pidToRecordedCPUShares = new HashMap<>(); + cpuShares.forEach(cpuShare -> cpuShare.forEach( + (p, cpu) -> { + if (cpu != null && cpu > 0) { // drop null values to avoid skewing average even more + pidToRecordedCPUShares.computeIfAbsent(p, unused -> new ArrayList<>()).add(cpu); + } + })); + // then reduce each cpu shares list to their average + Map averages = new HashMap<>(pidToRecordedCPUShares.size()); + pidToRecordedCPUShares.forEach((p, values) -> averages.put(p, + values.stream().mapToDouble(Double::doubleValue).average().orElse(0))); + return averages; + }); + periodicSensorCheck = Multi.createBy() + .combining() + .streams(samplingTicks, cpuSharesMulti) + .asTuple() + .log() + .map(this::updateSensor); + } else { + periodicSensorCheck = samplingTicks + .map(tick -> sensor.update(tick, Map.of())); + } + + periodicSensorCheck = periodicSensorCheck .broadcast() .withCancellationAfterLastSubscriberDeparture() .toAtLeast(1) From 5f1ea6b430b153996c5a3c015ad291778e906c26 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 18:37:27 +0100 Subject: [PATCH 10/13] fix: correctly record cpu share when enabled This requires creating copies of the "original" measure, so maybe the cpu share shouldn't be part of the components? --- .../power/sensors/AbstractPowerSensor.java | 2 -- .../sensors/linux/rapl/IntelRAPLSensor.java | 21 ++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java index 83a68bb5..4f798a89 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/AbstractPowerSensor.java @@ -46,13 +46,11 @@ public SensorMetadata metadata() { @Override public boolean wantsCPUShareSamplingEnabled() { - Log.infof("CPU Share sampling enabled: %b", cpuSharesEnabled); return cpuSharesEnabled; } @Override public void enableCPUShareSampling(boolean enable) { - Log.infof("Enabling CPU Share sampling: %b", enable); cpuSharesEnabled = enable; } diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java index a0049971..371c2387 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java @@ -12,6 +12,7 @@ import net.laprun.sustainability.power.SensorMetadata; import net.laprun.sustainability.power.sensors.AbstractPowerSensor; import net.laprun.sustainability.power.sensors.Measures; +import net.laprun.sustainability.power.sensors.RegisteredPID; /** * A sensor using Intel's RAPL accessed via Linux' powercap system. @@ -21,6 +22,7 @@ public class IntelRAPLSensor extends AbstractPowerSensor { private final int rawOffset; private SensorMetadata nativeMetadata; private final long[] lastMeasuredSensorValues; + private boolean needMultipleMeasures; /** * Initializes the RAPL sensor @@ -103,6 +105,8 @@ private static boolean addFileIfReadable(String raplFileAsString, SortedMap 0; } /** @@ -145,15 +149,22 @@ protected Measures doUpdate(long lastUpdateEpoch, long newUpdateStartEpoch, Map< }, newUpdateStartEpoch); - int cpuShareIndex = externalCPUShareComponentIndex(); - boolean needMultipleMeasures = wantsCPUShareSamplingEnabled() && cpuShareIndex > 0 && cpuShares != null - && !cpuShares.isEmpty(); final var single = new SensorMeasure(measure, lastUpdateEpoch, newUpdateStartEpoch); measures.trackedPIDs().forEach(pid -> { final SensorMeasure m; if (needMultipleMeasures) { - measure[cpuShareIndex] = cpuShares.get(pid.pidAsString()); - m = new SensorMeasure(measure, lastUpdateEpoch, newUpdateStartEpoch); + double cpuShare; + if(RegisteredPID.SYSTEM_TOTAL_REGISTERED_PID.equals(pid)) { + cpuShare = 1.0; + } else { + cpuShare = cpuShares.getOrDefault(pid.pidAsString(), 0.0); + } + // todo: avoid copying array, external cpu share should be recorded as a separate value, not a component maybe? + // copy array + final var copy = new double[measure.length]; + System.arraycopy(measure, 0, copy, 0, measure.length); + copy[externalCPUShareComponentIndex()] = cpuShare; + m = new SensorMeasure(copy, lastUpdateEpoch, newUpdateStartEpoch); } else { m = single; } From 4538ee9d3651ee5f0be57876cfbc5c99762d1b19 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 18:40:06 +0100 Subject: [PATCH 11/13] feat: use external CPU measure if not provided by sensor Note that the accuracy of this should be evaluated --- .../net/laprun/sustainability/cli/Power.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/cli/src/main/java/net/laprun/sustainability/cli/Power.java b/cli/src/main/java/net/laprun/sustainability/cli/Power.java index e18c0a81..148b795d 100644 --- a/cli/src/main/java/net/laprun/sustainability/cli/Power.java +++ b/cli/src/main/java/net/laprun/sustainability/cli/Power.java @@ -1,5 +1,7 @@ package net.laprun.sustainability.cli; +import static net.laprun.sustainability.power.sensors.PowerSensor.EXTERNAL_CPU_SHARE_COMPONENT_NAME; + import java.time.Instant; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -37,6 +39,10 @@ public class Power implements Runnable { public Power(SamplingMeasurer measurer) { this.measurer = measurer; + final var sensor = measurer.sensor(); + if (!sensor.supportsProcessAttribution()) { + sensor.enableCPUShareSampling(true); + } final var metadata = measurer.metadata(); totaler = new Totaler(metadata, SensorUnit.W); } @@ -68,15 +74,20 @@ public void run() { final var measureTime = measurer.persistence() .synthesizeAndAggregateForSession(Persistence.SYSTEM_TOTAL_APP_NAME, session, m -> (double) m.duration()) .orElseThrow(() -> new RuntimeException("Could not compute measure duration")); - final var systemPower = extractPowerConsumption(Persistence.SYSTEM_TOTAL_APP_NAME); + final var systemPower = extractPowerConsumption(Persistence.SYSTEM_TOTAL_APP_NAME, false); Log.infof("Command ran for: %dms, measure time: %3fms", commandHandler.duration(), measureTime); Log.infof("Total system power consumption: %3.2f%s", systemPower.value(), systemPower.unit()); if (totaler.isAttributed()) { - final var appPower = extractPowerConsumption(name); + final var appPower = extractPowerConsumption(name, false); Log.infof("App '%s' power consumption: %3.2f%s", cmd, appPower.value(), appPower.unit()); } else { - Log.info( - "Power consumption for this platform is not currently attributed: no per-process power is currently measured"); + if (measurer.sensor().wantsCPUShareSamplingEnabled()) { + final var appPower = extractPowerConsumption(name, true); + Log.infof("App '%s' power consumption: %3.2f%s", cmd, appPower.value(), appPower.unit()); + } else { + Log.info( + "Power consumption for this platform is not currently attributed: no per-process power is currently measured"); + } } } catch (Exception e) { throw new RuntimeException(e); @@ -87,10 +98,14 @@ public void run() { Quarkus.waitForExit(); } - private Measure extractPowerConsumption(String applicationName) { + private Measure extractPowerConsumption(String applicationName, boolean useExternalCPUShare) { + int cpuShareComponent = measurer.metadata().metadataFor(EXTERNAL_CPU_SHARE_COMPONENT_NAME).index(); final var appPower = measurer.persistence() .synthesizeAndAggregateForSession(applicationName, session, - m -> totaler.computeTotalFrom(m.components)) + m -> { + double factor = useExternalCPUShare ? m.components[cpuShareComponent] : 1.0; + return factor * totaler.computeTotalFrom(m.components); + }) .map(measure -> new Measure(measure, totaler.expectedResultUnit())) .orElseThrow(() -> new RuntimeException("Could not extract power consumption")); From f3d8d8868963c657d4cd3ad4839aa8bf25680588 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 19:12:31 +0100 Subject: [PATCH 12/13] fix: do not cache whether multiple measures are needed --- .../power/sensors/linux/rapl/IntelRAPLSensor.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java b/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java index 371c2387..51a0449f 100644 --- a/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java +++ b/backend/src/main/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensor.java @@ -22,7 +22,6 @@ public class IntelRAPLSensor extends AbstractPowerSensor { private final int rawOffset; private SensorMetadata nativeMetadata; private final long[] lastMeasuredSensorValues; - private boolean needMultipleMeasures; /** * Initializes the RAPL sensor @@ -105,8 +104,6 @@ private static boolean addFileIfReadable(String raplFileAsString, SortedMap 0; } /** @@ -149,12 +146,13 @@ protected Measures doUpdate(long lastUpdateEpoch, long newUpdateStartEpoch, Map< }, newUpdateStartEpoch); + final var needMultipleMeasures = wantsCPUShareSamplingEnabled() && externalCPUShareComponentIndex() > 0; final var single = new SensorMeasure(measure, lastUpdateEpoch, newUpdateStartEpoch); measures.trackedPIDs().forEach(pid -> { final SensorMeasure m; if (needMultipleMeasures) { double cpuShare; - if(RegisteredPID.SYSTEM_TOTAL_REGISTERED_PID.equals(pid)) { + if (RegisteredPID.SYSTEM_TOTAL_REGISTERED_PID.equals(pid)) { cpuShare = 1.0; } else { cpuShare = cpuShares.getOrDefault(pid.pidAsString(), 0.0); From 0d3cdfe0aa281b322bfda0df52d66e6390cc3380 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Fri, 14 Nov 2025 19:12:47 +0100 Subject: [PATCH 13/13] fix: timing issue with test --- .../power/sensors/linux/rapl/IntelRAPLSensorTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/test/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensorTest.java b/backend/src/test/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensorTest.java index 0cf17881..7eab2b97 100644 --- a/backend/src/test/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensorTest.java +++ b/backend/src/test/java/net/laprun/sustainability/power/sensors/linux/rapl/IntelRAPLSensorTest.java @@ -101,6 +101,7 @@ void wattComputationShouldWork() throws Exception { final var raplFile = new TestRAPLFile(10000L, 20000L, 30000L); final var sensor = new TestIntelRAPLSensor(new TreeMap<>(Map.of("sensor", raplFile))); sensor.start(500); + Thread.sleep(10); // ensure we get enough time between the measure performed during start and the first update final var pid = sensor.register(1234L); final var measures = sensor.update(1L, Map.of()); final var components = measures.getOrDefault(pid).components();