diff options
author | Joseph Hunkeler <jhunkeler@gmail.com> | 2022-09-14 15:54:37 -0400 |
---|---|---|
committer | Joseph Hunkeler <jhunkeler@gmail.com> | 2022-09-14 15:54:37 -0400 |
commit | 7971cc02be59a7d9115a8e436474433807588c6c (patch) | |
tree | 4376277edec4dfe00599c4a8a17e316b287a4778 | |
download | mstat-7971cc02be59a7d9115a8e436474433807588c6c.tar.gz |
Initial commit
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | CMakeLists.txt | 12 | ||||
-rw-r--r-- | LICENSE | 29 | ||||
-rw-r--r-- | README.md | 24 | ||||
-rw-r--r-- | common.c | 572 | ||||
-rw-r--r-- | common.h | 89 | ||||
-rw-r--r-- | mstat.c | 208 | ||||
-rw-r--r-- | mstat_export.c | 67 | ||||
-rw-r--r-- | mstat_plot.c | 408 |
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} +) @@ -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 @@ -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; +} |