aboutsummaryrefslogtreecommitdiff
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
downloadmstat-7971cc02be59a7d9115a8e436474433807588c6c.tar.gz
Initial commit
-rw-r--r--.gitignore4
-rw-r--r--CMakeLists.txt12
-rw-r--r--LICENSE29
-rw-r--r--README.md24
-rw-r--r--common.c572
-rw-r--r--common.h89
-rw-r--r--mstat.c208
-rw-r--r--mstat_export.c67
-rw-r--r--mstat_plot.c408
9 files changed, 1413 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ee49081
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.idea
+cmake-*
+*.dat
+*.mstat
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..8068760
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,12 @@
+cmake_minimum_required(VERSION 3.0)
+project(mstat C)
+include(GNUInstallDirs)
+
+set(CMAKE_C_STANDARD 99)
+add_executable(mstat mstat.c common.c)
+add_executable(mstat_plot mstat_plot.c common.c)
+add_executable(mstat_export mstat_export.c common.c)
+
+install(TARGETS mstat mstat_plot mstat_export
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e4a74af
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2022, Joseph Hunkeler
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a9a1943
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# MSTAT
+
+Record the memory usage of a process over time.
+
+# How to use MSTAT
+
+```shell
+mstat <pid_here>
+```
+
+## Plotting
+
+Requires `gnuplot` to be installed
+
+```shell
+mstat_plot <pid_here>.mstat
+```
+
+## CSV export
+
+```shell
+mstat_export <pid_here>.mstat > data.csv
+```
+
diff --git a/common.c b/common.c
new file mode 100644
index 0000000..9602867
--- /dev/null
+++ b/common.c
@@ -0,0 +1,572 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include "common.h"
+
+// Globals
+const char mstat_magic_bytes[] = MSTAT_MAGIC;
+char *mstat_field_names[] = {
+ "pid",
+ "timestamp",
+ "rss",
+ "pss",
+ "pss_anon",
+ "pss_file",
+ "pss_shmem",
+ "shared_clean",
+ "shared_dirty",
+ "private_clean",
+ "private_dirty",
+ "referenced",
+ "anonymous",
+ "lazy_free",
+ "anon_huge_pages",
+ "shmem_pmd_mapped",
+ "file_pmd_mapped",
+ "shared_hugetlb",
+ "private_hugetlb",
+ "swap",
+ "swap_pss",
+ "locked",
+ NULL,
+};
+
+/**
+ * Get total number of fields stored in MSTAT file header
+ * @param fp pointer to MSTAT file stream
+ * @return
+ */
+int mstat_get_field_count(FILE *fp) {
+ int count;
+ ssize_t pos;
+
+ count = 0;
+ pos = ftell(fp);
+ if (pos < 0) {
+ return -1;
+ }
+ if (fseek(fp, MSTAT_FIELD_COUNT, SEEK_SET) < 0) {
+ return -1;
+ }
+ if (!fread(&count, sizeof(count), 1, fp)) {
+ return -1;
+ }
+ if (fseek(fp, pos, SEEK_SET) < 0) {
+ return -1;
+ }
+ return count;
+}
+
+/**
+ * Read fields stored in MSTAT header
+ * @param fp a pointer to MSTAT file
+ * @return array of MSTAT fields. NULL on error.
+ */
+char **mstat_read_fields(FILE *fp) {
+ char **fields;
+ int total = 0;
+ //fseek(fp, MSTAT_FIELD_COUNT, SEEK_SET);
+ //fread(&total, sizeof(total), 1, fp);
+ total = mstat_get_field_count(fp);
+ fseek(fp, MSTAT_MAGIC_SIZE, SEEK_SET);
+ fields = calloc(total + 1, sizeof(*fields));
+ if (!fields) {
+ perror("Unable to allocate memory for fields");
+ return NULL;
+ }
+ for (unsigned i = 0; i < total; i++) {
+ char buf[255] = {0};
+ unsigned len;
+ fread(&len, sizeof(len), 1, fp);
+ fread(buf, len, 1, fp);
+ fields[i] = strdup(buf);
+ }
+ return fields;
+}
+
+/**
+ * Check if `name` is present in `fields` array
+ * @param fields array of field names
+ * @param name field name to verify
+ * @return 0 on success. 1 on error.
+ */
+int mstat_is_valid_field(char **fields, const char *name) {
+ for (size_t i = 0; fields[i] != NULL; i++) {
+ if (!strcmp(fields[i], name)) {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+/**
+ * Return record value by field name
+ * @param p pointer to MSTAT record
+ * @param name field name
+ * @return MSTAT field union. ULLONG_MAX on error
+ */
+union mstat_field_t mstat_get_field_by_name(const struct mstat_record_t *p, const char *name) {
+ union mstat_field_t result;
+ result.u64 = ULLONG_MAX;
+
+ if (!strcmp(name, "pid")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_PID);
+ } else if (!strcmp(name, "timestamp")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_TIMESTAMP);
+ } else if (!strcmp(name, "rss")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_RSS);
+ } else if (!strcmp(name, "pss")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_PSS);
+ } else if (!strcmp(name, "pss_anon")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_PSS_ANON);
+ } else if (!strcmp(name, "pss_file")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_PSS_FILE);
+ } else if (!strcmp(name, "pss_shmem")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_PSS_SHMEM);
+ } else if (!strcmp(name, "shared_clean")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_SHARED_CLEAN);
+ } else if (!strcmp(name, "shared_dirty")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_SHARED_DIRTY);
+ } else if (!strcmp(name, "private_clean")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_PRIVATE_CLEAN);
+ } else if (!strcmp(name, "private_dirty")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_PRIVATE_DIRTY);
+ } else if (!strcmp(name, "referenced")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_REFERENCED);
+ } else if (!strcmp(name, "anonymous")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_ANONYMOUS);
+ } else if (!strcmp(name, "lazy_free")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_LAZY_FREE);
+ } else if (!strcmp(name, "anon_huge_pages")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_ANON_HUGE_PAGES);
+ } else if (!strcmp(name, "shmem_pmd_mapped")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_SHMEM_PMD_MAPPED);
+ } else if (!strcmp(name, "file_pmd_mapped")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_FILE_PMD_MAPPED);
+ } else if (!strcmp(name, "shared_hugetlb")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_SHARED_HUGETLB);
+ } else if (!strcmp(name, "private_hugetlb")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_PRIVATE_HUGETLB);
+ } else if (!strcmp(name, "swap")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_SWAP);
+ } else if (!strcmp(name, "swap_pss")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_SWAP_PSS);
+ } else if (!strcmp(name, "locked")) {
+ result = mstat_get_field_by_id(p, MSTAT_FIELD_LOCKED);
+ }
+
+ return result;
+}
+
+/**
+ * Return record value by identifier
+ * @param record pointer to MSTAT record
+ * @param id MSTAT_FIELD_* constant
+ * @return MSTAT field union. ULLONG_MAX on error
+ */
+union mstat_field_t mstat_get_field_by_id(const struct mstat_record_t *record, unsigned id) {
+ union mstat_field_t result;
+ result.u64 = ULLONG_MAX;
+
+ switch (id) {
+ case MSTAT_FIELD_PID:
+ result.u64 = record->pid;
+ break;
+ case MSTAT_FIELD_TIMESTAMP:
+ result.d64 = record->timestamp;
+ break;
+ case MSTAT_FIELD_RSS:
+ result.u64 = record->rss;
+ break;
+ case MSTAT_FIELD_PSS:
+ result.u64 = record->pss;
+ break;
+ case MSTAT_FIELD_PSS_ANON:
+ result.u64 = record->pss_anon;
+ break;
+ case MSTAT_FIELD_PSS_FILE:
+ result.u64 = record->pss_file;
+ break;
+ case MSTAT_FIELD_PSS_SHMEM:
+ result.u64 = record->pss_shmem;
+ break;
+ case MSTAT_FIELD_SHARED_CLEAN:
+ result.u64 = record->shared_clean;
+ break;
+ case MSTAT_FIELD_SHARED_DIRTY:
+ result.u64 = record->shared_dirty;
+ break;
+ case MSTAT_FIELD_PRIVATE_CLEAN:
+ result.u64 = record->private_clean;
+ break;
+ case MSTAT_FIELD_PRIVATE_DIRTY:
+ result.u64 = record->private_dirty;
+ break;
+ case MSTAT_FIELD_REFERENCED:
+ result.u64 = record->referenced;
+ break;
+ case MSTAT_FIELD_ANONYMOUS:
+ result.u64 = record->anonymous;
+ break;
+ case MSTAT_FIELD_LAZY_FREE:
+ result.u64 = record->lazy_free;
+ break;
+ case MSTAT_FIELD_ANON_HUGE_PAGES:
+ result.u64 = record->anon_huge_pages;
+ break;
+ case MSTAT_FIELD_SHMEM_PMD_MAPPED:
+ result.u64 = record->shmem_pmd_mapped;
+ break;
+ case MSTAT_FIELD_FILE_PMD_MAPPED:
+ result.u64 = record->file_pmd_mapped;
+ break;
+ case MSTAT_FIELD_SHARED_HUGETLB:
+ result.u64 = record->shared_hugetlb;
+ break;
+ case MSTAT_FIELD_PRIVATE_HUGETLB:
+ result.u64 = record->private_hugetlb;
+ break;
+ case MSTAT_FIELD_SWAP:
+ result.u64 = record->swap;
+ break;
+ case MSTAT_FIELD_SWAP_PSS:
+ result.u64 = record->swap_pss;
+ break;
+ case MSTAT_FIELD_LOCKED:
+ result.u64 = record->locked;
+ break;
+ default:
+ fprintf(stderr, "%s: unknown id id: %u\n", __FUNCTION__, id);
+ break;
+ }
+
+ return result;
+}
+
+int mstat_check_header(FILE *fp) {
+ int result;
+ char buf[MSTAT_MAGIC_SIZE] = {0};
+ ssize_t pos = ftell(fp);
+ fseek(fp, 0, SEEK_SET);
+ fread(buf, sizeof(buf), 1, fp);
+ if (!strcmp(buf, mstat_magic_bytes)) {
+ result = 0;
+ } else {
+ result = 1;
+ }
+ fseek(fp, pos, SEEK_SET);
+ return result;
+}
+
+/**
+ * Open an mstat file, or create one if it does not exist
+ * @param filename
+ * @return
+ */
+FILE *mstat_open(const char *filename) {
+ FILE *fp = NULL;
+ char mode[4] = {0};
+ int do_header = 0;
+
+ strcpy(mode, "rb+");
+ if (access(filename, F_OK) < 0) {
+ do_header = 1;
+ strcpy(mode, "wb+");
+ }
+
+ fp = fopen(filename, mode);
+ if (!fp) {
+ perror(filename);
+ return NULL;
+ }
+
+ if (do_header && mstat_write_header(fp)) {
+ fclose(fp);
+ fprintf(stderr, "unable to write header to mstat database\n");
+ return NULL;
+ } else {
+ if (mstat_check_header(fp)) {
+ fprintf(stderr, "%s is not an mstat database\n", filename);
+ fclose(fp);
+ return NULL;
+ }
+ }
+ mstat_rewind(fp);
+ return fp;
+}
+
+/**
+ * Rewind MSTAT file to the start of the data region
+ * @param fp pointer to MSTAT file stream
+ * @return
+ */
+int mstat_rewind(FILE *fp) {
+ int fields_end;
+ fseek(fp, MSTAT_EOH, SEEK_SET);
+ fread(&fields_end, sizeof(fields_end), 1, fp);
+ return fseek(fp, fields_end, SEEK_SET);
+}
+
+/**
+ * Return one record from a MSTAT file per call, until EOF
+ * @param fp pointer to MSTAT file stream
+ * @param record pointer to MSTAT record
+ * @return 0 on success. -1 on error
+ */
+int mstat_iter(FILE *fp, struct mstat_record_t *record) {
+ if (feof(fp))
+ return -1;
+ if (!fread(&record->pid, sizeof(record->pid), 1, fp)) return -1;
+ if (!fread(&record->timestamp, sizeof(record->timestamp), 1, fp)) return -1;
+ if (!fread(&record->rss, sizeof(record->rss), 1, fp)) return -1;
+ if (!fread(&record->pss, sizeof(record->pss), 1, fp)) return -1;
+ if (!fread(&record->pss_anon, sizeof(record->pss_anon), 1, fp)) return -1;
+ if (!fread(&record->pss_file, sizeof(record->pss_file), 1, fp)) return -1;
+ if (!fread(&record->pss_shmem, sizeof(record->pss_shmem), 1, fp)) return -1;
+ if (!fread(&record->shared_clean, sizeof(record->shared_clean), 1, fp)) return -1;
+ if (!fread(&record->shared_dirty, sizeof(record->shared_dirty), 1, fp)) return -1;
+ if (!fread(&record->private_clean, sizeof(record->private_clean), 1, fp)) return -1;
+ if (!fread(&record->private_dirty, sizeof(record->private_dirty), 1, fp)) return -1;
+ if (!fread(&record->referenced, sizeof(record->referenced), 1, fp)) return -1;
+ if (!fread(&record->anonymous, sizeof(record->anonymous), 1, fp)) return -1;
+ if (!fread(&record->lazy_free, sizeof(record->lazy_free), 1, fp)) return -1;
+ if (!fread(&record->anon_huge_pages, sizeof(record->anon_huge_pages), 1, fp)) return -1;
+ if (!fread(&record->shmem_pmd_mapped, sizeof(record->shmem_pmd_mapped), 1, fp)) return -1;
+ if (!fread(&record->file_pmd_mapped, sizeof(record->file_pmd_mapped), 1, fp)) return -1;
+ if (!fread(&record->shared_hugetlb, sizeof(record->shared_hugetlb), 1, fp)) return -1;
+ if (!fread(&record->private_hugetlb, sizeof(record->private_hugetlb), 1, fp)) return -1;
+ if (!fread(&record->swap, sizeof(record->swap), 1, fp)) return -1;
+ if (!fread(&record->swap_pss, sizeof(record->swap_pss), 1, fp)) return -1;
+ if (!fread(&record->locked, sizeof(record->locked), 1, fp)) return -1;
+ return 0;
+}
+
+/**
+ * Convert smaps_rollup data string to integer
+ * @param data value from smaps_rollup key pair
+ * @return integer value on success. -1 on error
+ */
+ssize_t mstat_get_value_smaps(char *data) {
+ ssize_t result = -1;
+ char *ptr = NULL;
+
+ ptr = strchr(data, ':');
+ if (ptr) {
+ ptr++;
+ result = strtol(ptr, NULL, 10);
+ }
+ return result;
+}
+
+/**
+ * Extract value (as string) from smaps_rollup key pair
+ * @param data smaps_rollup line
+ * @param key name to read
+ * @return data from key on success. NULL on error
+ */
+char *mstat_get_key_smaps(char *data, const char *key) {
+ char buf[255] = {0};
+ snprintf(buf, sizeof(buf) - 1, "%s:", key);
+ if (!strncmp(data, buf, strlen(buf))) {
+ return data;
+ }
+ return NULL;
+}
+
+/**
+ * Consume /proc/`pid`/smaps_rollup stream
+ * @param p pointer to MSTAT record
+ * @param fp pointer to file stream
+ * @return TODO
+ */
+int mstat_read_smaps(struct mstat_record_t *p, FILE *fp) {
+ char data[1024] = {0};
+ for (size_t i = 0; fgets(data, sizeof(data) - 1, fp) != NULL; i++) {
+ if (mstat_get_key_smaps(data, "Rss")) {
+ p->rss = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Pss")) {
+ p->pss = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Pss_Anon")) {
+ p->pss_anon = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Pss_File")) {
+ p->pss_file = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Pss_Shmem")) {
+ p->pss_shmem = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Shared_Clean")) {
+ p->shared_clean = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Shared_Dirty")) {
+ p->shared_dirty = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Private_Clean")) {
+ p->private_clean = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Private_Dirty")) {
+ p->private_dirty = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Referenced")) {
+ p->referenced = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Anonymous")) {
+ p->anonymous = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "LazyFree")) {
+ p->lazy_free = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "AnonHugePages")) {
+ p->anon_huge_pages = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "ShmemPmdMapped")) {
+ p->shmem_pmd_mapped = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "FilePmdMapped")) {
+ p->file_pmd_mapped = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Shared_Hugetlb")) {
+ p->shared_hugetlb = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Private_Hugetlb")) {
+ p->private_hugetlb = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Swap")) {
+ p->swap = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "SwapPss")) {
+ p->swap_pss = mstat_get_value_smaps(data);
+ }
+ if (mstat_get_key_smaps(data, "Locked")) {
+ p->locked = mstat_get_value_smaps(data);
+ }
+ }
+}
+
+/**
+ *
+ * @param p pointer to MSTAT record
+ * @param pid of target process
+ * @return 0 on success, -1 on error
+ */
+int mstat_attach(struct mstat_record_t *p, pid_t pid) {
+ FILE *fp;
+ char path[PATH_MAX] = {0};
+
+ snprintf(path, PATH_MAX, "/proc/%d/smaps_rollup", pid);
+ if (access(path, F_OK) < 0) {
+ return -1;
+ }
+ fp = fopen(path, "r");
+ if (!fp) {
+ return -1;
+ }
+
+ mstat_read_smaps(p, fp);
+ fclose(fp);
+
+ return 0;
+}
+
+/**
+ * Write MSTAT header to data file
+ *
+ * HEADER FORMAT
+ * 0x00 - 0x07 = file identifier (8 bytes)
+ * 0x08 - 0x0B = total field records (4 bytes)
+ * 0x0C - 0x0F = EOH offset (4 bytes)
+ * 0x10 - EOH = field_length (unsigned int), field (string) (n... bytes)
+ *
+ * @param fp pointer to stream
+ * @return 0 on success, -1 on error
+ */
+int mstat_write_header(FILE *fp) {
+ fwrite(mstat_magic_bytes, 6, 1, fp);
+ for (int i = 0; i < MSTAT_MAGIC_SIZE - sizeof(mstat_magic_bytes); i++) {
+ if (!fwrite("\0", 1, 1, fp)) {
+ return -1;
+ }
+ }
+ int rec;
+ ssize_t fields_end;
+
+ for (rec = 0; mstat_field_names[rec] != NULL; rec++) {
+ unsigned int len = strlen(mstat_field_names[rec]);
+ fwrite(&len, sizeof(len), 1, fp);
+ fwrite(mstat_field_names[rec], sizeof(char), len, fp);
+ }
+ fields_end = ftell(fp);
+
+ fseek(fp, MSTAT_FIELD_COUNT, SEEK_SET);
+ fwrite(&rec, sizeof(rec), 1, fp);
+
+ fseek(fp, MSTAT_EOH, SEEK_SET);
+ fwrite(&fields_end, sizeof(int), 1, fp);
+ fseek(fp, fields_end, SEEK_SET);
+ return 0;
+}
+
+/**
+ * Write a MSTAT record to data file
+ * @param fp pointer to MSTAT file stream
+ * @param record pointer to MSTAT record
+ * @return 0 on success. -1 on error
+ */
+int mstat_write(FILE *fp, struct mstat_record_t *record) {
+ if (!fwrite(&record->pid, sizeof(record->pid), 1, fp)) return -1;
+ if (!fwrite(&record->timestamp, sizeof(record->timestamp), 1, fp)) return -1;
+ if (!fwrite(&record->rss, sizeof(record->rss), 1, fp)) return -1;
+ if (!fwrite(&record->pss, sizeof(record->pss), 1, fp)) return -1;
+ if (!fwrite(&record->pss_anon, sizeof(record->pss_anon), 1, fp)) return -1;
+ if (!fwrite(&record->pss_file, sizeof(record->pss_file), 1, fp)) return -1;
+ if (!fwrite(&record->pss_shmem, sizeof(record->pss_shmem), 1, fp)) return -1;
+ if (!fwrite(&record->shared_clean, sizeof(record->shared_clean), 1, fp)) return -1;
+ if (!fwrite(&record->shared_dirty, sizeof(record->shared_dirty), 1, fp)) return -1;
+ if (!fwrite(&record->private_clean, sizeof(record->private_clean), 1, fp)) return -1;
+ if (!fwrite(&record->private_dirty, sizeof(record->private_dirty), 1, fp)) return -1;
+ if (!fwrite(&record->referenced, sizeof(record->referenced), 1, fp)) return -1;
+ if (!fwrite(&record->anonymous, sizeof(record->anonymous), 1, fp)) return -1;
+ if (!fwrite(&record->lazy_free, sizeof(record->lazy_free), 1, fp)) return -1;
+ if (!fwrite(&record->anon_huge_pages, sizeof(record->anon_huge_pages), 1, fp)) return -1;
+ if (!fwrite(&record->shmem_pmd_mapped, sizeof(record->shmem_pmd_mapped), 1, fp)) return -1;
+ if (!fwrite(&record->file_pmd_mapped, sizeof(record->file_pmd_mapped), 1, fp)) return -1;
+ if (!fwrite(&record->shared_hugetlb, sizeof(record->shared_hugetlb), 1, fp)) return -1;
+ if (!fwrite(&record->private_hugetlb, sizeof(record->private_hugetlb), 1, fp)) return -1;
+ if (!fwrite(&record->swap, sizeof(record->swap), 1, fp)) return -1;
+ if (!fwrite(&record->swap_pss, sizeof(record->swap_pss), 1, fp)) return -1;
+ if (!fwrite(&record->locked, sizeof(record->locked), 1, fp)) return -1;
+ return 0;
+}
+
+/**
+ * Compute difference between timespec structures
+ * @param end timespec
+ * @param start timespec
+ * @return seconds
+ */
+double mstat_difftimespec(const struct timespec end, const struct timespec start) {
+ return (double)(end.tv_sec - start.tv_sec) + (double)(end.tv_nsec - start.tv_nsec) / 1e9;
+}
+
+/**
+ * Compute the min/max of an array
+ * @param a input data
+ * @param size size of input data
+ * @param min pointer to return variable (modified)
+ * @param max pointer to return variable (modified)
+ */
+void mstat_get_mmax(const double a[], size_t size, double *min, double *max) {
+ *min = a[0];
+ *max = 0;
+
+ for (size_t i = 0; i < size; i++) {
+ if (a[i] > *max) {
+ *max = a[i];
+ }
+ if (a[i] < *min) {
+ *min = a[i];
+ }
+ }
+} \ No newline at end of file
diff --git a/common.h b/common.h
new file mode 100644
index 0000000..ccdf7e8
--- /dev/null
+++ b/common.h
@@ -0,0 +1,89 @@
+#ifndef MSTAT_COMMON_H
+#define MSTAT_COMMON_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <limits.h>
+#include <time.h>
+
+#define MSTAT_MAGIC "MSTAT"
+#define MSTAT_FIELD_COUNT 0x08
+#define MSTAT_EOH 0x0C
+#define MSTAT_MAGIC_SIZE 0x10
+
+struct mstat_record_t {
+ pid_t pid;
+ double timestamp;
+ size_t rss,
+ pss,
+ pss_anon,
+ pss_file,
+ pss_shmem,
+ shared_clean,
+ shared_dirty,
+ private_clean,
+ private_dirty,
+ referenced,
+ anonymous,
+ lazy_free,
+ anon_huge_pages,
+ shmem_pmd_mapped,
+ file_pmd_mapped,
+ shared_hugetlb,
+ private_hugetlb,
+ swap,
+ swap_pss,
+ locked;
+};
+
+enum {
+ MSTAT_FIELD_PID = 0,
+ MSTAT_FIELD_TIMESTAMP,
+ MSTAT_FIELD_RSS,
+ MSTAT_FIELD_PSS,
+ MSTAT_FIELD_PSS_ANON,
+ MSTAT_FIELD_PSS_FILE,
+ MSTAT_FIELD_PSS_SHMEM,
+ MSTAT_FIELD_SHARED_CLEAN,
+ MSTAT_FIELD_SHARED_DIRTY,
+ MSTAT_FIELD_PRIVATE_CLEAN,
+ MSTAT_FIELD_PRIVATE_DIRTY,
+ MSTAT_FIELD_REFERENCED,
+ MSTAT_FIELD_ANONYMOUS,
+ MSTAT_FIELD_LAZY_FREE,
+ MSTAT_FIELD_ANON_HUGE_PAGES,
+ MSTAT_FIELD_SHMEM_PMD_MAPPED,
+ MSTAT_FIELD_FILE_PMD_MAPPED,
+ MSTAT_FIELD_SHARED_HUGETLB,
+ MSTAT_FIELD_PRIVATE_HUGETLB,
+ MSTAT_FIELD_SWAP,
+ MSTAT_FIELD_SWAP_PSS,
+ MSTAT_FIELD_LOCKED,
+};
+
+union mstat_field_t {
+ size_t u64;
+ double d64;
+};
+
+int mstat_get_field_count(FILE *fp);
+char **mstat_read_fields(FILE *fp);
+int mstat_is_valid_field(char **fields, const char *name);
+union mstat_field_t mstat_get_field_by_id(const struct mstat_record_t *record, unsigned id);
+union mstat_field_t mstat_get_field_by_name(const struct mstat_record_t *p, const char *name);
+int mstat_check_header(FILE *fp);
+FILE *mstat_open(const char *filename);
+int mstat_rewind(FILE *fp);
+ssize_t mstat_get_value_smaps(char *data);
+char *mstat_get_key_smaps(char *data, const char *key);
+int mstat_read_smaps(struct mstat_record_t *p, FILE *fp);
+int mstat_attach(struct mstat_record_t *p, pid_t pid);
+int mstat_write_header(FILE *fp);
+int mstat_write(FILE *fp, struct mstat_record_t *p);
+int mstat_iter(FILE *fp, struct mstat_record_t *p);
+void mstat_get_mmax(const double a[], size_t size, double *min, double *max);
+double mstat_difftimespec(struct timespec end, struct timespec start);
+
+#endif //MSTAT_COMMON_H
diff --git a/mstat.c b/mstat.c
new file mode 100644
index 0000000..18e19b6
--- /dev/null
+++ b/mstat.c
@@ -0,0 +1,208 @@
+#include <errno.h>
+#include <dirent.h>
+#include <signal.h>
+#include <time.h>
+#include "common.h"
+
+
+struct Option {
+ /** Increased verbosity */
+ unsigned char verbose;
+ /** Overwrite existing file(s) */
+ unsigned char clobber;
+ /** PID to track */
+ pid_t pid;
+ /** Output file handle to track */
+ FILE *file;
+ /** Number of times per second mstat samples a pid */
+ double sample_rate;
+} option;
+
+/**
+ * Interrupt handler.
+ * Called on exit.
+ * @param sig the trapped signal
+ */
+static void handle_interrupt(int sig) {
+ switch (sig) {
+ case SIGUSR1:
+ if (option.file) {
+ fprintf(stderr, "flushed handle: %p\n", option.file);
+ fflush(option.file);
+ } else {
+ fprintf(stderr, "flush request ignored. no handle");
+ }
+ return;
+ case 0:
+ case SIGTERM:
+ case SIGINT:
+ if (option.file) {
+ fflush(option.file);
+ fclose(option.file);
+ }
+ exit(0);
+ default:
+ break;
+ }
+}
+
+static void usage(char *prog) {
+ char *sep;
+ char *name;
+
+ sep = strrchr(prog, '/');
+ name = prog;
+ if (sep) {
+ name = sep + 1;
+ }
+ printf("usage: %s [OPTIONS] <PID>\n"
+ "-h this help message\n"
+ "-c clobber output file if it exists\n"
+ "-s set sample rate (default: %0.2lf)\n"
+ "\n", name, option.sample_rate);
+}
+
+/**
+ * Parse program arguments and update global config
+ * @param argc
+ * @param argv
+ */
+void parse_options(int argc, char *argv[]) {
+ if (argc < 2) {
+ usage(argv[0]);
+ exit(1);
+ }
+ for (int x = 0, i = 1; i < argc; i++) {
+ if (strlen(argv[i]) > 1 && *argv[i] == '-') {
+ char *arg = argv[i] + 1;
+ if (!strcmp(arg, "h")) {
+ usage(argv[0]);
+ exit(0);
+ }
+ if (!strcmp(arg, "v")) {
+ option.verbose = 1;
+ } else if (!strcmp(arg, "s")) {
+ option.sample_rate = strtod(argv[i+1], NULL);
+ i++;
+ } else if (!strcmp(arg, "c")) {
+ option.clobber = 1;
+ }
+ } else {
+ option.pid = (pid_t) strtol(argv[i], NULL, 10);
+ x++;
+ }
+ }
+}
+
+/**
+ * Check if `pid` directory exists in /proc
+ * @param pid
+ * @return 0 if exists and can read the directory. -1 if not
+ */
+int pid_exists(pid_t pid) {
+ char path[PATH_MAX] = {0};
+ snprintf(path, sizeof(path) - 1, "/proc/%d", pid);
+ return access(path, F_OK | R_OK | X_OK);
+}
+
+/**
+ * Check /proc/`pid` provides the smaps_rollups file
+ * @param pid
+ * @return 0 if exists and can read the file. -1 if not
+ */
+int smaps_rollup_usable(pid_t pid) {
+ char path[PATH_MAX] = {0};
+ snprintf(path, sizeof(path) - 1, "/proc/%d/smaps_rollup", pid);
+ return access(path, F_OK | R_OK);
+}
+
+
+int main(int argc, char *argv[]) {
+ struct mstat_record_t record;
+ char path[PATH_MAX];
+
+ // Set default options
+ option.sample_rate = 1;
+ option.verbose = 0;
+ option.clobber = 0;
+ // Set options based on arguments
+ parse_options(argc, argv);
+
+ // Allow user to flush the data stream with USR1
+ signal(SIGUSR1, handle_interrupt);
+ // Always attempt to exit cleanly
+ signal(SIGINT, handle_interrupt);
+ signal(SIGTERM, handle_interrupt);
+
+ // For each PID passed to the program, begin gathering stats
+ sprintf(path, "%d.mstat", option.pid);
+
+ if (pid_exists(option.pid) < 0) {
+ fprintf(stderr, "no pid %d\n", option.pid);
+ exit(1);
+ }
+
+ if (smaps_rollup_usable(option.pid) < 0) {
+ fprintf(stderr, "pid %d: %s\n", option.pid, strerror(errno));
+ exit(1);
+ }
+
+ if (access(path, F_OK) == 0) {
+ if (option.clobber) {
+ remove(path);
+ fprintf(stderr, "%s clobbered\n", path);
+ } else {
+ fprintf(stderr, "%s file already exists\n", path);
+ exit(1);
+ }
+ }
+
+ option.file = mstat_open(path);
+ if (!option.file) {
+ perror(path);
+ exit(1);
+ }
+
+ if (mstat_check_header(option.file)) {
+ mstat_write_header(option.file);
+ }
+
+
+ size_t i;
+ struct timespec ts_start, ts_end;
+
+ // Begin tracking time.
+ clock_gettime(CLOCK_MONOTONIC, &ts_start);
+
+ // Begin sample loop
+ printf("PID: %d\nSamples per second: %.2lf\n(interrupt with ctrl-c...)\n", option.pid, option.sample_rate);
+ i = 0;
+ while (1) {
+ memset(&record, 0, sizeof(record));
+ record.pid = option.pid;
+
+ // Record run time since last call
+ clock_gettime(CLOCK_MONOTONIC, &ts_end);
+ record.timestamp = mstat_difftimespec(ts_end, ts_start);
+
+ // Sample memory values
+ if (mstat_attach(&record, record.pid) < 0) {
+ fprintf(stderr, "lost pid %d\n", option.pid);
+ break;
+ }
+
+ if (option.verbose) {
+ printf("pid: %d, sample: %zu, elapsed: %lf, rss: %li\n", record.pid, i, record.timestamp, record.rss);
+ }
+
+ if (mstat_write(option.file, &record) < 0) {
+ fprintf(stderr, "Unable to write record to mstat file for pid %d\n", option.pid);
+ break;
+ }
+ usleep((int) (1e6 / option.sample_rate));
+ i++;
+ }
+
+ handle_interrupt(0);
+ return 0;
+}
diff --git a/mstat_export.c b/mstat_export.c
new file mode 100644
index 0000000..2c51e13
--- /dev/null
+++ b/mstat_export.c
@@ -0,0 +1,67 @@
+#include "common.h"
+
+int main(int argc, char *argv[]) {
+ FILE *fp;
+ struct mstat_record_t p;
+ char **fields;
+ size_t fields_total;
+
+ if (argc < 2) {
+ fprintf(stderr, "Missing path to *.mstat data file\n");
+ exit(1);
+ }
+
+ if (access(argv[1], F_OK)) {
+ perror(argv[1]);
+ exit(1);
+ }
+
+ fp = mstat_open(argv[1]);
+ if (!fp) {
+ perror(argv[1]);
+ exit(1);
+ }
+
+ fields = mstat_read_fields(fp);
+ if (!fields) {
+ fprintf(stderr, "Unable to obtain field names from %s\n", argv[1]);
+ exit(1);
+ }
+
+ fields_total = mstat_get_field_count(fp);
+ for (size_t i = 0; i < fields_total; i++) {
+ printf("%s", fields[i]);
+ if (i < fields_total - 1) {
+ printf(",");
+ }
+ }
+ puts("");
+
+ if (mstat_rewind(fp) < 0) {
+ perror("Unable to rewind");
+ exit(1);
+ }
+
+ while (!mstat_iter(fp, &p)) {
+ char buf[1024] = {0};
+ for (size_t i = 0; i < fields_total; i++) {
+ struct mstat_record_t *pptr = &p;
+ union mstat_field_t result;
+ result = mstat_get_field_by_name(pptr, fields[i]);
+
+ if (!strcmp(fields[i], "timestamp")) {
+ snprintf(buf, sizeof(buf) - 1, "%lf", result.d64);
+ } else {
+ snprintf(buf, sizeof(buf) - 1, "%zu", result.u64);
+ }
+ if (i < fields_total - 1) {
+ strcat(buf, ",");
+ }
+ printf("%s", buf);
+ }
+ puts("");
+ }
+
+ fclose(fp);
+ return 0;
+} \ No newline at end of file
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;
+}