Plan 9 from Bell Labs’s /usr/web/sources/patch/sorry/font-updates/trace.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#include <u.h>
#include <tos.h>
#include <libc.h>
#include <thread.h>
#include <ip.h>
#include <bio.h>
#include <draw.h>
#include <mouse.h>
#include <cursor.h>
#include <keyboard.h>
#include "trace.h"

#pragma	varargck	type	"t"		vlong
#pragma	varargck	type	"U"		uvlong

#define NS(x)	((vlong)x)
#define US(x)	(NS(x) * 1000ULL)
#define MS(x)	(US(x) * 1000ULL)
#define S(x)	(MS(x) * 1000ULL)

#define numblocks(a, b)	(((a) + (b) - 1) / (b))
#define roundup(a, b)	(numblocks((a), (b)) * (b))

enum {
	OneRound = MS(1)/2LL,
	MilliRound = US(1)/2LL,
};

typedef struct Event	Event;
typedef struct Task	Task;
struct Event {
	Traceevent;
	vlong	etime;	/* length of block to draw */
};

struct Task {
	int	pid;
	char	*name;
	int	nevents;	
	Event	*events;
	vlong	tstart;
	vlong	total;
	vlong	runtime;
	vlong	runmax;
	vlong	runthis;
	long	runs;
	ulong	tevents[Nevent];
};

enum {
	Nevents = 1024,
	Ncolor = 6,
	K = 1024,
};

vlong	now, prevts;

int	newwin;
int	Width = 1000;		
int	Height = 100;		// Per task
int	topmargin = 8;
int	bottommargin = 4;
int	lineht = 12;
int	wctlfd;
int	nevents;
Traceevent *eventbuf;
Event	*event;

void drawtrace(void);
int schedparse(char*, char*, char*);
int timeconv(Fmt*);

char *schedstatename[] = {
	[SAdmit] =	"Admit",
	[SSleep] =	"Sleep",
	[SDead] =	"Dead",
	[SDeadline] =	"Deadline",
	[SEdf] =	"Edf",
	[SExpel] =	"Expel",
	[SReady] =	"Ready",
	[SRelease] =	"Release",
	[SRun] =	"Run",
	[SSlice] =	"Slice",
	[SInts] =	"Ints",
	[SInte] =	"Inte",
	[SUser] = 	"User",
	[SYield] =	"Yield",
};

struct {
	vlong	scale;
	vlong	bigtics;
	vlong	littletics;
	int	sleep;
} scales[] = {
	{	US(500),	US(100),	US(50),		  0},
	{	US(1000),	US(500),	US(100),	  0},
	{	US(2000),	US(1000),	US(200),	  0},
	{	US(5000),	US(1000),	US(500),	  0},
	{	MS(10),		MS(5),		MS(1),		 20},
	{	MS(20),		MS(10),		MS(2),		 20},
	{	MS(50),		MS(10),		MS(5),		 20},
	{	MS(100),	MS(50),		MS(10),		 20},	/* starting scaleno */
	{	MS(200),	MS(100),	MS(20),		 20},
	{	MS(500),	MS(100),	MS(50),		 50},
	{	MS(1000),	MS(500),	MS(100),	100},
	{	MS(2000),	MS(1000),	MS(200),	100},
	{	MS(5000),	MS(1000),	MS(500),	100},
	{	S(10),		S(50),		S(1),		100},
	{	S(20),		S(10),		S(2),		100},
	{	S(50),		S(10),		S(5),		100},
	{	S(100),		S(50),		S(10),		100},
	{	S(200),		S(100),		S(20),		100},
	{	S(500),		S(100),		S(50),		100},
	{	S(1000),	S(500),		S(100),		100},
};

int ntasks, verbose, triggerproc, paused;
Task *tasks;
Image *cols[Ncolor][4];
Font *mediumfont, *tinyfont;
char *mediumfontname, *tinyfontname;
Image *grey, *red, *green, *blue, *bg, *fg;
char*profdev = "/proc/trace";

static void
usage(void)
{
	fprint(2, "Usage: %s [-f tinyfont] [-F mediumfont] [-d profdev] [-w] [-v] [-t triggerproc] [processes]\n", argv0);
	exits(nil);
}

