/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
// This program is as dumb as possible -- it reads a whole bunch of data
// from /proc and reports when it changes. It's up to analysis tools to
// actually parse the data. This program only does enough parsing to split
// large files (/proc/stat, /proc/yaffs) into individual values.
//
// The output format is a repeating series of observed differences:
//
// T + <beforetime.stamp>
// /proc/<new_filename> + <contents of newly discovered file>
// /proc/<changed_filename> = <contents of changed file>
// /proc/<deleted_filename> -
// /proc/<filename>:<label> = <part of a multiline file>
// T - <aftertime.stamp>
//
//
// Files read:
//
// /proc/*/stat - for all running/selected processes
// /proc/*/wchan - for all running/selected processes
// /proc/binder/stats - per line: "/proc/binder/stats:BC_REPLY"
// /proc/diskstats - per device: "/proc/diskstats:mmcblk0"
// /proc/net/dev - per interface: "/proc/net/dev:rmnet0"
// /proc/stat - per line: "/proc/stat:intr"
// /proc/yaffs - per device/line: "/proc/yaffs:userdata:nBlockErasures"
// /sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state
// - per line: "/sys/.../time_in_state:245000"
struct data {
char *name; // filename, plus ":var" for many-valued files
char *value; // text to be reported when it changes
};
// Like memcpy, but replaces spaces and unprintables with '_'.
static void unspace(char *dest, const char *src, int len) {
while (len-- > 0) {
char ch = *src++;
*dest++ = isgraph(ch) ? ch : '_';
}
}
// Set data->name and data->value to malloc'd strings with the
// filename and contents of the file. Trims trailing whitespace.
static void read_data(struct data *data, const char *filename) {
char buf[4096];
data->name = strdup(filename);
int fd = open(filename, O_RDONLY);
if (fd < 0) {
data->value = NULL;
return;
}
int len = read(fd, buf, sizeof(buf));
if (len < 0) {
perror(filename);
close(fd);
data->value = NULL;
return;
}
close(fd);
while (len > 0 && isspace(buf[len - 1])) --len;
data->value = malloc(len + 1);
memcpy(data->value, buf, len);
data->value[len] = '\0';
}
// Read a name/value file and write data entries for each line.
// Returns the number of entries written (always <= stats_count).
//
// delimiter: used to split each line into name and value
// terminator: if non-NULL, processing stops after this string
// skip_words: skip this many words at the start of each line
static int read_lines(
const char *filename,
char delimiter, const char *terminator, int skip_words,
struct data *stats, int stats_count) {
char buf[8192];
int fd = open(filename, O_RDONLY);
if (fd < 0) return 0;
int len = read(fd, buf, sizeof(buf) - 1);
if (len < 0) {
perror(filename);
close(fd);
return 0;
}
buf[len] = '\0';
close(fd);
if (terminator != NULL) {
char *end = strstr(buf, terminator);
if (end != NULL) *end = '\0';
}
int filename_len = strlen(filename);
int num = 0;
char *line;
for (line = strtok(buf, "\n");
line != NULL && num < stats_count;
line = strtok(NULL, "\n")) {
// Line format: <sp>name<delim><sp>value
int i;
while (isspace(*line)) ++line;
for (i = 0; i < skip_words; ++i) {
while (isgraph(*line)) ++line;
while (isspace(*line)) ++line;
}
char *name_end = strchr(line, delimiter);
if (name_end == NULL) continue;
// Key format: <filename>:<name>
struct data *data = &stats[num++];
data->name = malloc(filename_len + 1 + (name_end - line) + 1);
unspace(data->name, filename, filename_len);
data->name[filename_len] = ':';
unspace(data->name + filename_len + 1, line, name_end - line);
data->name[filename_len + 1 + (name_end - line)] = '\0';
char *value = name_end + 1;
while (isspace(*value)) ++value;
data->value = strdup(value);
}
return num;
}
// Read /proc/yaffs and write data entries for each line.
// Returns the number of entries written (always <= stats_count).
static int read_proc_yaffs(struct data *stats, int stats_count) {
char buf[8192];
int fd = open("/proc/yaffs", O_RDONLY);
if (fd < 0) return 0;
int len = read(fd, buf, sizeof(buf) - 1);
if (len < 0) {
perror("/proc/yaffs");
close(fd);
return 0;
}
buf[len] = '\0';
close(fd);
int num = 0, device_len = 0;
char *line, *device = NULL;
for (line = strtok(buf, "\n");
line != NULL && num < stats_count;
line = strtok(NULL, "\n")) {
if (strncmp(line, "Device ", 7) == 0) {
device = strchr(line, '"');
if (device != NULL) {
char *end = strchr(++device, '"');
if (end != NULL) *end = '\0';
device_len = strlen(device);
}
continue;
}
if (device == NULL) continue;
char *name_end = line + strcspn(line, " .");
if (name_end == line || *name_end == '\0') continue;
struct data *data = &stats[num++];
data->name = malloc(12 + device_len + 1 + (name_end - line) + 1);
memcpy(data->name, "/proc/yaffs:", 12);
unspace(data->name + 12, device, device_len);
data->name[12 + device_len] = ':';
unspace(data->name + 12 + device_len + 1, line, name_end - line);
data->name[12 + device_len + 1 + (name_end - line)] = '\0';
char *value = name_end;
while (*value == '.' || isspace(*value)) ++value;
data->value = strdup(value);
}
return num;
}
// Compare two "struct data" records by their name.
static int compare_data(const void *a, const void *b) {
const struct data *data_a = (const struct data *) a;
const struct data *data_b = (const struct data *) b;
return strcmp(data_a->name, data_b->name);
}
// Return a malloc'd array of "struct data" read from all over /proc.
// The array is sorted by name and terminated by a record with name == NULL.
static struct data *read_stats(char *names[], int name_count) {
static int bad[4096]; // Cache pids known not to match patterns
static size_t bad_count = 0;
int pids[4096];
size_t pid_count = 0;
DIR *proc_dir = opendir("/proc");
if (proc_dir == NULL) {
perror("Can't scan /proc");
exit(1);
}
size_t bad_pos = 0;
char filename[1024];
struct dirent *proc_entry;
while ((proc_entry = readdir(proc_dir))) {
int pid = atoi(proc_entry->d_name);
if (pid <= 0) continue;
if (name_count > 0) {
while (bad_pos < bad_count && bad[bad_pos] < pid) ++bad_pos;
if (bad_pos < bad_count && bad[bad_pos] == pid) continue;
char cmdline[4096];
sprintf(filename, "/proc/%d/cmdline", pid);
int fd = open(filename, O_RDONLY);
if (fd < 0) {
perror(filename);
continue;
}
int len = read(fd, cmdline, sizeof(cmdline) - 1);
if (len < 0) {
perror(filename);
close(fd);
continue;
}
close(fd);
cmdline[len] = '\0';
int n;
for (n = 0; n < name_count && !strstr(cmdline, names[n]); ++n);
if (n == name_count) {
// Insertion sort -- pids mostly increase so this makes sense
if (bad_count < sizeof(bad) / sizeof(bad[0])) {
int pos = bad_count++;
while (pos > 0 && bad[pos - 1] > pid) {
bad[pos] = bad[pos - 1];
--pos;
}
bad[pos] = pid;
}
continue;
}
}
if (pid_count >= sizeof(pids) / sizeof(pids[0])) {
fprintf(stderr, "warning: >%zu processes\n", pid_count);
} else {
pids[pid_count++] = pid;
}
}
closedir(proc_dir);
size_t i, stats_count = pid_count * 2 + 200; // 200 for stat, yaffs, etc.
struct data *stats = malloc((stats_count + 1) * sizeof(struct data));
struct data *next = stats;
for (i = 0; i < pid_count; i++) {
assert(pids[i] > 0);
sprintf(filename, "/proc/%d/stat", pids[i]);
read_data(next++, filename);
sprintf(filename, "/proc/%d/wchan", pids[i]);
read_data(next++, filename);
}
struct data *end = stats + stats_count;
next += read_proc_yaffs(next, stats + stats_count - next);
next += read_lines("/proc/net/dev", ':', NULL, 0, next, end - next);
next += read_lines("/proc/stat", ' ', NULL, 0, next, end - next);
next += read_lines("/proc/binder/stats", ':', "\nproc ", 0, next, end - next);
next += read_lines("/proc/diskstats", ' ', NULL, 2, next, end - next);
next += read_lines(
"/sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state",
' ', NULL, 0, next, end - next);
assert(next < stats + stats_count);
next->name = NULL;
next->value = NULL;
qsort(stats, next - stats, sizeof(struct data), compare_data);
return stats;
}
// Print stats which have changed from one sorted array to the next.
static void diff_stats(struct data *old_stats, struct data *new_stats) {
while (old_stats->name != NULL || new_stats->name != NULL) {
int compare;
if (old_stats->name == NULL) {
compare = 1;
} else if (new_stats->name == NULL) {
compare = -1;
} else {
compare = compare_data(old_stats, new_stats);
}
if (compare < 0) {
// old_stats no longer present
if (old_stats->value != NULL) {
printf("%s -\n", old_stats->name);
}
++old_stats;
} else if (compare > 0) {
// new_stats is new
if (new_stats->value != NULL) {
printf("%s + %s\n", new_stats->name, new_stats->value);
}
++new_stats;
} else {
// changed
if (new_stats->value == NULL) {
if (old_stats->value != NULL) {
printf("%s -\n", old_stats->name);
}
} else if (old_stats->value == NULL) {
printf("%s + %s\n", new_stats->name, new_stats->value);
} else if (strcmp(old_stats->value, new_stats->value)) {
printf("%s = %s\n", new_stats->name, new_stats->value);
}
++old_stats;
++new_stats;
}
}
}
// Free a "struct data" array and all the strings within it.
static void free_stats(struct data *stats) {
int i;
for (i = 0; stats[i].name != NULL; ++i) {
free(stats[i].name);
free(stats[i].value);
}
free(stats);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr,
"usage: procstatlog poll_interval [procname ...] > procstat.log\n\n"
"\n"
"Scans process status every poll_interval seconds (e.g. 0.1)\n"
"and writes data from /proc/stat, /proc/*/stat files, and\n"
"other /proc status files every time something changes.\n"
"\n"
"Scans all processes by default. Listing some process name\n"
"substrings will limit scanning and reduce overhead.\n"
"\n"
"Data is logged continuously until the program is killed.\n");
return 2;
}
long poll_usec = (long) (atof(argv[1]) * 1000000l);
if (poll_usec <= 0) {
fprintf(stderr, "illegal poll interval: %s\n", argv[1]);
return 2;
}
struct data *old_stats = malloc(sizeof(struct data));
old_stats->name = NULL;
old_stats->value = NULL;
while (1) {
struct timeval before, after;
gettimeofday(&before, NULL);
printf("T + %ld.%06ld\n", before.tv_sec, before.tv_usec);
struct data *new_stats = read_stats(argv + 2, argc - 2);
diff_stats(old_stats, new_stats);
free_stats(old_stats);
old_stats = new_stats;
gettimeofday(&after, NULL);
printf("T - %ld.%06ld\n", after.tv_sec, after.tv_usec);
long elapsed_usec = (long) after.tv_usec - before.tv_usec;
elapsed_usec += 1000000l * (after.tv_sec - before.tv_sec);
if (poll_usec > elapsed_usec) usleep(poll_usec - elapsed_usec);
}
return 0;
}