diff --git a/lib/Makefile b/lib/Makefile
index d200ab9..8a4c6f7 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -28,7 +28,7 @@ SUBDIR=	csu \
 	libpcap libposix1e libsdp libthread_xu libpthread librpcsvc ${_libsm} \
 	${_libsmb} ${_libsmdb} ${_libsmutil} libstand libtelnet libusbhid \
 	${_libvgl} libwrap libxpg4 liby libypclnt libz i18n_module pam_module \
-	libc_rtld libsctp
+	libc_rtld libsctp libevtr
 
 .if exists(${.CURDIR}/compat/${MACHINE_ARCH}/Makefile)
 SUBDIR+= compat/${MACHINE_ARCH}
diff --git a/lib/libevtr/Makefile b/lib/libevtr/Makefile
new file mode 100644
index 0000000..8dfcdf7
--- /dev/null
+++ b/lib/libevtr/Makefile
@@ -0,0 +1,9 @@
+LIB=evtr
+SHLIB_MAJOR=1
+
+SRCS=evtr.c
+INCS=evtr.h
+
+CFLAGS+= -W -Wall
+
+.include <bsd.lib.mk>
diff --git a/lib/libevtr/evtr.c b/lib/libevtr/evtr.c
new file mode 100644
index 0000000..62b70b1
--- /dev/null
+++ b/lib/libevtr/evtr.c
@@ -0,0 +1,1430 @@
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/queue.h>
+#include <sys/stat.h>
+#include <sys/tree.h>
+
+
+#include "evtr.h"
+
+#define printd(...)
+
+enum {
+	MAX_EVHDR_SIZE = PATH_MAX + 200,
+	/* string namespaces */
+	EVTR_NS_PATH = 0x1,
+	EVTR_NS_FUNC,
+	EVTR_NS_MAX,
+	NR_BUCKETS = 1023, /* XXX */
+	REC_ALIGN = 8,
+	REC_BOUNDARY = 1 << 14,
+	FILTF_ID = 0x10,
+	EVTRF_WR = 0x1,		/* open for writing */
+};
+
+typedef uint16_t fileid_t;
+typedef uint16_t funcid_t;
+typedef uint16_t fmtid_t;
+
+struct trace_event_header {
+	uint8_t type;
+	uint64_t ts;	/* XXX: this should only be part of probe */
+} __attribute__((packed));
+
+struct probe_event_header {
+	struct trace_event_header eh;
+	/*
+	 * For these fields, 0 implies "not available"
+	 */
+	fileid_t file;
+	funcid_t caller1;
+	funcid_t caller2;
+	funcid_t func;
+	uint16_t line;
+	fmtid_t fmt;
+	uint16_t datalen;
+	uint8_t cpu;	/* -1 if n/a */
+} __attribute__((packed));
+
+struct string_event_header {
+	struct trace_event_header eh;
+	uint16_t ns;
+	uint32_t id;
+	uint16_t len;
+} __attribute__((packed));
+
+struct fmt_event_header {
+	struct trace_event_header eh;
+	uint16_t id;
+	uint8_t subsys_len;
+	uint8_t fmt_len;
+} __attribute__((packed));
+
+struct hashentry {
+	const char *str;
+	uint16_t id;
+	struct hashentry *next;
+};
+
+struct hashtab {
+	struct hashentry *buckets[NR_BUCKETS];
+	uint16_t id;
+};
+
+struct event_fmt {
+	const char *subsys;
+	const char *fmt;
+};
+
+struct event_filter_unresolved {
+	TAILQ_ENTRY(event_filter_unresolved) link;
+	evtr_filter_t filt;
+};
+
+struct id_map {
+	RB_ENTRY(id_map) rb_node;
+	int id;
+	const void *data;
+};
+
+RB_HEAD(id_tree, id_map);
+struct string_map {
+	struct id_tree root;
+};
+
+struct fmt_map {
+	struct id_tree root;
+};
+
+RB_HEAD(thread_tree, evtr_thread);
+
+struct thread_map {
+	struct thread_tree root;
+};
+
+struct event_callback {
+	void (*cb)(evtr_event_t, void *data);
+	void *data;	/* this field must be malloc()ed */
+};
+
+struct cpu {
+	struct evtr_thread *td;	/* currently executing thread */
+};
+
+struct evtr {
+	FILE *f;
+	int err;
+	int flags;
+	char *errmsg;
+	off_t bytes;
+	union {
+		/*
+		 * When writing, we keep track of the strings we've
+		 * already dumped so we only dump them once.
+		 * Paths, function names etc belong to different
+		 * namespaces.
+		 */
+		struct hashtab *strings[EVTR_NS_MAX - 1];
+		/*
+		 * When reading, we build a map from id to string.
+		 * Every id must be defined at the point of use.
+		 */
+		struct string_map maps[EVTR_NS_MAX - 1];
+	};
+	union {
+		/* same as above, but for subsys+fmt pairs */
+		struct fmt_map fmtmap;
+		struct hashtab *fmts;
+	};
+	/*
+	 * Filters that have a format specified and we
+	 * need to resolve that to an fmtid
+	 */
+	TAILQ_HEAD(, event_filter_unresolved) unresolved_filtq;
+	struct event_callback **cbs;
+	int ncbs;
+	struct thread_map threads;
+	struct cpu *cpus;
+	int ncpus;
+};
+
+struct evtr_query {
+	evtr_t evtr;
+	off_t off;
+	evtr_filter_t filt;
+	int nfilt;
+	int nmatched;
+	int ntried;
+	void *buf;
+	int bufsize;
+};
+
+static int id_map_cmp(struct id_map *, struct id_map *);
+RB_PROTOTYPE2(id_tree, id_map, rb_node, id_map_cmp, int);
+RB_GENERATE2(id_tree, id_map, rb_node, id_map_cmp, int, id);
+
+static int thread_cmp(struct evtr_thread *, struct evtr_thread *);
+RB_PROTOTYPE2(thread_tree, evtr_thread, rb_node, thread_cmp, void *);
+RB_GENERATE2(thread_tree, evtr_thread, rb_node, thread_cmp, void *, id);
+
+static
+void
+id_tree_free(struct id_tree *root)
+{
+	struct id_map *v, *n;
+
+	for (v = RB_MIN(id_tree, root); v; v = n) {
+		n = RB_NEXT(id_tree, root, v);
+		RB_REMOVE(id_tree, root, v);
+	}
+}
+
+static
+int
+evtr_register_callback(evtr_t evtr, void (*fn)(evtr_event_t, void *), void *d)
+{
+	struct event_callback *cb;
+	void *cbs;
+
+	if (!(cb = malloc(sizeof(*cb)))) {
+		evtr->err = ENOMEM;
+		return !0;
+	}
+	cb->cb = fn;
+	cb->data = d;
+	if (!(cbs = realloc(evtr->cbs, (++evtr->ncbs) * sizeof(cb)))) {
+		--evtr->ncbs;
+		free(cb);
+		evtr->err = ENOMEM;
+		return !0;
+	}
+	evtr->cbs = cbs;
+	evtr->cbs[evtr->ncbs - 1] = cb;
+	return 0;
+}
+
+static
+void
+evtr_deregister_callbacks(evtr_t evtr)
+{
+	int i;
+
+	for (i = 0; i < evtr->ncbs; ++i) {
+		free(evtr->cbs[i]);
+	}
+	free(evtr->cbs);
+	evtr->cbs = NULL;
+}
+
+static
+void
+evtr_run_callbacks(evtr_event_t ev, evtr_t evtr)
+{
+	struct event_callback *cb;
+	int i;
+
+	for (i = 0; i < evtr->ncbs; ++i) {
+		cb = evtr->cbs[i];
+		cb->cb(ev, cb->data);
+	}
+}
+
+static
+struct cpu *
+evtr_cpu(evtr_t evtr, int c)
+{
+	if ((c < 0) || (c >= evtr->ncpus))
+		return NULL;
+	return &evtr->cpus[c];
+}
+
+static
+int
+parse_format_data(evtr_event_t ev, const char *fmt, ...) __attribute__((format (scanf, 2, 3)));
+static
+int
+parse_format_data(evtr_event_t ev, const char *fmt, ...)
+{
+	va_list ap;
+	char buf[2048];
+
+	if (strcmp(fmt, ev->fmt))
+		return 0;
+	vsnprintf(buf, sizeof(buf), fmt, ev->fmtdata);
+	printd("string is: %s\n", buf);
+	va_start(ap, fmt);
+	return vsscanf(buf, fmt, ap);
+}
+
+static
+void
+evtr_deregister_filters(evtr_t evtr, evtr_filter_t filt, int nfilt)
+{
+	struct event_filter_unresolved *u, *tmp;
+	int i;
+	TAILQ_FOREACH_MUTABLE(u, &evtr->unresolved_filtq, link, tmp) {
+		for (i = 0; i < nfilt; ++i) {
+			if (u->filt == &filt[i]) {
+				TAILQ_REMOVE(&evtr->unresolved_filtq, u, link);
+			}
+		}
+	}
+}
+
+static
+void
+evtr_resolve_filters(evtr_t evtr, const char *fmt, int id)
+{
+	struct event_filter_unresolved *u, *tmp;
+	TAILQ_FOREACH_MUTABLE(u, &evtr->unresolved_filtq, link, tmp) {
+		if ((u->filt->fmt != NULL) && !strcmp(fmt, u->filt->fmt)) {
+			u->filt->fmtid = id;
+			u->filt->flags |= FILTF_ID;
+			TAILQ_REMOVE(&evtr->unresolved_filtq, u, link);
+		}
+	}
+}
+
+static
+int
+evtr_filter_register(evtr_t evtr, evtr_filter_t filt)
+{
+	struct event_filter_unresolved *res;
+
+	if (!(res = malloc(sizeof(*res)))) {
+		evtr->err = ENOMEM;
+		return !0;
+	}
+	res->filt = filt;
+	TAILQ_INSERT_TAIL(&evtr->unresolved_filtq, res, link);
+	return 0;
+}
+
+void
+evtr_event_data(evtr_event_t ev, char *buf, size_t len)
+{
+	/*
+	 * XXX: we implicitly trust the format string.
+	 * We shouldn't.
+	 */
+	if (ev->fmtdatalen) {
+		vsnprintf(buf, len, ev->fmt, ev->fmtdata);
+	} else {
+		strlcpy(buf, ev->fmt, len);
+	}
+}
+
+
+int
+evtr_error(evtr_t evtr)
+{
+	return evtr->err || (evtr->errmsg == NULL);
+}
+
+const char *
+evtr_errmsg(evtr_t evtr)
+{
+	return evtr->errmsg ? evtr->errmsg : strerror(evtr->err);
+}
+
+static
+int
+id_map_cmp(struct id_map *a, struct id_map *b)
+{
+	return a->id - b->id;
+}
+
+static
+int
+thread_cmp(struct evtr_thread *a, struct evtr_thread *b)
+{
+	return (int)a->id - (int)b->id;
+}
+
+#define DEFINE_MAP_FIND(prefix, type)		\
+	static					\
+	type				\
+	prefix ## _map_find(struct id_tree *tree, int id)\
+	{						 \
+		struct id_map *sid;			 \
+							\
+		sid = id_tree_RB_LOOKUP(tree, id);	\
+		return sid ? sid->data : NULL;		\
+	}
+
+DEFINE_MAP_FIND(string, const char *)
+DEFINE_MAP_FIND(fmt, const struct event_fmt *)
+
+static
+struct evtr_thread *
+thread_map_find(struct thread_map *map, void *id)
+{
+	return thread_tree_RB_LOOKUP(&map->root, id);
+}
+
+#define DEFINE_MAP_INSERT(prefix, type, _cmp, _dup)	\
+	static					\
+	int								\
+	prefix ## _map_insert(struct id_tree *tree, type data, int id) \
+	{								\
+	struct id_map *sid, *osid;					\
+									\
+	sid = malloc(sizeof(*sid));					\
+	if (!sid) {							\
+		return ENOMEM;						\
+	}								\
+	sid->id = id;							\
+	sid->data = data;						\
+	if ((osid = id_tree_RB_INSERT(tree, sid))) {			\
+		free(sid);						\
+		if (_cmp((type)osid->data, data)) {			\
+			return EEXIST;					\
+		}							\
+		/* we're OK with redefinitions of an id to the same string */ \
+		return 0;						\
+	}								\
+	/* only do the strdup if we're inserting a new string */	\
+	sid->data = _dup(data);		/* XXX: oom */			\
+	return 0;							\
+}
+
+static
+void
+thread_map_insert(struct thread_map *map, struct evtr_thread *td)
+{
+	struct evtr_thread *otd;
+
+	if ((otd = thread_tree_RB_INSERT(&map->root, td))) {
+		/*
+		 * Thread addresses might be reused, we're
+		 * ok with that.
+		 * DANGER, Will Robinson: this means the user
+		 * of the API needs to copy event->td if they
+		 * want it to remain stable.
+		 */
+		free((void *)otd->comm);
+		otd->comm = td->comm;
+		free(td);
+	}
+}
+
+static
+int
+event_fmt_cmp(const struct event_fmt *a, const struct event_fmt *b)
+{
+	int ret = 0;
+
+	if (a->subsys) {
+		if (b->subsys) {
+			ret = strcmp(a->subsys, b->subsys);
+		} else {
+			ret = strcmp(a->subsys, "");
+		}
+	} else if (b->subsys) {
+			ret = strcmp("", b->subsys);
+	}
+	if (ret)
+		return ret;
+	return strcmp(a->fmt, b->fmt);
+}
+
+static
+struct event_fmt *
+event_fmt_dup(const struct event_fmt *o)
+{
+	struct event_fmt *n;
+
+	if (!(n = malloc(sizeof(*n)))) {
+		return n;
+	}
+	memcpy(n, o, sizeof(*n));
+	return n;
+}
+
+DEFINE_MAP_INSERT(string, const char *, strcmp, strdup)
+DEFINE_MAP_INSERT(fmt, const struct event_fmt *, event_fmt_cmp, event_fmt_dup)
+
+static
+int
+hashfunc(const char *str)
+{
+        unsigned long hash = 5381;
+        int c;
+
+        while ((c = *str++))
+            hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
+	return hash  % NR_BUCKETS;
+}
+
+static
+struct hashentry *
+hash_find(struct hashtab *tab, const char *str)
+{
+	struct hashentry *ent;
+
+	for(ent = tab->buckets[hashfunc(str)]; ent && strcmp(ent->str, str);
+	    ent = ent->next);
+
+	return ent;
+}
+
+static
+struct hashentry *
+hash_insert(struct hashtab *tab, const char *str)
+{
+	struct hashentry *ent;
+	int hsh;
+
+	if (!(ent = malloc(sizeof(*ent)))) {
+		fprintf(stderr, "out of memory\n");
+		return NULL;
+	}
+	hsh = hashfunc(str);
+	ent->next = tab->buckets[hsh];
+	ent->str = strdup(str);
+	ent->id = ++tab->id;
+	if (tab->id == 0) {
+		fprintf(stderr, "too many strings\n");
+		free(ent);
+		return NULL;
+	}
+	tab->buckets[hsh] = ent;
+	return ent;
+}
+
+static
+void
+thread_creation_callback(evtr_event_t ev, void *d)
+{
+	evtr_t evtr = (evtr_t)d;
+	struct evtr_thread *td;
+	void *ktd;
+	char buf[20];
+
+	printd("thread_creation_callback\n");
+	if (parse_format_data(ev, "tdnew %p %s", &ktd, buf) != 2) {
+		return;
+	}
+	buf[19] = '\0';
+
+	if (!(td = malloc(sizeof(*td)))) {
+		evtr->err = ENOMEM;
+		return;
+	}
+	td->id = ktd;
+	td->userdata = NULL;
+	if (!(td->comm = strdup(buf))) {
+		free(td);
+		evtr->err = ENOMEM;
+		return;
+	}
+	printd("inserting new thread %p: %s\n", td->id, td->comm);
+	thread_map_insert(&evtr->threads, td);
+}
+
+static
+void
+thread_switch_callback(evtr_event_t ev, void *d)
+{
+	evtr_t evtr = (evtr_t)d;
+	struct evtr_thread *tdp, *tdn;
+	void *ktdp, *ktdn;
+	struct cpu *cpu;
+	static struct evtr_event tdcr;
+	static char *fmt = "tdnew %p %s";
+	char tidstr[40];
+	char fmtdata[sizeof(void *) + sizeof(char *)];
+
+	printd("thread_switch_callback\n");
+	cpu = evtr_cpu(evtr, ev->cpu);
+	if (!cpu) {
+		printd("invalid cpu %d\n", ev->cpu);
+		return;
+	}
+	if (parse_format_data(ev, "sw  %p > %p", &ktdp, &ktdn) != 2) {
+		return;
+	}
+	tdp = thread_map_find(&evtr->threads, ktdp);
+	if (!tdp) {
+		printd("switching from unknown thread %p\n", ktdp);
+	}
+	tdn = thread_map_find(&evtr->threads, ktdn);
+	if (!tdn) {
+		/*
+		 * Fake a thread creation event for threads we
+		 * haven't seen before.
+		 */
+		tdcr.type = EVTR_TYPE_PROBE;
+		tdcr.ts = ev->ts;
+		tdcr.file = NULL;
+		tdcr.func = NULL;
+		tdcr.line = 0;
+		tdcr.fmt = fmt;
+		tdcr.fmtdata = &fmtdata;
+		tdcr.fmtdatalen = sizeof(fmtdata);
+		tdcr.cpu = ev->cpu;
+		tdcr.td = NULL;
+		snprintf(tidstr, sizeof(tidstr), "%p", ktdn);
+		((void **)fmtdata)[0] = ktdn;
+		((char **)fmtdata)[1] = &tidstr[0];
+		thread_creation_callback(&tdcr, evtr);
+
+		tdn = thread_map_find(&evtr->threads, ktdn);
+		assert(tdn != NULL);
+		printd("switching to unknown thread %p\n", ktdn);
+		cpu->td = tdn;
+		return;
+	}
+	printd("cpu %d: switching to thread %p\n", ev->cpu, ktdn);
+	cpu->td = tdn;
+}
+
+static
+void
+assert_foff_in_sync(evtr_t evtr)
+{
+	off_t off;
+
+	/*
+	 * We keep our own offset because we
+	 * might want to support mmap()
+	 */
+	off = ftello(evtr->f);
+	if (evtr->bytes != off) {
+		fprintf(stderr, "bytes %jd, off %jd\n", evtr->bytes, off);
+		abort();
+	}
+}
+
+static
+int
+evtr_write(evtr_t evtr, const void *buf, size_t bytes)
+{
+	assert_foff_in_sync(evtr);
+	if (fwrite(buf, bytes, 1, evtr->f) != 1) {
+		evtr->err = errno;
+		evtr->errmsg = strerror(errno);
+		return !0;
+	}
+	evtr->bytes += bytes;
+	assert_foff_in_sync(evtr);
+	return 0;
+}
+
+/*
+ * Called after dumping a record to make sure the next
+ * record is REC_ALIGN aligned. This does not make much sense,
+ * as we shouldn't be using packed structs anyway.
+ */
+static
+int
+evtr_dump_pad(evtr_t evtr)
+{
+	size_t pad;
+	static char buf[REC_ALIGN];
+
+	pad = REC_ALIGN - (evtr->bytes % REC_ALIGN);
+	if (pad > 0) {
+		return evtr_write(evtr, buf, pad);
+	}
+	return 0;
+}
+
+/*
+ * We make sure that there is a new record every REC_BOUNDARY
+ * bytes, this costs next to nothing in space and allows for
+ * fast seeking.
+ */
+static
+int
+evtr_dump_avoid_boundary(evtr_t evtr, size_t bytes)
+{
+	unsigned pad, i;
+	static char buf[256];
+
+	pad = REC_BOUNDARY - (evtr->bytes % REC_BOUNDARY);
+	/* if adding @bytes would cause us to cross a boundary... */
+	if (bytes > pad) {
+		/* then pad to the boundary */
+		for (i = 0; i < (pad / sizeof(buf)); ++i) {
+			if (evtr_write(evtr, buf, sizeof(buf))) {
+				return !0;
+			}
+		}
+		i = pad % sizeof(buf);
+		if (i) {
+			if (evtr_write(evtr, buf, i)) {
+				return !0;
+			}
+		}
+	}
+	return 0;
+}
+
+static
+int
+evtr_dump_fmt(evtr_t evtr, uint64_t ts, const evtr_event_t ev)
+{
+	struct fmt_event_header fmt;
+	struct hashentry *ent;
+	char *subsys = "", buf[1024];
+
+	if (strlcpy(buf, subsys, sizeof(buf)) >= sizeof(buf)) {
+		evtr->errmsg = "name of subsystem is too large";
+		evtr->err = ERANGE;
+		return 0;
+	}
+	if (strlcat(buf, ev->fmt, sizeof(buf)) >= sizeof(buf)) {
+		evtr->errmsg = "fmt + name of subsystem is too large";
+		evtr->err = ERANGE;
+		return 0;
+	}
+
+	if ((ent = hash_find(evtr->fmts, buf))) {
+		return ent->id;
+	}
+	if (!(ent = hash_insert(evtr->fmts, buf))) {
+		evtr->err = evtr->fmts->id ? ENOMEM : ERANGE;
+		return 0;
+	}
+
+	fmt.eh.type = EVTR_TYPE_FMT;
+	fmt.eh.ts = ts;
+	fmt.subsys_len = strlen(subsys);
+	fmt.fmt_len = strlen(ev->fmt);
+	fmt.id = ent->id;
+	if (evtr_dump_avoid_boundary(evtr, sizeof(fmt) + fmt.subsys_len +
+				     fmt.fmt_len))
+		return 0;
+	if (evtr_write(evtr, &fmt, sizeof(fmt)))
+		return 0;
+	if (evtr_write(evtr, subsys, fmt.subsys_len))
+		return 0;
+	if (evtr_write(evtr, ev->fmt, fmt.fmt_len))
+		return 0;
+	if (evtr_dump_pad(evtr))
+		return 0;
+	return fmt.id;
+}
+
+/* XXX: do we really want the timestamp? */
+static
+int
+evtr_dump_string(evtr_t evtr, uint64_t ts, const char *str, int ns)
+{
+	struct string_event_header s;
+	struct hashentry *ent;
+
+	assert((0 <= ns) && (ns < EVTR_NS_MAX));
+	if ((ent = hash_find(evtr->strings[ns], str))) {
+		return ent->id;
+	}
+	if (!(ent = hash_insert(evtr->strings[ns], str))) {
+		evtr->err = evtr->strings[ns]->id ? ENOMEM : ERANGE;
+		return 0;
+	}
+
+	s.eh.type = EVTR_TYPE_STR;
+	s.eh.ts = ts;
+	s.ns = ns;
+	s.id = ent->id;
+	s.len = strnlen(str, PATH_MAX);
+
+	if (evtr_dump_avoid_boundary(evtr, sizeof(s) + s.len))
+		return 0;
+	if (evtr_write(evtr, &s, sizeof(s)))
+		return 0;
+	if (evtr_write(evtr, str, s.len))
+		return 0;
+	if (evtr_dump_pad(evtr))
+		return 0;
+	return s.id;
+}
+
+static
+int
+evtr_dump_probe(evtr_t evtr, evtr_event_t ev)
+{
+	struct probe_event_header kev;
+
+	memset(&kev, '\0', sizeof(kev));
+	kev.eh.type = ev->type;
+	kev.eh.ts = ev->ts;
+	kev.line = ev->line;
+	kev.cpu = ev->cpu;
+	if (ev->file) {
+		kev.file = evtr_dump_string(evtr, kev.eh.ts, ev->file,
+					    EVTR_NS_PATH);
+	}
+	if (ev->func) {
+		kev.func = evtr_dump_string(evtr, kev.eh.ts, ev->func,
+					    EVTR_NS_FUNC);
+	}
+	if (ev->fmt) {
+		kev.fmt = evtr_dump_fmt(evtr, kev.eh.ts, ev);
+	}
+	if (ev->fmtdata)
+		kev.datalen = ev->fmtdatalen;
+	if (evtr_dump_avoid_boundary(evtr, sizeof(kev) + ev->fmtdatalen))
+		return !0;
+	if (evtr_write(evtr, &kev, sizeof(kev)))
+		return !0;
+	if (evtr_write(evtr, ev->fmtdata, ev->fmtdatalen))
+		return !0;
+	if (evtr_dump_pad(evtr))
+		return !0;
+	return 0;
+}
+
+static
+int
+evtr_dump_cpuinfo(evtr_t evtr, evtr_event_t ev)
+{
+	uint8_t type = EVTR_TYPE_CPUINFO;
+	uint16_t ncpus = ev->ncpus;
+
+	if (ncpus <= 0) {
+		evtr->errmsg = "invalid number of cpus";
+		return !0;
+	}
+	if (evtr_dump_avoid_boundary(evtr, sizeof(type) + sizeof(ncpus)))
+		return !0;
+	if (evtr_write(evtr, &type, sizeof(type))) {
+		return !0;
+	}
+	if (evtr_write(evtr, &ncpus, sizeof(ncpus))) {
+		return !0;
+	}
+	if (evtr_dump_pad(evtr))
+		return !0;
+	return 0;
+}
+
+int
+evtr_rewind(evtr_t evtr)
+{
+	assert((evtr->flags & EVTRF_WR) == 0);
+	evtr->bytes = 0;
+	if (fseek(evtr->f, 0, SEEK_SET)) {
+		evtr->err = errno;
+		return !0;
+	}
+	return 0;
+}
+
+int
+evtr_dump_event(evtr_t evtr, evtr_event_t ev)
+{
+	switch (ev->type) {
+	case EVTR_TYPE_PROBE:
+		return evtr_dump_probe(evtr, ev);
+	case EVTR_TYPE_CPUINFO:
+		return evtr_dump_cpuinfo(evtr, ev);
+	}
+	evtr->errmsg = "unknown event type";
+	return !0;
+}
+
+static
+evtr_t
+evtr_alloc(FILE *f)
+{
+	evtr_t evtr;
+	if (!(evtr = malloc(sizeof(*evtr)))) {
+		return NULL;
+	}
+
+	evtr->f = f;
+	evtr->err = 0;
+	evtr->errmsg = NULL;
+	evtr->bytes = 0;
+	TAILQ_INIT(&evtr->unresolved_filtq);
+	return evtr;
+}
+
+evtr_t
+evtr_open_read(FILE *f)
+{
+	evtr_t evtr;
+	struct evtr_event ev;
+	int i;
+
+	if (!(evtr = evtr_alloc(f))) {
+		return NULL;
+	}
+	evtr->flags = 0;
+	for (i = 0; i < (EVTR_NS_MAX - 1); ++i) {
+		RB_INIT(&evtr->maps[i].root);
+	}
+	RB_INIT(&evtr->fmtmap.root);
+	TAILQ_INIT(&evtr->unresolved_filtq);
+	evtr->cbs = 0;
+	evtr->ncbs = 0;
+	RB_INIT(&evtr->threads.root);
+	evtr->cpus = NULL;
+	evtr->ncpus = 0;
+	if (evtr_register_callback(evtr, &thread_creation_callback, evtr)) {
+		goto free_evtr;
+	}
+	if (evtr_register_callback(evtr, &thread_switch_callback, evtr)) {
+		goto free_cbs;
+	}
+	/*
+	 * Load the first event so we can pick up any
+	 * cpuinfo entries.
+	 */
+	if (evtr_next_event(evtr, &ev)) {
+		goto free_cbs;
+	}
+	if (evtr_rewind(evtr))
+		goto free_cbs;
+	return evtr;
+free_cbs:
+	evtr_deregister_callbacks(evtr);
+free_evtr:
+	free(evtr);
+	return NULL;
+}
+
+evtr_t
+evtr_open_write(FILE *f)
+{
+	evtr_t evtr;
+	int i, j;
+
+	if (!(evtr = evtr_alloc(f))) {
+		return NULL;
+	}
+
+	evtr->flags = EVTRF_WR;
+	if (!(evtr->fmts = calloc(sizeof(struct hashtab), 1)))
+		goto free_evtr;
+
+	for (i = 0; i < EVTR_NS_MAX; ++i) {
+		evtr->strings[i] = calloc(sizeof(struct hashtab), 1);
+		if (!evtr->strings[i]) {
+			for (j = 0; j < i; ++j) {
+				free(evtr->strings[j]);
+			}
+			goto free_fmts;
+		}
+	}
+
+	return evtr;
+free_fmts:
+	free(evtr->fmts);
+free_evtr:
+	free(evtr);
+	return NULL;
+}
+
+static
+void
+hashtab_destroy(struct hashtab *h)
+{
+	struct hashentry *ent, *next;
+	int i;
+	for (i = 0; i < NR_BUCKETS; ++i) {
+		for (ent = h->buckets[i]; ent; ent = next) {
+			next = ent->next;
+			free(ent);
+		}
+	}
+	free(h);
+}
+
+void
+evtr_close(evtr_t evtr)
+{
+	int i;
+
+	if (evtr->flags & EVTRF_WR) {
+		hashtab_destroy(evtr->fmts);
+		for (i = 0; i < EVTR_NS_MAX; ++i)
+			hashtab_destroy(evtr->strings[i]);
+	} else {
+		id_tree_free(&evtr->fmtmap.root);
+		for (i = 0; i < EVTR_NS_MAX; ++i) {
+			id_tree_free(&evtr->maps[i].root);
+		}
+	}
+	free(evtr);
+}
+
+static
+int
+evtr_read(evtr_t evtr, void *buf, size_t size)
+{
+	assert(size > 0);
+	assert_foff_in_sync(evtr);
+	printd("evtr_read at %#jx, %zd bytes\n", evtr->bytes, size);
+	if (fread(buf, size, 1, evtr->f) != 1) {
+		if (feof(evtr->f)) {
+			evtr->errmsg = "incomplete record";
+		} else {
+			evtr->errmsg = strerror(errno);
+		}
+		return !0;
+	}
+	evtr->bytes += size;
+	assert_foff_in_sync(evtr);
+	return 0;
+}
+
+static
+int
+evtr_load_fmt(evtr_t evtr, char *buf)
+{
+	struct fmt_event_header *evh = (struct fmt_event_header *)buf;
+	struct event_fmt *fmt;
+	char *subsys = NULL, *fmtstr;
+
+	if (!(fmt = malloc(sizeof(*fmt)))) {
+		evtr->err = errno;
+		return !0;
+	}
+	if (evtr_read(evtr, buf + sizeof(struct trace_event_header),
+		      sizeof(*evh) - sizeof(evh->eh))) {
+		goto free_fmt;
+	}
+	if (evh->subsys_len) {
+		if (!(subsys = malloc(evh->subsys_len))) {
+			evtr->err = errno;
+			goto free_fmt;
+		}
+		if (evtr_read(evtr, subsys, evh->subsys_len)) {
+			goto free_subsys;
+		}
+		fmt->subsys = subsys;
+	} else {
+		fmt->subsys = "";
+	}
+	if (!(fmtstr = malloc(evh->fmt_len + 1))) {
+		evtr->err = errno;
+		goto free_subsys;
+	}
+	if (evtr_read(evtr, fmtstr, evh->fmt_len)) {
+		goto free_fmtstr;
+	}
+	fmtstr[evh->fmt_len] = '\0';
+	fmt->fmt = fmtstr;
+
+	printd("fmt_map_insert (%d, %s)\n", evh->id, fmt->fmt);
+	evtr->err = fmt_map_insert(&evtr->fmtmap.root, fmt, evh->id);
+	switch (evtr->err) {
+	case ENOMEM:
+		evtr->errmsg = "out of memory";
+		break;
+	case EEXIST:
+		evtr->errmsg = "redefinition of an id to a "
+			"different format (corrupt input)";
+		break;
+	default:
+		evtr_resolve_filters(evtr, fmt->fmt, evh->id);
+	}
+	return 0;
+
+free_fmtstr:
+	free(fmtstr);
+free_subsys:
+	if (subsys)
+		free(subsys);
+free_fmt:
+	free(fmt);
+	return !0;
+}
+
+static
+int
+evtr_load_string(evtr_t evtr, char *buf)
+{
+	char sbuf[PATH_MAX + 1];
+	struct string_event_header *evh = (struct string_event_header *)buf;
+
+	if (evtr_read(evtr, buf + sizeof(struct trace_event_header),
+		      sizeof(*evh) - sizeof(evh->eh))) {
+		return !0;
+	}
+	if (evh->len > PATH_MAX) {
+		evtr->errmsg = "string too large (corrupt input)";
+		return !0;
+	}
+	if (evtr_read(evtr, sbuf, evh->len)) {
+		return !0;
+	}
+	sbuf[evh->len] = 0;
+	if (evh->ns >= EVTR_NS_MAX) {
+		evtr->errmsg = "invalid namespace (corrupt input)";
+		return !0;
+	}
+	evtr->err = string_map_insert(&evtr->maps[evh->ns].root, sbuf, evh->id);
+	switch (evtr->err) {
+	case ENOMEM:
+		evtr->errmsg = "out of memory";
+		break;
+	case EEXIST:
+		evtr->errmsg = "redefinition of an id to a "
+			"different string (corrupt input)";
+		break;
+	default:
+		;
+	}
+	return 0;
+}
+
+static
+int
+evtr_filter_match(evtr_filter_t f, struct probe_event_header *pev)
+{
+	if ((f->cpu != -1) && (f->cpu != pev->cpu))
+		return 0;
+	if (!f->fmtid)
+		return !0;
+	/*
+	 * If we don't have an id for the required format
+	 * string, the format string won't match anyway
+	 * (we require that id <-> fmt mappings appear
+	 * before the first appearance of the fmt string),
+	 * so don't bother comparing.
+	 */
+	if (!(f->flags & FILTF_ID))
+		return 0;
+	if(pev->fmt == f->fmtid)
+		return !0;
+	return 0;
+}
+
+static
+int
+evtr_match_filters(struct evtr_query *q, struct probe_event_header *pev)
+{
+	int i;
+
+	/* no filters means we're interested in all events */
+	if (!q->nfilt)
+		return !0;
+	++q->ntried;
+	for (i = 0; i < q->nfilt; ++i) {
+		if (evtr_filter_match(&q->filt[i], pev)) {
+			++q->nmatched;
+			return !0;
+		}
+	}
+	return 0;
+}
+
+static
+int
+evtr_skip(evtr_t evtr, off_t bytes)
+{
+	if (fseek(evtr->f, bytes, SEEK_CUR)) {
+		evtr->err = errno;
+		evtr->errmsg = strerror(errno);
+		return !0;
+	}
+	evtr->bytes += bytes;
+	return 0;
+}
+
+/*
+ * Make sure q->buf is at least len bytes
+ */
+static
+int
+evtr_query_reserve_buf(struct evtr_query *q, int len)
+{
+	void *tmp;
+
+	if (q->bufsize >= len)
+		return 0;
+	if (!(tmp = realloc(q->buf, len)))
+		return !0;
+	q->buf = tmp;
+	q->bufsize = len;
+	return 0;
+}
+
+static
+int
+evtr_load_probe(evtr_t evtr, evtr_event_t ev, char *buf, struct evtr_query *q)
+{
+	struct probe_event_header *evh = (struct probe_event_header *)buf;
+	struct cpu *cpu;
+
+	if (evtr_read(evtr, buf + sizeof(struct trace_event_header),
+		      sizeof(*evh) - sizeof(evh->eh)))
+		return !0;
+	memset(ev, '\0', sizeof(*ev));
+	ev->ts = evh->eh.ts;
+	ev->type = EVTR_TYPE_PROBE;
+	ev->line = evh->line;
+	ev->cpu = evh->cpu;
+	if ((cpu = evtr_cpu(evtr, evh->cpu))) {
+		ev->td = cpu->td;
+	} else {
+		ev->td = NULL;
+	}
+	if (evh->file) {
+		ev->file = string_map_find(&evtr->maps[EVTR_NS_PATH].root, evh->file);
+		if (!ev->file) {
+			evtr->errmsg = "unknown id for file path";
+			evtr->err = !0;
+			ev->file = "<unknown>";
+		}
+	} else {
+		ev->file = "<unknown>";
+	}
+	if (evh->fmt) {
+		const struct event_fmt *fmt;
+		if (!(fmt = fmt_map_find(&evtr->fmtmap.root, evh->fmt))) {
+			evtr->errmsg = "unknown id for event fmt";
+			evtr->err = !0;
+			ev->fmt = NULL;
+		} else {
+			ev->fmt = fmt->fmt;
+		}
+	}
+	if (evh->datalen) {
+		if (evtr_query_reserve_buf(q, evh->datalen + 1)) {
+			evtr->err = ENOMEM;
+		} else if (!evtr_read(evtr, q->buf, evh->datalen)) {
+			ev->fmtdata = q->buf;
+			((char *)ev->fmtdata)[evh->datalen] = '\0';
+			ev->fmtdatalen = evh->datalen;
+		}
+	}
+	evtr_run_callbacks(ev, evtr);
+	/* we can't filter before running the callbacks */ 
+	if (!evtr_match_filters(q, evh)) {
+		return -1;	/* no match */
+	}
+
+	return evtr->err;
+}
+
+static
+int
+evtr_skip_to_record(evtr_t evtr)
+{
+	int skip;
+	
+	skip = REC_ALIGN - (evtr->bytes % REC_ALIGN);
+	if (skip > 0) {
+		if (fseek(evtr->f, skip, SEEK_CUR)) {
+			evtr->err = errno;
+			evtr->errmsg = strerror(errno);
+			return !0;
+		}
+		evtr->bytes += skip;
+	}
+	return 0;
+}
+
+static
+int
+evtr_load_cpuinfo(evtr_t evtr)
+{
+	uint16_t ncpus;
+	int i;
+
+	if (evtr_read(evtr, &ncpus, sizeof(ncpus))) {
+		return !0;
+	}
+	if (evtr->cpus)
+		return 0;
+	evtr->cpus = malloc(ncpus * sizeof(struct cpu));
+	if (!evtr->cpus) {
+		evtr->err = ENOMEM;
+		return !0;
+	}
+	evtr->ncpus = ncpus;
+	for (i = 0; i < ncpus; ++i) {
+		evtr->cpus[i].td = NULL;
+	}
+	return 0;
+}
+
+static
+int
+_evtr_next_event(evtr_t evtr, evtr_event_t ev, struct evtr_query *q)
+{
+	char buf[MAX_EVHDR_SIZE];
+	int ret, err, ntried, nmatched;
+	struct trace_event_header *evhdr = (struct trace_event_header *)buf;
+
+	for (ret = 0; !ret;) {
+		/*
+		 * skip pad records -- this will only happen if there's a
+		 * variable sized record close to the boundary
+		 */
+		if (evtr_read(evtr, &evhdr->type, 1))
+			return feof(evtr->f) ? -1 : !0;
+		if (evhdr->type == EVTR_TYPE_PAD) {
+			evtr_skip_to_record(evtr);
+			continue;
+		}
+		if (evhdr->type == EVTR_TYPE_CPUINFO) {
+			evtr_load_cpuinfo(evtr);
+			continue;
+		}
+		if (evtr_read(evtr, buf + 1, sizeof(*evhdr) - 1))
+			return feof(evtr->f) ? -1 : !0;
+		switch (evhdr->type) {
+		case EVTR_TYPE_PROBE:
+			ntried = q->ntried;
+			nmatched = q->nmatched;
+			if ((err = evtr_load_probe(evtr, ev, buf, q))) {
+				if (err == -1) {
+					/* no match */
+					ret = 0;
+				} else {
+					return !0;
+				}
+			} else {
+				ret = !0;
+			}
+			break;
+		case EVTR_TYPE_STR:
+			if (evtr_load_string(evtr, buf)) {
+				return !0;
+			}
+			break;
+		case EVTR_TYPE_FMT:
+			if (evtr_load_fmt(evtr, buf)) {
+				return !0;
+			}
+			break;
+		default:
+			evtr->err = !0;
+			evtr->errmsg = "unknown event type (corrupt input?)";
+			return !0;
+		}
+		evtr_skip_to_record(evtr);
+		if (ret) {
+			q->off = evtr->bytes;
+			return 0;
+		}
+	}
+	/* can't get here */
+	return !0;
+}
+
+int
+evtr_next_event(evtr_t evtr, evtr_event_t ev)
+{
+	struct evtr_query *q;
+	int ret;
+
+	if (!(q = evtr_query_init(evtr, NULL, 0))) {
+		evtr->err = ENOMEM;
+		return !0;
+	}
+	ret = _evtr_next_event(evtr, ev, q);
+	evtr_query_destroy(q);
+	return ret;
+}
+
+int
+evtr_last_event(evtr_t evtr, evtr_event_t ev)
+{
+	struct stat st;
+	int fd;
+	off_t last_boundary;
+
+	fd = fileno(evtr->f);
+	if (fstat(fd, &st))
+		return !0;
+	/*
+	 * This skips pseudo records, so we can't provide
+	 * an event with all fields filled in this way.
+	 * It's doable, just needs some care. TBD.
+	 */
+	if (0 && (st.st_mode & S_IFREG)) {
+		/*
+		 * Skip to last boundary, that's the closest to the EOF
+		 * location that we are sure contains a header so we can
+		 * pick up the stream.
+		 */
+		last_boundary = (st.st_size / REC_BOUNDARY) * REC_BOUNDARY;
+		/* XXX: ->bytes should be in query */
+		assert(evtr->bytes == 0);
+		evtr_skip(evtr, last_boundary);
+	}
+
+
+	/*
+	 * If we can't seek, we need to go through the whole file.
+	 * Since you can't seek back, this is pretty useless unless
+	 * you really are interested only in the last event.
+	 */
+	while (!evtr_next_event(evtr, ev))
+		;
+	if (evtr_error(evtr))
+		return !0;
+	evtr_rewind(evtr);
+	return 0;
+}
+
+struct evtr_query *
+evtr_query_init(evtr_t evtr, evtr_filter_t filt, int nfilt)
+{
+	struct evtr_query *q;
+	int i;
+
+	if (!(q = malloc(sizeof(*q)))) {
+		return q;
+	}
+	q->bufsize = 2;
+	if (!(q->buf = malloc(q->bufsize))) {
+		goto free_q;
+	}
+	q->evtr = evtr;
+	q->off = 0;
+	q->filt = filt;
+	q->nfilt = nfilt;
+	q->nmatched = 0;
+	for (i = 0; i < nfilt; ++i) {
+		filt[i].flags = 0;
+		if (filt[i].fmt == NULL)
+			continue;
+		if (evtr_filter_register(evtr, &filt[i])) {
+			evtr_deregister_filters(evtr, filt, i);
+			goto free_buf;
+		}
+	}
+
+	return q;
+free_buf:
+	free(q->buf);
+free_q:
+	free(q);
+	return NULL;
+}
+
+void
+evtr_query_destroy(struct evtr_query *q)
+{
+	evtr_deregister_filters(q->evtr, q->filt, q->nfilt);
+	free(q->buf);
+	free(q);
+}
+
+int
+evtr_query_next(struct evtr_query *q, evtr_event_t ev)
+{
+	/* we may support that in the future */
+	if (q->off != q->evtr->bytes)
+		return !0;
+	return _evtr_next_event(q->evtr, ev, q);
+}
+
+int
+evtr_ncpus(evtr_t evtr)
+{
+	return evtr->ncpus;
+}
diff --git a/lib/libevtr/evtr.h b/lib/libevtr/evtr.h
new file mode 100644
index 0000000..af3c8cf
--- /dev/null
+++ b/lib/libevtr/evtr.h
@@ -0,0 +1,122 @@
+#ifndef EVTR_H
+#define EVTR_H
+
+#include <stdint.h>
+#include <stdio.h>
+/* XXX: remove */
+#include <sys/tree.h>
+
+enum {
+	EVTR_TYPE_PAD = 0x0,
+	EVTR_TYPE_PROBE = 0x1,
+	EVTR_TYPE_STR = 0x2,
+	EVTR_TYPE_FMT = 0x3,
+	EVTR_TYPE_CPUINFO = 0x4,
+};
+
+struct evtr_thread {
+	RB_ENTRY(evtr_thread) rb_node;
+	void *id;
+	const char *comm;
+	/* available for the user of the library, NULL if not set */
+	void *userdata;
+};
+
+/*
+ * This structure is used for interchange of data with
+ * the user of the library
+ */
+typedef struct evtr_event {
+	uint8_t type;
+	union {
+		/* timestamp. Must be nondecreasing */
+		uint64_t ts;
+		uint16_t ncpus;	/* EVTR_TYPE_CPUINFO */
+	};
+	/*
+	 * Pointer to filename. NULL if n/a.
+	 * For an event returned by the library,
+	 * it is a pointer to storage allocated
+	 * by the library that will be around
+	 * until the call to evtr_close.
+	 */
+	const char *file;
+	/* Same as above */ 
+	const char *func;
+	/* line number. 0 if n/a */
+	uint16_t line;
+	/*
+	 * Format string, also used to identify
+	 * the event. Ownership rules are the same
+	 * as for file.
+	 */
+	const char *fmt;
+	/*
+	 * Data corresponding to the format string.
+	 * For an event returned by the library,
+	 * it is a pointer to an internal buffer
+	 * that becomes invalid when the next record
+	 * is returned. If the user wants to keep this
+	 * data around, they must copy it.
+	 */
+	const void *fmtdata;
+	/* Length of fmtdata */
+	int fmtdatalen;
+	/* Cpu on which the event occured */
+	uint8_t cpu;
+	/*
+	 * Thread, which generated the event (if applicable). The
+	 * storage pointed to belongs to the library and may (even
+	 * though it's highly unlikely) point to a different thread
+	 * in the future. The user needs to copy it if they want
+	 * this data.
+	 */
+	struct evtr_thread *td;
+} *evtr_event_t;
+
+/*
+ * Specifies which conditions to filter query results
+ * with. It is modified by the library and should
+ * not be touched after initialization.
+ */
+typedef struct evtr_filter {
+	int flags;	/* must be initialized to 0 */
+	/*
+	 * Which cpu we are interested in. -1 means
+	 * any cpu. XXX: use mask? (note we could just
+	 * do that internally)
+	 */
+	int cpu;
+	/*
+	 * If the user sets fmt, only events with a format
+	 * string identical to the one specified will be
+	 * returned. This field is modified by the library.
+	 */
+	union {
+		const char *fmt;
+		int fmtid;
+	};
+} *evtr_filter_t;
+
+struct evtr_query;
+struct evtr;
+typedef struct evtr *evtr_t;
+
+int evtr_next_event(evtr_t, evtr_event_t);
+evtr_t evtr_open_read(FILE *);
+evtr_t evtr_open_write(FILE *);
+void evtr_close(evtr_t);
+int evtr_dump_event(evtr_t, evtr_event_t);
+int evtr_error(evtr_t);
+const char * evtr_errmsg(evtr_t);
+void evtr_event_data(evtr_event_t, char *, size_t);
+struct evtr_query * evtr_query_init(evtr_t, evtr_filter_t, int);
+void evtr_query_destroy(struct evtr_query *);
+int evtr_query_next(struct evtr_query *, evtr_event_t);
+int evtr_last_event(evtr_t, evtr_event_t);
+int evtr_rewind(evtr_t);
+
+int evtr_ncpus(evtr_t);
+
+
+#endif	/* EVTR_H */
diff --git a/usr.bin/Makefile b/usr.bin/Makefile
index c19ca50..e37106c 100644
--- a/usr.bin/Makefile
+++ b/usr.bin/Makefile
@@ -50,6 +50,7 @@ SUBDIR=	alias \
 	ee \
 	enigma \
 	env \