void
threadmain(int argc, char **argv)
{
	int fd, i;
	char fname[80];

	fmtinstall('t', timeconv);
	ARGBEGIN {
	case 'd':
		profdev = EARGF(usage());
		break;
	case 'F':
		mediumfontname = ARGF();
		if(mediumfontname == nil)
			usage();
		break;
	case 'f':
		tinyfontname = ARGF();
		if(tinyfontname == nil)
			usage();
		break;
	case 'v':
		verbose = 1;
		break;
	case 'w':
		newwin++;
		break;
	case 't':
		triggerproc = (int)strtol(EARGF(usage()), nil, 0);
		break;
	default:
		usage();
	}
	ARGEND;

	if(mediumfontname == nil)
		mediumfontname = getenv("mediumfont");
	if(mediumfontname == nil)
		mediumfontname = "/lib/font/bit/lucidasans/unicode.10.font";
	if(tinyfontname == nil)
		tinyfontname = getenv("tinyfont");
	if(tinyfontname == nil)
		tinyfontname = "/lib/font/bit/lucidasans/unicode.7.font";

	fname[sizeof fname - 1] = 0;
	for(i = 0; i < argc; i++){
		snprint(fname, sizeof fname - 2, "/proc/%s/ctl", 
					argv[i]);
		if((fd = open(fname, OWRITE)) < 0){
			fprint(2, "%s: cannot open %s: %r\n",
						argv[0], fname);
			continue;
		}

		if(fprint(fd, "trace 1") < 0)
			fprint(2, "%s: cannot enable tracing on %s: %r\n",
						argv[0], fname);
		close(fd);
	}

	drawtrace();
}

static void
mkcol(int i, int c0, int c1, int c2)
{
	cols[i][0] = allocimagemix(display, c0, DWhite);
	cols[i][1] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, c1);
	cols[i][2] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, c2);
	cols[i][3] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, c0);
}

static void
colinit(void)
{
	mediumfont = openfont(display, mediumfontname);
	if(mediumfont == nil)
		mediumfont = font;
	tinyfont = openfont(display, tinyfontname);
	if(tinyfont == nil)
		tinyfont = font;
	topmargin = mediumfont->height+2;
	bottommargin = tinyfont->height+2;

	/* Peach */
	mkcol(0, 0xFFAAAAFF, 0xFFAAAAFF, 0xBB5D5DFF);
	/* Aqua */
	mkcol(1, DPalebluegreen, DPalegreygreen, DPurpleblue);
	/* Yellow */
	mkcol(2, DPaleyellow, DDarkyellow, DYellowgreen);
	/* Green */
	mkcol(3, DPalegreen, DMedgreen, DDarkgreen);
	/* Blue */
	mkcol(4, 0x00AAFFFF, 0x00AAFFFF, 0x0088CCFF);
	/* Grey */
	cols[5][0] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xEEEEEEFF);
	cols[5][1] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xCCCCCCFF);
	cols[5][2] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x888888FF);
	cols[5][3] = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xAAAAAAFF);
	grey = cols[5][2];
	red = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xFF0000FF);
	green = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x00FF00FF);
	blue = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x0000FFFF);
	bg = display->white;
	fg = display->black;
}

