From 7971cc02be59a7d9115a8e436474433807588c6c Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Wed, 14 Sep 2022 15:54:37 -0400 Subject: Initial commit --- .gitignore | 4 + CMakeLists.txt | 12 ++ LICENSE | 29 +++ README.md | 24 +++ common.c | 572 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ common.h | 89 +++++++++ mstat.c | 208 +++++++++++++++++++++ mstat_export.c | 67 +++++++ mstat_plot.c | 408 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1413 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 common.c create mode 100644 common.h create mode 100644 mstat.c create mode 100644 mstat_export.c create mode 100644 mstat_plot.c 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 +``` + +## Plotting + +Requires `gnuplot` to be installed + +```shell +mstat_plot .mstat +``` + +## CSV export + +```shell +mstat_export .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 +#include +#include +#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 +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#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] \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 +#include +#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] \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; +} -- cgit