szaydel
11/4/2019 - 5:01 AM

Obtain combined System / User CPU times from process and compute rates based on process age

#define _GNU_SOURCE

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

typedef unsigned long int u_num;

typedef struct {
  u_num user;
  u_num sys;
  u_num start;
  struct timespec ts;
} times_t;

// time_since_boot returns duration since kernel came online in ticks.
static inline int time_since_boot(void) {
  FILE *procuptime;
  int sec, ssec;

  procuptime = fopen("/proc/uptime", "r");
  fscanf(procuptime, "%d.%ds", &sec, &ssec);
  fclose(procuptime);
  return (sec*sysconf(_SC_CLK_TCK))+ssec;
}

// parse_times extracts CPU times from buf, which is expected to be
// contents of /proc/<PID>/stat file.
static inline times_t parse_times(char *buf) {
  #define UTIME_IDX 13
  #define STIME_IDX 14
  #define CUTIME_IDX 15
  #define CSTIME_IDX 16
  #define STARTTIME_IDX 21

  times_t t = {
      .user = 0,
      .sys = 0,
      .start = 0,
  };
  char *tok;

  for (size_t i = 0 ; (tok = strsep(&buf, " ")) != NULL ; i++) {
      switch (i) {
      case UTIME_IDX:
      case CUTIME_IDX:
        t.user += strtoul(tok, NULL, 10);
        break;
      case CSTIME_IDX:
      case STIME_IDX:
        t.sys += strtoul(tok, NULL, 10);
        break;
      case STARTTIME_IDX:
        t.start = strtoul(tok, NULL, 10);
        break;
      }
  }
  return t;
}
// process_age returns duration in ticks, given a process' start time.
static inline u_num process_age(u_num start_time) {
  int since_boot = time_since_boot();
  int age_ticks = since_boot - start_time;
  return age_ticks;
}

// lifetime_cpu_ticks_per_tick returns effectively a ratio of CPU times over
// age of the process, i.e. an average load over process' lifetime.
static inline double lifetime_cpu_ticks_per_tick(times_t t) {
  double total_cpu_ticks = (double)(t.user + t.sys);
  return total_cpu_ticks / (double)process_age(t.start);
}

// cpu_ticks_per_tick returns a ratio of CPU time over walltime measured
// between two samples.
// Seconds per second should not be > 1.0, where 1.0 means process spent
// 100% of measured sample interval consuming CPU.
static inline double cpu_ticks_per_tick(times_t first, times_t second) {
  double cpu_times[2], times[2];
  cpu_times[0] = first.user + first.sys;
  cpu_times[1] = second.user + second.sys;
  times[0] = first.ts.tv_sec + ((double)first.ts.tv_nsec / 1e9);
  times[1] = second.ts.tv_sec + ((double)second.ts.tv_nsec / 1e9);
  #ifdef DEBUG
  printf("cpu_times[0] = %f cpu_times[1] = %f\n", cpu_times[0], cpu_times[1]);
  printf("times[0] = %f times[1] = %f\n", times[0], times[1]);
  #endif
  return (cpu_times[1] - cpu_times[0]) /
        ((times[1] - times[0]) * sysconf(_SC_CLK_TCK));
}

// read_stat reads process stat information into a buffer for given PID.
static inline char *read_stat(char *buf, size_t len, pid_t pid) {
    FILE *input = NULL;
    char *path = malloc(sizeof(char) * 1024);
    if (!path) {
      perror("malloc(...)");
      return NULL;
    }
    asprintf(&path, "/proc/%d/stat", pid);
    input = fopen(path, "r");
    if(!input) {
      free(path);
      return NULL;
    }
    free(path);

  fread(buf, len, 1, input);
  if (ferror(input)) {
    strerror(errno);
    fclose(input);
    return NULL;
  }
  fclose(input);
  return buf;
}

// collect_times repeatedly samples process' /proc/<PID>/stat file and
// populates an allocated times_t array with these observations.
// This array is returned after all samples are gathered. Delay between
// observations is fixed at 1-second.
static inline times_t *collect_times(size_t n, pid_t pid) {
  size_t bufsz = 1024;
  char *buf = malloc(sizeof(char) * bufsz);
  if (!buf) {
    perror("malloc(...)");
    return NULL;
  }
  times_t *tt = malloc(n * sizeof(times_t));
  if (!tt) {
    perror("malloc(...)");
    free(buf);
    return NULL;
  }
  for (size_t i = 0 ; i < n ; i++) {
    if (read_stat(buf, bufsz, pid) == NULL) {
      free(tt);
      free(buf);
      return NULL;
    }
    tt[i] = parse_times(buf);
    if (clock_gettime(CLOCK_MONOTONIC, &tt[i].ts) == -1) {
        free(tt);
        free(buf);
      return NULL;
    }
    #ifdef DEBUG
    printf("index[%zu] => %ld(sec) %ld(ns)\n",
        i, tt[i].ts.tv_sec, tt[i].ts.tv_nsec);
    #endif
    sleep(1);
  }
  free(buf);
  return tt;
}

// avg_times returns an average value for times_t array by computing
// a diff between neighboring pairs, and then dividing that sum by
// number of observations.
static inline double avg_times(times_t *tt, size_t n) {
    double total = 0;
    for (size_t i = 0; i < n-1; i++) {
        total += cpu_ticks_per_tick(tt[i], tt[i+1]);
    }
    return total / n;
}

void usage(char *name) {
  fprintf(stderr, "usage: %s <PID> [number of 1-second samples]\n", name);
  exit(2);
}

int main(int argc, char *argv[]) {
  if (argc < 2) {
      usage(argv[0]);
  }

  if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
    usage(argv[0]);
  }

  pid_t pid = atoi(argv[1]);

  if (!pid) {
      fprintf(stderr, "Failed to parse PID from argument\n");
      return 1;
  }
  int intervals = 2;
  if (argc > 2) {
    int parsed_intervals = atoi(argv[2]);
    if (parsed_intervals) {
      if (parsed_intervals < 2) {
        parsed_intervals = 2;
      }
    intervals = parsed_intervals;
    }
  }

  times_t *points = collect_times(intervals, pid);
  if (points) {
    printf("%f %f %f\n",
        avg_times(points, intervals),
        cpu_ticks_per_tick(points[0], points[intervals-1]),
        lifetime_cpu_ticks_per_tick(points[intervals-1])
    );
  // pointless to free(points) here
  } else {
      fprintf(stderr, "Failed to collect information, PID not valid?\n");
      return 1;
  }
  return 0;
}