static void
redraw(int scaleno)
{
	int n, i, j, x;
	char buf[256];
	Point p, q;
	Rectangle r, rtime;
	Task *t;
	vlong ts, oldestts, newestts, period, ppp, scale, s, ss;

#	define time2x(t)	((int)(((t) - oldestts) / ppp))

	scale = scales[scaleno].scale;
	period = scale + scales[scaleno].littletics;
	ppp = period / Width;	// period per pixel.

	/* Round `now' to a nice number */
	newestts = now - (now % scales[scaleno].bigtics) + 
			(scales[scaleno].littletics>>1);

	oldestts = newestts - period;

//print("newestts %t, period %t, %d-%d\n", newestts, period, time2x(oldestts), time2x(newestts));
	if (prevts < oldestts){
		oldestts = newestts - period;

		prevts = oldestts;
		draw(screen, screen->r, bg, nil, ZP);
	}else{
		/* just white out time */
		rtime = screen->r;
		rtime.min.x = rtime.max.x - stringwidth(mediumfont, "00000000000.000s");
		rtime.max.y = rtime.min.y + mediumfont->height;
		draw(screen, rtime, bg, nil, ZP);
	}
	p = screen->r.min;
	for (n = 0; n != ntasks; n++) {
		t = &tasks[n];
		/* p is upper left corner for this task */
		rtime = Rpt(p, addpt(p, Pt(500, mediumfont->height)));
		draw(screen, rtime, bg, nil, ZP);
		snprint(buf, sizeof(buf), "%d %s", t->pid, t->name);
		q = string(screen, p, fg, ZP, mediumfont, buf);
		s = now - t->tstart;
		if(t->tevents[SRelease])
			snprint(buf, sizeof(buf), " per %t — avg: %t max: %t",
				(vlong)(s/t->tevents[SRelease]),
				(vlong)(t->runtime/t->tevents[SRelease]),
				t->runmax);
		else if((s /=1000000000LL) != 0)
			snprint(buf, sizeof(buf), " per 1s — avg: %t total: %t",
				t->total/s,
				t->total);
		else
			snprint(buf, sizeof(buf), " total: %t", t->total);
		string(screen, q, fg, ZP, tinyfont, buf);
		p.y += Height;
	}
	x = time2x(prevts);

	p = screen->r.min;
	for (n = 0; n != ntasks; n++) {
		t = &tasks[n];

		/* p is upper left corner for this task */

		/* Move part already drawn */
		r = Rect(p.x, p.y + topmargin, p.x + x, p.y+Height);
		draw(screen, r, screen, nil, Pt(p.x + Width - x, p.y + topmargin));

		r.max.x = screen->r.max.x;
		r.min.x += x;
		draw(screen, r, bg, nil, ZP);

		line(screen, addpt(p, Pt(x, Height - lineht)), Pt(screen->r.max.x, p.y + Height - lineht),
			Endsquare, Endsquare, 0, cols[n % Ncolor][1], ZP);

		for (i = 0; i < t->nevents-1; i++)
			if (prevts < t->events[i + 1].time)
				break;
			
		if (i > 0) {
			memmove(t->events, t->events + i, (t->nevents - i) * sizeof(Event));
			t->nevents -= i;
		}

		for (i = 0; i != t->nevents; i++) {
			Event *e = &t->events[i], *_e;
			int sx, ex;

			switch (e->etype & 0xffff) {
			case SAdmit:
				if (e->time > prevts && e->time <= newestts) {
					sx = time2x(e->time);
					line(screen, addpt(p, Pt(sx, topmargin)), 
						addpt(p, Pt(sx, Height - bottommargin)), 
						Endarrow, Endsquare, 1, green, ZP);
				}
				break;
			case SExpel:
				if (e->time > prevts && e->time <= newestts) {
					sx = time2x(e->time);
					line(screen, addpt(p, Pt(sx, topmargin)), 
						addpt(p, Pt(sx, Height - bottommargin)), 
						Endsquare, Endarrow, 1, red, ZP);
				}
				break;
			case SRelease:
				if (e->time > prevts && e->time <= newestts) {
					sx = time2x(e->time);
					line(screen, addpt(p, Pt(sx, topmargin)), 
						addpt(p, Pt(sx, Height - bottommargin)), 
						Endarrow, Endsquare, 1, fg, ZP);
				}
				break;
			case SDeadline:
				if (e->time > prevts && e->time <= newestts) {
					sx = time2x(e->time);
					line(screen, addpt(p, Pt(sx, topmargin)), 
						addpt(p, Pt(sx, Height - bottommargin)), 
						Endsquare, Endarrow, 1, fg, ZP);
				}
				break;

			case SYield:
			case SUser:
				if (e->time > prevts && e->time <= newestts) {
					sx = time2x(e->time);
					line(screen, addpt(p, Pt(sx, topmargin)), 
						addpt(p, Pt(sx, Height - bottommargin)), 
						Endsquare, Endarrow, 0, 
						(e->etype == SYield)? green: blue, ZP);
				}
				break;
			case SSlice:
				if (e->time > prevts && e->time <= newestts) {
					sx = time2x(e->time);
					line(screen, addpt(p, Pt(sx, topmargin)), 
						addpt(p, Pt(sx, Height - bottommargin)), 
						Endsquare, Endarrow, 0, red, ZP);
				}
				break;

			case SRun:
			case SEdf:
				sx = time2x(e->time);
				ex = time2x(e->etime);
				if(ex == sx)
					ex++;

				r = Rect(sx, topmargin + 8, ex, Height - lineht);
				r = rectaddpt(r, p);

				draw(screen, r, cols[n % Ncolor][e->etype==SRun?1:3], nil, ZP);

				if(t->pid == triggerproc && ex < Width)
					paused ^= 1;

				for(j = 0; j < t->nevents; j++){
					_e = &t->events[j];
					switch(_e->etype & 0xffff){
					case SInts:
						if (_e->time > prevts && _e->time <= newestts){
							sx = time2x(_e->time);
							line(screen, addpt(p, Pt(sx, topmargin)), 
												addpt(p, Pt(sx, Height / 2 - bottommargin)), 	
												Endsquare, Endsquare, 0, 
												green, ZP);
						}
						break;
					case SInte:
						if (_e->time > prevts && _e->time <= newestts) {
							sx = time2x(_e->time);
							line(screen, addpt(p, Pt(sx, Height / 2 - bottommargin)), 
												addpt(p, Pt(sx, Height - bottommargin)), 
												Endsquare, Endsquare, 0, 
												blue, ZP);
						}
						break;
					}
				}
				break;
			}
		}
		p.y += Height;
	}

	ts = prevts + scales[scaleno].littletics - (prevts % scales[scaleno].littletics);
	x = time2x(ts);

	while(x < Width){
		p = screen->r.min;
		for(n = 0; n < ntasks; n++){
			int height, width;

			/* p is upper left corner for this task */
			if ((ts % scales[scaleno].scale) == 0){
				height = 10 * Height;
				width = 1;
			}else if ((ts % scales[scaleno].bigtics) == 0){
				height = 12 * Height;
				width = 0;
			}else{
				height = 13 * Height;
				width = 0;
			}
			height >>= 4;

			line(screen, addpt(p, Pt(x, height)), addpt(p, Pt(x, Height - lineht)),
				Endsquare, Endsquare, width, cols[n % Ncolor][2], ZP);

			p.y += Height;
		}
		ts += scales[scaleno].littletics;
		x = time2x(ts);
	}

	rtime = screen->r;
	rtime.min.y = rtime.max.y - tinyfont->height + 2;
	draw(screen, rtime, bg, nil, ZP);
	ts = oldestts + scales[scaleno].bigtics - (oldestts % scales[scaleno].bigtics);
	x = time2x(ts);
	ss = 0;
	while(x < Width){
		snprint(buf, sizeof(buf), "%t", ss);
		string(screen, addpt(p, Pt(x - stringwidth(tinyfont, buf)/2, - tinyfont->height - 1)), 
			fg, ZP, tinyfont, buf);
		ts += scales[scaleno].bigtics;
		ss += scales[scaleno].bigtics;
		x = time2x(ts);
	}

	snprint(buf, sizeof(buf), "%t", now);
	string(screen, Pt(screen->r.max.x - stringwidth(mediumfont, buf), screen->r.min.y), 
		fg, ZP, mediumfont, buf);
	
	flushimage(display, 1);
	prevts = newestts;
}