+	evtranalyze \
 	expand \
 	false \
 	fetch \
diff --git a/usr.bin/evtranalyze/Makefile b/usr.bin/evtranalyze/Makefile
new file mode 100644
index 0000000..3099607
--- /dev/null
+++ b/usr.bin/evtranalyze/Makefile
@@ -0,0 +1,6 @@
+PROG=	evtranalyze
+
+SRCS= xml.c svg.c evtranalyze.c
+DPADD=	${LIBEVTR}
+LDADD=  -levtr -lm
+.include <bsd.prog.mk>
diff --git a/usr.bin/evtranalyze/evtranalyze.1 b/usr.bin/evtranalyze/evtranalyze.1
new file mode 100644
index 0000000..6806391
--- /dev/null
+++ b/usr.bin/evtranalyze/evtranalyze.1
@@ -0,0 +1,54 @@
+.\"-
+.\" Copyright (c) 2009 Aggelos Economopoulos
+.\" All rights reserved.
+.\"
+.\" 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.
+.\"
+.\"
+.Dd September 28, 2009
+.Dt EVTRANALYZE 8
+.Os
+.Sh NAME
+.Nm evtranalyze
+.Nd analyze a trace stream
+.Sh SYNOPSIS
+.Nm
+.Op Fl f Ar infile
+.Sh DESCRIPTION
+The
+.Nm
+utility is used to analyze an event trace stream.
+.Sh SEE ALSO
+.Xr ktrdump 8 ,
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Dx 2.5.0 .
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+utility was implemented by
+.An Aggelos Economopoulos Aq aggelos@dragonflybsd.org .
+for
+.Dx .
diff --git a/usr.bin/evtranalyze/evtranalyze.c b/usr.bin/evtranalyze/evtranalyze.c
new file mode 100644
index 0000000..3ac84d0
--- /dev/null
+++ b/usr.bin/evtranalyze/evtranalyze.c
@@ -0,0 +1,672 @@
+#include <assert.h>
+#include <err.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <evtr.h>
+#include "xml.h"
+#include "svg.h"
+
+#define printd printf
+
+enum {
+	NR_TOP_THREADS = 5,
+};
+
+struct rows {
+	double row_increment;
+	double row_off;
+};
+
+#define CMD_PROTO(name)	\
+	static int cmd_ ## name(int, char **)
+
+CMD_PROTO(show);
+CMD_PROTO(svg);
+
+struct command {
+	const char *name;
+	int (*func)(int argc, char **argv);
+} commands[] = {
+	{
+		.name = "show",
+		.func = &cmd_show,
+	},
+	{
+		.name = "svg",
+		.func = &cmd_svg,
+	},
+	{
+		.name = NULL,
+	},
+};
+
+evtr_t evtr;
+char *opt_infile;
+
+static
+void
+usage(void)
+{
+	fprintf(stderr, "bad usage :P\n");
+	exit(2);
+}
+
+static
+void
+rows_init(struct rows *rows, int n, double height, double perc)
+{
+	double row_h;
+	rows->row_increment = height / n;
+	/* actual row height */
+        row_h = perc * rows->row_increment;
+	rows->row_off = (rows->row_increment - row_h) / 2.0;
+	assert(!isnan(rows->row_increment));
+	assert(!isnan(rows->row_off));
+}
+
+static
+void
+rows_n(struct rows *rows, int n, double *y, double *height)
+{
+	*y = n * rows->row_increment + rows->row_off;
+	*height = rows->row_increment - 2 * rows->row_off;
+}
+
+/*
+ * Which fontsize to use so that the string fits in the
+ * given rect.
+ */
+static
+double
+fontsize_for_rect(double width, double height, int textlen)
+{
+	double wpc, maxh;
+	/*
+	 * We start with a font size equal to the height
+	 * of the rectangle and round down so that we only
+	 * use a limited number of sizes.
+	 *
+	 * For a rectangle width comparable to the height,
+	 * the text might extend outside of the rectangle.
+	 * In that case we need to limit it.
+	 */
+	/* available width per character */
+	wpc = width / textlen;
+	/*
+	 * Assuming a rough hight/width ratio for characters,
+	 * calculate the available height and round it down
+	 * just to be on the safe side.
+	 */
+#define GLYPH_HIGHT_TO_WIDTH 1.5
+	maxh = GLYPH_HIGHT_TO_WIDTH * wpc * 0.9;
+	if (height > maxh) {
+		height = maxh;
+	} else if (height < 0.01) {
+		height = 0.01;
+	} else {
+		/* rounding (XXX: make cheaper)*/
+		height = log(height);
+		height = round(height);
+		height = exp(height);
+	}
+	return height;
+}
+
+struct pass_hook {
+	void (*pre)(void *);
+	void (*event)(void *, evtr_event_t);
+	void (*post)(void *);
+	void *data;
+	struct evtr_filter *filts;
+	int nfilts;
+};
+
+struct thread_info {
+	uint64_t runtime;
+};
+
+struct td_switch_ctx {
+	svg_document_t svg;
+	struct rows *cpu_rows;
+	struct rows *thread_rows;
+	uint64_t interval_start, interval_end;
+	uint64_t first_ts, last_ts;
+	double width;
+	double xscale;	/* scale factor applied to x */
+	svg_rect_t cpu_sw_rect;
+	svg_rect_t thread_rect;
+	svg_rect_t inactive_rect;
+	svg_text_t thread_label;
+	struct cpu *cpus;
+	int ncpus;
+	struct evtr_thread **top_threads;
+	int nr_top_threads;
+	double thread_rows_yoff;
+};
+
+struct cpu {
+	struct evtr_thread *td;
+	int i;		/* cpu index */
+	uint64_t ts;	/* time cpu switched to td */
+	uint64_t first_ts, last_ts;
+};
+
+static
+void
+do_pass(struct pass_hook *hooks, int nhooks)
+{
+	struct evtr_filter *filts = NULL;
+	int nfilts = 0, i;
+	struct evtr_query *q;
+	struct evtr_event ev;
+
+	for (i = 0; i < nhooks; ++i) {
+		struct pass_hook *h = &hooks[i];
+		if (h->pre)
+			h->pre(h->data);
+		if (h->nfilts > 0) {
+			filts = realloc(filts, (nfilts + h->nfilts) *
+					sizeof(struct evtr_filter));
+			if (!filts)
+				err(1, "Out of memory");
+			memcpy(filts + nfilts, &h->filts,
+			       h->nfilts * sizeof(struct evtr_filter));
+			nfilts += h->nfilts;
+		}
+	}
+	q = evtr_query_init(evtr, filts, nfilts);
+	if (!q)
+		err(1, "Can't initialize query\n");
+	while(!evtr_query_next(q, &ev)) {
+		for (i = 0; i < nhooks; ++i) {
+			if (hooks[i].event)
+				hooks[i].event(hooks[i].data, &ev);
+		}
+	}
+	if (evtr_error(evtr)) {
+		err(1, evtr_errmsg(evtr));
+	}
+	evtr_query_destroy(q);
+
+	for (i = 0; i < nhooks; ++i) {
+		if (hooks[i].post)
+			hooks[i].post(hooks[i].data);
+	}
+	if (evtr_rewind(evtr))
+		err(1, "Can't rewind event stream\n");
+}
+
+static
+void
+draw_thread_run(struct td_switch_ctx *ctx, struct cpu *c, evtr_event_t ev, int row)
+{
+	double x, w, fs, y, height;
+	w = (ev->ts - c->ts) * ctx->xscale;
+	x = (ev->ts - ctx->first_ts) * ctx->xscale;
+	rows_n(ctx->thread_rows, row, &y, &height);
+	svg_rect_draw(ctx->svg, ctx->thread_rect, x - w,
+		      y + ctx->thread_rows_yoff, w, height);
+}
+
+static
+void
+draw_ctx_switch(struct td_switch_ctx *ctx, struct cpu *c, evtr_event_t ev)
+{
+	struct svg_transform textrot;
+	char comm[100];
+	double x, w, fs, y, height;
+	int textlen;
+
+	assert(ctx->xscale > 0.0);
+	if (!c->ts)
+		return;
+	/* distance to previous context switch */
+	w = (ev->ts - c->ts) * ctx->xscale;
+	x = (ev->ts - ctx->first_ts) * ctx->xscale;
+	if ((x - w) < 0) {
+		fprintf(stderr, "(%llu - %llu) * %.20lf\n", ev->ts,
+			ctx->first_ts, ctx->xscale);
+		abort();
+	}
+
+	rows_n(ctx->cpu_rows, c->i, &y, &height);
+	assert(!isnan(y));
+	assert(!isnan(height));
+
+	svg_rect_draw(ctx->svg, ctx->cpu_sw_rect, x - w, y, w, height);
+
+	/*
+	 * Draw the text label describing the thread we
+	 * switched out of.
+	 */
+	textrot.tx = x - w;
+	textrot.ty = y;
+	textrot.sx = 1.0;
+	textrot.sy = 1.0;
+	textrot.rot = 90.0;
+	textlen = snprintf(comm, sizeof(comm) - 1, "%s (%p)",
+			   c->td ? c->td->comm : "unknown",
+				 c->td ? c->td->id: NULL);
+	if (textlen > (int)sizeof(comm))
+		textlen = sizeof(comm) - 1;
+	comm[sizeof(comm) - 1] = '\0';
+	/*
+	 * Note the width and hight are exchanged because
+	 * the bounding rectangle is rotated by 90 degrees.
+	 */
+	fs = fontsize_for_rect(height, w, textlen);
+	svg_text_draw(ctx->svg, ctx->thread_label, &textrot, comm,
+		      fs);
+}
+
+
+/*
+ * The stats for ntd have changed, update ->top_threads
+ */
+static
+void
+top_threads_update(struct td_switch_ctx *ctx, struct evtr_thread *ntd)
+{
+	struct thread_info *tdi = ntd->userdata;
+	int i, j;
+	for (i = 0; i < ctx->nr_top_threads; ++i) {
+		struct evtr_thread *td = ctx->top_threads[i];
+		if (td == ntd) {
+			/*
+			 * ntd is already in top_threads and it is at
+			 * the correct ranking
+			 */
+			break;
+		}
+		if (!td) {
+			/* empty slot -- just insert our thread */
+			ctx->top_threads[i] = ntd;
+			break;
+		}
+		if (((struct thread_info *)td->userdata)->runtime >=
+		    tdi->runtime) {
+			/* this thread ranks higher than we do. Move on */
+			continue;
+		}
+		/*
+		 * OK, we've found the first thread that we outrank, so we
+		 * need to replace it w/ our thread.
+		 */
+		td = ntd;	/* td holds the thread we will insert next */
+		for (j = i + 1; j < ctx->nr_top_threads; ++j, ++i) {
+			struct evtr_thread *tmp;
+
+			/* tmp holds the thread we replace */
+			tmp = ctx->top_threads[j];
+			ctx->top_threads[j] = td;
+			if (tmp == ntd) {
+				/*
+				 * Our thread was already in the top list,
+				 * and we just removed the second instance.
+				 * Nothing more to do.
+				 */
+				break;
+			}
+			td = tmp;
+		}
+		break;
+	}
+}
+
+static
+void
+ctxsw_prepare_event(void *_ctx, evtr_event_t ev)
+{
+	struct td_switch_ctx *ctx = _ctx;
+	struct cpu *c, *cpus = ctx->cpus;
+	struct thread_info *tdi;
+
+	(void)evtr;
+	printd("test1 (%llu:%llu) : %llu\n", ctx->interval_start,
+	       ctx->interval_end, ev->ts);
+	if ((ev->ts > ctx->interval_end) ||
+	    (ev->ts < ctx->interval_start))
+		return;
+	printf("FPEV\n");
+
+	/* update first/last timestamps */
+	c = &cpus[ev->cpu];
+	if (!c->first_ts) {
+		c->first_ts = ev->ts;
+		printd("setting first_ts (%d) = %llu\n", ev->cpu,
+		       c->first_ts);
+	}
+	c->last_ts = ev->ts;
+	/*
+	 * c->td can be null when this is the first ctxsw event we
+	 * observe for a cpu
+	 */
+	if (c->td) {
+		/* update thread stats */
+		if (!c->td->userdata) {
+			if (!(tdi = malloc(sizeof(struct thread_info))))
+				err(1, "Out of memory");
+			c->td->userdata = tdi;
+			tdi->runtime = 0;
+		}
+		tdi = c->td->userdata;
+		tdi->runtime += ev->ts - c->ts;
+		printd("EVCPU %d\n", c->i);
+		top_threads_update(ctx, c->td);
+	}
+
+	/* Notice that ev->td is the new thread for ctxsw events */
+	c->td = ev->td;
+	c->ts = ev->ts;
+}
+
+static
+void
+ctxsw_prepare_post(void *_ctx)
+{
+	struct td_switch_ctx *ctx = _ctx;
+	struct cpu *cpus = ctx->cpus;
+	int i;
+
+	(void)evtr;
+	ctx->first_ts = -1;
+	ctx->last_ts = 0;
+	printd("first_ts[0] = %llu\n",cpus[0].first_ts);
+	for (i = 0; i < ctx->ncpus; ++i) {
+		printd("first_ts[%d] = %llu\n", i, cpus[i].first_ts);
+		if (cpus[i].first_ts && (cpus[i].first_ts < ctx->first_ts))
+			ctx->first_ts = cpus[i].first_ts;
+		if (cpus[i].last_ts && (cpus[i].last_ts > ctx->last_ts))
+			ctx->last_ts = cpus[i].last_ts;
+		cpus[i].td = NULL;
+		cpus[i].ts = 0;
+	}
+}
+
+static
+void
+ctxsw_draw_pre(void *_ctx)
+{
+	struct td_switch_ctx *ctx = _ctx;
+	struct svg_transform textrot;
+	char comm[100];
+	double y, height, fs;
+	int i, textlen;
+	struct evtr_thread *td;
+
+	textrot.tx = 0.0 - 0.2;	/* XXX */
+	textrot.sx = 1.0;
+	textrot.sy = 1.0;
+	textrot.rot = 270.0;
+
+	for (i = 0; i < ctx->nr_top_threads; ++i) {
+		td = ctx->top_threads[i];
+		rows_n(ctx->thread_rows, i, &y, &height);
+		svg_rect_draw(ctx->svg, ctx->inactive_rect, 0.0,
+			      y + ctx->thread_rows_yoff, ctx->width, height);
+		textlen = snprintf(comm, sizeof(comm) - 1, "%s (%p)",
+				   td->comm, td->id);
+		if (textlen > (int)sizeof(comm))
+			textlen = sizeof(comm) - 1;
+		comm[sizeof(comm) - 1] = '\0';
+		fs = fontsize_for_rect(height, 100.0, textlen);
+
+		textrot.ty = y + ctx->thread_rows_yoff + height;
+		svg_text_draw(ctx->svg, ctx->thread_label, &textrot,
+			      comm, fs);
+	}
+}
+
+static
+void
+ctxsw_draw_event(void *_ctx, evtr_event_t ev)
+{
+	struct td_switch_ctx *ctx = _ctx;
+	struct cpu *c = &ctx->cpus[ev->cpu];
+	int i;
+
+	/*
+	 * ctx->last_ts can be 0 if there were no events
+	 * in the specified interval, in which case
+	 * ctx->first_ts is invalid too.
+	 */
+	assert(!ctx->last_ts || (ev->ts >= ctx->first_ts));
+	printd("test2 (%llu:%llu) : %llu\n", ctx->interval_start,
+	       ctx->interval_end, ev->ts);
+	if ((ev->ts > ctx->interval_end) ||
+	    (ev->ts < ctx->interval_start))
+		return;
+	printd("SPEV %d\n", ev->cpu);
+	if (c->td != ev->td) {	/* thread switch (or preemption) */
+		draw_ctx_switch(ctx, c, ev);
+		/* XXX: this is silly */
+		for (i = 0; i < ctx->nr_top_threads; ++i) {
+			if (ctx->top_threads[i] == c->td) {
+				draw_thread_run(ctx, c, ev, i);
+				break;
+			}
+		}
+		c->td = ev->td;
+		c->ts = ev->ts;
+	}
+}
+
+static
+int
+cmd_svg(int argc, char **argv)
+{
+	svg_document_t svg;
+	int ncpus, i, ch;
+	double height, width;
+	struct rows cpu_rows, thread_rows;
+	struct cpu *cpus;
+	struct td_switch_ctx td_ctx;
+	struct evtr_filter ctxsw_filts[2] = {
+		{
+			.flags = 0,
+			.cpu = -1,
+		},
+		{
+			.flags = 0,
+			.cpu = -1,
+		},
+	};
+	struct pass_hook ctxsw_prepare = {
+		.pre = NULL,
+		.event = ctxsw_prepare_event,
+		.post = ctxsw_prepare_post,
+		.data = &td_ctx,
+		.filts = ctxsw_filts,
+		.nfilts = sizeof(ctxsw_filts)/sizeof(ctxsw_filts[0]),
+	}, ctxsw_draw = {
+		.pre = ctxsw_draw_pre,
+		.event = ctxsw_draw_event,
+		.post = NULL,
+		.data = &td_ctx,
+		.filts = ctxsw_filts,
+		.nfilts = sizeof(ctxsw_filts)/sizeof(ctxsw_filts[0]),
+	};
+
+	/*
+	 * We are interested in thread switch and preemption
+	 * events, but we don't use the data directly. Instead
+	 * we rely on ev->td.
+	 */
+	ctxsw_filts[0].fmt = "sw %p > %p";
+	ctxsw_filts[1].fmt = "pre %p > %p";
+	td_ctx.interval_start = 0;
+	td_ctx.interval_end = -1;	/* i.e. no interval given */
+	td_ctx.nr_top_threads = NR_TOP_THREADS;
+
+	printd("argc: %d, argv[0] = %s\n", argc, argv[0] ? argv[0] : "NULL");
+	optind = 0;
+	optreset = 1;
+	while ((ch = getopt(argc, argv, "i:")) != -1) {
+		switch (ch) {
+		case 'i':
+			if (sscanf(optarg, "%llu:%llu", &td_ctx.interval_start,
+				   &td_ctx.interval_end) != 2) {
+				usage();
+			}
+			break;
+		default:
+			usage();
+		}
+
+	}
+	argc -= optind;
+	argv += optind;
+
+	height = 200.0;
+	width = 800.0;
+	td_ctx.width = width;
+	if ((ncpus = evtr_ncpus(evtr)) <= 0)
+		err(1, "No cpu information!\n");
+	printd("evtranalyze: ncpus %d\n", ncpus);
+
+	if (!(cpus = malloc(ncpus * sizeof(struct cpu))))
+		err(1, "Can't allocate memory\n");
+	/* initialize cpu array */
+	for (i = 0; i < ncpus; ++i) {
+		cpus[i].td = NULL;
+		cpus[i].ts = 0;
+		cpus[i].i = i;
+		cpus[i].first_ts = 0;
+		cpus[i].last_ts = 0;
+	}
+	td_ctx.cpus = cpus;
+	td_ctx.ncpus = ncpus;
+	if (!(td_ctx.top_threads = calloc(td_ctx.nr_top_threads, sizeof(struct evtr_thread *))))
+		err(1, "Can't allocate memory\n");
+	if (!(svg = svg_document_create("output.svg")))
+		err(1, "Can't open svg document\n");
+
+	/*
+	 * Create rectangles to use for output.
+	 */
+	if (!(td_ctx.cpu_sw_rect = svg_rect_new("generic")))
+		err(1, "Can't create rectangle\n");
+	if (!(td_ctx.thread_rect = svg_rect_new("thread")))
+		err(1, "Can't create rectangle\n");
+	if (!(td_ctx.inactive_rect = svg_rect_new("inactive")))
+		err(1, "Can't create rectangle\n");
+	/* text for thread names */
+	if (!(td_ctx.thread_label = svg_text_new("generic")))
+		err(1, "Can't create text\n");
+	rows_init(&cpu_rows, ncpus, height, 0.9);
+	td_ctx.svg = svg;
+	td_ctx.xscale = -1.0;
+	td_ctx.cpu_rows = &cpu_rows;
+
+	do_pass(&ctxsw_prepare, 1);
+	td_ctx.thread_rows_yoff = height;
+	td_ctx.thread_rows = &thread_rows;
+	rows_init(td_ctx.thread_rows, td_ctx.nr_top_threads, 300, 0.9);
+	td_ctx.xscale = width / (td_ctx.last_ts - td_ctx.first_ts);
+	printd("first %llu, last %llu, xscale %lf\n", td_ctx.first_ts,
+	       td_ctx.last_ts, td_ctx.xscale);
+
+	do_pass(&ctxsw_draw, 1);
+
+	svg_document_close(svg);
+	return 0;
+}
+
+static
+int
+cmd_show(int argc, char **argv)
+{
+	struct evtr_event ev;
+	struct evtr_query *q;
+	struct evtr_filter filt;
+	int ch;
+
+	filt.fmt = NULL;
+	optind = 0;
+	optreset = 1;
+	while ((ch = getopt(argc, argv, "f:")) != -1) {
+		switch (ch) {
+		case 'f':
+			filt.fmt = optarg;
+			break;
+		}
+	}
+	filt.flags = 0;
+	filt.cpu = -1;
+	printd("fmt = %s\n", filt.fmt ? filt.fmt : "NULL");
+	q = evtr_query_init(evtr, &filt, 1);
+	if (!q)
+		err(1, "Can't initialize query\n");
+	while(!evtr_query_next(q, &ev)) {
+		char buf[1024];
+		printf("timestamp = %llu, cpu = %d, file = %s, "
+		       "line = %d, tdcomm =\"%s\", fmt=\"%s\"", ev.ts, ev.cpu,
+		       ev.file, ev.line, ev.td ? ev.td->comm : "n/a",
+		       ev.fmt ? ev.fmt : "no fmt");
+		if (ev.fmt) {
+			evtr_event_data(&ev, buf, sizeof(buf));
+			printf(" data=\"%s\"\n", buf);
+		} else {
+			printf("\n");
+		}
+	}
+	if (evtr_error(evtr)) {
+		err(1, evtr_errmsg(evtr));
+	}
+	evtr_query_destroy(q);
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	int ch;
+	FILE *inf;
+	struct command *cmd;
+
+	while ((ch = getopt(argc, argv, "f:")) != -1) {
+		switch (ch) {
+		case 'f':
+			opt_infile = optarg;
+			break;
+		default:
+			usage();
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (argc == 0) {
+		err(2, "need to specify a command\n");
+	}
+	if (!opt_infile || !strcmp(opt_infile, "-")) {
+		inf = stdin;
+	} else {
+		inf = fopen(opt_infile, "r");
+		if (!inf) {
+			err(2, "Can't open input file\n");
+		}
+	}
+
+	if (!(evtr = evtr_open_read(inf))) {
+		err(1, "Can't open evtr stream\n");
+	}
+
+
+	for (cmd = commands; cmd->name != NULL; ++cmd) {
+		if (strcmp(argv[0], cmd->name))
+			continue;
+		cmd->func(argc, argv);
+		break;
+	}
+	if (!cmd->name) {
+		err(2, "no such command: %s\n", argv[0]);
+	}
+		
+	evtr_close(evtr);
+	return 0;
+}
diff --git a/usr.bin/evtranalyze/svg.c b/usr.bin/evtranalyze/svg.c
new file mode 100644
index 0000000..b625394
--- /dev/null
+++ b/usr.bin/evtranalyze/svg.c
@@ -0,0 +1,314 @@
+#include <assert.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "xml.h"
+#include "svg.h"
+
+enum {
+	MAX_VALSTR_LEN = 30,
+};
+
+struct svg_rect {
+	struct xml_element el;
+	struct xml_attribute x, y, w, h, cl;
+	char x_val[MAX_VALSTR_LEN];
+	char y_val[MAX_VALSTR_LEN];
+	char w_val[MAX_VALSTR_LEN];
+	char h_val[MAX_VALSTR_LEN];
+};
+
+struct svg_text {
+	struct xml_element el;
+	struct xml_attribute x, y, cl;
+	struct xml_attribute fontsize, transform;
+	char x_val[MAX_VALSTR_LEN];
+	char y_val[MAX_VALSTR_LEN];
+	char fontsize_val[MAX_VALSTR_LEN];
+	char transform_val[MAX_VALSTR_LEN * 4];
+};
+
+struct svg_line {
+	struct xml_element el;
+	struct xml_attribute x1, y1, x2, y2, cl;
+	struct xml_attribute transform;
+	char x1_val[MAX_VALSTR_LEN], y1_val[MAX_VALSTR_LEN];
+	char x2_val[MAX_VALSTR_LEN], y2_val[MAX_VALSTR_LEN];
+	char transform_val[MAX_VALSTR_LEN * 6];
+};
+
+struct svg_document {
+	xml_document_t xml;
+	const char *css;
+	struct xml_element svg;
+	struct xml_attribute svg_attrs[2];
+	struct svg_text text;
+};
+
+static char default_css[] =
+	"<![CDATA["
+	"rect.generic { fill: green; stroke: black; stroke-width: 0.01;}"
+	"rect.thread { fill: yellow; stroke: black; stroke-width: 0.01;}"
+	"rect.inactive { fill: grey; stroke: black; stroke-width: 0.01;}"
+	"text.generic { fill: black; stroke: none;}]]>";
+
+static struct svg_transform noop_transform = {
+	.tx = 0.0,
+	.ty = 0.0,
+	.sx = 1.0,
+	.sy = 1.0,
+	.rot = 0.0,
+};
+
+static
+int
+svg_transform_print(svg_transform_t tf, char *buf, size_t len)
+{
+	static double eps = 0.0001;
+	char *p;
+	int c;
+
+	if (!tf) {
+		assert(len >= 1);
+		buf[0] = '\0';
+		return 0;
+	}
+	p = buf;
+	if ((fabs(tf->tx) > eps) && (fabs(tf->ty) > eps)) {
+		c = snprintf(buf, len, "translate(%.20lf,%.20lf)", tf->tx,
+			     tf->ty);
+		len -= c;
+		if (len <= 0)
+			return !0;
+		p += c;
+	}
+	if ((fabs(tf->sx - 1) > eps) && (fabs(tf->sy - 1) > eps)) {
+		c = snprintf(p, len, "%sscale(%.20lf,%.20lf)",
+			     (p == buf) ? "" : " ", tf->sx, tf->sy);
+		len -= c;
+		if (len <= 0)
+			return !0;
+		p += c;
+	}
+	if (fabs(tf->rot) > eps) {
+		c = snprintf(p, len, "%srotate(%.2lf)",
+			     (p == buf) ? "" : " ", tf->rot);
+		len -= c;
+		if (len <= 0)
+			return !0;
+		p += c;
+	}
+	return 0;
+}
+
+static
+void
+svg_rect_init(struct svg_rect *rect, const char *cl)
+{
+	xml_elem_init(&rect->el, "rect");
+	xml_attribute_init(&rect->x, "x", NULL);
+	xml_elem_set_attribute(&rect->el, &rect->x);
+	xml_attribute_init(&rect->y, "y", NULL);
+	xml_elem_set_attribute(&rect->el, &rect->y);
+	xml_attribute_init(&rect->w, "width", NULL);
+	xml_elem_set_attribute(&rect->el, &rect->w);
+	xml_attribute_init(&rect->h, "height", NULL);
+	xml_elem_set_attribute(&rect->el, &rect->h);
+	if (cl) {
+		xml_attribute_init(&rect->cl, "class", cl);
+		xml_elem_set_attribute(&rect->el, &rect->cl);
+	}
+}
+
+/*
+ * In the future, we might want to stick the rectangle in the
+ * <defs> element at this point and then <use> it in the rest
+ * of the document.
+ */
+struct svg_rect *
+svg_rect_new(const char *cl)
+{
+	struct svg_rect *r;
+
+	if (!(r = malloc(sizeof(*r))))
+		return r;
+	svg_rect_init(r, cl);
+	return r;
+}
+
+
+int
+svg_rect_draw(svg_document_t doc, struct svg_rect *rect, double x,
+	      double y, double w, double h)
+{
+	snprintf(&rect->x_val[0], sizeof(rect->x_val), "%.20lf", x);
+	xml_attribute_set_value(&rect->x, &rect->x_val[0]);
+	snprintf(&rect->y_val[0], sizeof(rect->y_val), "%lf", y);
+	xml_attribute_set_value(&rect->y, &rect->y_val[0]);
+	snprintf(&rect->w_val[0], sizeof(rect->w_val), "%.20lf", w);
+	xml_attribute_set_value(&rect->w, &rect->w_val[0]);
+	snprintf(&rect->h_val[0], sizeof(rect->h_val), "%lf", h);
+	xml_attribute_set_value(&rect->h, &rect->h_val[0]);
+
+	xml_elem_closed(doc->xml, &rect->el);
+	return 0;
+}
+
+static
+void
+svg_text_init(struct svg_text *text, const char *cl)
+{
+	xml_elem_init(&text->el, "text");
+#if remove
+	xml_attribute_init(&text->x, "x", NULL);
+	xml_elem_set_attribute(&text->el, &text->x);
+	xml_attribute_init(&text->y, "y", NULL);
+	xml_elem_set_attribute(&text->el, &text->y);
+#endif
+	xml_attribute_init(&text->fontsize, "font-size", NULL);
+	xml_elem_set_attribute(&text->el, &text->fontsize);
+	xml_attribute_init(&text->transform, "transform", NULL);
+	xml_elem_set_attribute(&text->el, &text->transform);
+
+	if (cl) {
+		xml_attribute_init(&text->cl, "class", cl);
+		xml_elem_set_attribute(&text->el, &text->cl);
+	}
+
+}
+
+struct svg_text *
+svg_text_new(const char *cl)
+{
+	svg_text_t text;
+
+	if (!(text = malloc(sizeof(*text))))
+		return text;
+	svg_text_init(text, cl);
+	return text;
+}
+
+int
+svg_text_draw(svg_document_t doc, svg_text_t text, svg_transform_t tf,
+	      const char *str, double fontsize)
+{
+#if remove
+	snprintf(&text->x_val[0], sizeof(text->x_val), "%.20lf", x);
+	xml_attribute_set_value(&text->x, &text->x_val[0]);
+	snprintf(&text->y_val[0], sizeof(text->y_val), "%.20lf", y);
+	xml_attribute_set_value(&text->y, &text->y_val[0]);
+#endif
+	snprintf(&text->fontsize_val[0], sizeof(text->fontsize_val), "%.20lf",
+		 fontsize);
+	xml_attribute_set_value(&text->fontsize, &text->fontsize_val[0]);
+	if (svg_transform_print(tf, &text->transform_val[0],
+				sizeof(text->transform_val)))
+		return !0;
+	xml_attribute_set_value(&text->transform, &text->transform_val[0]);
+	xml_elem_set_value(&text->el, str);
+
+	xml_elem_closed(doc->xml, &text->el);
+	return 0;
+}
+
+static
+void
+svg_line_init(struct svg_line *line, const char *cl)
+{
+	xml_elem_init(&line->el, "line");
+	xml_attribute_init(&line->x1, "x1", NULL);
+	xml_elem_set_attribute(&line->el, &line->x1);
+	xml_attribute_init(&line->x2, "x2", NULL);
+	xml_elem_set_attribute(&line->el, &line->x2);
+	xml_attribute_init(&line->y1, "y1", NULL);
+	xml_elem_set_attribute(&line->el, &line->y1);
+	xml_attribute_init(&line->y2, "y2", NULL);
+	xml_elem_set_attribute(&line->el, &line->y2);
+
+	xml_attribute_init(&line->transform, "transform", NULL);
+	xml_elem_set_attribute(&line->el, &line->transform);
+
+	if (cl) {
+		xml_attribute_init(&line->cl, "class", cl);
+		xml_elem_set_attribute(&line->el, &line->cl);
+	}
+
+}
+
+struct svg_line *
+svg_line_new(const char *cl)
+{
+	svg_line_t line;
+
+	if (!(line = malloc(sizeof(*line))))
+		return line;
+	svg_line_init(line, cl);
+	return line;
+}
+
+int
+svg_line_draw(svg_document_t doc, svg_line_t line, double x1, double _y1,
+	      double x2, double y2, svg_transform_t tf)
+{
+	snprintf(&line->x1_val[0], sizeof(line->x1_val), "%.20lf", x1);
+	xml_attribute_set_value(&line->x1, &line->x1_val[0]);
+
+	snprintf(&line->x2_val[0], sizeof(line->x2_val), "%.20lf", x2);
+	xml_attribute_set_value(&line->x2, &line->x2_val[0]);
+
+	snprintf(&line->y1_val[0], sizeof(line->y1_val), "%.10lf", _y1);
+	xml_attribute_set_value(&line->y1, &line->y1_val[0]);
+
+	snprintf(&line->y2_val[0], sizeof(line->y2_val), "%.20lf", y2);
+	xml_attribute_set_value(&line->y2, &line->y2_val[0]);
+
+	xml_attribute_set_value(&line->transform, &line->transform_val[0]);
+	if (svg_transform_print(tf,
+				&line->transform_val[0],
+				sizeof(line->transform_val)))
+		return !0;
+	xml_elem_closed(doc->xml, &line->el);
+	return 0;
+}
+
+svg_document_t
+svg_document_create(const char *path)
+{
+	svg_document_t svg;
+	struct xml_element style, defs;
+	struct xml_attribute type;
+
+	if (!(svg = malloc(sizeof(*svg))))
+		return NULL;
+	if (!(svg->xml = xml_document_create(path))) {
+		free(svg);
+		return NULL;
+	}
+	svg->css = &default_css[0];
+	xml_attribute_init(&type, "type", "text/css");
+	xml_elem_init(&defs, "defs");
+	xml_elem_init(&style, "style");
+	xml_elem_set_attribute(&style, &type);
+	xml_elem_init(&svg->svg, "svg");
+	xml_attribute_init(&svg->svg_attrs[0], "version", "1.1");
+	xml_elem_set_attribute(&svg->svg, &svg->svg_attrs[0]);
+	xml_attribute_init(&svg->svg_attrs[1], "xmlns",
+			   "http://www.w3.org/2000/svg");
+	xml_elem_set_attribute(&svg->svg, &svg->svg_attrs[1]);
+	xml_elem_begin(svg->xml, &svg->svg);
+	xml_elem_begin(svg->xml, &defs);
+	xml_elem_set_value(&style, svg->css);
+	xml_elem_closed(svg->xml, &style);
+	xml_elem_close(svg->xml, &defs);
+	
+	return svg;
+}
+
+int
+svg_document_close(svg_document_t svg)
+{
+	xml_elem_close(svg->xml, &svg->svg);
+	xml_document_close(svg->xml);
+	return 0;
+}
diff --git a/usr.bin/evtranalyze/svg.h b/usr.bin/evtranalyze/svg.h
new file mode 100644
index 0000000..eeec4be
--- /dev/null
+++ b/usr.bin/evtranalyze/svg.h
@@ -0,0 +1,33 @@
+#ifndef SVG_H
+#define SVG_H
+
+#include <stdint.h>
+
+struct svg_document;
+struct svg_rect;
+struct svg_text;
+struct svg_line;
+typedef struct svg_document *svg_document_t;
+typedef struct svg_rect *svg_rect_t;
+typedef struct svg_text *svg_text_t;
+typedef struct svg_line *svg_line_t;
+
+typedef struct svg_transform {
+	double tx, ty;
+	double sx, sy;
+	double rot;
+} *svg_transform_t;
+
+svg_document_t svg_document_create(const char *);
+int svg_document_close(svg_document_t);
+struct svg_rect *svg_rect_new(const char *);
+int svg_rect_draw(svg_document_t, svg_rect_t, double, double, double,
+		  double);
+struct svg_text *svg_text_new(const char *);
+int svg_text_draw(svg_document_t, svg_text_t, svg_transform_t,
+	      const char *, double);
+struct svg_line *svg_line_new(const char *);
+int svg_line_draw(svg_document_t, svg_line_t, double, double, double, double,
+		  svg_transform_t);
+
+#endif	/* SVG_H */
diff --git a/usr.bin/evtranalyze/xml.c b/usr.bin/evtranalyze/xml.c
new file mode 100644
index 0000000..616b18a
--- /dev/null
+++ b/usr.bin/evtranalyze/xml.c
@@ -0,0 +1,144 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "xml.h"
+
+
+xml_document_t
+xml_document_create(const char *file)
+{
+	xml_document_t doc;
+
+	if ((doc = malloc(sizeof(xml_document_t))) == NULL)
+		return (NULL);
+
+	if ((doc->file = fopen(file, "w")) == NULL) {
+		free(doc);
+		return (NULL);
+	}
+	STAILQ_INIT(&doc->open_elems);
+	doc->nr_open = 0;
+	doc->errmsg = NULL;
+
+	fprintf(doc->file, "<?xml version=\"1.0\" encoding=\"UTF-8\" "
+		"standalone=\"no\"?>\n");
+	fprintf(doc->file, "<!-- Created by evtranalyze -->\n");
+
+	return doc;
+}
+
+int
+xml_document_close(xml_document_t doc)
+{
+	fclose(doc->file);
+	return 0;
+}
+
+
+static
+void
+indent(xml_document_t doc)
+{
+	int i;
+
+	for (i = 0; i < doc->nr_open; ++i) {
+		fprintf(doc->file, "  ");
+	}
+}
+
+#if 0
+int
+xml_elem_compile(xml_element_t el)
+{
+	char *buf, *p;
+	int bufsize, c, ret;
+
+	bufsize = 2;
+	if (!(buf = malloc(bufsize))) {
+		return !0;
+	}
+again_name:
+	p = buf;
+	ret = snprintf(p, sizeof(buf), "<%s ", el->name);
+	if (ret > sizeof(buf)) {
+		bufsize *= 2;
+		buf = realloc(bufsize);
+		if (!buf) {
+			free(p);
+			return !0;
+		}
+		goto again_name;
+	}
+	c += ret;
+}
+#endif
+
+static
+int
+xml_elem_print(xml_document_t doc, xml_element_t el, int closed, int nl)
+{
+	xml_attribute_t at;
+	fprintf(doc->file, "<%s", el->name);
+	STAILQ_FOREACH(at, &el->attributes, next) {
+		fprintf(doc->file, " %s=\"%s\"", at->name, at->value);
+	}
+	fprintf(doc->file, "%s%s", closed ? "/>" : ">", nl ? "\n" : "");
+	return 0;
+}
+
+static
+int
+_xml_elem_begin(xml_document_t doc, xml_element_t el, int closed, int nl)
+{
+	STAILQ_INSERT_HEAD(&doc->open_elems, el, link);
+	indent(doc);
+	++doc->nr_open;
+	xml_elem_print(doc, el, closed, nl);
+	return 0;
+}
+
+static
+int
+_xml_elem_close(xml_document_t doc, xml_element_t el, int do_indent)
+{
+	if (el != STAILQ_FIRST(&doc->open_elems)) {
+		return !0;
+	}
+
+	STAILQ_REMOVE_HEAD(&doc->open_elems, link);
+	--doc->nr_open;
+	if (do_indent)
+		indent(doc);
+	fprintf(doc->file, "</%s>\n", el->name);
+	return 0;
+}
+
+int
+xml_elem_close(xml_document_t doc, xml_element_t el)
+{
+	return _xml_elem_close(doc, el, !0);
+}
+
+int
+xml_elem_begin(xml_document_t doc, xml_element_t el)
+{
+	if (el->value) {
+		return !0;
+	}
+	return _xml_elem_begin(doc, el, 0, !0);
+}
+
+int
+xml_elem_closed(xml_document_t doc, xml_element_t el)
+{
+	if (el->value) {
+		_xml_elem_begin(doc, el, 0, 0);
+		fprintf(doc->file, "%s", el->value);
+		_xml_elem_close(doc, el, 0);
+		return 0;
+	}
+	indent(doc);
+	return xml_elem_print(doc, el, !0, !0);
+}
+
diff --git a/usr.bin/evtranalyze/xml.h b/usr.bin/evtranalyze/xml.h
new file mode 100644
index 0000000..8f4a8e9
--- /dev/null
+++ b/usr.bin/evtranalyze/xml.h
@@ -0,0 +1,73 @@
+#ifndef _EVTRANALYZE_XML_H_
+#define _EVTRANALYZE_XML_H_
+
+#include <stdio.h>
+#include <sys/queue.h>
+
+
+typedef struct xml_attribute {
+	const char    *name;
+	const char    *value;
+	STAILQ_ENTRY(xml_attribute) next;
+} *xml_attribute_t;
+
+typedef struct xml_element {
+	const char    *name;
+	const char    *value;
+	STAILQ_HEAD(, xml_attribute) attributes;
+	STAILQ_ENTRY(xml_element) link;
+} *xml_element_t;
+
+typedef struct xml_document {
+	FILE    *file;
+	STAILQ_HEAD(, xml_element) open_elems;
+	int nr_open;
+	const char *errmsg;
+} *xml_document_t;
+
+static inline
+void
+xml_elem_init(xml_element_t el, const char *name)
+{
+	el->name = name;
+	el->value = NULL;
+	STAILQ_INIT(&el->attributes);
+}
+
+static inline
+void
+xml_elem_set_value(xml_element_t el, const char *value)
+{
+	el->value = value;
+}
+
+static inline
+void
+xml_attribute_init(xml_attribute_t at, const char *name, const char *value)
+{
+	at->name = name;
+	at->value = value;
+}
+
+static inline
+void
+xml_attribute_set_value(xml_attribute_t at, const char *value)
+{
+	at->value = value;
+}
+
+static inline
+void
+xml_elem_set_attribute(xml_element_t el, xml_attribute_t at)
+{
+	STAILQ_INSERT_TAIL(&el->attributes, at, next);
+}
+
+
+xml_document_t xml_document_create(const char *);
+int xml_document_close(xml_document_t);
+int xml_elem_closed(xml_document_t, xml_element_t);
+int xml_elem_begin(xml_document_t, xml_element_t);
+int xml_elem_close(xml_document_t, xml_element_t);
+
+#endif /* !_EVTRANALYZE_XML_H_ */
diff --git a/usr.bin/ktrdump/Makefile b/usr.bin/ktrdump/Makefile
index 806fa6b..72a74b4 100644
--- a/usr.bin/ktrdump/Makefile
+++ b/usr.bin/ktrdump/Makefile
@@ -2,8 +2,8 @@
 # $FreeBSD: src/usr.bin/ktrdump/Makefile,v 1.3 2002/06/06 11:27:03 ru Exp $
 
 PROG=	ktrdump
-DPADD=	${LIBKVM}
-LDADD=	-lkvm
+DPADD=	${LIBKVM} ${LIBEVTR}
+LDADD=	-lkvm -levtr
 MAN=	ktrdump.8
 
 .include <bsd.prog.mk>
diff --git a/usr.bin/ktrdump/ktrdump.8 b/usr.bin/ktrdump/ktrdump.8
index 5e5f45c..66909f1 100644
--- a/usr.bin/ktrdump/ktrdump.8
+++ b/usr.bin/ktrdump/ktrdump.8
@@ -34,7 +34,7 @@
 .Nd print kernel ktr trace buffer
 .Sh SYNOPSIS
 .Nm
-.Op Fl acfilnpqrstx
+.Op Fl acdfilnpqrstx
 .Op Fl A Ar factor
 .Op Fl N Ar execfile
 .Op Fl M Ar corefile
@@ -61,6 +61,11 @@ Note that
 is not included.
 .It Fl c
 Print the CPU number that each entry was logged from.
+.It Fl d
+Dump an event stream to the file specified with
+.Fl o .
+This stream can be examined with
+.Xr ktrdump 8 .
 .It Fl f
 Print the file and line number that each entry was logged from.
 .It Fl i
diff --git a/usr.bin/ktrdump/ktrdump.c b/usr.bin/ktrdump/ktrdump.c
index 31020fb..a5612a9 100644
--- a/usr.bin/ktrdump/ktrdump.c
+++ b/usr.bin/ktrdump/ktrdump.c
@@ -46,6 +46,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
+#include <evtr.h>
 
 #define	SBUFLEN	128
 
@@ -80,7 +81,15 @@ static struct nlist nl_version_ktr_cpu[] = {
 	{ .n_name = NULL }
 };
 
+struct save_ctx {
+	char save_buf[128];
+	const void *save_kptr;
+};
+
+typedef void (*ktr_iter_cb_t)(void *, int, int, struct ktr_entry *, uint64_t *);
+
 static int cflag;
+static int dflag;
 static int fflag;
 static int iflag;
 static int lflag;
@@ -108,18 +117,21 @@ static int ktr_version;
 
 static void usage(void);
 static int earliest_ts(struct ktr_buffer *);
+static void dump_machine_info(evtr_t);
 static void print_header(FILE *, int);
 static void print_entry(FILE *, int, int, struct ktr_entry *, u_int64_t *);
-static struct ktr_info *kvm_ktrinfo(void *);
-static const char *kvm_string(const char *);
+static void print_callback(void *, int, int, struct ktr_entry *, uint64_t *);
+static void dump_callback(void *, int, int, struct ktr_entry *, uint64_t *);
+static struct ktr_info *kvm_ktrinfo(void *, struct save_ctx *);
+static const char *kvm_string(const char *, struct save_ctx *);
 static const char *trunc_path(const char *, int);
 static void read_symbols(const char *);
-static const char *address_to_symbol(void *);
+static const char *address_to_symbol(void *, struct save_ctx *);
 static struct ktr_buffer *ktr_bufs_init(void);
 static void get_indices(struct ktr_entry **, int *);
 static void load_bufs(struct ktr_buffer *, struct ktr_entry **, int *);
-static void print_buf(FILE *, struct ktr_buffer *, int, u_int64_t *);
-static void print_bufs_timesorted(FILE *, struct ktr_buffer *, u_int64_t *);
+static void iterate_buf(FILE *, struct ktr_buffer *, int, u_int64_t *, ktr_iter_cb_t);
+static void iterate_bufs_timesorted(FILE *, struct ktr_buffer *, u_int64_t *, ktr_iter_cb_t);
 
 
 /*
@@ -130,8 +142,10 @@ main(int ac, char **av)
 {
 	struct ktr_buffer *ktr_bufs;
 	struct ktr_entry **ktr_kbuf;
+	ktr_iter_cb_t callback = &print_callback;
 	int *ktr_idx;
 	FILE *fo;
+	void *ctx;
 	int64_t tts;
 	int *ktr_start_index;
 	int c;
@@ -141,7 +155,7 @@ main(int ac, char **av)
 	 * Parse commandline arguments.
 	 */
 	fo = stdout;
-	while ((c = getopt(ac, av, "acfinqrtxpslA:N:M:o:")) != -1) {
+	while ((c = getopt(ac, av, "acfinqrtxpslA:N:M:o:d")) != -1) {
 		switch (c) {
 		case 'a':
 			cflag = 1;
@@ -155,6 +169,10 @@ main(int ac, char **av)
 		case 'c':
 			cflag = 1;
 			break;
+		case 'd':
+			dflag = 1;
+			callback = &dump_callback;
+			break;
 		case 'N':
 			if (strlcpy(execfile, optarg, sizeof(execfile))
 			    >= sizeof(execfile))
@@ -209,6 +227,13 @@ main(int ac, char **av)
 			usage();
 		}
 	}
+	ctx = fo;
+	if (dflag) {
+		ctx = evtr_open_write(fo);
+		if (!ctx) {
+			err(1, "Can't create event stream");
+		}
+	}
 	if (cflag + iflag + tflag + xflag + fflag + pflag == 0) {
 		cflag = 1;
 		iflag = 1;
@@ -252,6 +277,9 @@ main(int ac, char **av)
 
 	printf("TSC frequency is %6.3f MHz\n", tsc_frequency / 1000000.0);
 
+	if (dflag) {
+		dump_machine_info((evtr_t)ctx);
+	}
 	ktr_kbuf = calloc(ncpus, sizeof(*ktr_kbuf));
 	ktr_idx = calloc(ncpus, sizeof(*ktr_idx));
 
@@ -274,7 +302,8 @@ main(int ac, char **av)
 		u_int64_t last_timestamp = 0;
 		do {
 			load_bufs(ktr_bufs, ktr_kbuf, ktr_idx);
-			print_bufs_timesorted(fo, ktr_bufs, &last_timestamp);
+			iterate_bufs_timesorted(ctx, ktr_bufs, &last_timestamp,
+						callback);
 			if (lflag)
 				usleep(1000000 / 10);
 		} while (lflag);
@@ -283,14 +312,29 @@ main(int ac, char **av)
 		do {
 			load_bufs(ktr_bufs, ktr_kbuf, ktr_idx);
 			for (n = 0; n < ncpus; ++n)
-				print_buf(fo, ktr_bufs, n, &last_timestamp[n]);
+				iterate_buf(ctx, ktr_bufs, n, &last_timestamp[n],
+					callback);
 			if (lflag)
 				usleep(1000000 / 10);
 		} while (lflag);
 	}
+	if (dflag)
+		evtr_close(ctx);
 	return (0);
 }
 
+static
+void
+dump_machine_info(evtr_t evtr)
+{
+	struct evtr_event ev;
+
+	ev.type = EVTR_TYPE_CPUINFO;
+	ev.ncpus = ncpus;
+
+	evtr_dump_event(evtr, &ev);
+}
+
 static void
 print_header(FILE *fo, int row)
 {
@@ -321,6 +365,8 @@ print_entry(FILE *fo, int n, int row, struct ktr_entry *entry,
 	    u_int64_t *last_timestamp)
 {
 	struct ktr_info *info = NULL;
+	static struct save_ctx nctx, pctx, fmtctx, symctx, infoctx;
+
 
 	fprintf(fo, " %06x ", row & 0x00FFFFFF);
 	if (cflag)
@@ -343,71 +389,111 @@ print_entry(FILE *fo, int n, int row, struct ktr_entry *entry,
 			    entry->ktr_caller2, entry->ktr_caller1);
 		} else {
 		    fprintf(fo, "%-20s ", 
-			    address_to_symbol(entry->ktr_caller2));
+			    address_to_symbol(entry->ktr_caller2, &symctx));
 		    fprintf(fo, "%-20s ", 
-			    address_to_symbol(entry->ktr_caller1));
+			    address_to_symbol(entry->ktr_caller1, &symctx));
 		}
 	}
 	if (iflag) {
-		info = kvm_ktrinfo(entry->ktr_info);
+		info = kvm_ktrinfo(entry->ktr_info, &infoctx);
 		if (info)
-			fprintf(fo, "%-20s ", kvm_string(info->kf_name));
+			fprintf(fo, "%-20s ", kvm_string(info->kf_name, &nctx));
 		else
 			fprintf(fo, "%-20s ", "<empty>");
 	}
 	if (fflag)
