diff mbox series

[RFC] contrib: add vhost-user-sim

Message ID 20190917122644.15736-1-johannes@sipsolutions.net (mailing list archive)
State New, archived
Headers show
Series [RFC] contrib: add vhost-user-sim | expand

Commit Message

Johannes Berg Sept. 17, 2019, 12:26 p.m. UTC
From: Johannes Berg <johannes.berg@intel.com>

This implements
 * the UM protocol to access the simulation calendar,
 * the underlying simulation calendar, and
 * a very trivial simulated network as a vhost-user
   virtio_net device.

Currently the code is a bit rough and mostly an example,
things such as the network propagation delay and the
number of clients expected to connect before the whole
simulation starts are both hardcoded; nevertheless, it
serves as an example of how to build such a simulation.

There are also some bugs in the calendar code that are
hard to fix using glib - all the file descriptors should
be handled recursively at all times. The code does some
of that, but then may deadlock in the calendar.

Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
 .gitignore                                    |   1 +
 Makefile                                      |   3 +
 Makefile.objs                                 |   1 +
 contrib/vhost-user-sim/Makefile.objs          |   1 +
 contrib/vhost-user-sim/cal.c                  | 288 ++++++++++++++++++
 contrib/vhost-user-sim/cal.h                  |  45 +++
 contrib/vhost-user-sim/main.c                 | 122 ++++++++
 contrib/vhost-user-sim/main.h                 |  22 ++
 contrib/vhost-user-sim/net.c                  | 246 +++++++++++++++
 contrib/vhost-user-sim/simtime.c              | 267 ++++++++++++++++
 .../standard-headers/linux/um_timetravel.h    | 107 +++++++
 11 files changed, 1103 insertions(+)
 create mode 100644 contrib/vhost-user-sim/Makefile.objs
 create mode 100644 contrib/vhost-user-sim/cal.c
 create mode 100644 contrib/vhost-user-sim/cal.h
 create mode 100644 contrib/vhost-user-sim/main.c
 create mode 100644 contrib/vhost-user-sim/main.h
 create mode 100644 contrib/vhost-user-sim/net.c
 create mode 100644 contrib/vhost-user-sim/simtime.c
 create mode 100644 include/standard-headers/linux/um_timetravel.h

Comments

no-reply@patchew.org Sept. 17, 2019, 6:51 p.m. UTC | #1
Patchew URL: https://patchew.org/QEMU/20190917122644.15736-1-johannes@sipsolutions.net/



Hi,

This series seems to have some coding style problems. See output below for
more information:

Subject: [Qemu-devel] [RFC] contrib: add vhost-user-sim
Message-id: 20190917122644.15736-1-johannes@sipsolutions.net
Type: series

=== TEST SCRIPT BEGIN ===
#!/bin/bash
git rev-parse base > /dev/null || exit 0
git config --local diff.renamelimit 0
git config --local diff.renames True
git config --local diff.algorithm histogram
./scripts/checkpatch.pl --mailback base..
=== TEST SCRIPT END ===

From https://github.com/patchew-project/qemu
 * [new tag]         patchew/20190917122644.15736-1-johannes@sipsolutions.net -> patchew/20190917122644.15736-1-johannes@sipsolutions.net
Switched to a new branch 'test'
e897496 contrib: add vhost-user-sim

=== OUTPUT BEGIN ===
WARNING: added, moved or deleted file(s), does MAINTAINERS need updating?
#73: 
new file mode 100644