Task*
newtask(ulong pid)
{
	Task *t;
	char buf[64], *p;
	int fd,n;

	tasks = realloc(tasks, (ntasks + 1) * sizeof(Task));
	assert(tasks);

	t = &tasks[ntasks++];
	memset(t, 0, sizeof(Task));
	t->events = nil;
	snprint(buf, sizeof buf, "/proc/%ld/status", pid);
	t->name = nil;
	fd = open(buf, OREAD);
	if (fd >= 0){
		n = read(fd, buf, sizeof buf);
		if(n > 0){
			p = buf + sizeof buf - 1;
			*p = 0;
			p = strchr(buf, ' ');
			if (p) *p = 0;
			t->name = strdup(buf);
		}else
			print("%s: %r\n", buf);
		close(fd);
	}else
		print("%s: %r\n", buf);
	t->pid = pid;
	prevts = 0;
	if (newwin){
		fprint(wctlfd, "resize -dx %d -dy %d\n",
			Width + 20, (ntasks * Height) + 5);
	}else
		Height = ntasks ? Dy(screen->r)/ntasks : Dy(screen->r);
	return t;
}

void
doevent(Task *t, Traceevent *ep)
{
	int i, n;
	Event *event;
	vlong runt;

	t->tevents[ep->etype & 0xffff]++;
	n = t->nevents++;
	t->events = realloc(t->events, t->nevents*sizeof(Event));
	assert(t->events);
	event = &t->events[n];
	memmove(event, ep, sizeof(Traceevent));
	event->etime = 0;

	switch(event->etype & 0xffff){
	case SRelease:
		if (t->runthis > t->runmax)
			t->runmax = t->runthis;
		t->runthis = 0;
		break;

	case SSleep:
	case SYield:
	case SReady:
	case SSlice:
		for(i = n-1; i >= 0; i--)
			if (t->events[i].etype == SRun || 
				t->events[i].etype == SEdf)
				break;
		if(i < 0 || t->events[i].etime != 0)
			break;
		runt = event->time - t->events[i].time;
		if(runt > 0){
			t->events[i].etime = event->time;
			t->runtime += runt;
			t->total += runt;
			t->runthis += runt;
			t->runs++;
		}
		break;
	case SDead:
print("task died %ld %t %s\n", event->pid, event->time, schedstatename[event->etype & 0xffff]);
		free(t->events);
		free(t->name);
		ntasks--;
		memmove(t, t+1, sizeof(Task)*(&tasks[ntasks]-t));
		if (newwin)
			fprint(wctlfd, "resize -dx %d -dy %d\n",
				Width + 20, (ntasks * Height) + 5);
		else
			Height = ntasks ? Dy(screen->r)/ntasks : Dy(screen->r);
		prevts = 0;
	}
}

