pax_global_header00006660000000000000000000000064136161663540014525gustar00rootroot0000000000000052 comment=1a1520d41251b229884a9292dcc7fb922c8d2941 topline-0.3/000077500000000000000000000000001361616635400130415ustar00rootroot00000000000000topline-0.3/.gitignore000066400000000000000000000000171361616635400150270ustar00rootroot00000000000000topline *.o *~ topline-0.3/LICENSE000066400000000000000000000004201361616635400140420ustar00rootroot00000000000000ⓒ 2019 Adam Borowski This software can be used under the terms of GNU General Public License, version 2 or, if you choose so, any higher that is still a free software license. Of GPL variants published by FSF, only the Affero branch fails that requirement as of 2019. topline-0.3/Makefile000066400000000000000000000003141361616635400144770ustar00rootroot00000000000000ALL=topline CC=gcc CFLAGS=-Wall -Og -g all: $(ALL) .c.o: $(CC) $(CFLAGS) -c $< *.o: topline.h topline: topline.o cpu.o disk.o signals.o $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ clean: rm -f $(ALL) *.o topline-0.3/README.md000066400000000000000000000046021361616635400143220ustar00rootroot00000000000000topline ======= This tool provides a hardcopy (ie, unadorned plain text) graph of per-CPU load, with hyperthread siblings kept together, and NUMA node separation graphically marked. It is optimized for modern many-core processors -- today, servers often have north of 100 CPUs (Xeon Scalable with 48 or 56 per socket, CL-AP up to 112 per socket), and even fat desktops reach 64 threads. It works correctly on machines with only a few CPUs, but the display looks very narrow. Disk load is also shown, as % read/write utilization time. example ======= Start of a kernel compile on a 64-way 4-node box with 4 NVMe disks and one spinner: ``` nvme(⡆⠀⠀⠀)sd(⠀) (⠀⠀⠀⠀⠀⠀⠀⠀≬⠀⠀⠀⠀⠀⠀⠀⠀≬⣀⣀⣀⣀⣀⣀⣀⣀≬⠀⠀⠀⠀⠀⠀⠀⠀) nvme(⡇⠀⠀⠀)sd(⠀) (⣄⣀⣀⣄⣀⣄⣀⣄≬⣀⣀⣀⣀⣄⣄⣠⣠≬⣀⣠⣀⣀⣀⣀⣀⣀≬⣀⣄⣀⣀⣀⣄⣀⣀) nvme(⣇⠀⠀⠀)sd(⠀) (⣀⣀⣀⣀⣀⣀⣀⣀≬⠀⠀⠀⠀⡄⠀⠀⠀≬⣀⠀⠀⠀⠀⠀⠀⠀≬⣀⣀⣀⣀⣀⣀⣀⣀) nvme(⡀⠀⠀⠀)sd(⠀) (⣶⣶⣶⣶⣶⣶⣶⣶≬⣶⣶⣶⣶⣶⣶⣶⣶≬⣶⣶⣶⣶⣶⣶⣶⣶≬⣶⣾⣶⣶⣶⣶⣶⣶) nvme(⣀⠀⠀⠀)sd(⠀) (⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿) nvme(⣀⠀⠀⠀)sd(⠀) (⣿⣿⣿⣷⣿⣿⣾⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿) nvme(⣀⠀⠀⠀)sd(⠀) (⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣷⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿) nvme(⣀⠀⠀⠀)sd(⠀) (⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣷) nvme(⣀⠀⠀⠀)sd(⠀) (⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿) nvme(⣀⠀⠀⠀)sd(⠀) (⣿⣿⣷⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣾⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿) nvme(⣀⠀⠀⠀)sd(⠀) (⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿) nvme(⣀⠀⠀⠀)sd(⠀) (⣿⣿⣿⣿⣿⣿⣿⣿≬⣾⣿⣷⣿⣷⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿≬⣿⣿⣿⣿⣿⣿⣿⣿) ``` Here, after a brief underutilized warm-up bottlenecked on disk read, CPU parallelization becomes near-perfect, while the primary disk works at a small fraction of its bandwidth. topline-0.3/cpu.c000066400000000000000000000077311361616635400140040ustar00rootroot00000000000000#include #include "topline.h" static FILE *psf; static int ncpus, ht; static int cpuorder[MAXCPUS], cpunodes[MAXCPUS]; static struct { unsigned long u; unsigned long s; } prev[MAXCPUS]; void do_cpus() { int cpul[MAXCPUS]; for (int i=0; i= MAXCPUS) continue; unsigned long sum = 0; for (int i=0; ids) die("overflow\n"); cpul[c] = du*RES/(ds?ds:1); /// printf("> %u %u %lu\n", c, cpul[c], ds); } int lastnode=-1; fprintf(log_output, "("); if (ht) { for (int i=0; i #include #include "topline.h" #define NMAJORS 512 typedef struct { unsigned long rd; unsigned long wr; const char *name; int part; } bdevstat_t; static bdevstat_t *bdev[NMAJORS]; static int bdevn[NMAJORS]; static struct timeval t0; static FILE *ds; void init_disks() { ds = fopen("/proc/diskstats", "re"); if (!ds) die("Can't open /proc/diskstats: %m\n"); } static const char *bdprefs[] = { "nvme", "sd", "hd", "mmcblk", "loop", "nbd", "sr", "fd", "md", "dm", // pmem is usually dax, which doesn't update stats here 0 }; static const char *get_name(const char *inst) { for (const char **pref=bdprefs; *pref; pref++) if (!strncmp(inst, *pref, strlen(*pref))) return *pref; return strdup(inst); } void do_disks() { char buf[4096]; rewind(ds); struct timeval t1; gettimeofday(&t1, 0); unsigned int td = (t1.tv_sec-t0.tv_sec)*1000000+t1.tv_usec-t0.tv_usec; if (!td) td = 1; t0 = t1; unsigned int prev_major = -1; while (fgets(buf, sizeof(buf), ds)) { char namebuf[64]; unsigned int major, minor; unsigned long rd, wr; if (sscanf(buf, "%u %u %63s %*u %*u %*u %lu %*u %*u %*u %lu", &major, &minor, namebuf, &rd, &wr) != 5) { die("A line of /proc/diskstats is corrupted: “%s”\n", buf); } if (major>=NMAJORS || minor>NMAJORS) die("Invalid major:minor : %u:%u\n", major, minor); if (!rd && !wr) continue; if (bdevn[major] <= minor) { bdevstat_t *newmem = realloc(bdev[major], sizeof(bdevstat_t)*(minor+1)); if (!newmem) die("realloc failed: %m\n"); memset(newmem+bdevn[major], 0, sizeof(bdevstat_t)*(minor+1-bdevn[major])); bdev[major] = newmem; bdevn[major] = minor+1; } bdevstat_t *bs = &bdev[major][minor]; if (!bs->name) { if (!strncmp(namebuf, "mmcblk", 6) && strstr(namebuf, "boot")) { // Early boot partitions are not marked as such. bs->name = "mmcboot"; bs->part = 1; continue; } if (!strncmp(namebuf, "mtdblock", 8)) { // Raw legacy MTD -- special uses only. bs->name = "mtd"; bs->part = 1; continue; } bs->name=get_name(namebuf); sprintf(namebuf, "/sys/dev/block/%u:%u/partition", major, minor); if (!access(namebuf, F_OK)) { bs->part = 1; continue; } bs->part = 0; } else if (bs->part) continue; int r = ((int64_t)rd-bs->rd)*RES*1000/td; int w = ((int64_t)wr-bs->wr)*RES*1000/td; bs->rd = rd; bs->wr = wr; if (prev_major != major) { fprintf(log_output, prev_major==-1 ? "%s(" : ")%s(", bs->name); prev_major = major; } write_dual(r, w); } if (prev_major!=-1) fprintf(log_output, ") "); } topline-0.3/signals.c000066400000000000000000000030141361616635400146430ustar00rootroot00000000000000#include #include #include "topline.h" static const char* sigobits[NSIG]= { [SIGHUP] = "Hangup", [SIGINT] = "Interrupt", [SIGQUIT] = "Quit", [SIGILL] = "Illegal instruction", [SIGTRAP] = "Breakpoint trap", [SIGABRT] = "Aborted", [SIGBUS] = "Bus error", [SIGFPE] = "Floating point exception", [SIGKILL] = "Killed", [SIGUSR1] = "User signal 1", [SIGSEGV] = "Segmentation fault", [SIGUSR2] = "User signal 2", [SIGPIPE] = "Broken pipe", [SIGALRM] = "Alarm clock", [SIGTERM] = "Terminated", #ifdef SIGSTKFLT [SIGSTKFLT] = "Stack fault on coprocessor", #endif [SIGCHLD] = "Child died", [SIGCONT] = "Continue", [SIGSTOP] = "Stopped", [SIGTSTP] = "Stop request on terminal", [SIGTTIN] = "Stopped on tty input", [SIGTTOU] = "Stopped on tty output", [SIGURG] = "Urgent condition on socket", [SIGXCPU] = "CPU limit exceeded", [SIGXFSZ] = "File size limit exceeded", [SIGVTALRM] = "Virtual alarm clock", [SIGPROF] = "Profiler timer expired", [SIGWINCH] = "Window size changed", [SIGIO] = "I/O ready", [SIGPWR] = "Power loss", [SIGSYS] = "Bad system call", }; void sigobit(int ret) { int core = WCOREDUMP(ret); int s = WTERMSIG(ret); if (s>0 && s ... Runs a program and terminates the graph once the program exits. The graph still exhibits the global state of the system rather than just the program you chose and its children. .P If no program is given, \fBtopline\fR will keep logging forever (ie, until you press ^C or similar). .TP .BR -l ", " --line-output ", " --linearize Marshalls the program's output line-by-line, avoiding mix-ups with \fBtopline\fR's data. They will be interspersed in separate lines. .br The program will know it is being piped; if you want it to believe it's ran on a terminal (to get colors, etc) you may use a tool like \fBpipetty\fR. .TP .BI "-i " Sets the interval between data samples; the default is 1s. Floating-point values are allowed; the number may be suffixed by a "s" (seconds, default), "m" (minutes), "h" (hours), "d" (days), "ms" (milliseconds), "us" or "µs" (microseconds). .TP .BI "-o " "\fR," " " --output " Redirects \fBtopline\fR's output to the given file. The program being ran can then use stdout and stderr unimpeded. .SH CAVEATS If the machine's CPUs are hyperthreaded with more than one or two per core, the graph won't make it obvious which columns share a core. All siblings are still given consecutively, unless forced into separate NUMA nodes with fakenuma settings. .P Machines above 140-150 CPUs may not fit on an 80-column terminal. .SH "SEE ALSO" .BR htop , .BR dstat , .BR VTUNE . topline-0.3/topline.c000066400000000000000000000177001361616635400146640ustar00rootroot00000000000000#define _GNU_SOURCE #include #include #include #include #include "topline.h" FILE* log_output; int read_proc_int(const char *path) { int fd = open(path, O_RDONLY|O_CLOEXEC); if (fd==-1) return -1; char buf[32]; int r = read(fd, buf, sizeof(buf)); if (r<=0 || r>=sizeof(buf)) return close(fd), -1; buf[r]=0; int x = atoi(buf); close(fd); return x; } int read_proc_set(const char *path, set_t *set) { int setl=1; int set_max=-1; int fd = open(path, O_RDONLY|O_CLOEXEC); if (fd==-1) return -1; char buf[16384], *bp=buf; int r = read(fd, buf, sizeof(buf)); if (r<=0 || r>=sizeof(buf)) return close(fd), -1; close(fd); buf[r]=0; do { if (setl >= ARRAYSZ(*set)) return -1; if (*bp<'0' || *bp>'9') return -1; long x = strtol(bp, &bp, 10); if (x<0 || x>=MAXCPUS) return -1; switch (*bp) { case ',': bp++; case 0: case '\n': (*set)[setl].a=(*set)[setl].b=x; setl++; if (x > set_max) set_max=x; break; case '-':; long y = strtol(bp+1, &bp, 10); if (*bp==',') bp++; if (y<0 || y>=MAXCPUS) return -1; (*set)[setl].a=x; (*set)[setl].b=y; setl++; if (y > set_max) set_max=y; break; default: return -1; } } while (*bp && *bp!='\n'); close(fd); SET_CNT(*set)=setl; SET_MAX(*set)=set_max; return 0; } static const char *single[9] = {" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"}; static const uint8_t bx[5] = {0, 0x40, 0x44, 0x46, 0x47}; static const uint8_t by[5] = {0, 0x80, 0xA0, 0xB0, 0xB8}; static inline int step(int x, int ml) { if (x>=RES || x<0) return ml; #define SMIN 5 #define SMAX 900 if (x=SMAX) return ml; return (x-SMIN)*(ml-1)/(SMAX-SMIN)+1; } void write_single(int x) { fprintf(log_output, "%s", single[step(x, 8)]); } void write_dual(int x, int y) { if (x>RES) x=RES; if (y>RES) y=RES; x = step(x, 4); y = step(y, 4); uint8_t ch = bx[x] + by[y]; fprintf(log_output, "\xe2%c%c", (ch>>6)+0xA0, (ch&0x3F)|0x80); } static void do_line(int quiet) { FILE *out; if (quiet) { out = log_output; if (!(log_output = fopen("/dev/null", "w"))) die("Can't open /dev/null: %m\n"); } do_disks(); do_cpus(); if (quiet) { fclose(log_output); log_output = out; return; } fprintf(log_output, "\n"); fflush(log_output); } static struct linebuf { int fd; int len; FILE *destf; char buf[1024]; } linebuf[2]; static void copy_line(struct linebuf *restrict lb) { int len = lb->len; int r = read(lb->fd, lb->buf+len, sizeof(lb->buf)-len); if (r==-1) die("read: %m\n"); else if (!r) return (void)(lb->fd=0); len+=r; char *start=lb->buf, *nl; while ((nl=memchr(start, '\n', len))) { nl++; fwrite(start, 1, nl-start, lb->destf); len-= nl-start; start=nl; } if (len >= sizeof(lb->buf)/2) { // break the overlong line fwrite(start, 1, len, lb->destf); fputc('\n', lb->destf); lb->len=0; } else { memmove(lb->buf, start, len); lb->len=len; } } static volatile int done; static void sigchld(__attribute__((unused)) int dummy) { done = 1; } static int out_lines; static int child_pid; static struct timeval interval={1,0}; static void do_args(char **argv) { argv++; while (*argv && **argv=='-') { if (!strcmp(*argv, "-l") || !strcmp(*argv, "--line-output") || !strcmp(*argv, "--linearize")) { out_lines=1; argv++; continue; } if (!strncmp(*argv, "-i", 2) || !strcmp(*argv, "--interval")) { char *arg = (*argv)[1]=='i' && (*argv)[2] ? *argv+2 : *++argv; if (!arg || !*arg) die("Missing argument to -i\n"); char *rest; double in = strtod(arg, &rest); if (arg == rest) die("Invalid argument to -i 「%s」\n", arg); if (*rest) { if (!strcmp(rest, "s")) ; else if (!strcmp(rest, "m")) in*=60; else if (!strcmp(rest, "h")) in*=60*60; else if (!strcmp(rest, "d")) in*=60*60*24; else if (!strcmp(rest, "w")) in*=60*60*24*7; else if (!strcmp(rest, "ms")) in/=1000; else if (!strcmp(rest, "us") || !strcmp(rest, "µs") || !strcmp(rest, "μs")) in/=1000000; else die("Invalid suffix to -i 「%s」 in 「%s」\n", rest, arg); } int64_t i = in*1000000; if (i<=0) die("Interval in -i must be positive.\n"); interval.tv_sec = i/1000000; interval.tv_usec = i%1000000; argv++; continue; } if (!strncmp(*argv, "-o", 2) || !strcmp(*argv, "--output")) { char *arg = (*argv)[1]=='o' && (*argv)[2] ? *argv+2 : *++argv; if (!arg || !*arg) die("Missing argument to -o\n"); FILE *f = fopen(arg, "we"); if (!f) die("Can't write to 「%s」: %m\n", arg); log_output = f; argv++; continue; } if (!strcmp(*argv, "--")) break; die("Unknown option: '%s'\n", *argv); } if (out_lines && !*argv) die("-l given but no program to run.\n"); // -l and -o together are of little use, but as programs behave differently // when piped, not outright useless. if (*argv) { int s[2], e[2]; if (out_lines && (pipe2(s, O_CLOEXEC) || pipe2(e, O_CLOEXEC))) die("pipe2: %m\n"); if ((child_pid=fork()) < 0) die("fork: %m\n"); if (!child_pid) { if (out_lines && (dup2(s[1], 1)==-1 || dup2(e[1], 2)==-1)) die("dup2: %m\n"); execvp(*argv, argv); die("Couldn't run 「%s」: %m\n", *argv); } if (out_lines) { close(s[1]); close(e[1]); linebuf[0].fd = s[0]; linebuf[0].destf=stdout; linebuf[1].fd = e[0]; linebuf[1].destf=stderr; } } } int main(int argc, char **argv) { log_output = stdout; init_cpus(); init_disks(); signal(SIGCHLD, sigchld); do_args(argv); do_line(1); struct timeval delay=interval; while (!done) { if (!delay.tv_sec && !delay.tv_usec) { do_line(0); delay = interval; } int fds=0; #define NFDS (sizeof(fds)*8) for (int i=0; i #include #include #include #include #define RES 1000 #define MAXCPUS 4096 #define die(...) do {fprintf(stderr, __VA_ARGS__); exit(1);} while(0) #define ARRAYSZ(x) (sizeof(x)/sizeof(x[0])) typedef struct set { int a; int b; } set_t[256]; #define SET_CNT(s) ((s)[0].a) #define SET_MAX(s) ((s)[0].b) #define SET_ITER(i,s) \ for (int e##__LINE_=1; e##__LINE_