Date: Tue, 28 Jul 2026 19:29:22 +0000 From: Warner Losh <imp@FreeBSD.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org Cc: Ali Mashtizadeh <ali@mashtizadeh.com> Subject: git: a53d2e4f5db5 - main - pmc: console configuration and table rendering for new PMC tools Message-ID: <6a690312.43502.47493578@gitrepo.freebsd.org>
index | next in thread | raw e-mail
The branch main has been updated by imp: URL: https://cgit.FreeBSD.org/src/commit/?id=a53d2e4f5db588cfd59cdebf04536b6ca7a70e4b commit a53d2e4f5db588cfd59cdebf04536b6ca7a70e4b Author: Ali Mashtizadeh <ali@mashtizadeh.com> AuthorDate: 2026-07-28 17:46:14 +0000 Commit: Warner Losh <imp@FreeBSD.org> CommitDate: 2026-07-28 19:27:44 +0000 pmc: console configuration and table rendering for new PMC tools Initializes the terminal rendering code used by the new pmc tools. Then provides a table abstraction for collecting, sorting and rendering tables. It provides pretty printed results with typed fields that print several types used throughout the new PMC tools. By default the fields are formatted in engineering notation. Sponsored by: Netflix Reviewed by: adrian, imp Differential Revision: https://reviews.freebsd.org/D57775 --- usr.sbin/pmc/display.cc | 571 ++++++++++++++++++++++++++++++++++++++++++++++++ usr.sbin/pmc/display.hh | 105 +++++++++ 2 files changed, 676 insertions(+) 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; +}; +home | help
Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?6a690312.43502.47493578>