void
drawtrace(void)
{
	char *wsys, line[256];
	int wfd, logfd;
	Mousectl *mousectl;
	Keyboardctl *keyboardctl;
	int scaleno;
	Rune r;
	int i, n;
	Task *t;
	Traceevent *ep;

	eventbuf = malloc(Nevents*sizeof(Traceevent));
	assert(eventbuf);

	if((logfd = open(profdev, OREAD)) < 0)
		sysfatal("%s: Cannot open %s: %r", argv0, profdev);

	if(newwin){
		if((wsys = getenv("wsys")) == nil)
			sysfatal("%s: Cannot find windowing system: %r",
						argv0);
	
		if((wfd = open(wsys, ORDWR)) < 0)
			sysfatal("%s: Cannot open windowing system: %r",
						argv0);
	
		snprint(line, sizeof(line), "new -pid %d -dx %d -dy %d",
				getpid(), Width + 20, Height + 5);
		line[sizeof(line) - 1] = '\0';
		rfork(RFNAMEG);
	
		if(mount(wfd, -1, "/mnt/wsys", MREPL, line) < 0) 
			sysfatal("%s: Cannot mount %s under /mnt/wsys: %r",
						argv0, line);
	
		if(bind("/mnt/wsys", "/dev", MBEFORE) < 0) 
			sysfatal("%s: Cannot bind /mnt/wsys in /dev: %r",
						argv0);
	
	}
	if((wctlfd = open("/dev/wctl", OWRITE)) < 0)
		sysfatal("%s: Cannot open /dev/wctl: %r", argv0);
	if(initdraw(nil, nil, "trace") < 0)
		sysfatal("%s: initdraw failure: %r", argv0);

	Width = Dx(screen->r);
	Height = Dy(screen->r);

	if((mousectl = initmouse(nil, screen)) == nil)
		sysfatal("%s: cannot initialize mouse: %r", argv0);

	if((keyboardctl = initkeyboard(nil)) == nil)
		sysfatal("%s: cannot initialize keyboard: %r", argv0);

	colinit();

	paused = 0;
	scaleno = 7;	/* 100 milliseconds */
	now = nsec();
	for(;;) {
		Alt a[] = {
			{ mousectl->c,			nil,		CHANRCV		},
			{ mousectl->resizec,	nil,		CHANRCV		},
			{ keyboardctl->c,		&r,			CHANRCV		},
			{ nil,					nil,		CHANNOBLK	},
		};

		switch (alt(a)) {
		case 0:
			continue;

		case 1:
			if(getwindow(display, Refnone) < 0)
				sysfatal("drawrt: Cannot re-attach window");
			if(newwin){
				if(Dx(screen->r) != Width || 
					Dy(screen->r) != (ntasks * Height)){
					fprint(2, "resize: x: have %d, need %d; y: have %d, need %d\n",
							Dx(screen->r), Width + 8, Dy(screen->r), (ntasks * Height) + 8);
					fprint(wctlfd, "resize -dx %d -dy %d\n", 
							Width + 8, (ntasks * Height) + 8);
				}
			}
			else{
				Width = Dx(screen->r);
				Height = ntasks? Dy(screen->r)/ntasks: 
							Dy(screen->r);
			}
			break;

		case 2:

			switch(r){
			case 'r':
				for(i = 0; i < ntasks; i++){
					tasks[i].tstart = now;
					tasks[i].total = 0;
					tasks[i].runtime = 0;
					tasks[i].runmax = 0;
					tasks[i].runthis = 0;
					tasks[i].runs = 0;
					memset(tasks[i].tevents, 0, Nevent*sizeof(ulong));
					
				}
				break;

			case 'p':
				paused ^= 1;
				prevts = 0;
				break;

			case '-':
				if (scaleno < nelem(scales) - 1)
					scaleno++;
				prevts = 0;
				break;

			case '+':
				if (scaleno > 0)
					scaleno--;
				prevts = 0;
				break;

			case 'q':
				threadexitsall(nil);

			case 'v':
				verbose ^= 1;

			default:
				break;
			}
			break;
			
		case 3:
			now = nsec();
			while((n = read(logfd, eventbuf, Nevents*sizeof(Traceevent))) > 0){
				assert((n % sizeof(Traceevent)) == 0);
				nevents = n / sizeof(Traceevent);
				for (ep = eventbuf; ep < eventbuf + nevents; ep++){
					if ((ep->etype & 0xffff) >= Nevent){
						print("%ld %t Illegal event %ld\n",
							ep->pid, ep->time, ep->etype & 0xffff);
						continue;
					}
					if (verbose)
						print("%ld %t %s\n",
							ep->pid, ep->time, schedstatename[ep->etype & 0xffff]);

					for(i = 0; i < ntasks; i++)
						if(tasks[i].pid == ep->pid)
							break;

					if(i == ntasks){
						t = newtask(ep->pid);
						t->tstart = ep->time;
					}else
						t = &tasks[i];

					doevent(t, ep);
				}
			}
			if(!paused)
				redraw(scaleno);
		}
		sleep(scales[scaleno].sleep);
	}
}

int
timeconv(Fmt *f)
{
	char buf[128], *sign;
	vlong t;

	buf[0] = 0;
	switch(f->r) {
	case 'U':
		t = va_arg(f->args, vlong);
		break;
	case 't':		// vlong in nanoseconds
		t = va_arg(f->args, vlong);
		break;
	default:
		return fmtstrcpy(f, "(timeconv)");
	}
	if (t < 0) {
		sign = "-";
		t = -t;
	}else
		sign = "";
	if (t > S(1)){
		t += OneRound;
		sprint(buf, "%s%d.%.3ds", sign, (int)(t / S(1)), (int)(t % S(1))/1000000);
	}else if (t > MS(1)){
		t += MilliRound;
		sprint(buf, "%s%d.%.3dms", sign, (int)(t / MS(1)), (int)(t % MS(1))/1000);
	}else if (t > US(1))
		sprint(buf, "%s%d.%.3dµs", sign, (int)(t / US(1)), (int)(t % US(1)));
	else
		sprint(buf, "%s%dns", sign, (int)t);
	return fmtstrcpy(f, buf);
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to [email protected].