-		fprintf(fo, "%34s:%-4d ", trunc_path(kvm_string(entry->ktr_file), 34), entry->ktr_line);
+		fprintf(fo, "%34s:%-4d ",
+			trunc_path(kvm_string(entry->ktr_file, &pctx), 34),
+			entry->ktr_line);
 	if (pflag) {
 		if (info == NULL)
-			info = kvm_ktrinfo(entry->ktr_info);
+			info = kvm_ktrinfo(entry->ktr_info, &infoctx);
 		if (info)
-			vfprintf(fo, kvm_string(info->kf_format), (void *)&entry->ktr_data);
+			vfprintf(fo, kvm_string(info->kf_format, &fmtctx),
+				 (void *)&entry->ktr_data);
 	}
 	fprintf(fo, "\n");
 	*last_timestamp = entry->ktr_timestamp;
 }
 
 static
+void
+print_callback(void *ctx, int n, int row, struct ktr_entry *entry, uint64_t *last_ts)
+{
+	FILE *fo = (FILE *)ctx;
+	print_header(fo, row);
+	print_entry(fo, n, row, entry, last_ts);
+}
+
+
+static
+void
+dump_callback(void *ctx, int n, int row __unused, struct ktr_entry *entry,
+	      uint64_t *last_ts __unused)
+{
+	evtr_t evtr = (evtr_t)ctx;
+	struct evtr_event ev;
+	static struct save_ctx pctx, fmtctx, infoctx;
+	struct ktr_info *ki;
+
+	ev.ts = entry->ktr_timestamp;
+	ev.type = EVTR_TYPE_PROBE;
+	ev.line = entry->ktr_line;
+	ev.file = kvm_string(entry->ktr_file, &pctx);
+	ev.func = NULL;
+	ev.cpu = n;
+	if ((ki = kvm_ktrinfo(entry->ktr_info, &infoctx))) {
+		ev.fmt = kvm_string(ki->kf_format, &fmtctx);
+		ev.fmtdata = entry->ktr_data;
+		ev.fmtdatalen = ki->kf_data_size;
+	} else {
+		ev.fmt = ev.fmtdata = NULL;
+		ev.fmtdatalen = 0;
+	}
+		
+	if (evtr_dump_event(evtr, &ev)) {
+		err(1, evtr_errmsg(evtr));
+	}
+}
+
+static
 struct ktr_info *
