cosmopolitan/tool/viz/memzoom.c

934 lines
21 KiB
C

/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2020 Justine Alexandra Roberts Tunney │
│ │
│ This program is free software; you can redistribute it and/or modify │
│ it under the terms of the GNU General Public License as published by │
│ the Free Software Foundation; version 2 of the License. │
│ │
│ This program is distributed in the hope that it will be useful, but │
│ WITHOUT ANY WARRANTY; without even the implied warranty of │
│ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU │
│ General Public License for more details. │
│ │
│ You should have received a copy of the GNU General Public License │
│ along with this program; if not, write to the Free Software │
│ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA │
│ 02110-1301 USA │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "dsp/scale/cdecimate2xuint8x8.h"
#include "libc/bits/bits.h"
#include "libc/bits/hilbert.h"
#include "libc/bits/morton.h"
#include "libc/bits/safemacros.h"
#include "libc/calls/calls.h"
#include "libc/calls/ioctl.h"
#include "libc/calls/struct/sigaction.h"
#include "libc/calls/struct/siginfo.h"
#include "libc/calls/struct/stat.h"
#include "libc/calls/struct/termios.h"
#include "libc/calls/struct/winsize.h"
#include "libc/calls/ucontext.h"
#include "libc/conv/conv.h"
#include "libc/conv/itoa.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/log/log.h"
#include "libc/macros.h"
#include "libc/nexgen32e/bsf.h"
#include "libc/runtime/runtime.h"
#include "libc/sock/sock.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/str/tpenc.h"
#include "libc/sysv/consts/ex.h"
#include "libc/sysv/consts/exit.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/poll.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/consts/termios.h"
#include "libc/time/time.h"
#include "libc/unicode/unicode.h"
#include "third_party/getopt/getopt.h"
#define USAGE \
" [-hznmH] [-p PID] [PATH]\n\
\n\
DESCRIPTION\n\
\n\
Memory Viewer\n\
\n\
FLAGS\n\
\n\
-h help\n\
-z zoom\n\
-w white bg color\n\
-m morton ordering\n\
-H hilbert ordering\n\
-n natural scrolling\n\
-p PID shows process virtual memory\n\
-f INT frames per second [default 10]\n\
\n\
SHORTCUTS\n\
\n\
z or + zoom\n\
Z or - unzoom\n\
ctrl+wheel zoom point\n\
wheel scroll\n\
l linearize\n\
m mortonize\n\
h hilbertify\n\
n next mapping\n\
N next mapping ending\n\
p prev mapping\n\
P prev mapping ending\n\
k up\n\
j down\n\
b page up\n\
space page down\n\
g home\n\
G end\n\
q quit\n\
\n"
#define CTRL(C) ((C) ^ 0100)
#define MAXZOOM 14
#define COLOR 253
#define LINEAR 0
#define MORTON 1
#define HILBERT 2
#define INTERRUPTED 0x1
#define RESIZED 0x2
#define MOUSE_LEFT_DOWN 0
#define MOUSE_MIDDLE_DOWN 1
#define MOUSE_RIGHT_DOWN 2
#define MOUSE_LEFT_UP 4
#define MOUSE_MIDDLE_UP 5
#define MOUSE_RIGHT_UP 6
#define MOUSE_LEFT_DRAG 32
#define MOUSE_MIDDLE_DRAG 33
#define MOUSE_RIGHT_DRAG 34
#define MOUSE_WHEEL_UP 64
#define MOUSE_WHEEL_DOWN 65
#define MOUSE_CTRL_WHEEL_UP 80
#define MOUSE_CTRL_WHEEL_DOWN 81
struct Ranges {
long i;
struct Range {
long a;
long b;
} p[512];
};
static const signed char kThePerfectKernel[8] = {-1, -3, 3, 17, 17, 3, -3, -1};
static bool white;
static bool natural;
static bool mousemode;
static int fd;
static int pid;
static int out;
static int fps;
static int zoom;
static int order;
static int action;
static long tyn;
static long txn;
static long size;
static long offset;
static long lowest;
static long highest;
static long canvassize;
static long buffersize;
static long displaysize;
static char *buffer;
static uint8_t *canvas;
static struct stat st;
static struct Ranges ranges;
static struct termios oldterm;
static char path[PATH_MAX];
static char mapspath[PATH_MAX];
static int Write(const char *s) {
return write(out, s, strlen(s));
}
static void HideCursor(void) {
Write("\e[?25l");
}
static void ShowCursor(void) {
Write("\e[?25h");
}
static void EnableMouse(void) {
mousemode = true;
Write("\e[?1000;1002;1015;1006h");
}
static void DisableMouse(void) {
mousemode = false;
Write("\e[?1000;1002;1015;1006l");
}
static void LeaveScreen(void) {
Write("\e[H\e[J");
}
static void GetTtySize(void) {
struct winsize wsize;
wsize.ws_row = tyn + 1;
wsize.ws_col = txn;
getttysize(out, &wsize);
tyn = MAX(2, wsize.ws_row) - 1;
txn = MAX(17, wsize.ws_col) - 16;
tyn = rounddown2pow(tyn);
txn = rounddown2pow(txn);
tyn = MIN(tyn, txn);
}
static void EnableRaw(void) {
struct termios term;
memcpy(&term, &oldterm, sizeof(term));
term.c_cc[VMIN] = 1;
term.c_cc[VTIME] = 1;
term.c_iflag &= ~(INPCK | ISTRIP | PARMRK | INLCR | IGNCR | ICRNL | IXON);
term.c_lflag &= ~(IEXTEN | ICANON | ECHO | ECHONL);
term.c_cflag &= ~(CSIZE | PARENB);
term.c_cflag |= CS8;
term.c_iflag |= IUTF8;
ioctl(out, TCSETS, &term);
}
static void OnExit(void) {
LeaveScreen();
ShowCursor();
DisableMouse();
ioctl(out, TCSETS, &oldterm);
}
static void OnSigInt(int sig, struct siginfo *sa, struct ucontext *uc) {
action |= INTERRUPTED;
}
static void OnSigWinch(int sig, struct siginfo *sa, struct ucontext *uc) {
action |= RESIZED;
}
static void Setup(void) {
tyn = 80;
txn = 24;
action = RESIZED;
ioctl(out, TCGETS, &oldterm);
HideCursor();
EnableRaw();
EnableMouse();
atexit(OnExit);
sigaction(SIGINT, &(struct sigaction){.sa_sigaction = OnSigInt}, NULL);
sigaction(SIGWINCH, &(struct sigaction){.sa_sigaction = OnSigWinch}, NULL);
}
static noreturn void FailPath(const char *s, int rc) {
Write("error: ");
Write(s);
Write(": ");
Write(path);
Write("\n");
exit(rc);
}
static void SetExtent(long lo, long hi) {
lowest = lo;
highest = hi;
offset = MIN(hi, MAX(lo, offset));
}
static void Open(void) {
int err;
if ((fd = open(path, O_RDONLY)) == -1) {
FailPath("open() failed", errno);
}
fstat(fd, &st);
size = st.st_size;
SetExtent(0, size);
}
static void *Allocate(size_t n) {
return mmap(NULL, n, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1,
0);
}
static void SetupCanvas(void) {
if (canvassize) {
munmap(canvas, canvassize);
munmap(buffer, buffersize);
}
displaysize = ROUNDUP(ROUNDUP(tyn * txn * (1ul << zoom), 16), 1ul << zoom);
canvassize = ROUNDUP(displaysize, FRAMESIZE);
buffersize = ROUNDUP(tyn * txn * 16 + PAGESIZE, FRAMESIZE);
canvas = Allocate(canvassize);
buffer = Allocate(buffersize);
}
static long IndexSquare(long y, long x) {
switch (order) {
case LINEAR:
return y * txn + x;
case MORTON:
return morton(y, x);
case HILBERT:
return hilbert(txn, y, x);
default:
return 0;
}
}
static long Index(long y, long x) {
long i;
if (order == LINEAR) {
i = 0;
} else {
i = x / tyn;
x = x % tyn;
}
return i * tyn * tyn + IndexSquare(y, x);
}
static void PreventBufferbloat(void) {
long double now, rate;
static long double last;
now = nowl();
rate = 1. / fps;
if (now - last < rate) {
dsleep(rate - (now - last));
}
last = now;
}
static bool HasPendingInput(void) {
struct pollfd fds[1];
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[0].revents = 0;
poll(fds, ARRAYLEN(fds), 0);
return fds[0].revents & (POLLIN | POLLERR);
}
static int GetCurrentRange(void) {
int i;
if (ranges.i) {
for (i = 0; i < ranges.i; ++i) {
if (offset < ranges.p[i].a) return MAX(0, i - 1);
if (offset < ranges.p[i].b) return i;
}
return ranges.i - 1;
} else {
return -1;
}
}
static void Move(long d) {
offset = MIN(highest, MAX(lowest, ROUNDDOWN(offset + d, 1L << zoom)));
}
static void SetZoom(long y, long x, int d) {
long a, b, i;
if ((0 <= y && y < tyn) && (0 <= x && x < txn)) {
i = Index(y, x);
a = zoom;
b = MIN(MAXZOOM, MAX(0, a + d));
zoom = b;
Move(i * (1L << a) - i * (1L << b));
SetupCanvas();
}
}
static void OnZoom(long y, long x) {
SetZoom(y, x, +1);
}
static void OnUnzoom(long y, long x) {
SetZoom(y, x, -1);
}
static void OnUp(void) {
Move(-(txn * (1l << zoom)));
}
static void OnDown(void) {
Move(txn * (1l << zoom));
}
static void OnPageUp(void) {
Move(-(txn * (tyn - 2) * (1l << zoom)));
}
static void OnPageDown(void) {
Move(txn * (tyn - 2) * (1l << zoom));
}
static void OnHome(void) {
offset = lowest;
}
static void OnEnd(void) {
offset = MAX(lowest, highest - txn * tyn * (1l << zoom));
}
static void OnLinear(void) {
order = LINEAR;
GetTtySize();
SetupCanvas();
}
static void OnMorton(void) {
order = MORTON;
SetupCanvas();
}
static void OnHilbert(void) {
order = HILBERT;
SetupCanvas();
}
static void OnNext(void) {
int i;
if ((i = GetCurrentRange()) != -1) {
if (i + 1 < ranges.i) {
offset = ranges.p[i + 1].a;
}
}
}
static void OnPrev(void) {
int i;
if ((i = GetCurrentRange()) != -1) {
if (i) {
offset = ranges.p[i - 1].a;
}
}
}
static void OnNextEnd(void) {
long i, n;
if ((i = GetCurrentRange()) != -1) {
n = tyn * txn * (1L << zoom);
if (offset < ranges.p[i].b - n) {
offset = ranges.p[i].b - n;
} else if (i + 1 < ranges.i) {
offset = MAX(ranges.p[i + 1].a, ranges.p[i + 1].b - n);
}
}
}
static void OnPrevEnd(void) {
long i, n;
if ((i = GetCurrentRange()) != -1) {
n = tyn * txn * (1L << zoom);
if (i) {
offset = MAX(ranges.p[i - 1].a, ranges.p[i - 1].b - n);
}
}
}
static void OnMouse(char *p) {
int e, x, y;
e = strtol(p, &p, 10);
if (*p == ';') ++p;
x = min(txn, max(1, strtol(p, &p, 10))) - 1;
if (*p == ';') ++p;
y = min(tyn, max(1, strtol(p, &p, 10))) - 1;
e |= (*p == 'm') << 2;
switch (e) {
case MOUSE_WHEEL_UP:
if (natural) {
OnDown();
OnDown();
OnDown();
} else {
OnUp();
OnUp();
OnUp();
}
break;
case MOUSE_WHEEL_DOWN:
if (natural) {
OnUp();
OnUp();
OnUp();
} else {
OnDown();
OnDown();
OnDown();
}
break;
case MOUSE_CTRL_WHEEL_UP:
if (natural) {
OnZoom(y, x);
} else {
OnUnzoom(y, x);
}
break;
case MOUSE_CTRL_WHEEL_DOWN:
if (natural) {
OnUnzoom(y, x);
} else {
OnZoom(y, x);
}
break;
default:
break;
}
}
static void ReadKeyboard(void) {
char buf[32], *p = buf;
memset(buf, 0, sizeof(buf));
if (readansi(0, buf, sizeof(buf)) == -1) {
if (errno == EINTR) return;
exit(errno);
}
switch (*p++) {
case 'q':
exit(0);
case '+':
case 'z':
OnZoom(0, 0);
break;
case '-':
case 'Z':
OnUnzoom(0, 0);
break;
case 'b':
OnPageUp();
break;
case 'n':
OnNext();
break;
case 'p':
OnPrev();
break;
case 'N':
OnNextEnd();
break;
case 'P':
OnPrevEnd();
break;
case ' ':
case CTRL('V'):
OnPageDown();
break;
case 'g':
OnHome();
break;
case 'G':
OnEnd();
break;
case 'k':
case CTRL('P'):
OnUp();
break;
case 'j':
case CTRL('N'):
OnDown();
break;
case 'l':
OnLinear();
break;
case 'm':
if (order == MORTON) {
OnLinear();
} else {
OnMorton();
}
break;
case 'M':
if (mousemode) {
DisableMouse();
} else {
EnableMouse();
}
break;
case 'h':
case 'H':
if (order == HILBERT) {
OnLinear();
} else {
OnHilbert();
}
break;
case '\e':
switch (*p++) {
case 'v':
OnPageUp();
break;
case '[':
switch (*p++) {
case '<':
OnMouse(p);
break;
case 'A':
OnUp();
break;
case 'B':
OnDown();
break;
case 'F':
OnEnd();
break;
case 'H':
OnHome();
break;
case '1':
switch (*p++) {
case '~':
OnHome();
break;
default:
break;
}
break;
case '4':
switch (*p++) {
case '~':
OnEnd();
break;
default:
break;
}
break;
case '5':
switch (*p++) {
case '~':
OnPageUp();
break;
default:
break;
}
break;
case '6':
switch (*p++) {
case '~':
OnPageDown();
break;
default:
break;
}
break;
case '7':
switch (*p++) {
case '~':
OnHome();
break;
default:
break;
}
break;
case '8':
switch (*p++) {
case '~':
OnEnd();
break;
default:
break;
}
break;
default:
break;
}
break;
default:
break;
}
break;
default:
break;
}
}
static void LoadRanges(void) {
char b[512];
struct Range range;
int i, t, n, fd, err;
if ((fd = open(mapspath, O_RDONLY)) == -1) {
err = errno;
Write("error: process died\n");
exit(err);
}
t = 0;
range.a = 0;
range.b = 0;
ranges.i = 0;
for (;;) {
if ((n = read(fd, b, sizeof(b))) == -1) exit(1);
if (!n) break;
for (i = 0; i < n; ++i) {
switch (t) {
case 0:
if (isxdigit(b[i])) {
range.a <<= 4;
range.a += hextoint(b[i]);
} else if (b[i] == '-') {
t = 1;
}
break;
case 1:
if (isxdigit(b[i])) {
range.b <<= 4;
range.b += hextoint(b[i]);
} else if (b[i] == ' ') {
t = 2;
}
break;
case 2:
if (b[i] == '\n') {
if (ranges.i < ARRAYLEN(ranges.p)) {
ranges.p[ranges.i++] = range;
}
range.a = 0;
range.b = 0;
t = 0;
}
break;
default:
unreachable;
}
}
}
close(fd);
if (ranges.i) {
SetExtent(ranges.p[0].a, ranges.p[ranges.i - 1].b);
} else {
SetExtent(0, 0);
}
}
static int InvertXtermGreyscale(int x) {
return -(x - 232) + 255;
}
static void Render(void) {
char *p;
int c, fg2, rc, fg;
long i, y, x, w, n, got;
p = buffer;
p = stpcpy(p, "\e[H");
for (y = 0; y < tyn; ++y) {
fg = -1;
for (x = 0; x < txn; ++x) {
c = canvas[Index(y, x)];
if (c < 32) {
fg2 = 237 + c * ((COLOR - 237) / 32.);
} else if (c >= 232) {
fg2 = COLOR + (c - 232) * ((255 - COLOR) / (256. - 232));
} else {
fg2 = COLOR;
}
if (fg2 != fg) {
fg = fg2;
if (white) {
fg = InvertXtermGreyscale(fg);
}
p = stpcpy(p, "\e[38;5;");
p += int64toarray_radix10(fg, p);
*p++ = 'm';
}
w = tpenc(kCp437[c]);
do {
*p++ = w & 0xff;
w >>= 8;
} while (w);
}
p = stpcpy(p, "\e[0m ");
p += uint64toarray_radix16(offset + y * txn * (1ul << zoom), p);
p = stpcpy(p, "\e[K\r\n");
}
p = stpcpy(p, "\e[7m\e[K");
n = strlen(path);
if (n > txn - 3 - 1 - 7) {
p = mempcpy(p, path, txn - 1 - 7 - 3);
p = stpcpy(p, "...");
} else {
p = stpcpy(p, path);
for (i = n; i < txn - 1 - 7; ++i) {
*p++ = ' ';
}
}
p = stpcpy(p, " memzoom\e[0m ");
if (!pid) {
p += uint64toarray_radix10(MIN(offset / (long double)size * 100, 100), p);
p = stpcpy(p, "%-");
p += uint64toarray_radix10(
MIN((offset + tyn * txn * (1l << zoom)) / (long double)size * 100, 100),
p);
p = stpcpy(p, "% ");
}
p += uint64toarray_radix10(1L << zoom, p);
p = stpcpy(p, "x\e[J");
PreventBufferbloat();
for (i = 0, n = p - buffer; i < n; i += got) {
got = 0;
if ((rc = write(out, buffer + i, n - i)) == -1) {
if (errno == EINTR) continue;
exit(errno);
}
got = rc;
}
}
static void Zoom(long have) {
long i, n, r;
n = canvassize;
for (i = 0; i < zoom; ++i) {
cDecimate2xUint8x8(n, canvas, kThePerfectKernel);
n >>= 1;
}
if (n < tyn * txn) {
memset(canvas + n, 0, canvassize - n);
}
if (have != -1) {
n = have / (1L << zoom);
i = n / txn;
r = n % txn;
if (r) ++i;
if (order == LINEAR) {
for (; i < tyn; ++i) {
canvas[txn * i] = '~';
}
}
}
}
static void FileZoom(void) {
long have;
have = MIN(displaysize, size - offset);
have = pread(fd, canvas, have, offset);
have = MAX(0, have);
memset(canvas + have, 0, canvassize - have);
Zoom(have);
Render();
}
static void RangesZoom(void) {
long a, b, c, d, i;
LoadRanges();
memset(canvas, 1, canvassize);
a = offset;
b = MIN(highest, offset + tyn * txn * (1ul << zoom));
for (i = 0; i < ranges.i; ++i) {
if ((a >= ranges.p[i].a && a < ranges.p[i].b) ||
(b >= ranges.p[i].a && b < ranges.p[i].b) ||
(a < ranges.p[i].a && b >= ranges.p[i].b)) {
c = MAX(a, ranges.p[i].a);
d = MIN(b, ranges.p[i].b);
pread(fd, canvas + (c - offset), d - c, c);
}
}
Zoom(-1);
Render();
}
static void MemZoom(void) {
do {
if (action & RESIZED) {
GetTtySize();
SetupCanvas();
action &= ~RESIZED;
}
if (HasPendingInput()) {
ReadKeyboard();
continue;
}
if (pid) {
RangesZoom();
} else {
FileZoom();
}
} while (!(action & INTERRUPTED));
}
static noreturn void PrintUsage(int rc) {
Write("SYNOPSIS\n\n ");
Write(program_invocation_name);
Write(USAGE);
exit(rc);
}
static void GetOpts(int argc, char *argv[]) {
int opt;
char *p;
fps = 10;
while ((opt = getopt(argc, argv, "hzHnwf:p:")) != -1) {
switch (opt) {
case 'z':
++zoom;
break;
case 'n':
natural = true;
break;
case 'm':
order = MORTON;
break;
case 'H':
order = HILBERT;
break;
case 'w':
white = true;
break;
case 'f':
fps = strtol(optarg, NULL, 0);
fps = MAX(1, fps);
break;
case 'p':
if (strcmp(optarg, "self") == 0) {
pid = getpid();
} else {
pid = strtol(optarg, NULL, 0);
}
break;
case 'h':
PrintUsage(EXIT_SUCCESS);
default:
PrintUsage(EX_USAGE);
}
}
if (pid) {
p = stpcpy(path, "/proc/");
p += int64toarray_radix10(pid, p);
stpcpy(p, "/mem");
p = stpcpy(mapspath, "/proc/");
p += int64toarray_radix10(pid, p);
stpcpy(p, "/maps");
} else {
if (optind == argc) {
PrintUsage(EX_USAGE);
}
if (!memccpy(path, argv[optind], '\0', sizeof(path))) {
PrintUsage(EX_USAGE);
}
}
}
int main(int argc, char *argv[]) {
if (!NoDebug()) showcrashreports();
out = 1;
GetOpts(argc, argv);
Open();
Setup();
MemZoom();
return 0;
}