diff options
| -rw-r--r-- | usr.sbin/pmc/display.cc | 571 | ||||
| -rw-r--r-- | usr.sbin/pmc/display.hh | 105 |
2 files changed, 676 insertions, 0 deletions
diff --git a/usr.sbin/pmc/display.cc b/usr.sbin/pmc/display.cc new file mode 100644 index 000000000000..454cb74e29bd --- /dev/null +++ b/usr.sbin/pmc/display.cc @@ -0,0 +1,571 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026, Netflix, Inc. + * + * This software was developed by Ali Mashtizadeh under the sponsorship from + * Netflix, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + */ + +#include <sys/ioctl.h> + +#include <inttypes.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <term.h> +#include <math.h> + +#include <curses.h> +#include <unistd.h> + +#include <cxxabi.h> +#include <iomanip> +#include <ios> +#include <iostream> +#include <sstream> +#include <string> +#include <vector> + +#include "display.hh" + +enum termmode { + TERMMODE_BASIC, + TERMMODE_VT, + TERMMODE_XTERM +}; + +static termmode tmode; +static int disp_height; +static int disp_width; + +#define TITLE_TITLE 0 +#define TITLE_SUBTITLE 1 + +static const char **title_char; + +static const char *title_vt[] = { + "\xe2\x95\x90", "\xe2\x94\x80" +}; +static const char *title_basic[] = { "=", "-" }; + +#define ARROW_DOWN 0 +#define ARROW_UP 1 + +static const char **arrow_char; + +static const char *arrow_vt[] = { + "\xe2\x96\xbc", "\xe2\x96\xb2" +}; +static const char *arrow_basic[] = { "D", "U" }; + +#define BORDER_HBAR 0 +#define BORDER_VBAR 1 +#define BORDER_TOPRIGHT 2 +#define BORDER_TOPLEFT 3 +#define BORDER_BOTTOMLEFT 4 +#define BORDER_BOTTOMRIGHT 5 +#define BORDER_TOPSPLIT 6 +#define BORDER_BOTTOMSPLIT 7 +#define BORDER_RIGHTSPLIT 8 +#define BORDER_LEFTSPLIT 9 +#define BORDER_CROSS 10 + +static const char **border_char; + +static const char *border_vt[] = { + "\xe2\x94\x80", "\xe2\x94\x82", + "\xe2\x94\x8c", "\xe2\x94\x90", "\xe2\x94\x94", "\xe2\x94\x98", + "\xe2\x94\xac", "\xe2\x94\xb4", "\xe2\x94\xa4", "\xe2\x94\x9c", + "\xe2\x94\xbc" +}; +static const char *border_basic[] = { "-", "|", "+", "+", "+", "+", "+", "+", "+", "+" }; + +static char tc_boldbuf[16]; +static char tc_sgr0buf[16]; +static char tc_afbuf[16]; +static char tc_green[16]; +static char tc_yellow[16]; +static char tc_red[16]; + +static const char *TC_BOLD; +static const char *TC_SGR0; +static const char *TC_SETAF; + +void +tcemit(const char *c) +{ + if (c) + tputs(c, 1, putchar); +} + +void +tcfg(int n) +{ + if (TC_SETAF) + tputs(tgoto(TC_SETAF, 0, n), 1, putchar); +} + +static void +setup_termcap(const char *term) +{ + bool xcolor; + char *buf; + char *clicolor; + char termcapbuf[4096]; + + if (tgetent(termcapbuf, "xterm-256color") != 1) { + TC_BOLD = NULL; + TC_SGR0 = NULL; + TC_SETAF = NULL; + return; + } + + buf = tc_boldbuf; + TC_BOLD = tgetstr("md", &buf); + buf = tc_sgr0buf; + TC_SGR0 = tgetstr("me", &buf); + buf = tc_afbuf; + TC_SETAF = tgetstr("AF", &buf); + + clicolor = getenv("CLICOLOR"); + if (clicolor == NULL || strcmp(clicolor, "1") != 0) { + return; + } + + if (strcmp(term, "xterm-256color") == 0) { + xcolor = true; + } else { + xcolor = false; + } + + buf = tgoto(TC_SETAF, 1, xcolor ? COLOR256_GREEN : COLOR_GREEN); + if (buf) { + strlcpy(tc_green, buf, sizeof(tc_green)); + } else { + tc_green[0] = '\0'; + } + + buf = tgoto(TC_SETAF, 1, xcolor ? COLOR256_YELLOW : COLOR_YELLOW); + if (buf) { + strlcpy(tc_yellow, buf, sizeof(tc_yellow)); + } else { + tc_yellow[0] = '\0'; + } + + buf = tgoto(TC_SETAF, 1, xcolor ? COLOR256_RED : COLOR_RED); + if (buf) { + strlcpy(tc_red, buf, sizeof(tc_red)); + } else { + tc_red[0] = '\0'; + } +} + +void +setup_screen() +{ + int status; + const char *term; + struct winsize wsz; + + /* Sane defaults */ + tmode = TERMMODE_BASIC; + title_char = title_basic; + arrow_char = arrow_basic; + border_char = border_basic; + disp_height = 25; + disp_width = 80; + + if (isatty(STDOUT_FILENO) == 0) { + tmode = TERMMODE_XTERM; + title_char = title_vt; + arrow_char = arrow_vt; + border_char = border_vt; + return; + } + + status = ioctl(STDIN_FILENO, TIOCGWINSZ, &wsz); + if (status < 0) { + perror("ioctl(TIOCGWINSZ)"); + return; + } + + disp_height = wsz.ws_row; + disp_width = wsz.ws_col; + + term = getenv("TERM"); + if (term == NULL) { + tmode = TERMMODE_BASIC; + title_char = title_basic; + arrow_char = arrow_basic; + border_char = border_basic; + return; + } + + if (strncmp(term, "xterm", 5) == 0) { + tmode = TERMMODE_XTERM; + title_char = title_vt; + arrow_char = arrow_vt; + border_char = border_vt; + } else if (strncmp(term, "vt", 2) == 0) { + tmode = TERMMODE_VT; + title_char = title_vt; + arrow_char = arrow_vt; + border_char = border_vt; + } + + setup_termcap(term); +} + +void +title(const std::string &t) +{ + size_t i; + + if (tmode != TERMMODE_BASIC) + tcemit(TC_BOLD); + printf("%s\n", t.c_str()); + if (tmode != TERMMODE_BASIC) + tcemit(TC_SGR0); + for (i = 0; i < t.size(); i++) + printf("%s", title_char[TITLE_TITLE]); + printf("\n\n"); +} + +void +header(const std::string &h) +{ + size_t i; + + if (tmode != TERMMODE_BASIC) + tcemit(TC_BOLD); + printf("%s\n", h.c_str()); + if (tmode != TERMMODE_BASIC) + tcemit(TC_SGR0); + for (i = 0; i < h.size(); i++) + printf("%s", title_char[TITLE_SUBTITLE]); + printf("\n"); +} + +void +printval(const std::string &msg, uint64_t val, siunit ui) +{ + switch (ui) { + case siunit::seconds: + printf("%s: %" PRIu64 " s\n", msg.c_str(), val); + break; + case siunit::percent: + printf("%s: %" PRIu64 "%%\n", msg.c_str(), val); + break; + case siunit::cycles: + printf("%s: %" PRIu64 " cycles\n", msg.c_str(), val); + break; + } +} + +void +printval(const std::string &msg, float val, siunit ui) +{ + switch (ui) { + case siunit::seconds: + printf("%s: %.2f s\n", msg.c_str(), val); + break; + case siunit::percent: + printf("%s: %6.2f%%\n", msg.c_str(), val); + break; + case siunit::cycles: + printf("%s: %.2f cycles\n", msg.c_str(), val); + break; + } +} + +std::string +format_siprefix(uint64_t count) +{ + unsigned long index = 0; + char prefix[] = " kMGTP"; + char buf[10]; + + while (count > 10000 && index < sizeof(prefix)) { + index++; + count /= 1000; + } + + if (index == 0) { + snprintf(buf, sizeof(buf), "%" PRIu64, count); + } else { + snprintf(buf, sizeof(buf), "%" PRIu64 "%c", count, prefix[index]); + } + + return buf; +} + +std::string +format_binprefix(uint64_t count) +{ + unsigned long index = 0; + char prefix[] = " KMGTP"; + char buf[10]; + + while (count > 10000 && index < sizeof(prefix)) { + index++; + count /= 1000; + } + + if (index == 0) { + snprintf(buf, sizeof(buf), "%" PRIu64 " B", count); + } else { + snprintf(buf, sizeof(buf), "%" PRIu64 " %ciB", count, prefix[index]); + } + + return buf; +} + +std::string +format_sample(uint64_t count, uint64_t total) +{ + char buf[20]; + std::string fmt = format_siprefix(count); + + snprintf(buf, sizeof(buf), "%5s (%4.1f%%)", fmt.c_str(), + 100.0 * (float)count / (float)total); + + return buf; +} + +std::string +format_percent(uint64_t count, uint64_t total) +{ + char buf[20]; + + if (total == 0) { + snprintf(buf, sizeof(buf), "%6s", ""); + } else { + snprintf(buf, sizeof(buf), "%5.1f%%", + 100.0 * (float)count / (float)total); + } + + return buf; +} + +/* fields */ + +field::field(int64_t v) +{ + type = FIELD::INTEGER; + value[0] = v; +} + +field::field(float v) +{ + type = FIELD::FLOAT; + fvalue = v; +} + +field::field(const std::string &s) +{ + type = FIELD::STRING; + svalue = s; +} + +field::field(int64_t c, int64_t t, bool percent) +{ + if (percent) + type = FIELD::PERCENT; + else + type = FIELD::SAMPLE; + value[0] = c; + value[1] = t; +} + +field::field(int64_t uc, int64_t ut, int64_t ku, int64_t kt) +{ + type = FIELD::DSAMPLE; + value[0] = uc; + value[1] = ut; + value[2] = ku; + value[3] = kt; +} + +bool +field::operator>(const field &b) const +{ + switch (type) { + case FIELD::STRING: + return (svalue > b.svalue); + case FIELD::FLOAT: + return (fvalue > b.fvalue); + case FIELD::INTEGER: [[fallthrough]]; + case FIELD::SAMPLE: + return (value[0] > b.value[0]); + case FIELD::DSAMPLE: + return ((value[0] + value[2]) > (b.value[0] + b.value[2])); + case FIELD::PERCENT: + return (((float)value[0] / (float)value[1]) > + ((float)b.value[0] / (float)b.value[1])); + } +} + +bool +field::operator<(const field &b) const +{ + switch (type) { + case FIELD::STRING: + return (svalue < b.svalue); + case FIELD::FLOAT: + return (fvalue < b.fvalue); + case FIELD::INTEGER: [[fallthrough]]; + case FIELD::SAMPLE: + return (value[0] < b.value[0]); + case FIELD::DSAMPLE: + return ((value[0] + value[2]) < (b.value[0] + b.value[2])); + case FIELD::PERCENT: + return (((float)value[0] / (float)value[1]) < + ((float)b.value[0] / (float)b.value[1])); + } +} + +size_t +field::length() +{ + return (to_string().length()); +} + +std::string +field::to_string() +{ + switch (type) { + case FIELD::STRING: + return (svalue); + case FIELD::FLOAT: + return (std::to_string(fvalue)); + case FIELD::INTEGER: + return (std::to_string(value[0])); + case FIELD::SAMPLE: + return (format_sample(value[0], value[1])); + case FIELD::DSAMPLE: + return (format_sample(value[0], value[1]) + " / " + + format_sample(value[2], value[3])); + case FIELD::PERCENT: + return (format_percent(value[0], value[1])); + } +} + +/* table class */ + +table::table() : sortcol(-1), sortdir(true), cols(), align(), rows() +{ +} + +table::~table() +{ +} + +void +table::addcolumn(const std::string &c, bool alignleft) +{ + cols.push_back(c); + align.push_back(alignleft); +} + +void +table::addrow(std::vector<field> r) +{ + rows.push_back(std::move(r)); +} + +void +table::sort(int col, bool descending) +{ + sortcol = col; + sortdir = descending; + if (descending) + std::sort(rows.begin(), rows.end(), [col](auto &a, auto &b) + { return (a[col] > b[col]); }); + else + std::sort(rows.begin(), rows.end(), [col](auto &a, auto &b) + { return (a[col] < b[col]); }); +} + +void +table::print() +{ + unsigned long c; + std::vector<unsigned long> width; + + // Compute column width + for (auto h : cols) + width.push_back(h.length() + 1); + if (sortcol != -1) + width[sortcol] += 1; + for (auto &r : rows) { + for (c = 0; c < r.size(); c++) { + if (width[c] < r[c].length()) + width[c] = r[c].length(); + } + } + + for (c = 0; c < width.size(); c++) { + if (align[c]) + std::cout << std::left; + std::cout << std::setw(width[c]); + tcemit(TC_BOLD); + if (c == (unsigned long)sortcol) + std::cout << cols[c] + " " + arrow_char[sortdir ? ARROW_DOWN : ARROW_UP]; + else + std::cout << cols[c]; + tcemit(TC_SGR0); + if (c != (width.size() - 1)) + std::cout << " " << border_char[BORDER_VBAR] << " "; + if (align[c]) + std::cout << std::right; + } + std::cout << std::endl; + + for (c = 0; c < width.size(); c++) { + for (unsigned long i = 0; i < width[c]; i++) + std::cout << border_char[BORDER_HBAR]; + if (c != (width.size() - 1)) + std::cout << border_char[BORDER_HBAR] << + border_char[BORDER_CROSS] << border_char[BORDER_HBAR]; + } + std::cout << std::setfill(' ') << std::endl; + + for (auto &r : rows) { + for (c = 0; c < width.size(); c++) { + if (align[c]) + std::cout << std::left; + std::cout << std::setw(width[c]) << r[c].to_string(); + if (c != (width.size() - 1)) + std::cout << " " << border_char[BORDER_VBAR] << " "; + if (align[c]) + std::cout << std::right; + } + std::cout << std::endl; + } + + std::cout << std::endl; +} + diff --git a/usr.sbin/pmc/display.hh b/usr.sbin/pmc/display.hh new file mode 100644 index 000000000000..4fa67a4beb30 --- /dev/null +++ b/usr.sbin/pmc/display.hh @@ -0,0 +1,105 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026, Netflix, Inc. + * + * This software was developed by Ali Mashtizadeh under the sponsorship from + * Netflix, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + * + */ + +#define COLOR256_GREEN 46 +#define COLOR256_RED 196 +#define COLOR256_YELLOW 226 + +void setup_screen(); + +/* Formatting */ +std::string format_siprefix(uint64_t count); +std::string format_binprefix(uint64_t count); +std::string format_sample(uint64_t count, uint64_t total); +std::string format_percent(uint64_t count, uint64_t total); + +/* Printing */ +enum class siunit { + seconds, + // Special + cycles, + percent +}; + +void title(const std::string &msg); +void header(const std::string &msg); +void printval(const std::string &msg, uint64_t val, siunit ui); +void printval(const std::string &msg, float val, siunit ui); +void printbar(float percent); + +/* Tables */ +class field +{ +public: + field() { } + field(const std::string &s); + field(int64_t v); + field(float v); + field(int64_t count, int64_t total, bool percent = false); + field(int64_t user_count, int64_t user_total, + int64_t kernel_count, int64_t kernel_total); + virtual ~field() { } + virtual bool operator>(const field &b) const; + virtual bool operator<(const field &b) const; + virtual std::string to_string(); + virtual size_t length(); +private: + enum class FIELD { + STRING, + INTEGER, + FLOAT, + SAMPLE, + DSAMPLE, + PERCENT + }; + FIELD type; + int64_t value[4]; + float fvalue; + std::string svalue; +}; + +class table +{ +public: + table(); + ~table(); + void addcolumn(const std::string &c, bool alignleft = false); + void addrow(std::vector<field> r); + void sort(int col, bool descending = true); + void print(); +private: + int sortcol; + bool sortdir; + std::vector<std::string> cols; + std::vector<bool> align; + std::vector<std::vector<field>> rows; +}; + |
