aboutsummaryrefslogtreecommitdiff
path: root/mstat_plot.c
diff options
context:
space:
mode:
authorJoseph Hunkeler <jhunkeler@gmail.com>2022-09-14 15:54:37 -0400
committerJoseph Hunkeler <jhunkeler@gmail.com>2022-09-14 15:54:37 -0400
commit7971cc02be59a7d9115a8e436474433807588c6c (patch)
tree4376277edec4dfe00599c4a8a17e316b287a4778 /mstat_plot.c
downloadmstat-7971cc02be59a7d9115a8e436474433807588c6c.tar.gz
Initial commit
Diffstat (limited to 'mstat_plot.c')
-rw-r--r--mstat_plot.c408
1 files changed, 408 insertions, 0 deletions
diff --git a/mstat_plot.c b/mstat_plot.c
new file mode 100644
index 0000000..e264d63
--- /dev/null
+++ b/mstat_plot.c
@@ -0,0 +1,408 @@
+//
+// Created by jhunk on 8/24/22.
+//
+
+#include <string.h>
+#include <stdarg.h>
+#include "common.h"
+
+extern char *mstat_field_names[];
+
+struct GNUPLOT_PLOT {
+ char *title;
+ char *xlabel;
+ char *ylabel;
+ char *line_type;
+ double line_width;
+ unsigned int line_color;
+ unsigned char grid_toggle;
+ unsigned char autoscale_toggle;
+ unsigned char legend_toggle;
+ unsigned char legend_enhanced;
+ char *legend_title;
+};
+
+struct Option {
+ unsigned char verbose;
+ char *fields[0xffff];
+ char filename[PATH_MAX];
+} option;
+
+
+/**
+ * Determine if `name` is available on `PATH`
+ * @param name of executable
+ * @return 0 on success. >0 on error
+ */
+static int find_program(const char *name) {
+ char *path;
+ char *pathtmp;
+ char *token;
+
+ pathtmp = getenv("PATH");
+ if (!pathtmp) {
+ return 1;
+ }
+ path = strdup(pathtmp);
+ while ((token = strsep(&path, ":")) != NULL) {
+ if (access(token, F_OK | X_OK) == 0) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+/**
+ * Open a new gnuplot handle
+ * @return stream on success, or NULL on error
+ */
+FILE *gnuplot_open() {
+ // -p = persistent window after exit
+ return popen("gnuplot -p", "w");
+}
+
+/**
+ * Close a gnuplot handle
+ * @param fp pointer to gnuplot stream
+ * @return 0 on success, or <0 on error
+ */
+int gnuplot_close(FILE *fp) {
+ return pclose(fp);
+}
+
+/**
+ * Send shell command to gnuplot instance
+ * @param fp pointer to gnuplot stream
+ * @param fmt command to execute (requires caller to end string with a LF "\n")
+ * @param ... formatter arguments
+ * @return value of `vfprintf()`. <0 on error
+ */
+int gnuplot_sh(FILE *fp, char *fmt, ...) {
+ int status;
+
+ va_list args;
+ va_start(args, fmt);
+ status = vfprintf(fp, fmt, args);
+ va_end(args);
+
+ return status;
+}
+
+/**
+ * Generate a plot
+ * Each GNUPLOT_PLOT pointer in the `gp` array corresponds to a line.
+ * @param fp pointer to gnuplot stream
+ * @param gp pointer to an array of GNUPLOT_PLOT structures
+ * @param x an array representing the x axis
+ * @param y an array of double-precision arrays representing the y axes
+ * @param x_count total length of array x
+ * @param y_count total number of arrays in y
+ */
+void gnuplot_plot(FILE *fp, struct GNUPLOT_PLOT **gp, double x[], double *y[], size_t x_count, size_t y_count) {
+ // Configure plot
+ gnuplot_sh(fp, "set title '%s'\n", gp[0]->title);
+ gnuplot_sh(fp, "set xlabel '%s'\n", gp[0]->xlabel);
+ gnuplot_sh(fp, "set ylabel '%s'\n", gp[0]->ylabel);
+ if (gp[0]->grid_toggle)
+ gnuplot_sh(fp, "set grid\n");
+ if (gp[0]->autoscale_toggle)
+ gnuplot_sh(fp, "set autoscale\n");
+ if (gp[0]->legend_toggle) {
+ gnuplot_sh(fp, "set key nobox\n");
+ //gnuplot_sh(fp, "set key bmargin\n");
+ gnuplot_sh(fp, "set key font ',5'\n");
+ gnuplot_sh(fp, "set key outside\n");
+ }
+ if (gp[0]->legend_enhanced) {
+ gnuplot_sh(fp, "set key enhanced\n");
+ } else {
+ gnuplot_sh(fp, "set key noenhanced\n");
+ }
+
+ // Begin plotting
+ gnuplot_sh(fp, "plot ");
+ for (size_t i = 0; i < y_count; i++) {
+ char pltbuf[1024] = {0};
+ sprintf(pltbuf, "'-' ");
+ if (gp[0]->legend_toggle) {
+ sprintf(pltbuf + strlen(pltbuf), "title '%s' ", gp[i]->legend_title);
+ sprintf(pltbuf + strlen(pltbuf), "with lines ");
+ if (gp[i]->line_width) {
+ sprintf(pltbuf + strlen(pltbuf), "lw %0.1f ", gp[i]->line_width);
+ }
+ if (gp[i]->line_type) {
+ sprintf(pltbuf + strlen(pltbuf), "lt %s ", gp[i]->line_type);
+ }
+ if (gp[i]->line_color) {
+ sprintf(pltbuf + strlen(pltbuf), "lc rgb '#%06x' ", gp[i]->line_color);
+ }
+ gnuplot_sh(fp, "%s ", pltbuf);
+ } else {
+ gnuplot_sh(fp, "with lines ");
+ }
+ if (i < y_count - 1) {
+ gnuplot_sh(fp, ", ");
+ }
+ }
+ gnuplot_sh(fp, "\n");
+
+ // Emit MSTAT data
+ for (size_t arr = 0; arr < y_count; arr++) {
+ for (size_t i = 0; i < x_count; i++) {
+ gnuplot_sh(fp, "%lf %lf\n", x[i], y[arr][i]);
+ }
+ // Commit plot and execute
+ gnuplot_sh(fp, "e\n");
+ }
+ fflush(fp);
+}
+
+unsigned int rgb(unsigned char r, unsigned char g, unsigned char b) {
+ unsigned int result = r;
+ result = result << 8 | g;
+ result = result << 8 | b;
+ return result;
+}
+
+
+static void show_fields(char **fields) {
+ size_t total;
+ for (total = 0; fields[total] != NULL; total++);
+
+ for (size_t i = 0, tokens = 0; i < total; i++) {
+ if (tokens == 4) {
+ printf("\n");
+ tokens = 0;
+ }
+ printf("%-20s", fields[i]);
+ tokens++;
+ }
+ printf("\n");
+}
+
+static void usage(char *prog) {
+ char *sep;
+ char *name;
+
+ sep = strrchr(prog, '/');
+ name = prog;
+ if (sep) {
+ name = sep + 1;
+ }
+ printf("usage: %s [OPTIONS] <FILE>\n"
+ "-h this help message\n"
+ "-l list mstat fields\n"
+ "-f fields (default: rss,pss,swap)\n"
+ "-v verbose mode\n"
+ "\n", name);
+}
+
+void parse_options(int argc, char *argv[]) {
+ if (argc < 2) {
+ fprintf(stderr, "Missing path to *.mstat data\n");
+ exit(1);
+ }
+ option.fields[0] = "rss";
+ option.fields[1] = "pss";
+ option.fields[2] = "swap";
+ option.fields[3] = NULL;
+
+ for (int x = 0, i = 1; i < argc; i++) {
+ char *arg = argv[i];
+ if (strlen(argv[i]) > 1 && !strncmp(argv[i], "-", 1)) {
+ arg = argv[i] + 1;
+ if (!strcmp(arg, "h")) {
+ usage(argv[0]);
+ exit(0);
+ }
+ if (!strcmp(arg, "l")) {
+ show_fields(&mstat_field_names[MSTAT_FIELD_RSS]);
+ exit(0);
+ }
+ if (!strcmp(arg, "v")) {
+ option.verbose = 1;
+ }
+ if (!strcmp(arg, "f")) {
+ char *val = argv[i+1];
+ char *token = NULL;
+ if (!val) {
+ fprintf(stderr, "%s requires an argument\n", argv[i]);
+ exit(1);
+ }
+ if (!strcmp(val, "all")) {
+ for (x = MSTAT_FIELD_RSS; mstat_field_names[x] != NULL; x++) {
+ option.fields[x] = strdup(mstat_field_names[x]);
+ }
+ option.fields[x] = NULL;
+ } else {
+ while ((token = strsep(&val, ",")) != NULL) {
+ option.fields[x] = token;
+ x++;
+ }
+ }
+ i++;
+ }
+ } else {
+ strcpy(option.filename, argv[i]);
+ }
+ }
+}
+
+int main(int argc, char *argv[]) {
+ struct mstat_record_t p;
+ char **stored_fields;
+ char **field;
+ int data_total;
+ double **axis_y;
+ double *axis_x;
+ double mem_min, mem_max;
+ size_t rec;
+ FILE *fp;
+
+ parse_options(argc, argv);
+
+ rec = 0;
+ data_total = 0;
+ mem_min = 0.0;
+ mem_max = 0.0;
+ field = option.fields;
+ stored_fields = NULL;
+ axis_x = NULL;
+ axis_y = NULL;
+ fp = NULL;
+
+ if (access(option.filename, F_OK) < 0) {
+ perror(option.filename);
+ exit(1);
+ }
+
+ fp = mstat_open(option.filename);
+ if (!fp) {
+ perror(option.filename);
+ exit(1);
+ }
+
+ // Get total number of user-requested fields
+ for (data_total = 0; field[data_total] != NULL; data_total++);
+
+ // Retrieve fields from MSTAT header
+ stored_fields = mstat_read_fields(fp);
+ for (size_t i = 0; field[i] != NULL; i++) {
+ if (mstat_is_valid_field(stored_fields, field[i])) {
+ fprintf(stderr, "Invalid field: '%s'\n", field[i]);
+ printf("requested field must be one or more of...\n");
+ show_fields(stored_fields);
+ exit(1);
+ }
+ }
+
+ // We don't store the number of records in the MSTAT data. Count them here.
+ while (!mstat_iter(fp, &p))
+ rec++;
+
+ axis_x = calloc(rec, sizeof(axis_x));
+ if (!axis_x) {
+ perror("Unable to allocate enough memory for axis_x array");
+ exit(1);
+ }
+
+ axis_y = calloc(data_total + 1, sizeof(**axis_y));
+ if (!axis_y) {
+ perror("Unable to allocate enough memory for axis_y array");
+ exit(1);
+ }
+
+ for (int i = 0, n = 0; field[i] != NULL; i++) {
+ axis_y[i] = calloc(rec, sizeof(*axis_y[0]));
+ n++;
+ }
+
+ mstat_rewind(fp);
+ printf("Reading: %s\n", argv[1]);
+
+ // Assign requested MSTAT data to y-axis. x-axis will always be time elapsed.
+ rec = 0;
+ while (!mstat_iter(fp, &p)) {
+ axis_x[rec] = mstat_get_field_by_name(&p, "timestamp").d64 / 3600;
+ for (int i = 0; i < data_total; i++) {
+ axis_y[i][rec] = (double) mstat_get_field_by_name(&p, field[i]).u64 / 1024;
+ }
+ rec++;
+ }
+
+ if (!rec) {
+ fprintf(stderr, "MSTAT axis_y file does not have any records\n");
+ exit(1);
+ } else {
+ printf("Records: %zu\n", rec);
+ }
+
+ // Show min/max
+ for (size_t i = 0; axis_y[i] != NULL && i < data_total; i++) {
+ mstat_get_mmax(axis_y[i], rec, &mem_min, &mem_max);
+ printf("%s min(%.2lf) max(%.2lf)\n", field[i], mem_min, mem_max);
+ }
+
+ if (find_program("gnuplot")) {
+ fprintf(stderr, "To render plots please install gnuplot\n");
+ exit(1);
+ }
+
+ struct GNUPLOT_PLOT **gp = calloc(data_total, sizeof(**gp));
+ if (!gp) {
+ perror("Unable to allocate memory for gnuplot configuration array");
+ exit(1);
+ }
+
+ for (size_t n = 0; n < data_total; n++) {
+ gp[n] = calloc(1, sizeof(*gp[0]));
+ if (!gp[n]) {
+ perror("Unable to allocate memory for GNUPLOT_PLOT structure");
+ exit(1);
+ }
+ }
+
+ unsigned char r, g, b;
+ char title[255] = {0};
+ r = 0x10;
+ g = 0x20;
+ b = 0x30;
+ snprintf(title, sizeof(title) - 1, "Memory Usage (PID %d)", p.pid);
+
+ gp[0]->xlabel = strdup("Time (HR)");
+ gp[0]->ylabel = strdup("MB");
+ gp[0]->title = strdup(title);
+ gp[0]->grid_toggle = 1;
+ gp[0]->autoscale_toggle = 1;
+ gp[0]->legend_toggle = 1;
+
+ for (size_t i = 0; i < data_total; i++) {
+ gp[i]->legend_title = strdup(field[i]);
+ gp[i]->line_color = rgb(r, g, b);
+ gp[i]->line_width = 0.50;
+ r -= 0x30;
+ b += 0x20;
+ g *= 3;
+ }
+
+ printf("Generating plot... ");
+ fflush(stdout);
+
+ FILE *plt;
+ plt = gnuplot_open();
+ if (!plt) {
+ fprintf(stderr, "Failed to open gnuplot stream\n");
+ exit(1);
+ }
+ gnuplot_plot(plt, gp, axis_x, axis_y, rec, data_total);
+ gnuplot_close(plt);
+ printf("done!\n");
+
+ free(axis_x);
+ for (size_t i = 0; i < data_total; i++) {
+ free(axis_y[i]);
+ free(gp[i]);
+ }
+
+ return 0;
+}