Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for #54 #55

Merged
merged 4 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
96 changes: 71 additions & 25 deletions src/main/java/org/noureddine/joularjx/cpu/PowermetricsMacOS.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
*/
public class PowermetricsMacOS implements Cpu {
private static final Logger logger = JoularJXLogging.getLogger();
private static final String POWER_INDICATOR = " Power: ";
private static final int POWER_INDICATOR_LENGTH = POWER_INDICATOR.length();
private static final String POWER_INDICATOR_M_CHIP = " Power: ";
private static final String POWER_INDICATOR_INTEL_CHIP = "Intel energy model derived package power (CPUs+GT+SA): ";
private Process process;
private BufferedReader reader;

private boolean initialized;
boolean intelCpu = false;

@Override
public void initialize() {
Expand All @@ -27,54 +30,84 @@ public void initialize() {
try {
// todo: detect when sudo fails as this currently won't throw an exception
process = Runtime.getRuntime().exec("sudo powermetrics --samplers cpu_power -i 1000");
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
initialized = true;
readHeader();
} catch (Exception exception) {
logger.log(Level.SEVERE, "Can't start powermetrics. Exiting...");
logger.throwing(getClass().getName(), "initialize", exception);
System.exit(1);
}
}

void readHeader() throws IOException {
BufferedReader reader = getReader();
for (int i=0; i<6; i++) {
String line = reader.readLine();
if (line.startsWith("EFI version")) {
intelCpu = true;
}
}
}

@Override
public double getInitialPower() {
return 0;
}

@Override
public double getCurrentPower(double cpuLoad) {
int headerLinesToSkip = 10;
int powerInMilliwatts = 0;
if (intelCpu) {
return getCurrentPowerIntel();
} else {
return getCurrentPowerM();
}
}

private double getCurrentPowerIntel() {
double powerInWatts = 0;
try {
// Should not be closed since it closes the process, so no try-with-resource
BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
boolean processingPower = false;
while ((line = input.readLine()) != null) {
if (headerLinesToSkip != 0) {
headerLinesToSkip--;
continue;
}
BufferedReader reader = getReader();
while (reader.ready() && (line = reader.readLine()) != null) {
alamers marked this conversation as resolved.
Show resolved Hide resolved

// skip empty / header lines
if (line.isEmpty() || line.startsWith("*")) {
continue;
}

// looking for line fitting the: "<name> Power: xxx mW" pattern and add all of the associated values together
final var powerIndicatorIndex = line.indexOf(POWER_INDICATOR);

// we need an exit condition to avoid looping forever (since there are always new lines, the process being periodical)
// if we started processing power lines and we don't find any anymore, we've reached the end of this "page" so exit the loop
if(processingPower && powerIndicatorIndex < 0) {
break;
// ofor Intel chips, the: "Intel energy model derived package power (CPUs+GT+SA): xxx W" pattern
alamers marked this conversation as resolved.
Show resolved Hide resolved
final var i = line.indexOf(POWER_INDICATOR_INTEL_CHIP);
if (i >= 0) {
powerInWatts += Double.parseDouble(line.substring(i + POWER_INDICATOR_INTEL_CHIP.length(), line.indexOf('W')));
}
}
return powerInWatts;
} catch (IOException e) {
logger.throwing(getClass().getName(), "getCurrentPower", e);
}

return 0.0;
}

public double getCurrentPowerM() {
int powerInMilliwatts = 0;
try {
String line;
BufferedReader reader = getReader();
while (reader.ready() && (line = reader.readLine()) != null) {

// lines with `-` as the second char are disregarded as of the form: "E-Cluster Power: 6 mW" which fits the pattern but shouldn't be considered
// also ignore Combined Power if available since it is the sum of the other components
if (powerIndicatorIndex >= 0 && '-' != line.charAt(1) && !line.startsWith("Combined")) {
powerInMilliwatts += extractPowerInMilliwatts(line, powerIndicatorIndex);
processingPower = true; // record we're in the power lines section of the powermetrics output
// skip empty / header lines
if (line.isEmpty() || line.startsWith("*")) {
continue;
}

// looking for line fitting the: "<name> Power: xxx mW" pattern and add all of the associated values together
// or, for Intel chips, the: "Intel energy model derived package power (CPUs+GT+SA): xxx W" pattern
final var i = line.indexOf(POWER_INDICATOR_M_CHIP);
if (i >= 0 && '-' != line.charAt(1) && !line.startsWith("Combined")) {
powerInMilliwatts += Integer.parseInt(line.substring(i + POWER_INDICATOR_M_CHIP.length(), line.indexOf('m') - 1));
}
}
return (double) powerInMilliwatts / 1000;
} catch (IOException e) {
Expand All @@ -84,9 +117,22 @@ public double getCurrentPower(double cpuLoad) {
return 0.0;
}

/**
* Override point for testing.
*/
protected BufferedReader getReader() {
return reader;
}

private static int extractPowerInMilliwatts(String line, int powerIndex) {
try {
return Integer.parseInt(line.substring(powerIndex + POWER_INDICATOR_LENGTH, line.indexOf('m') - 1));
if (line.trim().endsWith("mW")) {
alamers marked this conversation as resolved.
Show resolved Hide resolved
return Integer.parseInt(line.substring(powerIndex, line.indexOf('m') - 1));
} else if (line.trim().endsWith("W")) {
return (int) (1000.0 * Double.parseDouble(line.substring(powerIndex, line.indexOf('W'))));
} else {
logger.log(Level.SEVERE, "Power line does not end with mW or W, ignoring line: " + line);
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Cannot parse power value from line '" + line + "'", e);
}
Expand Down
141 changes: 141 additions & 0 deletions src/test/java/org/noureddine/joularjx/cpu/PowermetricsMacOSTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package org.noureddine.joularjx.cpu;

import org.junit.jupiter.api.Test;

import java.io.*;
import java.net.URISyntaxException;

import static org.junit.jupiter.api.Assertions.*;

public class PowermetricsMacOSTest {

@Test
void parseSonomaM1MaxPowerLines() {
PowermetricsMacOS cpu = new PowermetricsMacOS() {
@Override
protected BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/powermetrics-sonoma-m1max.txt")));
}
};
cpu.intelCpu = false;


// the ??-Cluster and Combined lines are to be ignored, hence do not count the 359mW
assertEquals(0.211d + 0.147d + 0d /* +0.359d */, cpu.getCurrentPower(0), 0.0001d);
}

@Test
void parseMontereyM2PowerLines() {
PowermetricsMacOS cpu = new PowermetricsMacOS() {
@Override
protected BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/powermetrics-monterey-m2.txt")));
}
};
cpu.intelCpu = false;

// the ??-Cluster and Combined lines are to be ignored, hence do not count the 6mW
assertEquals(/*0.006d*/ + 0d + 0.019d + 0.036d + 0.010d + 0d + 0.025d , cpu.getCurrentPower(0), 0.0001d);
}

@Test
void parseSonomaIntelPowerLines() {
PowermetricsMacOS cpu = new PowermetricsMacOS() {
@Override
protected BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/powermetrics-sonoma-intel.txt")));
}
};

cpu.intelCpu = true;

assertEquals(4.87d + 3.43d + 3.38d + 4.21d + 3.21d , cpu.getCurrentPower(0), 0.0001d);
}


@Test
void parseHeaderIntel() throws IOException {
PowermetricsMacOS cpu = new PowermetricsMacOS() {
@Override
protected BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/powermetrics-sonoma-intel.txt")));
}
};

cpu.readHeader();
assertTrue(cpu.intelCpu);
}

@Test
void parseHeaderM1() throws IOException {
PowermetricsMacOS cpu = new PowermetricsMacOS() {
@Override
protected BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/powermetrics-monterey-m2.txt")));
}
};

cpu.readHeader();
assertFalse(cpu.intelCpu);
}

/**
* Test if the reader returns whenever there are results.
* @throws IOException
*/
@Test
void testIntermittentResults() throws IOException, URISyntaxException, InterruptedException {
// hookup a writer to a reader
PipedInputStream intermittentInputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream(intermittentInputStream);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
BufferedReader reader = new BufferedReader(new InputStreamReader(intermittentInputStream));

// create the content blocks including headers
final String contents1 = "\n".repeat(10) + "CPU Power: 742 mW\n".repeat(2);
final String contents2 = "\n".repeat(10) + "CPU Power: 1200 mW\n".repeat(2);

PowermetricsMacOS cpu = new PowermetricsMacOS() {
@Override
protected BufferedReader getReader() {
return reader;
}
};

// nothing written yet, so expect 0
assertEquals(0d, cpu.getCurrentPower(0), 0.0001d);

Thread writerBlock1 = createWriter(writer, contents1);
writerBlock1.start();
writerBlock1.join();
assertEquals(2*0.742d, cpu.getCurrentPower(0), 0.0001d);

Thread writerBlock2 = createWriter(writer, contents2);
writerBlock2.start();
writerBlock2.join();
assertEquals(2*1.2d, cpu.getCurrentPower(0), 0.0001d);
}

/**
* Create a thread that writes the contents to the writer, simulating the actual process.
* @param writer the writer to write to
* @param contents the contents to write
* @return a thread
*/
private static Thread createWriter(BufferedWriter writer, String contents) {
Thread writerBlock1 = new Thread() {
@Override
public void run() {
try {
writer.write(contents);
writer.flush();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
return writerBlock1;
}


}
68 changes: 68 additions & 0 deletions src/test/resources/powermetrics-monterey-m2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
Machine model: Mac14,2
OS version: 21G72
Boot arguments:
Boot time: Mon Oct 2 10:37:21 2023



*** Sampled system activity (Mon Oct 23 16:51:12 2023 +0200) (1012.07ms elapsed) ***

*** Running tasks ***

Name ID CPU ms/s samp ms/s User% Deadlines (<2 ms, 2-5 ms) Wakeups (Intr, Pkg idle) GPU ms/s
WindowServer 391 281.98 283.25 82.97 1.97 0.00 428.87 8.85 0.00
mdworker_shared 29420 183.26 112.83 65.49 0.00 0.00 0.00 0.00 0.00
powermetrics 29419 23.78 23.88 6.05 0.00 0.00 0.98 0.00 0.00
ALL_TASKS -2 1222.65 1222.65 65.70 1161.97 15.81 2875.29 132.40 0.00

*** Sampled system activity (Tue Nov 28 11:28:55 2023 +0100) (1002.80ms elapsed) ***


**** Processor usage ****

E-Cluster Power: 6 mW
E-Cluster HW active frequency: 919 MHz
E-Cluster HW active residency: 5.37% (600 MHz: 0% 912 MHz: 100% 1284 MHz: 0% 1752 MHz: 0% 2004 MHz: 0% 2256 MHz: 0% 2424 MHz: .44%)
E-Cluster idle residency: 94.63%
E-Cluster instructions retired: 5.66564e+07
E-Cluster instructions per clock: 0.996462
CPU 0 frequency: 920 MHz
CPU 0 idle residency: 97.09%
CPU 0 active residency: 2.91% (600 MHz: 0% 912 MHz: 2.9% 1284 MHz: 0% 1752 MHz: 0% 2004 MHz: 0% 2256 MHz: 0% 2424 MHz: .02%)
CPU 1 frequency: 922 MHz
CPU 1 idle residency: 98.03%
CPU 1 active residency: 1.97% (600 MHz: 0% 912 MHz: 2.0% 1284 MHz: 0% 1752 MHz: 0% 2004 MHz: 0% 2256 MHz: 0% 2424 MHz: .01%)
CPU 2 frequency: 943 MHz
CPU 2 idle residency: 98.82%
CPU 2 active residency: 1.18% (600 MHz: 0% 912 MHz: 1.2% 1284 MHz: 0% 1752 MHz: 0% 2004 MHz: 0% 2256 MHz: 0% 2424 MHz: .02%)
CPU 3 frequency: 913 MHz
CPU 3 idle residency: 99.04%
CPU 3 active residency: 0.96% (600 MHz: 0% 912 MHz: .96% 1284 MHz: 0% 1752 MHz: 0% 2004 MHz: 0% 2256 MHz: 0% 2424 MHz: .00%)

P-Cluster Power: 4 mW
P-Cluster HW active frequency: 667 MHz
P-Cluster HW active residency: 0.00% (660 MHz: 100% 924 MHz: 0% 1188 MHz: 0% 1452 MHz: 0% 1704 MHz: 0% 1968 MHz: 0% 2208 MHz: 0% 2400 MHz: 0% 2568 MHz: 0% 2724 MHz: 0% 2868 MHz: 0% 2988 MHz: 0% 3096 MHz: 0% 3204 MHz: .03% 3324 MHz: .25% 3408 MHz: 0% 3504 MHz: 0%)
P-Cluster idle residency: 100.00%
P-Cluster instructions retired: 6.11748e+07
P-Cluster instructions per clock: 2.88735
CPU 4 frequency: 3079 MHz
CPU 4 idle residency: 99.81%
CPU 4 active residency: 0.19% (660 MHz: .03% 924 MHz: 0% 1188 MHz: 0% 1452 MHz: 0% 1704 MHz: 0% 1968 MHz: 0% 2208 MHz: 0% 2400 MHz: 0% 2568 MHz: 0% 2724 MHz: 0% 2868 MHz: 0% 2988 MHz: 0% 3096 MHz: 0% 3204 MHz: 0% 3324 MHz: .04% 3408 MHz: 0% 3504 MHz: .12%)
CPU 5 frequency: 1814 MHz
CPU 5 idle residency: 99.99%
CPU 5 active residency: 0.01% (660 MHz: .00% 924 MHz: 0% 1188 MHz: 0% 1452 MHz: 0% 1704 MHz: 0% 1968 MHz: 0% 2208 MHz: 0% 2400 MHz: 0% 2568 MHz: 0% 2724 MHz: 0% 2868 MHz: 0% 2988 MHz: 0% 3096 MHz: 0% 3204 MHz: 0% 3324 MHz: .00% 3408 MHz: 0% 3504 MHz: 0%)
CPU 6 frequency: 660 MHz
CPU 6 idle residency: 100.00%
CPU 6 active residency: 0.00% (660 MHz: .00% 924 MHz: 0% 1188 MHz: 0% 1452 MHz: 0% 1704 MHz: 0% 1968 MHz: 0% 2208 MHz: 0% 2400 MHz: 0% 2568 MHz: 0% 2724 MHz: 0% 2868 MHz: 0% 2988 MHz: 0% 3096 MHz: 0% 3204 MHz: 0% 3324 MHz: 0% 3408 MHz: 0% 3504 MHz: 0%)
CPU 7 frequency: 660 MHz
CPU 7 idle residency: 100.00%
CPU 7 active residency: 0.00% (660 MHz: .00% 924 MHz: 0% 1188 MHz: 0% 1452 MHz: 0% 1704 MHz: 0% 1968 MHz: 0% 2208 MHz: 0% 2400 MHz: 0% 2568 MHz: 0% 2724 MHz: 0% 2868 MHz: 0% 2988 MHz: 0% 3096 MHz: 0% 3204 MHz: 0% 3324 MHz: 0% 3408 MHz: 0% 3504 MHz: 0%)

System instructions retired: 1.17831e+08
System instructions per clock: 1.50979
ANE Power: 0 mW
DRAM Power: 19 mW
DCS Power: 36 mW
CPU Power: 10 mW
GPU Power: 0 mW
Package Power: 25 mW
Loading