WARNING: line over 80 characters
#254: FILE: contrib/vhost-user-sim/cal.c:170:
+        if (running_entry && entry != running_entry && running_entry->update_until) {

ERROR: line over 90 characters
#257: FILE: contrib/vhost-user-sim/cal.c:173:
+            DPRINT("update %s to be free until %lld (due to add unless of %s)\n", running_entry->name, nexttime, entry->name);

ERROR: line over 90 characters
#260: FILE: contrib/vhost-user-sim/cal.c:176:
+            DPRINT("no update for running entry %s\n", running_entry ? running_entry->name : "<none>");

ERROR: line over 90 characters
#278: FILE: contrib/vhost-user-sim/cal.c:194:
+        DPRINT("update %s to be free until %lld (due to add of %s)\n", running_entry->name, nexttime, entry->name);

ERROR: line over 90 characters
#281: FILE: contrib/vhost-user-sim/cal.c:197:
+        DPRINT("no update for running entry %s\n", running_entry ? running_entry->name : "<none>");

WARNING: line over 80 characters
#525: FILE: contrib/vhost-user-sim/main.c:96:
+            printf("Usage: %s -s time-device-socket [-n net-device-socket] | [ -h ]\n",

ERROR: braces {} are necessary for all arms of this statement
#547: FILE: contrib/vhost-user-sim/main.c:118:
+    if (net_socket)
[...]

ERROR: line over 90 characters
#741: FILE: contrib/vhost-user-sim/net.c:156:
+    fprintf(stderr, "insert IRQ from %s at %lld\n", ndev->entry.name, calendar_get_time());

WARNING: line over 80 characters
#951: FILE: contrib/vhost-user-sim/simtime.c:114:
+            DPRINT(" %d | calendar entry added for %lld\n", conn->idx, msg->time);

WARNING: line over 80 characters
#954: FILE: contrib/vhost-user-sim/simtime.c:117:
+            DPRINT(" %d | calendar entry time updated for %lld\n", conn->idx, msg->time);

ERROR: line over 90 characters
#980: FILE: contrib/vhost-user-sim/simtime.c:143:
+    g_assert(full_write(g_io_channel_unix_get_fd(conn->chan), &resp, sizeof(resp)) == sizeof(resp));

ERROR: line over 90 characters
#981: FILE: contrib/vhost-user-sim/simtime.c:144:
+    DPRINT(" %d | sent ACK for message %s (%lld, time %lld)\n", conn->idx, simtime_op_str(msg->op), msg->op, msg->time);

ERROR: line over 90 characters
#991: FILE: contrib/vhost-user-sim/simtime.c:154:
+    g_assert(full_write(g_io_channel_unix_get_fd(conn->chan), msg, sizeof(*msg)) == sizeof(*msg));

ERROR: line over 90 characters
#993: FILE: contrib/vhost-user-sim/simtime.c:156:
+        g_assert(full_read(g_io_channel_unix_get_fd(conn->chan), msg, sizeof(*msg)) == sizeof(*msg));

ERROR: braces {} are necessary for all arms of this statement
#996: FILE: contrib/vhost-user-sim/simtime.c:159:
+        if (msg->op == UM_TIMETRAVEL_ACK)
[...]

ERROR: line over 90 characters
#1043: FILE: contrib/vhost-user-sim/simtime.c:206:
+        printf("client disconnected, made %lld requests and waited %lld times, sent %lld updates\n",

ERROR: do not use C99 // comments
#1048: FILE: contrib/vhost-user-sim/simtime.c:211:
+        // leak for now ... source is still attached

ERROR: do not use C99 // comments
#1049: FILE: contrib/vhost-user-sim/simtime.c:212:
+        //g_free(conn);

total: 14 errors, 5 warnings, 1127 lines checked

Commit e89749692cba (contrib: add vhost-user-sim) has style problems, please review.  If any of these errors
are false positives report them to the maintainer, see
CHECKPATCH in MAINTAINERS.
=== OUTPUT END ===

Test command exited with code: 1


The full log is available at
http://patchew.org/logs/20190917122644.15736-1-johannes@sipsolutions.net/testing.checkpatch/?type=message.
---
Email generated automatically by Patchew [https://patchew.org/].
Please send your feedback to patchew-devel@redhat.com
no-reply@patchew.org Sept. 17, 2019, 7:01 p.m. UTC | #2
Patchew URL: https://patchew.org/QEMU/20190917122644.15736-1-johannes@sipsolutions.net/



Hi,

This series failed the asan build test. Please find the testing commands and
their output below. If you have Docker installed, you can probably reproduce it
locally.

=== TEST SCRIPT BEGIN ===
#!/bin/bash
make docker-image-fedora V=1 NETWORK=1
time make docker-test-debug@fedora TARGET_LIST=x86_64-softmmu J=14 NETWORK=1
=== TEST SCRIPT END ===

./tests/docker/docker.py --engine auto build qemu:fedora tests/docker/dockerfiles/fedora.docker   --add-current-user  
Image is up to date.
  LD      docker-test-debug@fedora.mo
cc: fatal error: no input files
compilation terminated.
make: *** [docker-test-debug@fedora.mo] Error 4



The full log is available at
http://patchew.org/logs/20190917122644.15736-1-johannes@sipsolutions.net/testing.asan/?type=message.
---
Email generated automatically by Patchew [https://patchew.org/].
Please send your feedback to patchew-devel@redhat.com
no-reply@patchew.org Sept. 17, 2019, 7:02 p.m. UTC | #3
Patchew URL: https://patchew.org/QEMU/20190917122644.15736-1-johannes@sipsolutions.net/



Hi,

This series failed the docker-mingw@fedora build test. Please find the testing commands and
their output below. If you have Docker installed, you can probably reproduce it
locally.

=== TEST SCRIPT BEGIN ===
#! /bin/bash
make docker-image-fedora V=1 NETWORK=1
time make docker-test-mingw@fedora J=14 NETWORK=1
=== TEST SCRIPT END ===

./tests/docker/docker.py --engine auto build qemu:fedora tests/docker/dockerfiles/fedora.docker   --add-current-user  
Image is up to date.
  LD      docker-test-mingw@fedora.mo
cc: fatal error: no input files
compilation terminated.
make: *** [docker-test-mingw@fedora.mo] Error 4



The full log is available at
http://patchew.org/logs/20190917122644.15736-1-johannes@sipsolutions.net/testing.docker-mingw@fedora/?type=message.
---
Email generated automatically by Patchew [https://patchew.org/].
Please send your feedback to patchew-devel@redhat.com
Stefan Hajnoczi Sept. 23, 2019, 9:25 a.m. UTC | #4
On Tue, Sep 17, 2019 at 02:26:44PM +0200, Johannes Berg wrote:
> +static int unix_sock_new(const char *unix_fn)
> +{
> +    int sock;
> +    struct sockaddr_un un;
> +    size_t len;
> +
> +    g_assert(unix_fn);
> +
> +    sock = socket(AF_UNIX, SOCK_STREAM, 0);
> +    if (sock <= 0) {
> +        perror("socket");
> +        g_assert(0);
> +        return -1;
> +    }
> +
> +    un.sun_family = AF_UNIX;
> +    (void)snprintf(un.sun_path, sizeof(un.sun_path), "%s", unix_fn);
> +    len = sizeof(un.sun_family) + strlen(un.sun_path);

According to unix(7):

  The addrlen argument that describes the enclosing sockaddr_un
  structure should have a value of at least:

    offsetof(struct sockaddr_un, sun_path)+strlen(addr.sun_path)+1

  or, more simply, addrlen can be specified as sizeof(struct sockaddr_un).

Please either increase len by 1 or use sizeof(struct sockaddr_un).

> +gboolean vu_net_client_connected(GIOChannel *src,
> +                                 GIOCondition cond,
> +                                 gpointer data)
> +{
> +    int lsock = g_io_channel_unix_get_fd(src);
> +    int csock = accept(lsock, NULL, NULL);
> +    VuNetDev *ndev;
> +
> +    if (csock < 0) {
> +        fprintf(stderr, "Accept error %s\n", strerror(errno));
> +        return TRUE;
> +    }
> +
> +    ndev = g_new0(VuNetDev, 1);
> +    if (!ndev) {

g_new0() cannot return NULL, please remove this if statement.

> +static int full_read(int fd, void *_buf, size_t len)
> +{
> +    unsigned char *buf = _buf;
> +    ssize_t ret;
> +
> +    do {
> +        ret = read(fd, buf, len);
> +        if (ret > 0) {
> +            buf += ret;
> +            len -= ret;
> +        } else if (ret == 0) {
> +            return 0;
> +        } else {
> +            return -errno;
> +        }

Want to loop on EINTR?

> +    } while (len > 0);
> +
> +    return buf - (unsigned char *)_buf;
> +}
> +
> +static int full_write(int fd, const void *_buf, size_t len)
> +{
> +    const unsigned char *buf = _buf;
> +    ssize_t ret;
> +
> +    do {
> +        ret = write(fd, buf, len);
> +        if (ret > 0) {
> +            buf += ret;
> +            len -= ret;
> +        } else if (ret == 0) {
> +            return 0;
> +        } else {
> +            return -errno;

EINTR?

> +        }
> +    } while (len > 0);
> +
> +    return buf - (const unsigned char *)_buf;
> +}
> +
> +static void simtime_handle_message(SimTimeConnection *conn,
> +                                   struct um_timetravel_msg *msg)
> +{
> +    struct um_timetravel_msg resp = {
> +        .op = UM_TIMETRAVEL_ACK,
> +    };
> +
> +    DPRINT(" %d | message %s (%lld, time=%lld)\n",
> +           conn->idx, simtime_op_str(msg->op), msg->op, msg->time);
> +
> +    switch (msg->op) {
> +    case UM_TIMETRAVEL_REQUEST:
> +        if (calendar_entry_remove(&conn->entry)) {
> +            conn->entry.time = conn->offset + msg->time;
> +            calendar_entry_add(&conn->entry);
> +            DPRINT(" %d | calendar entry added for %lld\n", conn->idx, msg->time);
> +        } else {
> +            conn->entry.time = conn->offset + msg->time;
> +            DPRINT(" %d | calendar entry time updated for %lld\n", conn->idx, msg->time);
> +        }

Just checking the expected semantics:

If the entry was already added, then we update the time and it stays
scheduled.  If the entry was not already added then we just stash away
the time but don't schedule it?

Also, are the DPRINT() messages swapped in the if ... else ... bodies?
They seem to be talking about the other case.

> +    conn = g_new0(SimTimeConnection, 1);
> +    if (!conn) {
> +        return TRUE;
> +    }

g_new0() does not fail.  If it did, then csock would be leaked here.
Please drop the if statement.

> diff --git a/include/standard-headers/linux/um_timetravel.h b/include/standard-headers/linux/um_timetravel.h
> new file mode 100644
> index 000000000000..3aaced426a92
> --- /dev/null
> +++ b/include/standard-headers/linux/um_timetravel.h
> @@ -0,0 +1,107 @@

Please use scripts/update-linux-headers.sh to import this header file
with the necessary conversions (e.g. #include <linux/types.h> ->
#include "standard-headers/linux/types.h", __u64 -> uint64_t).
Johannes Berg Sept. 23, 2019, 9:33 a.m. UTC | #5
On Mon, 2019-09-23 at 10:25 +0100, Stefan Hajnoczi wrote:
> 
> According to unix(7):
> 
>   The addrlen argument that describes the enclosing sockaddr_un
>   structure should have a value of at least:
> 
>     offsetof(struct sockaddr_un, sun_path)+strlen(addr.sun_path)+1
> 
>   or, more simply, addrlen can be specified as sizeof(struct sockaddr_un).
> 
> Please either increase len by 1 or use sizeof(struct sockaddr_un).

Good catch, I actually realized this later as I was porting it elsewhere
but forgot to update this code.

> g_new0() cannot return NULL, please remove this if statement.

:-)

Shows I don't work with glib much ...

> > +static int full_read(int fd, void *_buf, size_t len)
> > +{
> > +    unsigned char *buf = _buf;
> > +    ssize_t ret;
> > +
> > +    do {
> > +        ret = read(fd, buf, len);
> > +        if (ret > 0) {
> > +            buf += ret;
> > +            len -= ret;
> > +        } else if (ret == 0) {
> > +            return 0;
> > +        } else {
> > +            return -errno;
> > +        }
> 
> Want to loop on EINTR?

Can we even get EINTR? We're not really expecting to deal with any
signals in this code, do we?

But I guess we can.

> > +    switch (msg->op) {
> > +    case UM_TIMETRAVEL_REQUEST:
> > +        if (calendar_entry_remove(&conn->entry)) {
> > +            conn->entry.time = conn->offset + msg->time;
> > +            calendar_entry_add(&conn->entry);
> > +            DPRINT(" %d | calendar entry added for %lld\n", conn->idx, msg->time);
> > +        } else {
> > +            conn->entry.time = conn->offset + msg->time;
> > +            DPRINT(" %d | calendar entry time updated for %lld\n", conn->idx, msg->time);
> > +        }
> 
> Just checking the expected semantics:
> 
> If the entry was already added, then we update the time and it stays
> scheduled.  If the entry was not already added then we just stash away
> the time but don't schedule it?

Right.

The first case is the "normally running" case, where the request is
coming in after UM_TIMETRAVEL_WAIT but before we sent it
UM_TIMETRAVEL_RUN, i.e. when the sender of the message got an interrupt
from elsewhere and needs to change the next runtime.

The second case is when it's requesting before it went into
UM_TIMETRAVEL_WAIT, in which case we just want the entry to the calendar
to be added for when we actually get _WAIT.

> Also, are the DPRINT() messages swapped in the if ... else ... bodies?
> They seem to be talking about the other case.

Or better rephrased I guess :)

> > diff --git a/include/standard-headers/linux/um_timetravel.h b/include/standard-headers/linux/um_timetravel.h
> > new file mode 100644
> > index 000000000000..3aaced426a92
> > --- /dev/null
> > +++ b/include/standard-headers/linux/um_timetravel.h
> > @@ -0,0 +1,107 @@
> 
> Please use scripts/update-linux-headers.sh to import this header file
> with the necessary conversions (e.g. #include <linux/types.h> ->
> #include "standard-headers/linux/types.h", __u64 -> uint64_t).

Hah, sure, wasn't aware of that.


Note, I'm not happy with this code at all, it deadlocks all the time.
Unfortunately I haven't really been able to figure out how to make glib
do what I wanted.

The first issue is that sometimes glib actually seems to reports an FD
as readable when it's not, so I even put them into non-blocking mode :(

The second is that I can't seem to understand how to do recursive
mainloops.

To really do this *well*, it should remain a single-threaded
application, but would need a hook like

run_mainloop_until_fd_readable(vdev->call_fd)

inside the libvhost-user.c code, and that's a bit ugly ... if I even
could figure out how to implement that in glib.

johannes
Stefan Hajnoczi Oct. 9, 2019, 2 p.m. UTC | #6
On Mon, Sep 23, 2019 at 11:33:41AM +0200, Johannes Berg wrote:
> On Mon, 2019-09-23 at 10:25 +0100, Stefan Hajnoczi wrote:
> Note, I'm not happy with this code at all, it deadlocks all the time.
> Unfortunately I haven't really been able to figure out how to make glib
> do what I wanted.
> 
> The first issue is that sometimes glib actually seems to reports an FD
> as readable when it's not, so I even put them into non-blocking mode :(

Strange.  Spurious wakeups are possible in general.  I think when using
fd monitoring (select(), poll(), etc) the fds should be in non-blocking
mode.

But if you're seeing this often it makes me wonder if something else is
unintentionally reading available bytes...

> The second is that I can't seem to understand how to do recursive
> mainloops.
> 
> To really do this *well*, it should remain a single-threaded
> application, but would need a hook like
> 
> run_mainloop_until_fd_readable(vdev->call_fd)
> 
> inside the libvhost-user.c code, and that's a bit ugly ... if I even
> could figure out how to implement that in glib.

Recursive mainloops are tricky since usually event loop code isn't
written to be re-entrant.  It opens up a whole new dimension that
existing code usually wasn't designed for.  In this case you are writing
the code from scratch so maybe you can get it to work, but it makes me
wonder why the recursive mainloop is necessary.

Stefan
Johannes Berg Oct. 9, 2019, 3:01 p.m. UTC | #7
On Wed, 2019-10-09 at 15:00 +0100, Stefan Hajnoczi wrote:

> > The first issue is that sometimes glib actually seems to reports an FD
> > as readable when it's not, so I even put them into non-blocking mode :(
> 
> Strange.  Spurious wakeups are possible in general.  I think when using
> fd monitoring (select(), poll(), etc) the fds should be in non-blocking
> mode.

Are they? I reimplemented all this on top of my (internal) testing
framework and use blocking FDs there, and had no issues whatsoever.

> But if you're seeing this often it makes me wonder if something else is
> unintentionally reading available bytes...

I have no idea, it'd be strange though, what would even get to the FD?

> > The second is that I can't seem to understand how to do recursive
> > mainloops.
> > 
> > To really do this *well*, it should remain a single-threaded
> > application, but would need a hook like
> > 
> > run_mainloop_until_fd_readable(vdev->call_fd)
> > 
> > inside the libvhost-user.c code, and that's a bit ugly ... if I even
> > could figure out how to implement that in glib.
> 
> Recursive mainloops are tricky since usually event loop code isn't
> written to be re-entrant.  It opens up a whole new dimension that
> existing code usually wasn't designed for.  In this case you are writing
> the code from scratch so maybe you can get it to work, but it makes me
> wonder why the recursive mainloop is necessary.

It's needed to handle different clients / connections.

For example, consider this flow

VM           net device & simtime
  <-------------- VHOST_USER_SLAVE_CALL_VRING
  --------------> request for time handling
  <-------------- response for time handling
  --------------> REPLY_ACK for VHOST_USER_SLAVE_CALL_VRING

I sort of solved it with different threads for the different entities
(netdev & simtime) but that also just causes deadlocks with the locking
that needs to happen.

Like I said, I had reimplemented this in the meantime with a (hand-
written) recursive mainloop as a single thread, and it works way better.
One of these days I need to sit down and port that to be independent of
my testing framework, so I can show the code ...

johannes
diff mbox series

Patch

diff --git a/.gitignore b/.gitignore
index e9bbc006d39e..3417ffec3988 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,6 +67,7 @@ 
 /vhost-user-blk
 /vhost-user-gpu
 /vhost-user-input
+/vhost-user-sim
 /fsdev/virtfs-proxy-helper
 *.tmp
 *.[1-9]
diff --git a/Makefile b/Makefile
index 111082ce545d..f74f5d89cff2 100644
--- a/Makefile
+++ b/Makefile
@@ -418,6 +418,7 @@  dummy := $(call unnest-vars,, \
                 vhost-user-blk-obj-y \
                 vhost-user-input-obj-y \
                 vhost-user-gpu-obj-y \
+                vhost-user-sim-obj-y \
                 qga-vss-dll-obj-y \
                 block-obj-y \
                 block-obj-m \
@@ -638,6 +639,8 @@  vhost-user-scsi$(EXESUF): $(vhost-user-scsi-obj-y) libvhost-user.a
 	$(call LINK, $^)
 vhost-user-blk$(EXESUF): $(vhost-user-blk-obj-y) libvhost-user.a
 	$(call LINK, $^)
+vhost-user-sim$(EXESUF): $(vhost-user-sim-obj-y) libvhost-user.a
+	$(call LINK, $^)
 
 rdmacm-mux$(EXESUF): LIBS += "-libumad"
 rdmacm-mux$(EXESUF): $(rdmacm-mux-obj-y) $(COMMON_LDADDS)
diff --git a/Makefile.objs b/Makefile.objs
index 6a143dcd5790..0536665faff0 100644
--- a/Makefile.objs
+++ b/Makefile.objs
@@ -122,6 +122,7 @@  vhost-user-scsi.o-cflags := $(LIBISCSI_CFLAGS)
 vhost-user-scsi.o-libs := $(LIBISCSI_LIBS)
 vhost-user-scsi-obj-y = contrib/vhost-user-scsi/
 vhost-user-blk-obj-y = contrib/vhost-user-blk/
+vhost-user-sim-obj-y = contrib/vhost-user-sim/
 rdmacm-mux-obj-y = contrib/rdmacm-mux/
 vhost-user-input-obj-y = contrib/vhost-user-input/
 vhost-user-gpu-obj-y = contrib/vhost-user-gpu/
diff --git a/contrib/vhost-user-sim/Makefile.objs b/contrib/vhost-user-sim/Makefile.objs
new file mode 100644
index 000000000000..ffc9e25ac5d2
--- /dev/null
+++ b/contrib/vhost-user-sim/Makefile.objs
@@ -0,0 +1 @@ 
+vhost-user-sim-obj-y = cal.o simtime.o net.o main.o
diff --git a/contrib/vhost-user-sim/cal.c b/contrib/vhost-user-sim/cal.c
new file mode 100644
index 000000000000..a9e0ff9460fc
--- /dev/null
+++ b/contrib/vhost-user-sim/cal.c
@@ -0,0 +1,288 @@ 
+/*
+ * vhost-user-sim calendar
+ *
+ * Copyright (c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Author:
+ *  Johannes Berg <johannes.berg@intel.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 only.
+ * See the COPYING file in the top-level directory.
+ */
+#include <stdbool.h>
+#include <stdio.h>
+#include "cal.h"
+
+#define CAL_DEBUG 1
+
+static unsigned long long simtime;
+static G_LOCK_DEFINE(calendar);
+static GSequence *calendar;
+static GAsyncQueue *queue;
+static bool scheduling;
+static unsigned int required_clients;
+static unsigned int running_clients;
+static SimCalendarEntry *running_entry;
+
+#define DPRINT(...) do {              \
+    if (CAL_DEBUG) {                  \
+        fprintf(stderr, __VA_ARGS__); \
+        fflush(stderr);               \
+    }                                 \
+} while (0)
+
+typedef enum {
+    CAL_OP_INVALID,
+    CAL_OP_RUN_DONE,
+    CAL_OP_QUIT,
+} CalMessage;
+
+static void dump_scheduler(gpointer data, gpointer user)
+{
+    SimCalendarEntry *entry = data;
+
+    DPRINT("<CAL>| %.20lld | %s\n", entry->time, entry->name);
+}
+
+static void dump_calendar(void)
+{
+    DPRINT("<CAL>|----------- calendar state at %lld\n", simtime);
+    g_sequence_foreach(calendar, dump_scheduler, NULL);
+}
+
+unsigned long long calendar_get_time(void)
+{
+    return simtime;
+}
+
+static void count_clients(gpointer data, gpointer user)
+{
+    SimCalendarEntry *entry = data;
+    unsigned int *count = user;
+
+    if (entry->client) {
+        (*count)++;
+    }
+}
+
+static unsigned long long calendar_get_next_time(void)
+{
+    GSequenceIter *first;
+
+    first = g_sequence_get_iter_at_pos(calendar, 0);
+    if (!g_sequence_iter_is_end(first)) {
+        SimCalendarEntry *next = g_sequence_get(first);
+        return next->time;
+    }
+
+    return (unsigned long long)-1;
+}
+
+void calendar_set_time(unsigned long long time)
+{
+    g_assert(time >= simtime);
+    G_LOCK(calendar);
+    if (time > calendar_get_next_time()) {
+        dump_calendar();
+        DPRINT("CAL: setting time to %lld which is > %lld\n",
+                time, calendar_get_next_time());
+        g_assert(0);
+    }
+    simtime = time;
+    G_UNLOCK(calendar);
+}
+
+static void calendar_schedule(void)
+{
+    GSequenceIter *first;
+    SimCalendarEntry *entry;
+    unsigned long long nexttime;
+
+    G_LOCK(calendar);
+    if (CAL_DEBUG) {
+        dump_calendar();
+    }
+
+    if (!scheduling) {
+        unsigned int count = 0;
+
+        g_sequence_foreach(calendar, count_clients, &count);
+
+        if (count < required_clients) {
+            G_UNLOCK(calendar);
+            return;
+        }
+        scheduling = true;
+    }
+
+    first = g_sequence_get_iter_at_pos(calendar, 0);
+    if (g_sequence_iter_is_end(first)) {
+        /* Everything disappeared, stop the simulation. */
+        g_async_queue_push(queue, (void *)CAL_OP_QUIT);
+        G_UNLOCK(calendar);
+        return;
+    }
+
+    entry = g_sequence_get(first);
+    g_sequence_remove(first);
+    entry->iter = NULL;
+    simtime = entry->time;
+    entry->running = true;
+    running_clients++;
+
+    nexttime = calendar_get_next_time();
+    running_entry = entry;
+    G_UNLOCK(calendar);
+
+    if (entry->update_until) {
+        DPRINT("update %s to be free until %lld\n", entry->name, nexttime);
+        entry->update_until(entry, nexttime);
+    }
+    entry->callback(entry);
+}
+
+static int entry_cmp_func(gconstpointer _a, gconstpointer _b, gpointer data)
+{
+    const SimCalendarEntry *a = _a;
+    const SimCalendarEntry *b = _b;
+
+    if (a->time == b->time) {
+        return 0;
+    }
+
+    if (a->time < b->time) {
+        return -1;
+    }
+
+    return 1;
+}
+
+void calendar_entry_add_unless_present(SimCalendarEntry *entry,
+                                       unsigned long long time)
+{
+    G_LOCK(calendar);
+    if (!entry->iter) {
+        entry->time = time;
+        entry->iter = g_sequence_insert_sorted(calendar, entry,
+                                               entry_cmp_func, NULL);
+        g_assert(entry->iter);
+        dump_calendar();
+        if (running_entry && entry != running_entry && running_entry->update_until) {
+            unsigned long long nexttime = calendar_get_next_time();
+
+            DPRINT("update %s to be free until %lld (due to add unless of %s)\n", running_entry->name, nexttime, entry->name);
+            running_entry->update_until(running_entry, nexttime);
+        } else {
+            DPRINT("no update for running entry %s\n", running_entry ? running_entry->name : "<none>");
+        }
+    }
+    G_UNLOCK(calendar);
+}
+
+void calendar_entry_add(SimCalendarEntry *entry)
+{
+    G_LOCK(calendar);
+    g_assert(!entry->iter);
+    entry->iter = g_sequence_insert_sorted(calendar, entry,
+                                           entry_cmp_func, NULL);
+    g_assert(entry->iter);
+    dump_calendar();
+    if (running_entry && entry != running_entry &&
+        running_entry->update_until) {
+        unsigned long long nexttime = calendar_get_next_time();
+
+        DPRINT("update %s to be free until %lld (due to add of %s)\n", running_entry->name, nexttime, entry->name);
+        running_entry->update_until(running_entry, nexttime);
+    } else {
+        DPRINT("no update for running entry %s\n", running_entry ? running_entry->name : "<none>");
+    }
+    G_UNLOCK(calendar);
+}
+
+static void _calendar_run_done(SimCalendarEntry *entry)
+{
+    /*
+     * This will happen while new clients join the
+     * simulation and go into wait - their events
+     * will be marked as "done" even though they
+     * never started ...
+     * Currently we don't let any clients join the
+     * simulation after it has started, but that's
+     * not really controllable anyway, so better.
+     */
+    if (scheduling) {
+        g_assert(entry->running);
+        entry->running = false;
+        running_clients--;
+        g_assert(running_clients == 0);
+    }
+    g_async_queue_push(queue, (void *)CAL_OP_RUN_DONE);
+}
+
+static bool _calendar_entry_remove(SimCalendarEntry *entry)
+{
+    if (entry->iter) {
+        g_sequence_remove(entry->iter);
+        entry->iter = NULL;
+        return true;
+    }
+
+    return false;
+}
+
+bool calendar_entry_remove(SimCalendarEntry *entry)
+{
+    bool scheduled;
+
+    G_LOCK(calendar);
+    scheduled = _calendar_entry_remove(entry);
+    G_UNLOCK(calendar);
+
+    return scheduled;
+}
+
+void calendar_run_done(SimCalendarEntry *entry)
+{
+    G_LOCK(calendar);
+    _calendar_run_done(entry);
+    G_UNLOCK(calendar);
+}
+
+void calendar_entry_destroy(SimCalendarEntry *entry)
+{
+    G_LOCK(calendar);
+    if (running_entry == entry) {
+        running_entry = NULL;
+    }
+    if (entry->running) {
+        DPRINT("destroying running client %s\n", entry->name);
+        _calendar_run_done(entry);
+    }
+    _calendar_entry_remove(entry);
+    G_UNLOCK(calendar);
+}
+
+void calendar_init(unsigned int required)
+{
+    calendar = g_sequence_new(NULL);
+    queue = g_async_queue_new();
+    required_clients = required;
+}
+
+void calendar_run(void)
+{
+    while (1) {
+        CalMessage msg = (CalMessage)g_async_queue_pop(queue);
+
+        switch (msg) {
+        case CAL_OP_INVALID:
+            g_assert(0);
+            break;
+        case CAL_OP_RUN_DONE:
+            calendar_schedule();
+            break;
+        case CAL_OP_QUIT:
+            return;
+        }
+    }
+}
diff --git a/contrib/vhost-user-sim/cal.h b/contrib/vhost-user-sim/cal.h
new file mode 100644
index 000000000000..1468ecc4f4eb
--- /dev/null
+++ b/contrib/vhost-user-sim/cal.h
@@ -0,0 +1,45 @@ 
+/*
+ * vhost-user-sim calendar (header file)
+ *
+ * Copyright (c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Author:
+ *  Johannes Berg <johannes.berg@intel.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 only.
+ * See the COPYING file in the top-level directory.
+ */
+#ifndef _SIM_CAL_H
+#define _SIM_CAL_H
+#include <stdbool.h>
+#include <gmodule.h>
+
+typedef struct SimCalendarEntry SimCalendarEntry;
+typedef void (*start_callback_t)(SimCalendarEntry *entry);
+typedef void (*update_until_callback_t)(SimCalendarEntry *entry,
+                                        unsigned long long until);
+
+struct SimCalendarEntry {
+    unsigned long long time;
+    start_callback_t callback;
+    update_until_callback_t update_until;
+    gchar *name;
+    GSequenceIter *iter;
+    bool running;
+    bool client;
+};
+
+void calendar_init(unsigned int required_clients);
+void calendar_run(void);
+
+unsigned long long calendar_get_time(void);
+void calendar_set_time(unsigned long long time);
+void calendar_entry_add(SimCalendarEntry *entry);
+void calendar_entry_add_unless_present(SimCalendarEntry *entry,
+                                       unsigned long long time);
+bool calendar_entry_remove(SimCalendarEntry *entry);
+void calendar_entry_destroy(SimCalendarEntry *entry);
+
+void calendar_run_done(SimCalendarEntry *entry);
+
+#endif /* _SIM_CAL_H */
diff --git a/contrib/vhost-user-sim/main.c b/contrib/vhost-user-sim/main.c
new file mode 100644
index 000000000000..10123b28bb56
--- /dev/null
+++ b/contrib/vhost-user-sim/main.c
@@ -0,0 +1,122 @@ 
+/*
+ * vhost-user sim main application
+ *
+ * Copyright (c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Author:
+ *  Johannes Berg <johannes.berg@intel.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 only.
+ * See the COPYING file in the top-level directory.
+ */
+#include <gmodule.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include "main.h"
+#include "cal.h"
+
+static int unix_sock_new(const char *unix_fn)
+{
+    int sock;
+    struct sockaddr_un un;
+    size_t len;
+
+    g_assert(unix_fn);
+
+    sock = socket(AF_UNIX, SOCK_STREAM, 0);
+    if (sock <= 0) {
+        perror("socket");
+        g_assert(0);
+        return -1;
+    }
+
+    un.sun_family = AF_UNIX;
+    (void)snprintf(un.sun_path, sizeof(un.sun_path), "%s", unix_fn);
+    len = sizeof(un.sun_family) + strlen(un.sun_path);
+
+    (void)unlink(unix_fn);
+    if (bind(sock, (struct sockaddr *)&un, len) < 0) {
+        perror("bind");
+        goto fail;
+    }
+
+    if (listen(sock, 1) < 0) {
+        perror("listen");
+        goto fail;
+    }
+
+    return sock;
+
+fail:
+    (void)close(sock);
+    g_assert(0);
+    return -1;
+}
+
+static gpointer thread_func(gpointer data)
+{
+    g_main_context_push_thread_default(g_main_loop_get_context(data));
+    g_main_loop_run(data);
+    return NULL;
+}
+
+static GThread *new_device_thread(GIOFunc cb, const char *socket,
+                                  const char *name)
+{
+    GMainContext *ctx = g_main_context_new();
+    GMainLoop *loop = g_main_loop_new(ctx, FALSE);
+    int lsock = unix_sock_new(socket);
+    GIOChannel *chan = g_io_channel_unix_new(lsock);
+    GSource *src = g_io_create_watch(chan, G_IO_IN);
+
+    g_source_set_callback(src, G_SOURCE_FUNC(cb), NULL, NULL);
+    g_source_attach(src, ctx);
+
+    return g_thread_new(name, thread_func, loop);
+}
+
+int main(int argc, char **argv)
+{
+    char *time_socket = NULL, *net_socket = NULL;
+    int opt;
+
+    while ((opt = getopt(argc, argv, "s:n:h")) != -1) {
+        switch (opt) {
+        case 's':
+            time_socket = g_strdup(optarg);
+            break;
+        case 'n':
+            net_socket = g_strdup(optarg);
+            break;
+        case 'h':
+        default:
+            printf("Usage: %s -s time-device-socket [-n net-device-socket] | [ -h ]\n",
+                   argv[0]);
+            return 0;
+        }
+    }
+
+    g_assert(time_socket);
+#define N_CLIENTS 2
+    fprintf(stderr,
+            "============ starting up simulation, requires %d clients ============\n",
+            N_CLIENTS);
+
+    calendar_init(N_CLIENTS);
+
+    new_device_thread(simtime_client_connected, time_socket, "time");
+    if (net_socket) {
+        new_device_thread(vu_net_client_connected, net_socket, "net");
+    }
+
+    calendar_run();
+
+    unlink(time_socket);
+    if (net_socket)
+        unlink(net_socket);
+
+    return 0;
+}
diff --git a/contrib/vhost-user-sim/main.h b/contrib/vhost-user-sim/main.h
new file mode 100644
index 000000000000..9681474581ac
--- /dev/null
+++ b/contrib/vhost-user-sim/main.h
@@ -0,0 +1,22 @@ 
+/*
+ * vhost-user sim main application header file
+ *
+ * Copyright (c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Author:
+ *  Johannes Berg <johannes.berg@intel.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 only.
+ * See the COPYING file in the top-level directory.
+ */
+#ifndef _SIM_MAIN_H
+#define _SIM_MAIN_H
+
+gboolean simtime_client_connected(GIOChannel *src,
+                                  GIOCondition cond,
+                                  gpointer data);
+gboolean vu_net_client_connected(GIOChannel *src,
+                                 GIOCondition cond,
+                                 gpointer data);
+
+#endif /* _SIM_MAIN_H */
diff --git a/contrib/vhost-user-sim/net.c b/contrib/vhost-user-sim/net.c
new file mode 100644
index 000000000000..337341f5fdcb
--- /dev/null
+++ b/contrib/vhost-user-sim/net.c
@@ -0,0 +1,246 @@ 
+/*
+ * vhost-user sim network device
+ *
+ * Copyright (c) 2017 Intel Corporation. All rights reserved.
+ * Copyright (c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Author:
+ *  Johannes Berg <johannes.berg@intel.com>
+ *
+ * This work is based on the "vhost-user-blk" sample code by
+ * Changpeng Liu <changpeng.liu@intel.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 only.
+ * See the COPYING file in the top-level directory.
+ */
+
+#include "qemu/osdep.h"
+#include "contrib/libvhost-user/libvhost-user-glib.h"
+#include "contrib/libvhost-user/libvhost-user.h"
+#include "qemu/iov.h"
+#include <gmodule.h>
+#include "main.h"
+#include "cal.h"
+
+static unsigned int clients;
+static GList *netdevs;
+G_LOCK_DEFINE(net);
+
+typedef struct VuNetDev {
+    SimCalendarEntry entry;
+    VugDev parent;
+    VuVirtq *rxq, *txq;
+    GSequenceIter *iter;
+    int idx;
+} VuNetDev;
+
+typedef struct VuNetPacket {
+    SimCalendarEntry entry;
+    void *transmitter;
+    int txidx;
+    unsigned int len;
+    char buf[];
+} VuNetPacket;
+
+static void vu_net_panic_cb(VuDev *vu_dev, const char *buf)
+{
+    if (buf) {
+        g_warning("vu_net_panic_cb: %s", buf);
+    }
+}
+
+static void send_to_one(VuNetDev *ndev, VuNetPacket *pkt)
+{
+    VuVirtqElement *elem;
+    VuDev *vu_dev = &ndev->parent.parent;
+
+    elem = vu_queue_pop(vu_dev, ndev->rxq, sizeof(VuVirtqElement));
+    if (!elem) {
+        /* no space on this device, drop the frame for it */
+        fprintf(stderr, "dropped packet to net %d!\n", ndev->idx);
+        return;
+    }
+    fprintf(stderr, "forwarding packet to net %d\n", ndev->idx);
+    g_assert(elem->in_num && !elem->out_num);
+    iov_from_buf(elem->in_sg, elem->in_num, 0, pkt->buf, pkt->len);
+
+    vu_queue_push(vu_dev, ndev->rxq, elem, pkt->len);
+    /* this _sync is key so the recipient can request scheduler time */
+    vu_queue_notify_sync(vu_dev, ndev->rxq);
+
+    free(elem);
+}
+
+static void vu_netpkt_calendar_cb(SimCalendarEntry *entry)
+{
+    VuNetPacket *pkt = container_of(entry, VuNetPacket, entry);
+    /* send to all devices */
+    GList *l;
+
+    G_LOCK(net);
+    for (l = netdevs; l; l = l->next) {
+        if (l->data != pkt->transmitter) {
+            send_to_one(l->data, pkt);
+        }
+    }
+    G_UNLOCK(net);
+
+    calendar_run_done(&pkt->entry);
+    g_free((void *)pkt->entry.name);
+    g_free(pkt);
+}
+
+static int vu_net_virtio_process_pkt(VuNetDev *ndev)
+{
+    VuVirtq *vq = ndev->txq;
+    VugDev *gdev = &ndev->parent;
+    VuDev *vu_dev = &gdev->parent;
+    VuVirtqElement *elem;
+    VuNetPacket *pkt;
+    ssize_t sz;
+
+    elem = vu_queue_pop(vu_dev, vq, sizeof(VuVirtqElement));
+    if (!elem) {
+        return -1;
+    }
+
+    g_assert(elem->out_num);
+
+    sz = iov_size(elem->out_sg, elem->out_num);
+    pkt = g_malloc(sizeof(*pkt) + sz);
+    pkt->entry.time = calendar_get_time() + 50 * 1000 * 1000; /* 50ms */
+    pkt->entry.callback = vu_netpkt_calendar_cb;
+    pkt->entry.name = g_strdup_printf("packet from %d", ndev->idx);
+    pkt->len = sz;
+    pkt->transmitter = ndev;
+    pkt->txidx = ndev->idx;
+    iov_to_buf(elem->out_sg, elem->out_num, 0, pkt->buf, sz);
+    calendar_entry_add(&pkt->entry);
+
+    vu_queue_push(vu_dev, vq, elem, 0);
+    /* the reclaim interrupt should also be predictable, so _sync() */
+    vu_queue_notify_sync(vu_dev, vq);
+
+    return 0;
+}
+
+static void vu_netdev_calendar_cb(SimCalendarEntry *entry)
+{
+    VuNetDev *ndev = container_of(entry, VuNetDev, entry);
+    int ret;
+
+    G_LOCK(net);
+    fprintf(stderr, "NET: handle TX IRQ from net %d\n", ndev->idx);
+    /* here we handle TX from the VQ */
+    do {
+        ret = vu_net_virtio_process_pkt(ndev);
+    } while (!ret);
+    fprintf(stderr, "NET: handle TX from net %d completed\n", ndev->idx);
+    G_UNLOCK(net);
+
+    calendar_run_done(entry);
+}
+
+static uint64_t vu_net_get_protocol_features(VuDev *dev)
+{
+    return 1ULL << VHOST_USER_PROTOCOL_F_INBAND_NOTIFICATIONS;
+}
+
+static void vu_net_process_vq(VuDev *vu_dev, int idx)
+{
+    VugDev *gdev = container_of(vu_dev, VugDev, parent);
+    VuNetDev *ndev = container_of(gdev, VuNetDev, parent);
+
+    assert(vu_dev && ndev);
+
+    fprintf(stderr, "insert IRQ from %s at %lld\n", ndev->entry.name, calendar_get_time());
+
+    /* insert the calendar entry to handle the interrupt */
+    calendar_entry_add_unless_present(&ndev->entry, calendar_get_time());
+}
+
+static void vu_net_queue_set_started(VuDev *vu_dev, int idx, bool started)
+{
+    VugDev *gdev = container_of(vu_dev, VugDev, parent);
+    VuNetDev *ndev = container_of(gdev, VuNetDev, parent);
+    VuVirtq *vq;
+
+    assert(vu_dev);
+
+    vq = vu_get_queue(vu_dev, idx);
+    /* set up the read fd */
+    switch (idx) {
+    case 0:
+        ndev->rxq = vq;
+        break;
+    case 1:
+        ndev->txq = vq;
+        vu_set_queue_handler(vu_dev, vq, started ? vu_net_process_vq : NULL);
+        break;
+    }
+}
+
+static int
+vu_net_process_msg(VuDev *vu_dev, VhostUserMsg *msg, int *do_reply)
+{
+    VugDev *gdev = container_of(vu_dev, VugDev, parent);
+    VuNetDev *ndev = container_of(gdev, VuNetDev, parent);
+
+    if (msg->request != VHOST_USER_NONE) {
+        return 0;
+    }
+
+    *do_reply = 0;
+    fprintf(stderr, "net client %d disconnected\n", ndev->idx);
+
+    G_LOCK(net);
+    netdevs = g_list_remove(netdevs, ndev);
+    clients--;
+
+    vug_deinit(gdev);
+    G_UNLOCK(net);
+    calendar_entry_destroy(&ndev->entry);
+    g_free((void *)ndev->entry.name);
+    g_free(ndev);
+
+    return 1;
+}
+
+static const VuDevIface vu_net_iface = {
+    .queue_set_started = vu_net_queue_set_started,
+    .get_protocol_features = vu_net_get_protocol_features,
+    .process_msg = vu_net_process_msg,
+};
+
+gboolean vu_net_client_connected(GIOChannel *src,
+                                 GIOCondition cond,
+                                 gpointer data)
+{
+    int lsock = g_io_channel_unix_get_fd(src);
+    int csock = accept(lsock, NULL, NULL);
+    VuNetDev *ndev;
+
+    if (csock < 0) {
+        fprintf(stderr, "Accept error %s\n", strerror(errno));
+        return TRUE;
+    }
+
+    ndev = g_new0(VuNetDev, 1);
+    if (!ndev) {
+        return TRUE;
+    }
+
+    clients++;
+    ndev->idx = clients;
+    ndev->entry.name = g_strdup_printf("net-irq %d", clients);
+    ndev->entry.callback = vu_netdev_calendar_cb;
+    vug_init(&ndev->parent, 2, csock, vu_net_panic_cb,
+             &vu_net_iface);
+    fprintf(stderr, "net client %d connected\n", clients);
+
+    G_LOCK(net);
+    netdevs = g_list_prepend(netdevs, ndev);
+    G_UNLOCK(net);
+
+    return TRUE;
+}
diff --git a/contrib/vhost-user-sim/simtime.c b/contrib/vhost-user-sim/simtime.c
new file mode 100644
index 000000000000..96bd7863790a
--- /dev/null
+++ b/contrib/vhost-user-sim/simtime.c
@@ -0,0 +1,267 @@ 
+/*
+ * vhost-user simtime device application
+ *
+ * Copyright (c) 2017 Intel Corporation. All rights reserved.
+ * Copyright (c) 2019 Intel Corporation. All rights reserved.
+ *
+ * Author:
+ *  Johannes Berg <johannes.berg@intel.com>
+ *
+ * This work is based on the "vhost-user-blk" sample code by
+ * Changpeng Liu <changpeng.liu@intel.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 only.
+ * See the COPYING file in the top-level directory.
+ */
+
+#include "qemu/osdep.h"
+#include "standard-headers/linux/um_timetravel.h"
+#include "cal.h"
+#include "main.h"
+
+#define DEBUG 1
+#define DPRINT(...) do {              \
+    if (DEBUG) {                      \
+        fprintf(stderr, __VA_ARGS__); \
+        fflush(stderr);               \
+    }                                 \
+} while (0)
+
+typedef struct SimTimeConnection {
+    GMutex lock;
+    __u64 offset;
+    GIOChannel *chan;
+    GMainLoop *loop;
+    int idx;
+    SimCalendarEntry entry;
+    unsigned long long num_requests, num_waits, num_updates;
+    bool waiting;
+} SimTimeConnection;
+
+static int clients;
+
+static const char *simtime_op_str(uint64_t op)
+{
+#define OPSTR(x) case UM_TIMETRAVEL_##x: return "UM_TIMETRAVEL_" #x
+    switch (op) {
+    OPSTR(ACK);
+    OPSTR(REQUEST);
+    OPSTR(WAIT);
+    OPSTR(GET);
+    OPSTR(UPDATE);
+    OPSTR(RUN);
+    OPSTR(FREE_UNTIL);
+    default:
+        return "unknown";
+    }
+}
+
+static int full_read(int fd, void *_buf, size_t len)
+{
+    unsigned char *buf = _buf;
+    ssize_t ret;
+
+    do {
+        ret = read(fd, buf, len);
+        if (ret > 0) {
+            buf += ret;
+            len -= ret;
+        } else if (ret == 0) {
+            return 0;
+        } else {
+            return -errno;
+        }
+    } while (len > 0);
+
+    return buf - (unsigned char *)_buf;
+}
+
+static int full_write(int fd, const void *_buf, size_t len)
+{
+    const unsigned char *buf = _buf;
+    ssize_t ret;
+
+    do {
+        ret = write(fd, buf, len);
+        if (ret > 0) {
+            buf += ret;
+            len -= ret;
+        } else if (ret == 0) {
+            return 0;
+        } else {
+            return -errno;
+        }
+    } while (len > 0);
+
+    return buf - (const unsigned char *)_buf;
+}
+
+static void simtime_handle_message(SimTimeConnection *conn,
+                                   struct um_timetravel_msg *msg)
+{
+    struct um_timetravel_msg resp = {
+        .op = UM_TIMETRAVEL_ACK,
+    };
+
+    DPRINT(" %d | message %s (%lld, time=%lld)\n",
+           conn->idx, simtime_op_str(msg->op), msg->op, msg->time);
+
+    switch (msg->op) {
+    case UM_TIMETRAVEL_REQUEST:
+        if (calendar_entry_remove(&conn->entry)) {
+            conn->entry.time = conn->offset + msg->time;
+            calendar_entry_add(&conn->entry);
+            DPRINT(" %d | calendar entry added for %lld\n", conn->idx, msg->time);
+        } else {
+            conn->entry.time = conn->offset + msg->time;
+            DPRINT(" %d | calendar entry time updated for %lld\n", conn->idx, msg->time);
+        }
+        conn->num_requests++;
+        break;
+    case UM_TIMETRAVEL_WAIT:
+        conn->num_waits++;
+        calendar_entry_add(&conn->entry);
+        calendar_run_done(&conn->entry);
+        break;
+    case UM_TIMETRAVEL_GET:
+        resp.time = calendar_get_time() - conn->offset;
+        DPRINT(" %d | returning time %lld\n", conn->idx, resp.time);
+        break;
+    case UM_TIMETRAVEL_UPDATE:
+        if (conn->offset + msg->time > conn->entry.time) {
+            calendar_entry_remove(&conn->entry);
+        }
+        calendar_set_time(conn->offset + msg->time);
+        conn->num_updates++;
+        break;
+    default:
+        printf("ignoring invalid message %llu (time %llu)\n",
+               msg->op, msg->time);
+        break;
+    }
+
+    g_assert(full_write(g_io_channel_unix_get_fd(conn->chan), &resp, sizeof(resp)) == sizeof(resp));
+    DPRINT(" %d | sent ACK for message %s (%lld, time %lld)\n", conn->idx, simtime_op_str(msg->op), msg->op, msg->time);
+}
+
+static void simtime_send_message(SimTimeConnection *conn,
+                                 struct um_timetravel_msg *msg)
+{
+    g_mutex_lock(&conn->lock);
+    DPRINT(" %d | send %s (%lld, time=%lld)\n",
+           conn->idx, simtime_op_str(msg->op), msg->op, msg->time);
+    g_io_channel_set_flags(conn->chan, 0, NULL);
+    g_assert(full_write(g_io_channel_unix_get_fd(conn->chan), msg, sizeof(*msg)) == sizeof(*msg));
+    do {
+        g_assert(full_read(g_io_channel_unix_get_fd(conn->chan), msg, sizeof(*msg)) == sizeof(*msg));
+        DPRINT(" %d | read %s (%lld, time=%lld), expecting ACK (0)\n",
+               conn->idx, simtime_op_str(msg->op), msg->op, msg->time);
+        if (msg->op == UM_TIMETRAVEL_ACK)
+            break;
+        simtime_handle_message(conn, msg);
+    } while (1);
+    g_io_channel_set_flags(conn->chan, G_IO_FLAG_NONBLOCK, NULL);
+    g_mutex_unlock(&conn->lock);
+}
+
+static void simtime_calendar_cb(SimCalendarEntry *entry)
+{
+    SimTimeConnection *conn = container_of(entry, SimTimeConnection, entry);
+    struct um_timetravel_msg msg = {
+        .op = UM_TIMETRAVEL_RUN,
+        .time = entry->time - conn->offset,
+    };
+
+    simtime_send_message(conn, &msg);
+}
+
+static void __attribute__((used))
+simtime_update_until_cb(SimCalendarEntry *entry, unsigned long long time)
+{
+    SimTimeConnection *conn = container_of(entry, SimTimeConnection, entry);
+    struct um_timetravel_msg msg = {
+        .op = UM_TIMETRAVEL_FREE_UNTIL,
+        .time = time - conn->offset,
+    };
+
+    simtime_send_message(conn, &msg);
+}
+
+static gboolean simtime_read_cb(GIOChannel *src, GIOCondition c, gpointer data)
+{
+    SimTimeConnection *conn = data;
+    int fd = g_io_channel_unix_get_fd(src);
+    struct um_timetravel_msg msg;
+    int bytes;
+
+    g_mutex_lock(&conn->lock);
+    DPRINT(" %d | locked connection for reading\n", conn->idx);
+    bytes = full_read(fd, &msg, sizeof(msg));
+    if (bytes < 0 && bytes == -EAGAIN) {
+        g_mutex_unlock(&conn->lock);
+        return TRUE;
+    }
+    if (bytes <= 0) {
+        clients--;
+        printf("client disconnected, made %lld requests and waited %lld times, sent %lld updates\n",
+               conn->num_requests, conn->num_waits, conn->num_updates);
+        printf("we now have %d clients left\n", clients);
+
+        calendar_entry_destroy(&conn->entry);
+        // leak for now ... source is still attached
+        //g_free(conn);
+        g_mutex_unlock(&conn->lock);
+        return FALSE;
+    }
+    g_assert(bytes == sizeof(msg));
+
+    simtime_handle_message(conn, &msg);
+    g_mutex_unlock(&conn->lock);
+    DPRINT(" %d | unlocked connection\n", conn->idx);
+    return TRUE;
+}
+
+gboolean simtime_client_connected(GIOChannel *listen_src,
+                                  GIOCondition cond,
+                                  gpointer data)
+{
+    int lsock = g_io_channel_unix_get_fd(listen_src);
+    int csock = accept(lsock, NULL, NULL);
+    SimTimeConnection *conn;
+    GSource *src;
+
+    if (csock < 0) {
+        fprintf(stderr, "Accept error %s\n", strerror(errno));
+        return TRUE;
+    }
+
+    conn = g_new0(SimTimeConnection, 1);
+    if (!conn) {
+        return TRUE;
+    }
+
+    g_mutex_init(&conn->lock);
+
+    conn->chan = g_io_channel_unix_new(csock);
+    g_io_channel_set_flags(conn->chan, G_IO_FLAG_NONBLOCK, NULL);
+    src = g_io_create_watch(conn->chan, G_IO_IN);
+    g_source_set_callback(src, G_SOURCE_FUNC(simtime_read_cb), conn, NULL);
+    g_source_attach(src, g_main_context_get_thread_default());
+
+    conn->entry.callback = simtime_calendar_cb;
+    conn->entry.update_until = simtime_update_until_cb;
+    /*
+     * Mark this as a real scheduling client for purposes of
+     * tracking the number of them connected to the sim.
+     */
+    conn->entry.client = true;
+
+    clients++;
+    printf("client connected (now have %d)\n", clients);
+
+    conn->offset = calendar_get_time();
+    conn->idx = clients;
+    conn->entry.name = g_strdup_printf("time %d", clients);
+
+    return TRUE;
+}
diff --git a/include/standard-headers/linux/um_timetravel.h b/include/standard-headers/linux/um_timetravel.h
new file mode 100644
index 000000000000..3aaced426a92
--- /dev/null
+++ b/include/standard-headers/linux/um_timetravel.h
@@ -0,0 +1,107 @@ 
+/*
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ * Copyright (C) 2019 Intel Corporation
+ */
+#ifndef _UAPI_LINUX_UM_TIMETRAVEL_H
+#define _UAPI_LINUX_UM_TIMETRAVEL_H
+#include <linux/types.h>
+
+/**
+ * struct um_timetravel_msg - UM time travel message
+ *
+ * This is the basic message type, going in both directions.
+ *
+ * This is the message passed between the host (user-mode Linux instance)
+ * and the calendar (the application on the other side of the socket) in
+ * order to implement common scheduling.
+ *
+ * Whenever UML has an event it will request runtime for it from the
+ * calendar, and then wait for its turn until it can run, etc. Note
+ * that it will only ever request the single next runtime, i.e. multiple
+ * REQUEST messages override each other.
+ */
+struct um_timetravel_msg {
+	/**
+	 * @op: operation value from &enum um_timetravel_ops
+	 */
+	__u64 op;
+
+	/**
+	 * @time: time in nanoseconds
+	 */
+	__u64 time;
+};
+
+/**
+ * enum um_timetravel_ops - Operation codes
+ */
+enum um_timetravel_ops {
+	/**
+	 * @UM_TIMETRAVEL_ACK: response (ACK) to any previous message,
+	 *	this usually doesn't carry any data in the 'time' field
+	 *	unless otherwise specified below
+	 */
+	UM_TIMETRAVEL_ACK		= 0,
+
+	/**
+	 * @UM_TIMETRAVEL_REQUEST: request to run at the given time
+	 *	(host -> calendar)
+	 */
+	UM_TIMETRAVEL_REQUEST		= 1,
+
+	/**
+	 * @UM_TIMETRAVEL_WAIT: Indicate waiting for the previously requested
+	 *	runtime, new requests may be made while waiting (e.g. due to
+	 *	interrupts); the time field is ignored. The calendar must process
+	 *	this message and later	send a %UM_TIMETRAVEL_RUN message when
+	 *	the host can run again.
+	 *	(host -> calendar)
+	 */
+	UM_TIMETRAVEL_WAIT		= 2,
+
+	/**
+	 * @UM_TIMETRAVEL_GET: return the current time from the calendar in the
+	 *	ACK message, the time in the request message is ignored
+	 *	(host -> calendar)
+	 */
+	UM_TIMETRAVEL_GET		= 3,
+
+	/**
+	 * @UM_TIMETRAVEL_UPDATE: time update to the calendar, must be sent e.g.
+	 *	before kicking an interrupt to another calendar
+	 *	(host -> calendar)
+	 */
+	UM_TIMETRAVEL_UPDATE		= 4,
+
+	/**
+	 * @UM_TIMETRAVEL_RUN: run time request granted, current time is in
+	 *	the time field
+	 *	(calendar -> host)
+	 */
+	UM_TIMETRAVEL_RUN		= 5,
+
+	/**
+	 * @UM_TIMETRAVEL_FREE_UNTIL: Enable free-running until the given time,
+	 *	this is a message from the calendar telling the host that it can
+	 *	freely do its own scheduling for anything before the indicated
+	 *	time.
+	 *	Note that if a calendar sends this message once, the host may
+	 *	assume that it will also do so in the future, if it implements
+	 *	wraparound semantics for the time field.
+	 *	(calendar -> host)
+	 */
+	UM_TIMETRAVEL_FREE_UNTIL	= 6,
+};
+
+#endif /* _UAPI_LINUX_UM_TIMETRAVEL_H */