pax_global_header00006660000000000000000000000064145732023650014521gustar00rootroot0000000000000052 comment=e74e4478dbf1de6a51d8b8abcc7b4302043f4465 topline-0.6/000077500000000000000000000000001457320236500130405ustar00rootroot00000000000000topline-0.6/.gitignore000066400000000000000000000000171457320236500150260ustar00rootroot00000000000000topline *.o *~ topline-0.6/LICENSE000066400000000000000000000004201457320236500140410ustar00rootroot00000000000000ⓒ 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.6/Makefile000066400000000000000000000003441457320236500145010ustar00rootroot00000000000000ALL=topline CC=gcc CFLAGS=-Wall -Og -g all: $(ALL) .c.o: $(CC) $(CFLAGS) $(CPPFLAGS) -c $< *.o: topline.h topline: topline.o cpu.o disk.o signals.o $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ $^ clean: rm -f $(ALL) *.o topline-0.6/README.md000066400000000000000000000046021457320236500143210ustar00rootroot00000000000000topline ======= 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.6/cpu.c000066400000000000000000000077311457320236500140030ustar00rootroot00000000000000#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" typedef struct { unsigned long rd; unsigned long wr; const char *name; int minor; short major; char part; } bdevstat_t; static bdevstat_t bdev[4096]; static struct timespec 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 timespec t1; if (clock_gettime(CLOCK_MONOTONIC, &t1)) die("Broken clock: %m\n"); int64_t td = (t1.tv_sec-t0.tv_sec)*NANO + t1.tv_nsec-t0.tv_nsec; if (!td) td = 1; t0 = t1; unsigned int prev_major = -1; bdevstat_t *bs = &bdev[-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 > 65535) die("Invalid major:minor : %u:%u\n", major, minor); if (!rd && !wr) continue; #define BDEND &bdev[ARRAYSZ(bdev)] if (bs < BDEND && bs[1].major==major && bs[1].minor==minor) bs++; else for (bs = bdev; bs < BDEND; bs++) if ((bs->major==major && bs->minor==minor) || !bs->name) break; if (bs >= BDEND) die("Too many block devices.\n"); if (!bs->name) { bs->major = major; bs->minor = minor; 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*1000000/td; int w = ((int64_t)wr-bs->wr)*RES*1000000/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.6/signals.c000066400000000000000000000030141457320236500146420ustar00rootroot00000000000000#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. .TP .BR -d ", " --dump-after ", " --delay-dump Suppresses the graph output until after the program exits, then dumps the logged graph all at once to stderr. Good for non-interactive builds. NB. ^C assumes you want to abort both the program and \fBtopline\fR, please kill the former some other way if that is not the case. .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 circa 140 CPUs may not fit on an 80-column terminal. .P All utilization figures are \fBglobal\fR to the machine even if caused by something else than the program you run. .SH "SEE ALSO" .BR htop , .BR dstat , .BR VTUNE . topline-0.6/topline.c000066400000000000000000000230071457320236500146600ustar00rootroot00000000000000#define _GNU_SOURCE #include #include #include #include #include #include #include "topline.h" #define NFDS (sizeof(long)*8) 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; // Both top and bottom ranges are rigged: // • anything above 0.5% means the system is not quiescent // • 90% is the cut-off for the last notch #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 int dump_after; 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 (!strncmp(*argv, "-d", 2) || !strcmp(*argv, "--dump-after") || !strcmp(*argv, "--delay-dump")) { FILE *f = 0; int fd = open("/tmp", O_CREAT|O_TMPFILE|O_CLOEXEC|O_RDWR, 0600); if (fd != -1) // not all filesystems implement O_TMPFILE f = fdopen(fd, "w+"); if (!f) f = tmpfile(); if (!f) die("Creating tmpfile failed: %m\n"); if (fd == -1) fcntl(fileno(f), F_SETFL, O_CLOEXEC); log_output = f; dump_after = 1; 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 (dump_after && !*argv) die("-d given but no program to run.\n"); if (*argv) { int s[2], e[2]; if (out_lines) { if (pipe2(s, O_CLOEXEC) || pipe2(e, O_CLOEXEC)) die("pipe2: %m\n"); if (s[1] >= NFDS) die("bogus fd %d", s[1]); if (e[1] >= NFDS) die("bogus fd %d", e[1]); } 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; } } } static void do_dump() { char buf[65536]; size_t r; rewind(log_output); while ((r = fread(buf, 1, sizeof buf, log_output))) if (fwrite(buf, 1, r, stderr) < r) return; // if stderr is not available, we'd report error... where? if (ferror(log_output)) die("Error while dumping graph log: %m\n"); } 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; } long fds=0; static_assert(sizeof(fds)*8 >= NFDS, "FDS!=long"); for (int i=0; i #include #include #include #include #define RES 1000 #define MAXCPUS 4096 #define NANO 1000000000LL #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_