A playground to calculate CPU time from getrusage
and Proc file system.
CPU time consists of two values user CPU time and system CPU time:
- User CPU time: Time spent to execute your code
- System CPU time: Time spent to execute system calls
To know further about CPU time and how it is useful, read this blog which will tell you about how to use these values, why are they useful and some other interesting facts.
This struct API can help you to get the CPU time of a whole process or thread, based on a parameter value passed in the function getrusage
. This API will help you return both user CPU time and system CPU time which can be summed to calculate the CPU time value. Lets look at the usage of this API:
- Getting User Time and System Time: The first parameter of
getrusage
specifies the task for which you want to calculate the resource usage. In this case we passedRUSAGE_THREAD
which means we want to calculate it for the calling thread which would be main thread in the case where you want to optimize for some rendering issues. You can look at more possible values for this constant in this man page.
#include <sys/resource.h>
rusage rusageStats{};
getrusage(RUSAGE_THREAD, &rusageStats);
user_cpu_time = rusageStats.ru_utime
system_cpu_time = rusageStats.ru_stime
- The values returned in the previous section are in timval structure which is basically seconds plus microseconds. To convert each valiue from timval to milliseconds we do following:
static constexpr auto kMillisInSec = 1000;
static constexpr auto kMicrosInMillis = 1000;
inline uint64_t timeval_sum_to_millis(timeval &tv1, timeval &tv2){
return (tv1.tv_sec + tv2.tv_sec) * kMillisInSec +
(tv1.tv_usec + tv2.tv_usec) / kMicrosInMillis;
}
// CPU TIME can thus be determined by
uint64_t cpu_time_millis = timeval_sum_to_millis(
rusageStats.ru_utime,
rusageStats.ru_stime
);
Addition of the two values user CPU time and system CPU time will finally give you the CPU time.
- Interface the above C++ code to Java/Kotlin through a JNI method to obtain this sum. You can see details of how to implement simple JNI method here.
- Ensure you call this JNI method on main thread because we have used
RUSAGE_THREAD
in ourgetrusage
parameter which will evaluate CPU time for the calling thread (main thread). Calling it from another worker thread will give you values for the other thread and not the expected main thread.
One of the disadvantages using getrusage
is that we need to call it from the current profiling task, which induces an observer effect. This means, that you might end up interfering the original values. Thus we need, such an API which would be independent from such contexts of thread or process and would not interfere with original values.
Proc file system is a virtual file system in linux which can provide you the information about different tasks (process and threads) currently running on the system.
For the CPU time of our main thread, we must get the content of the file which are mapped directly to our main thread in proc file system.
Lets divide this approach to the following parts and dive into it one by one:
- Identifying the proc file for Main Thread
- Reading the Main thread Proc File
- Parsing the Main thread Proc File
- Calculating CPU time from stat file
All the processes and threads in linux have a directory mapping in proc file system. For referencing the proc directory of current process you must know your process id (pid). This can be obtained easily by Process.myPid()
.
All the processes in proc have a numeric directory and the threads of process gets mapped to their thread id (tid) under a task
directory as shown below:
To reference the current process you need to reference proc/self
directory which is nothing but a symbolic link (shortcut) to your process directory in proc.
The main thread has an id equal to that of pid because it is one of the first thread to get created from the process. You can check this by calling gettid()
from main thread. Thus, to reach the main thread directory you need to go to proc/self/task/<tid>
.
Every directory has a stat file which holds status information of the task which has the two entries that we need to calculate the CPU time: user CPU time and system CPU time. So, the path to this file for main thread id 24484 would be: proc/self/task/24484/stat
.
This file gives you status of many information including: current state of task, name of task, priority etc. Its a raw dump of values in order defined in man page of proc. This dump looks like as follows:
24484 (elopers.perfmon) R 1435 1435 0 0 -1 4219200 7210 0 0 0 16 17 0.....
Here, the first entry is thread id, second entry is name and third entry is the current state of thread.
You can also look out for the exact order in this stackoverflow answer.
Our objective would be to look up the utime
and stime
and then from these values we will evaluate the main thread CPU time.
From the above discussion, our main thread proc file is: proc/self/task/24484/stat
.
For opening this file I have used open
function from standard library header fcnt.h
(file control). They are readily available as part of standard libc
. Let's first look at the code snippet for opening stat file:
int ProcFs::openStatFile(const std::string &path) {
// 1
int statFile = open(path.c_str(), O_SYNC | O_RDONLY);
// Could'nt open stat file from proc
if (statFile == -1) {
throw std::system_error(
errno, std::system_category(), "Could not open stat file");
}
return statFile;
}
The open
function at 1
opens file at path
in read only mode and returns a file descriptor which will be used by further read
and lseek
calls.
For reading the contents of file I have used read
function from unistd.h
that are also readily available in UNIX distributions. Let's see how to use this read
function in our use case:
// Read proc stat file
constexpr size_t bufferLength = 512;
char buffer[bufferLength]{};
int bytes_read = read(fd, buffer, (sizeof(buffer) - 1));
if (bytes_read < 0) {
throw std::system_error(
errno,
std::system_category(),
"Could not read stat file"
);
}
The above code will read the contents of stat file in the char buffer
.
We now have the contents of the stat file in buffer. Let's, move on to the parsing of these files.
For parsing the file I have created a skipUntil
function which uses one of its argument (ch
) as delimiter to parse one entry in the file. Let's have a look on this function:
char *skipUntil(char *data, const char *end, char ch) {
// It's important that we check against `end`
// before we dereference `data`.
while (data < end && *data != ch) {
if (*data == '\0') {
throw std::runtime_error("Unexpected end of string");
}
++data;
}
if (data == end) {
throw std::runtime_error("Unexpected end of string");
}
// One past the `ch` character.
return ++data;
}
So, for reading the 24484 (elopers.perfmon) R
we will do:
data = skipUntil(data, end, ' '); // pid
data = skipUntil(data, end, ')'); // name
data = skipUntil(data, end, ' '); // space after name
To keep record of important readings I created a struct called as TaskStatInfo
which will hold all important information.
Let's assume you parsed the whole file according to above strategy. We will calculate CPU time as follows:
static int kClockTicksMs = systemClockTickIntervalMs();
TaskStatInfo info{};
info.cpuTime = kClockTicksMs * (utime + stime);
The utime
and stime
you directly get from proc file are not in millisecond. They are universally measured in linux with clock ticks. 1 tick corresponds to 10 ms.
The kClockTicksMs
from systemClockTickIntervalMs()
takes care of conversion of those ticks into milliseconds. Hence, we will get info.cpuTime
in ms.
You can now obtain these values by integrating a JNI function and call it from any background thread without interfering the original profile of Main thread. For me these values were:
D/Initial CPU Time: Initial value from Proc: 300
D/Final CPU Time: Final value from Proc: 1120
(Net CPU time: 1120 - 300 = 820ms)
The above aproaches can be used to get CPU time from the production environment and can be userful to point performance issues from production.