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 calculation of Hz on Linux #626

Merged
merged 1 commit into from
Sep 21, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
Fix calculation of Hz on Linux
  • Loading branch information
dbwiddis committed Sep 21, 2018
commit c22edf62119acf76f252f26bba3533e16097b0c3
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
3.9.0 (in progress)
================
* [#626](https://github.com/oshi/oshi/pull/626): Fix calculation of Hz on Linux - [@dbwiddis](https://github.com/dbwiddis).
* Your contribution here.

3.8.1 (09/01/2018), 3.8.2 (09/07/2018), 3.8.3 (09/14/2018)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,30 +153,7 @@ protected void calculateProcessorCounts() {
*/
@Override
public synchronized long[] getSystemCpuLoadTicks() {
long[] ticks = new long[TickType.values().length];
// /proc/stat expected format
// first line is overall user,nice,system,idle,iowait,irq, etc.
// cpu 3357 0 4313 1362393 ...
String tickStr;
List<String> procStat = FileUtil.readFile("/proc/stat");
if (!procStat.isEmpty()) {
tickStr = procStat.get(0);
} else {
return ticks;
}
// Split the line. Note the first (0) element is "cpu" so remaining
// elements are offset by 1 from the enum index
String[] tickArr = ParseUtil.whitespaces.split(tickStr);
if (tickArr.length <= TickType.IDLE.getIndex()) {
// If ticks don't at least go user/nice/system/idle, abort
return ticks;
}
// Note tickArr is offset by 1
for (int i = 0; i < TickType.values().length; i++) {
ticks[i] = ParseUtil.parseLongOrDefault(tickArr[i + 1], 0L);
}
// Ignore guest or guest_nice, they are included in user/nice
return ticks;
return ProcUtil.getSystemCpuLoadTicks();
}

/**
Expand Down Expand Up @@ -237,7 +214,7 @@ public long[][] getProcessorCpuLoadTicks() {
*/
@Override
public long getSystemUptime() {
return (long) ProcUtil.getSystemUptimeFromProc();
return (long) ProcUtil.getSystemUptimeSeconds();
}

/**
Expand Down
158 changes: 49 additions & 109 deletions oshi-core/src/main/java/oshi/software/os/linux/LinuxOperatingSystem.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

Expand All @@ -34,6 +32,7 @@
import com.sun.jna.Native;
import com.sun.jna.Pointer;

import oshi.hardware.CentralProcessor.TickType;
import oshi.jna.platform.linux.Libc;
import oshi.jna.platform.linux.Libc.Sysinfo;
import oshi.software.common.AbstractOperatingSystem;
Expand Down Expand Up @@ -69,11 +68,6 @@ public class LinuxOperatingSystem extends AbstractOperatingSystem {
// To get the actual size in bytes we need to multiply that with page size.
private final int memoryPageSize;

// Jiffies per second, used for process time counters.
private static long hz = 1000L;
// Boot time in MS
private static long bootTime = 0L;

// Order the field is in /proc/pid/stat
enum ProcPidStat {
// The parsing implementation in ParseUtil requires these to be declared
Expand Down Expand Up @@ -108,9 +102,11 @@ public int getOrder() {

private transient LinuxUserGroupInfo userGroupInfo = new LinuxUserGroupInfo();

static {
init();
}
// Jiffies per second, used for process time counters.
private static final long USER_HZ = calcHz();
// Boot time in MS
private static final long BOOT_TIME = System.currentTimeMillis()
- (long) (1000 * ProcUtil.getSystemUptimeSeconds());

public LinuxOperatingSystem() {
this.manufacturer = "GNU/Linux";
Expand All @@ -119,101 +115,9 @@ public LinuxOperatingSystem() {
// to pass to version constructor
this.version = new LinuxOSVersionInfoEx(this.versionId, this.codeName);
this.memoryPageSize = getMemoryPageSize();
init();
initBitness();
}

/**
* Correlate the youngest process start time in seconds with start time in
* jiffies
*/
private static void init() {
// To correlate a process start time in seconds with the same process
// start time in jiffies. We prefer the youngest (or close to it) which
// minimizes its up time (etime)
// Timeline:
// BOOT|<----jiffies---->|<----etime---->|NOW
// BOOT|<------------uptime------------->|NOW

// To avoid having to check all processes we can just pick the highest
// PID. This will either be the youngest one or at least big enough.

// Get all the pid files (guaranteed to be digit-only filenames)
File[] pids = ProcUtil.getPidFiles();
// Sort descending "numerically"
Arrays.sort(pids, new Comparator<File>() {
@Override
public int compare(File f1, File f2) {
return Integer.valueOf(f2.getName()).compareTo(Integer.valueOf(f1.getName()));
}
});

// Iterate /proc/[pid]/stat checking the creation time (field 22,
// jiffies since boot). Since we're working on descending PIDs, we
// expect the first (higher PIDs) to be younger, but may have processes
// that ended since we collected the files. The first time we get a
// value we'll save it as the youngest and quit.
long youngestJiffies = 0L;
String youngestPid = "";
for (File pid : pids) {
List<String> stat = FileUtil.readFile(String.format("/proc/%s/stat", pid.getName()), false);
if (!stat.isEmpty() && stat.get(0).contains(")")) {
String procPidStat = stat.get(0);
// 2nd elment is process name and may contain spaces, but is
// within parenthesis. Split from the ')' index.
int parenIndex = procPidStat.lastIndexOf(')');
String[] split = ParseUtil.whitespaces.split(procPidStat.substring(parenIndex));
if (split.length >= 21) {
// ')' is split index 0 but stat element 2.
// We want element 22 so it will be at index 20 of the split
youngestJiffies = ParseUtil.parseLongOrDefault(split[20], 0L);
youngestPid = pid.getName();
// Add 1 to account for pid that didn't make the split
procPidStatLength = split.length + 1;
break;
}
}
}
LOG.debug("Youngest PID is {} with {} jiffies", youngestPid, youngestJiffies);
// Shouldn't happen but avoiding Division by zero
if (youngestJiffies == 0) {
LOG.error("Couldn't find any running processes, which is odd since we are in a running process. "
+ "Process time values are in jiffies, not milliseconds.");
return;
}

float startTimeSecsSinceBoot = ProcUtil.getSystemUptimeFromProc();
bootTime = System.currentTimeMillis() - (long) (1000 * startTimeSecsSinceBoot);

// This takes advantage of the fact that ps does all the heavy lifting
// of sorting out HZ internally.
String etime = ExecutingCommand.getFirstAnswer(String.format("ps -p %s -o etimes=", youngestPid));
// Since we picked the youngest process, it's safe to assume an
// etime close to 0 in case this command fails; the longer the system
// has been up, the less impact this assumption will have
if (!etime.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Etime is {} seconds", etime.trim());
}
startTimeSecsSinceBoot -= Float.parseFloat(etime.trim());
}
// By subtracting etime (secs) from uptime (secs) we get uptime (in
// secs) when the process was started. This correlates with startTime in
// jiffies for this process
LOG.debug("Start time in secs: {}", startTimeSecsSinceBoot);
if (startTimeSecsSinceBoot <= 0) {
LOG.warn("Couldn't calculate jiffies per second. "
+ "Process time values are in jiffies, not milliseconds.");
return;
}

// divide jiffies (since boot) by seconds (since boot)
hz = (long) (youngestJiffies / startTimeSecsSinceBoot + 0.5f);
// reset to default if value is invalid
if (hz == 0)
hz = 1000L;
}

private void initBitness() {
if (this.bitness < 64 && ExecutingCommand.getFirstAnswer("uname -m").indexOf("64") != -1) {
this.bitness = 64;
Expand Down Expand Up @@ -286,9 +190,9 @@ public OSProcess getProcess(int pid) {
proc.setPriority((int) statArray[ProcPidStat.PRIORITY.ordinal()]);
proc.setVirtualSize(statArray[ProcPidStat.VSZ.ordinal()]);
proc.setResidentSetSize(statArray[ProcPidStat.RSS.ordinal()] * this.memoryPageSize);
proc.setKernelTime(statArray[ProcPidStat.KERNEL_TIME.ordinal()] * 1000L / hz);
proc.setUserTime(statArray[ProcPidStat.USER_TIME.ordinal()] * 1000L / hz);
proc.setStartTime(bootTime + statArray[ProcPidStat.START_TIME.ordinal()] * 1000L / hz);
proc.setKernelTime(statArray[ProcPidStat.KERNEL_TIME.ordinal()] * 1000L / USER_HZ);
proc.setUserTime(statArray[ProcPidStat.USER_TIME.ordinal()] * 1000L / USER_HZ);
proc.setStartTime(BOOT_TIME + statArray[ProcPidStat.START_TIME.ordinal()] * 1000L / USER_HZ);
proc.setUpTime(now - proc.getStartTime());
// See man proc for how to parse /proc/[pid]/io
proc.setBytesRead(ParseUtil.parseLongOrDefault(MapUtil.getOrDefault(io, "read_bytes", ""), 0L));
Expand Down Expand Up @@ -723,13 +627,49 @@ private static String filenameToFamily(String name) {
}

/**
* gets the calculated Jiffies per second, useful for converting ticks to
* milliseconds and vice versa
* Gets Jiffies per second, useful for converting ticks to milliseconds and
* vice versa.
*
* @return Jiffies per second
* @return Jiffies per second if it can be calculated. If not, returns 1000
* which assumes jiffies equal milliseconds.
*/
public static long getHz() {
return hz;
return USER_HZ;
}

/**
* Calculates Jiffies per second, useful for converting ticks to
* milliseconds and vice versa.
*
* @return Jiffies per second if it can be calculated. If not, returns 1000
* which assumes jiffies equal milliseconds.
*/
private static long calcHz() {
// Grab idle time before fetching ticks
double idleSecsSinceBoot = ProcUtil.getSystemIdletimeSeconds();
long[] ticks = ProcUtil.getSystemCpuLoadTicks();
// Grab idle time again. We would normally divide by 2 here to get an
// average, but will use the doubled value in the calculation later for
// rounding to a multiple of 2
idleSecsSinceBoot += ProcUtil.getSystemIdletimeSeconds();

// Calculations convert ticks per second to milliseconds by multiplying
// by 1000/Hz. If we failed to fetch the idle time or idle ticks, by
// returning 1000 here we simply remove the conversion factor.
if (idleSecsSinceBoot <= 0d || ticks[TickType.IDLE.getIndex()] <= 0L) {
LOG.warn("Couldn't calculate jiffies per second. "
+ "Process time values are in jiffies, not milliseconds.");
return 1000L;
}

// Divide ticks in the idle process by seconds in the idle process. Per
// http://man7.org/linux/man-pages/man5/proc.5.html this is the USER_HZ
// value. Note we added the seconds calculations before/after fetching
// proc/stat so the initial division (by 2x seconds) will result in half
// of the eventual hz value. We round to the nearest integer by adding
// 0.5 and casting to long. Then we multiply by 2, so the final Hz value
// returned is rounded to the nearest even number.
return 2L * (long) (ticks[TickType.IDLE.getIndex()] / idleSecsSinceBoot + 0.5d);
}

}
67 changes: 62 additions & 5 deletions oshi-core/src/main/java/oshi/util/platform/linux/ProcUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@

import java.io.File;
import java.io.FileFilter;
import java.util.List;
import java.util.regex.Pattern;

import oshi.hardware.CentralProcessor.TickType;
import oshi.util.FileUtil;
import oshi.util.ParseUtil;

/**
* Provides access to some /proc filesystem info on Linux
Expand All @@ -40,21 +43,75 @@ private ProcUtil() {
*
* @return Seconds since boot
*/
public static float getSystemUptimeFromProc() {
public static double getSystemUptimeSeconds() {
String uptime = FileUtil.getStringFromFile("/proc/uptime");
int spaceIndex = uptime.indexOf(' ');
try {
if (spaceIndex < 0) {
// No space, just parse
return Float.parseFloat(uptime);
// No space, error
return 0d;
} else {
return Float.parseFloat(uptime.substring(0, spaceIndex));
return Double.parseDouble(uptime.substring(0, spaceIndex));
}
} catch (NumberFormatException nfe) {
return 0f;
return 0d;
}
}

/**
* Parses the second value in /proc/uptime for seconds in the idle process
* since boot
*
* @return Seconds since boot in idle (if multiple processors, will probably
* exceed uptime)
*/
public static double getSystemIdletimeSeconds() {
String uptime = FileUtil.getStringFromFile("/proc/uptime");
int spaceIndex = uptime.indexOf(' ');
try {
if (spaceIndex < 0) {
// No space, error
return 0d;
} else {
return Double.parseDouble(uptime.substring(spaceIndex + 1));
}
} catch (NumberFormatException | IndexOutOfBoundsException e) {
return 0d;
}
}

/**
* Gets the CPU ticks array from /proc/stat
*
* @return Array of CPU ticks
*/
public static long[] getSystemCpuLoadTicks() {
long[] ticks = new long[TickType.values().length];
// /proc/stat expected format
// first line is overall user,nice,system,idle,iowait,irq, etc.
// cpu 3357 0 4313 1362393 ...
String tickStr;
List<String> procStat = FileUtil.readFile("/proc/stat");
if (!procStat.isEmpty()) {
tickStr = procStat.get(0);
} else {
return ticks;
}
// Split the line. Note the first (0) element is "cpu" so remaining
// elements are offset by 1 from the enum index
String[] tickArr = ParseUtil.whitespaces.split(tickStr);
if (tickArr.length <= TickType.IDLE.getIndex()) {
// If ticks don't at least go user/nice/system/idle, abort
return ticks;
}
// Note tickArr is offset by 1 because first element is "cpu"
for (int i = 0; i < TickType.values().length; i++) {
ticks[i] = ParseUtil.parseLongOrDefault(tickArr[i + 1], 0L);
}
// Ignore guest or guest_nice, they are included in user/nice
return ticks;
}

/**
* Gets an array of files in the /proc directory with only numeric digit
* filenames, corresponding to processes
Expand Down