-kvm_ktrinfo(void *kptr)
+kvm_ktrinfo(void *kptr, struct save_ctx *ctx)
 {
-	static struct ktr_info save_info;
-	static void *save_kptr;
+	struct ktr_info *ki = (void *)ctx->save_buf;
 
 	if (kptr == NULL)
 		return(NULL);
-	if (save_kptr != kptr) {
-		if (kvm_read(kd, (uintptr_t)kptr, &save_info, sizeof(save_info)) == -1) {
-			bzero(&save_info, sizeof(save_info));
+	if (ctx->save_kptr != kptr) {
+		if (kvm_read(kd, (uintptr_t)kptr, ki, sizeof(*ki)) == -1) {
+			bzero(&ki, sizeof(*ki));
 		} else {
-			save_kptr = kptr;
+			ctx->save_kptr = kptr;
 		}
 	}
-	return(&save_info);
+	return(ki);
 }
 
 static
 const char *
-kvm_string(const char *kptr)
+kvm_string(const char *kptr, struct save_ctx *ctx)
 {
-	static char save_str[128];
-	static const char *save_kptr;
 	u_int l;
 	u_int n;
 
 	if (kptr == NULL)
 		return("?");
-	if (save_kptr != kptr) {
-		save_kptr = kptr;
+	if (ctx->save_kptr != (const void *)kptr) {
+		ctx->save_kptr = (const void *)kptr;
 		l = 0;
-		while (l < sizeof(save_str) - 1) {
+		while (l < sizeof(ctx->save_buf) - 1) {
 			n = 256 - ((intptr_t)(kptr + l) & 255);
-			if (n > sizeof(save_str) - l - 1)
-				n = sizeof(save_str) - l - 1;
-			if (kvm_read(kd, (uintptr_t)(kptr + l), save_str + l, n) < 0)
+			if (n > sizeof(ctx->save_buf) - l - 1)
+				n = sizeof(ctx->save_buf) - l - 1;
+			if (kvm_read(kd, (uintptr_t)(kptr + l), ctx->save_buf + l, n) < 0)
 				break;
-			while (l < sizeof(save_str) && n) {
-			    if (save_str[l] == 0)
+			while (l < sizeof(ctx->save_buf) && n) {
+			    if (ctx->save_buf[l] == 0)
 				    break;
 			    --n;
 			    ++l;
@@ -415,9 +501,9 @@ kvm_string(const char *kptr)
 			if (n)
 			    break;
 		}
-		save_str[l] = 0;
+		ctx->save_buf[l] = 0;
 	}
-	return(save_str);
+	return(ctx->save_buf);
 }
 
 static
@@ -490,14 +576,15 @@ read_symbols(const char *file)
 
 static
 const char *
-address_to_symbol(void *kptr)
+address_to_symbol(void *kptr, struct save_ctx *ctx)
 {
-	static char buf[64];
+	char *buf = ctx->save_buf;
+	int size = sizeof(ctx->save_buf);
 
 	if (symcache == NULL ||
 	   (char *)kptr < symbegin || (char *)kptr >= symend
 	) {
-		snprintf(buf, sizeof(buf), "%p", kptr);
+		snprintf(buf, size, "%p", kptr);
 		return(buf);
 	}
 	while ((char *)symcache->symaddr < (char *)kptr) {
@@ -509,7 +596,7 @@ address_to_symbol(void *kptr)
 		if (symcache != TAILQ_FIRST(&symlist))
 			symcache = TAILQ_PREV(symcache, symlist, link);
 	}
-	snprintf(buf, sizeof(buf), "%s+%d", symcache->symname,
+	snprintf(buf, size, "%s+%d", symcache->symname,
 		(int)((char *)kptr - symcache->symaddr));
 	return(buf);
 }
@@ -638,8 +725,8 @@ earliest_ts(struct ktr_buffer *buf)
 
 static
 void
-print_buf(FILE *fo, struct ktr_buffer *ktr_bufs, int cpu,
-	  u_int64_t *last_timestamp)
+iterate_buf(FILE *fo, struct ktr_buffer *ktr_bufs, int cpu,
+	    u_int64_t *last_timestamp, ktr_iter_cb_t cb)
 {
 	struct ktr_buffer *buf = ktr_bufs + cpu;
 
@@ -650,10 +737,9 @@ print_buf(FILE *fo, struct ktr_buffer *ktr_bufs, int cpu,
 			buf->ents[buf->beg_idx & fifo_mask].ktr_timestamp;
 	}
 	while (buf->beg_idx != buf->end_idx) {
-		print_header(fo, buf->beg_idx);
-		print_entry(fo, cpu, buf->beg_idx,
-			    &buf->ents[buf->beg_idx & fifo_mask],
-			    last_timestamp);
+		cb(fo, cpu, buf->beg_idx,
+		   &buf->ents[buf->beg_idx & fifo_mask],
+		   last_timestamp);
 		++buf->beg_idx;
 	}
 	buf->modified = 0;
@@ -661,8 +747,8 @@ print_buf(FILE *fo, struct ktr_buffer *ktr_bufs, int cpu,
 
 static
 void
-print_bufs_timesorted(FILE *fo, struct ktr_buffer *ktr_bufs,
-		      u_int64_t *last_timestamp)
+iterate_bufs_timesorted(FILE *fo, struct ktr_buffer *ktr_bufs,
+			u_int64_t *last_timestamp, ktr_iter_cb_t cb)
 {
 	struct ktr_entry *ent;
 	struct ktr_buffer *buf;
@@ -686,10 +772,9 @@ print_bufs_timesorted(FILE *fo, struct ktr_buffer *ktr_bufs,
 		if ((bestn < 0) || (ts < *last_timestamp))
 			break;
 		buf = ktr_bufs + bestn;
-		print_header(fo, row);
-		print_entry(fo, bestn, row,
-			    &buf->ents[buf->beg_idx & fifo_mask],
-			    last_timestamp);
+		cb(fo, bestn, row,
+		   &buf->ents[buf->beg_idx & fifo_mask],
+		   last_timestamp);
 		++buf->beg_idx;
 		*last_timestamp = ts;
 		++row;
