diff --git a/.gitignore b/.gitignore
index 045a0e04dcf21703bb7ebb6f61ca0cdc41b0fe1c..f0db325a7eaf5e9263cfa160ef633618b1eb0f0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ _build
 *.so.*
 *_pch.h.cpp
 *_resource.rc
+.*
 
 # qtcreator generated files
 *.pro.user*
diff --git a/src/client/client.py b/src/client/client.py
index fa219afd13840c7f80217c0765dc8cbcfc719271..c00092e4de1fa44f9ccf877a0b1e92e3f51ac875 100644
--- a/src/client/client.py
+++ b/src/client/client.py
@@ -1,4 +1,5 @@
-from loomcomm_pb2 import Register, Data, ClientMessage
+from loomcomm_pb2 import Register, Data, ClientMessage, ClientSubmit
+from loomreport_pb2 import Report
 
 import socket
 from connection import Connection
@@ -23,7 +24,7 @@ class TaskFailed(LoomException):
 
 class Client(object):
 
-    def __init__(self, address, port, info=False):
+    def __init__(self, address, port):
         self.server_address = address
         self.server_port = port
 
@@ -36,32 +37,27 @@ class Client(object):
         s.connect((address, port))
         self.connection = Connection(s)
 
-        if info:
-            self.info = []
-        else:
-            self.info = None
-
         msg = Register()
         msg.type = Register.REGISTER_CLIENT
         msg.protocol_version = LOOM_PROTOCOL_VERSION
-        msg.info = info
         self._send_message(msg)
 
         while self.symbols is None:
             self._read_symbols()
 
-    def submit(self, plan, results):
-
-        msg = plan.create_message(self.symbols)
+    def submit(self, plan, results, report=None):
+        msg = ClientSubmit()
+        msg.report = bool(report)
+        plan.set_message(msg.plan, self.symbols)
 
         if isinstance(results, Task):
             single_result = True
-            msg.result_ids.extend((results.id,))
+            msg.plan.result_ids.extend((results.id,))
             expected = 1
         else:
             single_result = False
             r = set(results)
-            msg.result_ids.extend(r.id for r in r)
+            msg.plan.result_ids.extend(r.id for r in r)
             expected = len(r)
 
         self._send_message(msg)
@@ -74,18 +70,35 @@ class Client(object):
             if cmsg.type == ClientMessage.DATA:
                 prologue = cmsg.data
                 data[prologue.id] = self._receive_data()
-            elif cmsg.type == ClientMessage.INFO:
-                self.add_info(cmsg.info)
+            elif cmsg.type == ClientMessage.EVENT:
+                self.process_event(cmsg.event)
             elif cmsg.type == ClientMessage.ERROR:
                 self.process_error(cmsg)
             else:
                 assert 0
 
+        if report:
+            self._write_report(report, plan)
+
         if single_result:
             return data[results.id]
         else:
             return [data[task.id] for task in results]
 
+    def _symbol_list(self):
+        symbols = [None] * len(self.symbols)
+        for name, index in self.symbols.items():
+            symbols[index] = name
+        return symbols
+
+    def _write_report(self, report_filename, plan):
+        report_msg = Report()
+        report_msg.symbols.extend(self._symbol_list())
+        plan.set_message(report_msg.plan, self.symbols)
+
+        with open(report_filename + ".report", "w") as f:
+            f.write(report_msg.SerializeToString())
+
     def _read_symbols(self):
         msg = self.connection.receive_message()
         cmsg = ClientMessage()
@@ -102,8 +115,8 @@ class Client(object):
         error = cmsg.error
         raise TaskFailed(error.id, error.worker, error.error_msg)
 
-    def add_info(self, info):
-        self.info.append((info.id, info.worker))
+    def process_event(self, event):
+        assert 0
 
     def _receive_data(self):
         msg_data = Data()
diff --git a/src/client/loomcomm_pb2.py b/src/client/loomcomm_pb2.py
index 1e8ea5e558908d7b46628ec084534d60eb25a031..c8470af5291d6876fcb9ed17d3ab2cdcf5e15bcc 100644
--- a/src/client/loomcomm_pb2.py
+++ b/src/client/loomcomm_pb2.py
@@ -13,13 +13,15 @@ from google.protobuf import descriptor_pb2
 _sym_db = _symbol_database.Default()
 
 
+import loomplan_pb2
 
 
 DESCRIPTOR = _descriptor.FileDescriptor(
   name='loomcomm.proto',
   package='loomcomm',
-  serialized_pb=_b('\n\x0eloomcomm.proto\x12\x08loomcomm\"\xcf\x01\n\x08Register\x12\x18\n\x10protocol_version\x18\x01 \x02(\x05\x12%\n\x04type\x18\x02 \x02(\x0e\x32\x17.loomcomm.Register.Type\x12\x0c\n\x04port\x18\x03 \x01(\x05\x12\x12\n\ntask_types\x18\x04 \x03(\t\x12\x12\n\ndata_types\x18\x05 \x03(\t\x12\x0c\n\x04\x63pus\x18\x06 \x01(\x05\x12\x0c\n\x04info\x18\n \x01(\x08\"0\n\x04Type\x12\x13\n\x0fREGISTER_WORKER\x10\x01\x12\x13\n\x0fREGISTER_CLIENT\x10\x02\"&\n\rServerMessage\"\x15\n\x04Type\x12\r\n\tSTART_JOB\x10\x01\"\xf1\x01\n\rWorkerCommand\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.loomcomm.WorkerCommand.Type\x12\n\n\x02id\x18\x02 \x01(\x05\x12\x11\n\ttask_type\x18\x03 \x01(\x05\x12\x13\n\x0btask_config\x18\x04 \x01(\t\x12\x13\n\x0btask_inputs\x18\x05 \x03(\x05\x12\x0f\n\x07\x61\x64\x64ress\x18\n \x01(\t\x12\x11\n\twith_size\x18\x0b \x01(\x08\x12\x0f\n\x07symbols\x18\x64 \x03(\t\"6\n\x04Type\x12\x08\n\x04TASK\x10\x01\x12\x08\n\x04SEND\x10\x02\x12\n\n\x06REMOVE\x10\x03\x12\x0e\n\nDICTIONARY\x10\x04\"\x9a\x01\n\x0eWorkerResponse\x12+\n\x04type\x18\x01 \x02(\x0e\x32\x1d.loomcomm.WorkerResponse.Type\x12\n\n\x02id\x18\x02 \x02(\x05\x12\x0c\n\x04size\x18\x03 \x01(\x04\x12\x0e\n\x06length\x18\x04 \x01(\x04\x12\x11\n\terror_msg\x18\x64 \x01(\t\"\x1e\n\x04Type\x12\n\n\x06\x46INISH\x10\x01\x12\n\n\x06\x46\x41ILED\x10\x02\"\x18\n\x08\x41nnounce\x12\x0c\n\x04port\x18\x01 \x02(\x05\"-\n\x0c\x44\x61taPrologue\x12\n\n\x02id\x18\x01 \x02(\x05\x12\x11\n\tdata_size\x18\x03 \x01(\x04\"Y\n\x04\x44\x61ta\x12\x0f\n\x07type_id\x18\x01 \x02(\x05\x12\x0c\n\x04size\x18\x02 \x02(\x04\x12\x0e\n\x06length\x18\x03 \x01(\x04\x12\x10\n\x08\x61rg0_u64\x18\x08 \x01(\x04\x12\x10\n\x08\x61rg1_u64\x18\t \x01(\x04\"\"\n\x04Info\x12\n\n\x02id\x18\x01 \x02(\x05\x12\x0e\n\x06worker\x18\x02 \x02(\t\"6\n\x05\x45rror\x12\n\n\x02id\x18\x01 \x02(\x05\x12\x0e\n\x06worker\x18\x02 \x02(\t\x12\x11\n\terror_msg\x18\x03 \x02(\t\"\xe7\x01\n\rClientMessage\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.loomcomm.ClientMessage.Type\x12$\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x16.loomcomm.DataPrologue\x12\x1c\n\x04info\x18\x03 \x01(\x0b\x32\x0e.loomcomm.Info\x12\x1e\n\x05\x65rror\x18\x04 \x01(\x0b\x32\x0f.loomcomm.Error\x12\x0f\n\x07symbols\x18\x05 \x03(\t\"5\n\x04Type\x12\x08\n\x04\x44\x41TA\x10\x01\x12\x08\n\x04INFO\x10\x02\x12\t\n\x05\x45RROR\x10\x03\x12\x0e\n\nDICTIONARY\x10\x04\x42\x02H\x03')
-)
+  serialized_pb=_b('\n\x0eloomcomm.proto\x12\x08loomcomm\x1a\x0eloomplan.proto\"\xc1\x01\n\x08Register\x12\x18\n\x10protocol_version\x18\x01 \x02(\x05\x12%\n\x04type\x18\x02 \x02(\x0e\x32\x17.loomcomm.Register.Type\x12\x0c\n\x04port\x18\x03 \x01(\x05\x12\x12\n\ntask_types\x18\x04 \x03(\t\x12\x12\n\ndata_types\x18\x05 \x03(\t\x12\x0c\n\x04\x63pus\x18\x06 \x01(\x05\"0\n\x04Type\x12\x13\n\x0fREGISTER_WORKER\x10\x01\x12\x13\n\x0fREGISTER_CLIENT\x10\x02\"&\n\rServerMessage\"\x15\n\x04Type\x12\r\n\tSTART_JOB\x10\x01\"\xf1\x01\n\rWorkerCommand\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.loomcomm.WorkerCommand.Type\x12\n\n\x02id\x18\x02 \x01(\x05\x12\x11\n\ttask_type\x18\x03 \x01(\x05\x12\x13\n\x0btask_config\x18\x04 \x01(\t\x12\x13\n\x0btask_inputs\x18\x05 \x03(\x05\x12\x0f\n\x07\x61\x64\x64ress\x18\n \x01(\t\x12\x11\n\twith_size\x18\x0b \x01(\x08\x12\x0f\n\x07symbols\x18\x64 \x03(\t\"6\n\x04Type\x12\x08\n\x04TASK\x10\x01\x12\x08\n\x04SEND\x10\x02\x12\n\n\x06REMOVE\x10\x03\x12\x0e\n\nDICTIONARY\x10\x04\"\x9a\x01\n\x0eWorkerResponse\x12+\n\x04type\x18\x01 \x02(\x0e\x32\x1d.loomcomm.WorkerResponse.Type\x12\n\n\x02id\x18\x02 \x02(\x05\x12\x0c\n\x04size\x18\x03 \x01(\x04\x12\x0e\n\x06length\x18\x04 \x01(\x04\x12\x11\n\terror_msg\x18\x64 \x01(\t\"\x1e\n\x04Type\x12\n\n\x06\x46INISH\x10\x01\x12\n\n\x06\x46\x41ILED\x10\x02\"\x18\n\x08\x41nnounce\x12\x0c\n\x04port\x18\x01 \x02(\x05\"-\n\x0c\x44\x61taPrologue\x12\n\n\x02id\x18\x01 \x02(\x05\x12\x11\n\tdata_size\x18\x03 \x01(\x04\"Y\n\x04\x44\x61ta\x12\x0f\n\x07type_id\x18\x01 \x02(\x05\x12\x0c\n\x04size\x18\x02 \x02(\x04\x12\x0e\n\x06length\x18\x03 \x01(\x04\x12\x10\n\x08\x61rg0_u64\x18\x08 \x01(\x04\x12\x10\n\x08\x61rg1_u64\x18\t \x01(\x04\"\x9f\x01\n\x05\x45vent\x12\x0c\n\x04time\x18\x01 \x02(\x04\x12\"\n\x04type\x18\x02 \x02(\x0e\x32\x14.loomcomm.Event.Type\x12\n\n\x02id\x18\x03 \x02(\x05\x12\x14\n\x0cworker_index\x18\x04 \x01(\x05\"B\n\x04Type\x12\x0e\n\nTASK_START\x10\x01\x12\x0c\n\x08TASK_END\x10\x02\x12\x0e\n\nSEND_START\x10\x03\x12\x0c\n\x08SEND_END\x10\x04\"6\n\x05\x45rror\x12\n\n\x02id\x18\x01 \x02(\x05\x12\x0e\n\x06worker\x18\x02 \x02(\t\x12\x11\n\terror_msg\x18\x03 \x02(\t\"\xea\x01\n\rClientMessage\x12*\n\x04type\x18\x01 \x02(\x0e\x32\x1c.loomcomm.ClientMessage.Type\x12$\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x16.loomcomm.DataPrologue\x12\x1e\n\x05\x65vent\x18\x03 \x01(\x0b\x32\x0f.loomcomm.Event\x12\x1e\n\x05\x65rror\x18\x04 \x01(\x0b\x32\x0f.loomcomm.Error\x12\x0f\n\x07symbols\x18\x05 \x03(\t\"6\n\x04Type\x12\x08\n\x04\x44\x41TA\x10\x01\x12\t\n\x05\x45VENT\x10\x02\x12\t\n\x05\x45RROR\x10\x03\x12\x0e\n\nDICTIONARY\x10\x04\"<\n\x0c\x43lientSubmit\x12\x1c\n\x04plan\x18\x01 \x02(\x0b\x32\x0e.loomplan.Plan\x12\x0e\n\x06report\x18\x02 \x02(\x08\x42\x02H\x03')
+  ,
+  dependencies=[loomplan_pb2.DESCRIPTOR,])
 _sym_db.RegisterFileDescriptor(DESCRIPTOR)
 
 
@@ -41,8 +43,8 @@ _REGISTER_TYPE = _descriptor.EnumDescriptor(
   ],
   containing_type=None,
   options=None,
-  serialized_start=188,
-  serialized_end=236,
+  serialized_start=190,
+  serialized_end=238,
 )
 _sym_db.RegisterEnumDescriptor(_REGISTER_TYPE)
 
@@ -59,8 +61,8 @@ _SERVERMESSAGE_TYPE = _descriptor.EnumDescriptor(
   ],
   containing_type=None,
   options=None,
-  serialized_start=255,
-  serialized_end=276,
+  serialized_start=257,
+  serialized_end=278,
 )
 _sym_db.RegisterEnumDescriptor(_SERVERMESSAGE_TYPE)
 
@@ -89,8 +91,8 @@ _WORKERCOMMAND_TYPE = _descriptor.EnumDescriptor(
   ],
   containing_type=None,
   options=None,
-  serialized_start=466,
-  serialized_end=520,
+  serialized_start=468,
+  serialized_end=522,
 )
 _sym_db.RegisterEnumDescriptor(_WORKERCOMMAND_TYPE)
 
@@ -111,11 +113,41 @@ _WORKERRESPONSE_TYPE = _descriptor.EnumDescriptor(
   ],
   containing_type=None,
   options=None,
-  serialized_start=647,
-  serialized_end=677,
+  serialized_start=649,
+  serialized_end=679,
 )
 _sym_db.RegisterEnumDescriptor(_WORKERRESPONSE_TYPE)
 
+_EVENT_TYPE = _descriptor.EnumDescriptor(
+  name='Type',
+  full_name='loomcomm.Event.Type',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='TASK_START', index=0, number=1,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='TASK_END', index=1, number=2,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='SEND_START', index=2, number=3,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='SEND_END', index=3, number=4,
+      options=None,
+      type=None),
+  ],
+  containing_type=None,
+  options=None,
+  serialized_start=939,
+  serialized_end=1005,
+)
+_sym_db.RegisterEnumDescriptor(_EVENT_TYPE)
+
 _CLIENTMESSAGE_TYPE = _descriptor.EnumDescriptor(
   name='Type',
   full_name='loomcomm.ClientMessage.Type',
@@ -127,7 +159,7 @@ _CLIENTMESSAGE_TYPE = _descriptor.EnumDescriptor(
       options=None,
       type=None),
     _descriptor.EnumValueDescriptor(
-      name='INFO', index=1, number=2,
+      name='EVENT', index=1, number=2,
       options=None,
       type=None),
     _descriptor.EnumValueDescriptor(
@@ -141,8 +173,8 @@ _CLIENTMESSAGE_TYPE = _descriptor.EnumDescriptor(
   ],
   containing_type=None,
   options=None,
-  serialized_start=1114,
-  serialized_end=1167,
+  serialized_start=1244,
+  serialized_end=1298,
 )
 _sym_db.RegisterEnumDescriptor(_CLIENTMESSAGE_TYPE)
 
@@ -196,13 +228,6 @@ _REGISTER = _descriptor.Descriptor(
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       options=None),
-    _descriptor.FieldDescriptor(
-      name='info', full_name='loomcomm.Register.info', index=6,
-      number=10, type=8, cpp_type=7, label=1,
-      has_default_value=False, default_value=False,
-      message_type=None, enum_type=None, containing_type=None,
-      is_extension=False, extension_scope=None,
-      options=None),
   ],
   extensions=[
   ],
@@ -215,8 +240,8 @@ _REGISTER = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=29,
-  serialized_end=236,
+  serialized_start=45,
+  serialized_end=238,
 )
 
 
@@ -239,8 +264,8 @@ _SERVERMESSAGE = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=238,
-  serialized_end=276,
+  serialized_start=240,
+  serialized_end=278,
 )
 
 
@@ -319,8 +344,8 @@ _WORKERCOMMAND = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=279,
-  serialized_end=520,
+  serialized_start=281,
+  serialized_end=522,
 )
 
 
@@ -378,8 +403,8 @@ _WORKERRESPONSE = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=523,
-  serialized_end=677,
+  serialized_start=525,
+  serialized_end=679,
 )
 
 
@@ -408,8 +433,8 @@ _ANNOUNCE = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=679,
-  serialized_end=703,
+  serialized_start=681,
+  serialized_end=705,
 )
 
 
@@ -445,8 +470,8 @@ _DATAPROLOGUE = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=705,
-  serialized_end=750,
+  serialized_start=707,
+  serialized_end=752,
 )
 
 
@@ -503,29 +528,43 @@ _DATA = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=752,
-  serialized_end=841,
+  serialized_start=754,
+  serialized_end=843,
 )
 
 
-_INFO = _descriptor.Descriptor(
-  name='Info',
-  full_name='loomcomm.Info',
+_EVENT = _descriptor.Descriptor(
+  name='Event',
+  full_name='loomcomm.Event',
   filename=None,
   file=DESCRIPTOR,
   containing_type=None,
   fields=[
     _descriptor.FieldDescriptor(
-      name='id', full_name='loomcomm.Info.id', index=0,
-      number=1, type=5, cpp_type=1, label=2,
+      name='time', full_name='loomcomm.Event.time', index=0,
+      number=1, type=4, cpp_type=4, label=2,
       has_default_value=False, default_value=0,
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       options=None),
     _descriptor.FieldDescriptor(
-      name='worker', full_name='loomcomm.Info.worker', index=1,
-      number=2, type=9, cpp_type=9, label=2,
-      has_default_value=False, default_value=_b("").decode('utf-8'),
+      name='type', full_name='loomcomm.Event.type', index=1,
+      number=2, type=14, cpp_type=8, label=2,
+      has_default_value=False, default_value=1,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='id', full_name='loomcomm.Event.id', index=2,
+      number=3, type=5, cpp_type=1, label=2,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='worker_index', full_name='loomcomm.Event.worker_index', index=3,
+      number=4, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       options=None),
@@ -534,14 +573,15 @@ _INFO = _descriptor.Descriptor(
   ],
   nested_types=[],
   enum_types=[
+    _EVENT_TYPE,
   ],
   options=None,
   is_extendable=False,
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=843,
-  serialized_end=877,
+  serialized_start=846,
+  serialized_end=1005,
 )
 
 
@@ -584,8 +624,8 @@ _ERROR = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=879,
-  serialized_end=933,
+  serialized_start=1007,
+  serialized_end=1061,
 )
 
 
@@ -611,7 +651,7 @@ _CLIENTMESSAGE = _descriptor.Descriptor(
       is_extension=False, extension_scope=None,
       options=None),
     _descriptor.FieldDescriptor(
-      name='info', full_name='loomcomm.ClientMessage.info', index=2,
+      name='event', full_name='loomcomm.ClientMessage.event', index=2,
       number=3, type=11, cpp_type=10, label=1,
       has_default_value=False, default_value=None,
       message_type=None, enum_type=None, containing_type=None,
@@ -643,8 +683,45 @@ _CLIENTMESSAGE = _descriptor.Descriptor(
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=936,
-  serialized_end=1167,
+  serialized_start=1064,
+  serialized_end=1298,
+)
+
+
+_CLIENTSUBMIT = _descriptor.Descriptor(
+  name='ClientSubmit',
+  full_name='loomcomm.ClientSubmit',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='plan', full_name='loomcomm.ClientSubmit.plan', index=0,
+      number=1, type=11, cpp_type=10, label=2,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='report', full_name='loomcomm.ClientSubmit.report', index=1,
+      number=2, type=8, cpp_type=7, label=2,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1300,
+  serialized_end=1360,
 )
 
 _REGISTER.fields_by_name['type'].enum_type = _REGISTER_TYPE
@@ -654,11 +731,14 @@ _WORKERCOMMAND.fields_by_name['type'].enum_type = _WORKERCOMMAND_TYPE
 _WORKERCOMMAND_TYPE.containing_type = _WORKERCOMMAND
 _WORKERRESPONSE.fields_by_name['type'].enum_type = _WORKERRESPONSE_TYPE
 _WORKERRESPONSE_TYPE.containing_type = _WORKERRESPONSE
+_EVENT.fields_by_name['type'].enum_type = _EVENT_TYPE
+_EVENT_TYPE.containing_type = _EVENT
 _CLIENTMESSAGE.fields_by_name['type'].enum_type = _CLIENTMESSAGE_TYPE
 _CLIENTMESSAGE.fields_by_name['data'].message_type = _DATAPROLOGUE
-_CLIENTMESSAGE.fields_by_name['info'].message_type = _INFO
+_CLIENTMESSAGE.fields_by_name['event'].message_type = _EVENT
 _CLIENTMESSAGE.fields_by_name['error'].message_type = _ERROR
 _CLIENTMESSAGE_TYPE.containing_type = _CLIENTMESSAGE
+_CLIENTSUBMIT.fields_by_name['plan'].message_type = loomplan_pb2._PLAN
 DESCRIPTOR.message_types_by_name['Register'] = _REGISTER
 DESCRIPTOR.message_types_by_name['ServerMessage'] = _SERVERMESSAGE
 DESCRIPTOR.message_types_by_name['WorkerCommand'] = _WORKERCOMMAND
@@ -666,9 +746,10 @@ DESCRIPTOR.message_types_by_name['WorkerResponse'] = _WORKERRESPONSE
 DESCRIPTOR.message_types_by_name['Announce'] = _ANNOUNCE
 DESCRIPTOR.message_types_by_name['DataPrologue'] = _DATAPROLOGUE
 DESCRIPTOR.message_types_by_name['Data'] = _DATA
-DESCRIPTOR.message_types_by_name['Info'] = _INFO
+DESCRIPTOR.message_types_by_name['Event'] = _EVENT
 DESCRIPTOR.message_types_by_name['Error'] = _ERROR
 DESCRIPTOR.message_types_by_name['ClientMessage'] = _CLIENTMESSAGE
+DESCRIPTOR.message_types_by_name['ClientSubmit'] = _CLIENTSUBMIT
 
 Register = _reflection.GeneratedProtocolMessageType('Register', (_message.Message,), dict(
   DESCRIPTOR = _REGISTER,
@@ -719,12 +800,12 @@ Data = _reflection.GeneratedProtocolMessageType('Data', (_message.Message,), dic
   ))
 _sym_db.RegisterMessage(Data)
 
-Info = _reflection.GeneratedProtocolMessageType('Info', (_message.Message,), dict(
-  DESCRIPTOR = _INFO,
+Event = _reflection.GeneratedProtocolMessageType('Event', (_message.Message,), dict(
+  DESCRIPTOR = _EVENT,
   __module__ = 'loomcomm_pb2'
-  # @@protoc_insertion_point(class_scope:loomcomm.Info)
+  # @@protoc_insertion_point(class_scope:loomcomm.Event)
   ))
-_sym_db.RegisterMessage(Info)
+_sym_db.RegisterMessage(Event)
 
 Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict(
   DESCRIPTOR = _ERROR,
@@ -740,6 +821,13 @@ ClientMessage = _reflection.GeneratedProtocolMessageType('ClientMessage', (_mess
   ))
 _sym_db.RegisterMessage(ClientMessage)
 
+ClientSubmit = _reflection.GeneratedProtocolMessageType('ClientSubmit', (_message.Message,), dict(
+  DESCRIPTOR = _CLIENTSUBMIT,
+  __module__ = 'loomcomm_pb2'
+  # @@protoc_insertion_point(class_scope:loomcomm.ClientSubmit)
+  ))
+_sym_db.RegisterMessage(ClientSubmit)
+
 
 DESCRIPTOR.has_options = True
 DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003'))
diff --git a/src/client/loomreport_pb2.py b/src/client/loomreport_pb2.py
new file mode 100644
index 0000000000000000000000000000000000000000..3fbc151853fe2ff34321af60d94233e719150fd8
--- /dev/null
+++ b/src/client/loomreport_pb2.py
@@ -0,0 +1,88 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: loomreport.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+import loomplan_pb2
+import loomcomm_pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='loomreport.proto',
+  package='loomcomm',
+  serialized_pb=_b('\n\x10loomreport.proto\x12\x08loomcomm\x1a\x0eloomplan.proto\x1a\x0eloomcomm.proto\"X\n\x06Report\x12\x0f\n\x07symbols\x18\x01 \x03(\t\x12\x1c\n\x04plan\x18\x02 \x02(\x0b\x32\x0e.loomplan.Plan\x12\x1f\n\x06\x65vents\x18\x03 \x03(\x0b\x32\x0f.loomcomm.EventB\x02H\x03')
+  ,
+  dependencies=[loomplan_pb2.DESCRIPTOR,loomcomm_pb2.DESCRIPTOR,])
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_REPORT = _descriptor.Descriptor(
+  name='Report',
+  full_name='loomcomm.Report',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='symbols', full_name='loomcomm.Report.symbols', index=0,
+      number=1, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='plan', full_name='loomcomm.Report.plan', index=1,
+      number=2, type=11, cpp_type=10, label=2,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='events', full_name='loomcomm.Report.events', index=2,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=62,
+  serialized_end=150,
+)
+
+_REPORT.fields_by_name['plan'].message_type = loomplan_pb2._PLAN
+_REPORT.fields_by_name['events'].message_type = loomcomm_pb2._EVENT
+DESCRIPTOR.message_types_by_name['Report'] = _REPORT
+
+Report = _reflection.GeneratedProtocolMessageType('Report', (_message.Message,), dict(
+  DESCRIPTOR = _REPORT,
+  __module__ = 'loomreport_pb2'
+  # @@protoc_insertion_point(class_scope:loomcomm.Report)
+  ))
+_sym_db.RegisterMessage(Report)
+
+
+DESCRIPTOR.has_options = True
+DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('H\003'))
+# @@protoc_insertion_point(module_scope)
diff --git a/src/client/plan.py b/src/client/plan.py
index bae59c0eab15afe57f1ad6d1c4204261ad01b936..5db5887d39b5f3fe4bdbde35802a5cee19ea8fcc 100644
--- a/src/client/plan.py
+++ b/src/client/plan.py
@@ -1,7 +1,6 @@
 
 import loomplan_pb2
 import loomrun_pb2
-import gv
 
 import struct
 
@@ -204,9 +203,7 @@ class Plan(object):
         task.policy = POLICY_SIMPLE
         return self.add(task)
 
-    def create_message(self, symbols):
-        msg = loomplan_pb2.Plan()
-
+    def set_message(self, msg, symbols):
         requests = set()
         for task in self.tasks:
             if task.resource_request:
@@ -221,23 +218,3 @@ class Plan(object):
             t = msg.tasks.add()
             task.set_message(t, symbols, requests)
         return msg
-
-    def write_dot(self, filename, info=None):
-        colors = ["red", "green", "blue", "orange", "violet"]
-        if info:
-            w = sorted(set(worker for id, worker in info))
-            workers = {}
-            for id, worker in info:
-                workers[id] = w.index(worker)
-            del w
-        else:
-            workers = None
-        graph = gv.Graph()
-        for task in self.tasks:
-            node = graph.node(task.id)
-            if workers:
-                node.color = colors[workers[task.id] % len(colors)]
-            node.label = "{}\n{}".format(str(task.id), task.task_type)
-            for inp in task.inputs:
-                graph.node(inp.id).add_arc(node)
-        graph.write(filename)
diff --git a/src/libloom/loomcomm.pb.cc b/src/libloom/loomcomm.pb.cc
index 7aafa6458cf6ad9c93ff72bfad4483619a254660..7e8db2cf4698666e1a6fb52dd92898feee140a27 100644
--- a/src/libloom/loomcomm.pb.cc
+++ b/src/libloom/loomcomm.pb.cc
@@ -23,9 +23,10 @@ void protobuf_ShutdownFile_loomcomm_2eproto() {
   delete Announce::default_instance_;
   delete DataPrologue::default_instance_;
   delete Data::default_instance_;
-  delete Info::default_instance_;
+  delete Event::default_instance_;
   delete Error::default_instance_;
   delete ClientMessage::default_instance_;
+  delete ClientSubmit::default_instance_;
 }
 
 #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
@@ -40,6 +41,7 @@ void protobuf_AddDesc_loomcomm_2eproto() {
   GOOGLE_PROTOBUF_VERIFY_VERSION;
 
 #endif
+  ::loomplan::protobuf_AddDesc_loomplan_2eproto();
   Register::default_instance_ = new Register();
   ServerMessage::default_instance_ = new ServerMessage();
   WorkerCommand::default_instance_ = new WorkerCommand();
@@ -47,9 +49,10 @@ void protobuf_AddDesc_loomcomm_2eproto() {
   Announce::default_instance_ = new Announce();
   DataPrologue::default_instance_ = new DataPrologue();
   Data::default_instance_ = new Data();
-  Info::default_instance_ = new Info();
+  Event::default_instance_ = new Event();
   Error::default_instance_ = new Error();
   ClientMessage::default_instance_ = new ClientMessage();
+  ClientSubmit::default_instance_ = new ClientSubmit();
   Register::default_instance_->InitAsDefaultInstance();
   ServerMessage::default_instance_->InitAsDefaultInstance();
   WorkerCommand::default_instance_->InitAsDefaultInstance();
@@ -57,9 +60,10 @@ void protobuf_AddDesc_loomcomm_2eproto() {
   Announce::default_instance_->InitAsDefaultInstance();
   DataPrologue::default_instance_->InitAsDefaultInstance();
   Data::default_instance_->InitAsDefaultInstance();
-  Info::default_instance_->InitAsDefaultInstance();
+  Event::default_instance_->InitAsDefaultInstance();
   Error::default_instance_->InitAsDefaultInstance();
   ClientMessage::default_instance_->InitAsDefaultInstance();
+  ClientSubmit::default_instance_->InitAsDefaultInstance();
   ::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_loomcomm_2eproto);
 }
 
@@ -104,7 +108,6 @@ const int Register::kPortFieldNumber;
 const int Register::kTaskTypesFieldNumber;
 const int Register::kDataTypesFieldNumber;
 const int Register::kCpusFieldNumber;
-const int Register::kInfoFieldNumber;
 #endif  // !_MSC_VER
 
 Register::Register()
@@ -130,7 +133,6 @@ void Register::SharedCtor() {
   type_ = 1;
   port_ = 0;
   cpus_ = 0;
-  info_ = false;
   ::memset(_has_bits_, 0, sizeof(_has_bits_));
 }
 
@@ -179,11 +181,10 @@ void Register::Clear() {
     ::memset(&first, 0, n);                                \
   } while (0)
 
-  if (_has_bits_[0 / 32] & 103) {
+  if (_has_bits_[0 / 32] & 39) {
     ZR_(port_, cpus_);
     protocol_version_ = 0;
     type_ = 1;
-    info_ = false;
   }
 
 #undef OFFSET_OF_FIELD_
@@ -298,21 +299,6 @@ bool Register::MergePartialFromCodedStream(
         } else {
           goto handle_unusual;
         }
-        if (input->ExpectTag(80)) goto parse_info;
-        break;
-      }
-
-      // optional bool info = 10;
-      case 10: {
-        if (tag == 80) {
-         parse_info:
-          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
-                   bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
-                 input, &info_)));
-          set_has_info();
-        } else {
-          goto handle_unusual;
-        }
         if (input->ExpectAtEnd()) goto success;
         break;
       }
@@ -375,11 +361,6 @@ void Register::SerializeWithCachedSizes(
     ::google::protobuf::internal::WireFormatLite::WriteInt32(6, this->cpus(), output);
   }
 
-  // optional bool info = 10;
-  if (has_info()) {
-    ::google::protobuf::internal::WireFormatLite::WriteBool(10, this->info(), output);
-  }
-
   output->WriteRaw(unknown_fields().data(),
                    unknown_fields().size());
   // @@protoc_insertion_point(serialize_end:loomcomm.Register)
@@ -416,11 +397,6 @@ int Register::ByteSize() const {
           this->cpus());
     }
 
-    // optional bool info = 10;
-    if (has_info()) {
-      total_size += 1 + 1;
-    }
-
   }
   // repeated string task_types = 4;
   total_size += 1 * this->task_types_size();
@@ -466,9 +442,6 @@ void Register::MergeFrom(const Register& from) {
     if (from.has_cpus()) {
       set_cpus(from.cpus());
     }
-    if (from.has_info()) {
-      set_info(from.info());
-    }
   }
   mutable_unknown_fields()->append(from.unknown_fields());
 }
@@ -493,7 +466,6 @@ void Register::Swap(Register* other) {
     task_types_.Swap(&other->task_types_);
     data_types_.Swap(&other->data_types_);
     std::swap(cpus_, other->cpus_);
-    std::swap(info_, other->info_);
     std::swap(_has_bits_[0], other->_has_bits_[0]);
     _unknown_fields_.swap(other->_unknown_fields_);
     std::swap(_cached_size_, other->_cached_size_);
@@ -2298,44 +2270,65 @@ void Data::Swap(Data* other) {
 
 // ===================================================================
 
+bool Event_Type_IsValid(int value) {
+  switch(value) {
+    case 1:
+    case 2:
+    case 3:
+    case 4:
+      return true;
+    default:
+      return false;
+  }
+}
+
 #ifndef _MSC_VER
-const int Info::kIdFieldNumber;
-const int Info::kWorkerFieldNumber;
+const Event_Type Event::TASK_START;
+const Event_Type Event::TASK_END;
+const Event_Type Event::SEND_START;
+const Event_Type Event::SEND_END;
+const Event_Type Event::Type_MIN;
+const Event_Type Event::Type_MAX;
+const int Event::Type_ARRAYSIZE;
+#endif  // _MSC_VER
+#ifndef _MSC_VER
+const int Event::kTimeFieldNumber;
+const int Event::kTypeFieldNumber;
+const int Event::kIdFieldNumber;
+const int Event::kWorkerIndexFieldNumber;
 #endif  // !_MSC_VER
 
-Info::Info()
+Event::Event()
   : ::google::protobuf::MessageLite() {
   SharedCtor();
-  // @@protoc_insertion_point(constructor:loomcomm.Info)
+  // @@protoc_insertion_point(constructor:loomcomm.Event)
 }
 
-void Info::InitAsDefaultInstance() {
+void Event::InitAsDefaultInstance() {
 }
 
-Info::Info(const Info& from)
+Event::Event(const Event& from)
   : ::google::protobuf::MessageLite() {
   SharedCtor();
   MergeFrom(from);
-  // @@protoc_insertion_point(copy_constructor:loomcomm.Info)
+  // @@protoc_insertion_point(copy_constructor:loomcomm.Event)
 }
 
-void Info::SharedCtor() {
-  ::google::protobuf::internal::GetEmptyString();
+void Event::SharedCtor() {
   _cached_size_ = 0;
+  time_ = GOOGLE_ULONGLONG(0);
+  type_ = 1;
   id_ = 0;
-  worker_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+  worker_index_ = 0;
   ::memset(_has_bits_, 0, sizeof(_has_bits_));
 }
 
-Info::~Info() {
-  // @@protoc_insertion_point(destructor:loomcomm.Info)
+Event::~Event() {
+  // @@protoc_insertion_point(destructor:loomcomm.Event)
   SharedDtor();
 }
 
-void Info::SharedDtor() {
-  if (worker_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-    delete worker_;
-  }
+void Event::SharedDtor() {
   #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
   if (this != &default_instance()) {
   #else
@@ -2344,12 +2337,12 @@ void Info::SharedDtor() {
   }
 }
 
-void Info::SetCachedSize(int size) const {
+void Event::SetCachedSize(int size) const {
   GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
   _cached_size_ = size;
   GOOGLE_SAFE_CONCURRENT_WRITES_END();
 }
-const Info& Info::default_instance() {
+const Event& Event::default_instance() {
 #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
   protobuf_AddDesc_loomcomm_2eproto();
 #else
@@ -2358,26 +2351,37 @@ const Info& Info::default_instance() {
   return *default_instance_;
 }
 
-Info* Info::default_instance_ = NULL;
+Event* Event::default_instance_ = NULL;
 
-Info* Info::New() const {
-  return new Info;
+Event* Event::New() const {
+  return new Event;
 }
 
-void Info::Clear() {
-  if (_has_bits_[0 / 32] & 3) {
-    id_ = 0;
-    if (has_worker()) {
-      if (worker_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-        worker_->clear();
-      }
-    }
+void Event::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>(      \
+  &reinterpret_cast<Event*>(16)->f) - \
+   reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do {                              \
+    size_t f = OFFSET_OF_FIELD_(first);                    \
+    size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last);  \
+    ::memset(&first, 0, n);                                \
+  } while (0)
+
+  if (_has_bits_[0 / 32] & 15) {
+    ZR_(id_, worker_index_);
+    time_ = GOOGLE_ULONGLONG(0);
+    type_ = 1;
   }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
   ::memset(_has_bits_, 0, sizeof(_has_bits_));
   mutable_unknown_fields()->clear();
 }
 
-bool Info::MergePartialFromCodedStream(
+bool Event::MergePartialFromCodedStream(
     ::google::protobuf::io::CodedInputStream* input) {
 #define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
   ::google::protobuf::uint32 tag;
@@ -2385,15 +2389,51 @@ bool Info::MergePartialFromCodedStream(
       mutable_unknown_fields());
   ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
       &unknown_fields_string);
-  // @@protoc_insertion_point(parse_start:loomcomm.Info)
+  // @@protoc_insertion_point(parse_start:loomcomm.Event)
   for (;;) {
     ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
     tag = p.first;
     if (!p.second) goto handle_unusual;
     switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
-      // required int32 id = 1;
+      // required uint64 time = 1;
       case 1: {
         if (tag == 8) {
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   ::google::protobuf::uint64, ::google::protobuf::internal::WireFormatLite::TYPE_UINT64>(
+                 input, &time_)));
+          set_has_time();
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(16)) goto parse_type;
+        break;
+      }
+
+      // required .loomcomm.Event.Type type = 2;
+      case 2: {
+        if (tag == 16) {
+         parse_type:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::loomcomm::Event_Type_IsValid(value)) {
+            set_type(static_cast< ::loomcomm::Event_Type >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(24)) goto parse_id;
+        break;
+      }
+
+      // required int32 id = 3;
+      case 3: {
+        if (tag == 24) {
+         parse_id:
           DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
                    ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
                  input, &id_)));
@@ -2401,16 +2441,18 @@ bool Info::MergePartialFromCodedStream(
         } else {
           goto handle_unusual;
         }
-        if (input->ExpectTag(18)) goto parse_worker;
+        if (input->ExpectTag(32)) goto parse_worker_index;
         break;
       }
 
-      // required string worker = 2;
-      case 2: {
-        if (tag == 18) {
-         parse_worker:
-          DO_(::google::protobuf::internal::WireFormatLite::ReadString(
-                input, this->mutable_worker()));
+      // optional int32 worker_index = 4;
+      case 4: {
+        if (tag == 32) {
+         parse_worker_index:
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+                 input, &worker_index_)));
+          set_has_worker_index();
         } else {
           goto handle_unusual;
         }
@@ -2432,49 +2474,72 @@ bool Info::MergePartialFromCodedStream(
     }
   }
 success:
-  // @@protoc_insertion_point(parse_success:loomcomm.Info)
+  // @@protoc_insertion_point(parse_success:loomcomm.Event)
   return true;
 failure:
-  // @@protoc_insertion_point(parse_failure:loomcomm.Info)
+  // @@protoc_insertion_point(parse_failure:loomcomm.Event)
   return false;
 #undef DO_
 }
 
-void Info::SerializeWithCachedSizes(
+void Event::SerializeWithCachedSizes(
     ::google::protobuf::io::CodedOutputStream* output) const {
-  // @@protoc_insertion_point(serialize_start:loomcomm.Info)
-  // required int32 id = 1;
+  // @@protoc_insertion_point(serialize_start:loomcomm.Event)
+  // required uint64 time = 1;
+  if (has_time()) {
+    ::google::protobuf::internal::WireFormatLite::WriteUInt64(1, this->time(), output);
+  }
+
+  // required .loomcomm.Event.Type type = 2;
+  if (has_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      2, this->type(), output);
+  }
+
+  // required int32 id = 3;
   if (has_id()) {
-    ::google::protobuf::internal::WireFormatLite::WriteInt32(1, this->id(), output);
+    ::google::protobuf::internal::WireFormatLite::WriteInt32(3, this->id(), output);
   }
 
-  // required string worker = 2;
-  if (has_worker()) {
-    ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
-      2, this->worker(), output);
+  // optional int32 worker_index = 4;
+  if (has_worker_index()) {
+    ::google::protobuf::internal::WireFormatLite::WriteInt32(4, this->worker_index(), output);
   }
 
   output->WriteRaw(unknown_fields().data(),
                    unknown_fields().size());
-  // @@protoc_insertion_point(serialize_end:loomcomm.Info)
+  // @@protoc_insertion_point(serialize_end:loomcomm.Event)
 }
 
-int Info::ByteSize() const {
+int Event::ByteSize() const {
   int total_size = 0;
 
   if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
-    // required int32 id = 1;
+    // required uint64 time = 1;
+    if (has_time()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::UInt64Size(
+          this->time());
+    }
+
+    // required .loomcomm.Event.Type type = 2;
+    if (has_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->type());
+    }
+
+    // required int32 id = 3;
     if (has_id()) {
       total_size += 1 +
         ::google::protobuf::internal::WireFormatLite::Int32Size(
           this->id());
     }
 
-    // required string worker = 2;
-    if (has_worker()) {
+    // optional int32 worker_index = 4;
+    if (has_worker_index()) {
       total_size += 1 +
-        ::google::protobuf::internal::WireFormatLite::StringSize(
-          this->worker());
+        ::google::protobuf::internal::WireFormatLite::Int32Size(
+          this->worker_index());
     }
 
   }
@@ -2486,48 +2551,56 @@ int Info::ByteSize() const {
   return total_size;
 }
 
-void Info::CheckTypeAndMergeFrom(
+void Event::CheckTypeAndMergeFrom(
     const ::google::protobuf::MessageLite& from) {
-  MergeFrom(*::google::protobuf::down_cast<const Info*>(&from));
+  MergeFrom(*::google::protobuf::down_cast<const Event*>(&from));
 }
 
-void Info::MergeFrom(const Info& from) {
+void Event::MergeFrom(const Event& from) {
   GOOGLE_CHECK_NE(&from, this);
   if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    if (from.has_time()) {
+      set_time(from.time());
+    }
+    if (from.has_type()) {
+      set_type(from.type());
+    }
     if (from.has_id()) {
       set_id(from.id());
     }
-    if (from.has_worker()) {
-      set_worker(from.worker());
+    if (from.has_worker_index()) {
+      set_worker_index(from.worker_index());
     }
   }
   mutable_unknown_fields()->append(from.unknown_fields());
 }
 
-void Info::CopyFrom(const Info& from) {
+void Event::CopyFrom(const Event& from) {
   if (&from == this) return;
   Clear();
   MergeFrom(from);
 }
 
-bool Info::IsInitialized() const {
-  if ((_has_bits_[0] & 0x00000003) != 0x00000003) return false;
+bool Event::IsInitialized() const {
+  if ((_has_bits_[0] & 0x00000007) != 0x00000007) return false;
 
   return true;
 }
 
-void Info::Swap(Info* other) {
+void Event::Swap(Event* other) {
   if (other != this) {
+    std::swap(time_, other->time_);
+    std::swap(type_, other->type_);
     std::swap(id_, other->id_);
-    std::swap(worker_, other->worker_);
+    std::swap(worker_index_, other->worker_index_);
     std::swap(_has_bits_[0], other->_has_bits_[0]);
     _unknown_fields_.swap(other->_unknown_fields_);
     std::swap(_cached_size_, other->_cached_size_);
   }
 }
 
-::std::string Info::GetTypeName() const {
-  return "loomcomm.Info";
+::std::string Event::GetTypeName() const {
+  return "loomcomm.Event";
 }
 
 
@@ -2822,7 +2895,7 @@ bool ClientMessage_Type_IsValid(int value) {
 
 #ifndef _MSC_VER
 const ClientMessage_Type ClientMessage::DATA;
-const ClientMessage_Type ClientMessage::INFO;
+const ClientMessage_Type ClientMessage::EVENT;
 const ClientMessage_Type ClientMessage::ERROR;
 const ClientMessage_Type ClientMessage::DICTIONARY;
 const ClientMessage_Type ClientMessage::Type_MIN;
@@ -2832,7 +2905,7 @@ const int ClientMessage::Type_ARRAYSIZE;
 #ifndef _MSC_VER
 const int ClientMessage::kTypeFieldNumber;
 const int ClientMessage::kDataFieldNumber;
-const int ClientMessage::kInfoFieldNumber;
+const int ClientMessage::kEventFieldNumber;
 const int ClientMessage::kErrorFieldNumber;
 const int ClientMessage::kSymbolsFieldNumber;
 #endif  // !_MSC_VER
@@ -2851,10 +2924,10 @@ void ClientMessage::InitAsDefaultInstance() {
   data_ = const_cast< ::loomcomm::DataPrologue*>(&::loomcomm::DataPrologue::default_instance());
 #endif
 #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
-  info_ = const_cast< ::loomcomm::Info*>(
-      ::loomcomm::Info::internal_default_instance());
+  event_ = const_cast< ::loomcomm::Event*>(
+      ::loomcomm::Event::internal_default_instance());
 #else
-  info_ = const_cast< ::loomcomm::Info*>(&::loomcomm::Info::default_instance());
+  event_ = const_cast< ::loomcomm::Event*>(&::loomcomm::Event::default_instance());
 #endif
 #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
   error_ = const_cast< ::loomcomm::Error*>(
@@ -2876,7 +2949,7 @@ void ClientMessage::SharedCtor() {
   _cached_size_ = 0;
   type_ = 1;
   data_ = NULL;
-  info_ = NULL;
+  event_ = NULL;
   error_ = NULL;
   ::memset(_has_bits_, 0, sizeof(_has_bits_));
 }
@@ -2893,7 +2966,7 @@ void ClientMessage::SharedDtor() {
   if (this != default_instance_) {
   #endif
     delete data_;
-    delete info_;
+    delete event_;
     delete error_;
   }
 }
@@ -2924,8 +2997,8 @@ void ClientMessage::Clear() {
     if (has_data()) {
       if (data_ != NULL) data_->::loomcomm::DataPrologue::Clear();
     }
-    if (has_info()) {
-      if (info_ != NULL) info_->::loomcomm::Info::Clear();
+    if (has_event()) {
+      if (event_ != NULL) event_->::loomcomm::Event::Clear();
     }
     if (has_error()) {
       if (error_ != NULL) error_->::loomcomm::Error::Clear();
@@ -2979,16 +3052,16 @@ bool ClientMessage::MergePartialFromCodedStream(
         } else {
           goto handle_unusual;
         }
-        if (input->ExpectTag(26)) goto parse_info;
+        if (input->ExpectTag(26)) goto parse_event;
         break;
       }
 
-      // optional .loomcomm.Info info = 3;
+      // optional .loomcomm.Event event = 3;
       case 3: {
         if (tag == 26) {
-         parse_info:
+         parse_event:
           DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
-               input, mutable_info()));
+               input, mutable_event()));
         } else {
           goto handle_unusual;
         }
@@ -3060,10 +3133,10 @@ void ClientMessage::SerializeWithCachedSizes(
       2, this->data(), output);
   }
 
-  // optional .loomcomm.Info info = 3;
-  if (has_info()) {
+  // optional .loomcomm.Event event = 3;
+  if (has_event()) {
     ::google::protobuf::internal::WireFormatLite::WriteMessage(
-      3, this->info(), output);
+      3, this->event(), output);
   }
 
   // optional .loomcomm.Error error = 4;
@@ -3100,11 +3173,11 @@ int ClientMessage::ByteSize() const {
           this->data());
     }
 
-    // optional .loomcomm.Info info = 3;
-    if (has_info()) {
+    // optional .loomcomm.Event event = 3;
+    if (has_event()) {
       total_size += 1 +
         ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
-          this->info());
+          this->event());
     }
 
     // optional .loomcomm.Error error = 4;
@@ -3145,8 +3218,8 @@ void ClientMessage::MergeFrom(const ClientMessage& from) {
     if (from.has_data()) {
       mutable_data()->::loomcomm::DataPrologue::MergeFrom(from.data());
     }
-    if (from.has_info()) {
-      mutable_info()->::loomcomm::Info::MergeFrom(from.info());
+    if (from.has_event()) {
+      mutable_event()->::loomcomm::Event::MergeFrom(from.event());
     }
     if (from.has_error()) {
       mutable_error()->::loomcomm::Error::MergeFrom(from.error());
@@ -3167,8 +3240,8 @@ bool ClientMessage::IsInitialized() const {
   if (has_data()) {
     if (!this->data().IsInitialized()) return false;
   }
-  if (has_info()) {
-    if (!this->info().IsInitialized()) return false;
+  if (has_event()) {
+    if (!this->event().IsInitialized()) return false;
   }
   if (has_error()) {
     if (!this->error().IsInitialized()) return false;
@@ -3180,7 +3253,7 @@ void ClientMessage::Swap(ClientMessage* other) {
   if (other != this) {
     std::swap(type_, other->type_);
     std::swap(data_, other->data_);
-    std::swap(info_, other->info_);
+    std::swap(event_, other->event_);
     std::swap(error_, other->error_);
     symbols_.Swap(&other->symbols_);
     std::swap(_has_bits_[0], other->_has_bits_[0]);
@@ -3194,6 +3267,243 @@ void ClientMessage::Swap(ClientMessage* other) {
 }
 
 
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ClientSubmit::kPlanFieldNumber;
+const int ClientSubmit::kReportFieldNumber;
+#endif  // !_MSC_VER
+
+ClientSubmit::ClientSubmit()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:loomcomm.ClientSubmit)
+}
+
+void ClientSubmit::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  plan_ = const_cast< ::loomplan::Plan*>(
+      ::loomplan::Plan::internal_default_instance());
+#else
+  plan_ = const_cast< ::loomplan::Plan*>(&::loomplan::Plan::default_instance());
+#endif
+}
+
+ClientSubmit::ClientSubmit(const ClientSubmit& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:loomcomm.ClientSubmit)
+}
+
+void ClientSubmit::SharedCtor() {
+  _cached_size_ = 0;
+  plan_ = NULL;
+  report_ = false;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ClientSubmit::~ClientSubmit() {
+  // @@protoc_insertion_point(destructor:loomcomm.ClientSubmit)
+  SharedDtor();
+}
+
+void ClientSubmit::SharedDtor() {
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+    delete plan_;
+  }
+}
+
+void ClientSubmit::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ClientSubmit& ClientSubmit::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_loomcomm_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_loomcomm_2eproto();
+#endif
+  return *default_instance_;
+}
+
+ClientSubmit* ClientSubmit::default_instance_ = NULL;
+
+ClientSubmit* ClientSubmit::New() const {
+  return new ClientSubmit;
+}
+
+void ClientSubmit::Clear() {
+  if (_has_bits_[0 / 32] & 3) {
+    if (has_plan()) {
+      if (plan_ != NULL) plan_->::loomplan::Plan::Clear();
+    }
+    report_ = false;
+  }
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool ClientSubmit::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:loomcomm.ClientSubmit)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // required .loomplan.Plan plan = 1;
+      case 1: {
+        if (tag == 10) {
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_plan()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(16)) goto parse_report;
+        break;
+      }
+
+      // required bool report = 2;
+      case 2: {
+        if (tag == 16) {
+         parse_report:
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   bool, ::google::protobuf::internal::WireFormatLite::TYPE_BOOL>(
+                 input, &report_)));
+          set_has_report();
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:loomcomm.ClientSubmit)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:loomcomm.ClientSubmit)
+  return false;
+#undef DO_
+}
+
+void ClientSubmit::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:loomcomm.ClientSubmit)
+  // required .loomplan.Plan plan = 1;
+  if (has_plan()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      1, this->plan(), output);
+  }
+
+  // required bool report = 2;
+  if (has_report()) {
+    ::google::protobuf::internal::WireFormatLite::WriteBool(2, this->report(), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:loomcomm.ClientSubmit)
+}
+
+int ClientSubmit::ByteSize() const {
+  int total_size = 0;
+
+  if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    // required .loomplan.Plan plan = 1;
+    if (has_plan()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->plan());
+    }
+
+    // required bool report = 2;
+    if (has_report()) {
+      total_size += 1 + 1;
+    }
+
+  }
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void ClientSubmit::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const ClientSubmit*>(&from));
+}
+
+void ClientSubmit::MergeFrom(const ClientSubmit& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    if (from.has_plan()) {
+      mutable_plan()->::loomplan::Plan::MergeFrom(from.plan());
+    }
+    if (from.has_report()) {
+      set_report(from.report());
+    }
+  }
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ClientSubmit::CopyFrom(const ClientSubmit& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool ClientSubmit::IsInitialized() const {
+  if ((_has_bits_[0] & 0x00000003) != 0x00000003) return false;
+
+  if (has_plan()) {
+    if (!this->plan().IsInitialized()) return false;
+  }
+  return true;
+}
+
+void ClientSubmit::Swap(ClientSubmit* other) {
+  if (other != this) {
+    std::swap(plan_, other->plan_);
+    std::swap(report_, other->report_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string ClientSubmit::GetTypeName() const {
+  return "loomcomm.ClientSubmit";
+}
+
+
 // @@protoc_insertion_point(namespace_scope)
 
 }  // namespace loomcomm
diff --git a/src/libloom/loomcomm.pb.h b/src/libloom/loomcomm.pb.h
index a7c422a7deac1ee4ab5183b51f2baabedd469ad8..c57ae7dd44e22722a70c5896d5627d0807d37cd1 100644
--- a/src/libloom/loomcomm.pb.h
+++ b/src/libloom/loomcomm.pb.h
@@ -23,6 +23,7 @@
 #include <google/protobuf/message_lite.h>
 #include <google/protobuf/repeated_field.h>
 #include <google/protobuf/extension_set.h>
+#include "loomplan.pb.h"
 // @@protoc_insertion_point(includes)
 
 namespace loomcomm {
@@ -39,9 +40,10 @@ class WorkerResponse;
 class Announce;
 class DataPrologue;
 class Data;
-class Info;
+class Event;
 class Error;
 class ClientMessage;
+class ClientSubmit;
 
 enum Register_Type {
   Register_Type_REGISTER_WORKER = 1,
@@ -80,9 +82,20 @@ const WorkerResponse_Type WorkerResponse_Type_Type_MIN = WorkerResponse_Type_FIN
 const WorkerResponse_Type WorkerResponse_Type_Type_MAX = WorkerResponse_Type_FAILED;
 const int WorkerResponse_Type_Type_ARRAYSIZE = WorkerResponse_Type_Type_MAX + 1;
 
+enum Event_Type {
+  Event_Type_TASK_START = 1,
+  Event_Type_TASK_END = 2,
+  Event_Type_SEND_START = 3,
+  Event_Type_SEND_END = 4
+};
+bool Event_Type_IsValid(int value);
+const Event_Type Event_Type_Type_MIN = Event_Type_TASK_START;
+const Event_Type Event_Type_Type_MAX = Event_Type_SEND_END;
+const int Event_Type_Type_ARRAYSIZE = Event_Type_Type_MAX + 1;
+
 enum ClientMessage_Type {
   ClientMessage_Type_DATA = 1,
-  ClientMessage_Type_INFO = 2,
+  ClientMessage_Type_EVENT = 2,
   ClientMessage_Type_ERROR = 3,
   ClientMessage_Type_DICTIONARY = 4
 };
@@ -227,13 +240,6 @@ class Register : public ::google::protobuf::MessageLite {
   inline ::google::protobuf::int32 cpus() const;
   inline void set_cpus(::google::protobuf::int32 value);
 
-  // optional bool info = 10;
-  inline bool has_info() const;
-  inline void clear_info();
-  static const int kInfoFieldNumber = 10;
-  inline bool info() const;
-  inline void set_info(bool value);
-
   // @@protoc_insertion_point(class_scope:loomcomm.Register)
  private:
   inline void set_has_protocol_version();
@@ -244,8 +250,6 @@ class Register : public ::google::protobuf::MessageLite {
   inline void clear_has_port();
   inline void set_has_cpus();
   inline void clear_has_cpus();
-  inline void set_has_info();
-  inline void clear_has_info();
 
   ::std::string _unknown_fields_;
 
@@ -257,7 +261,6 @@ class Register : public ::google::protobuf::MessageLite {
   ::google::protobuf::int32 port_;
   ::google::protobuf::int32 cpus_;
   ::google::protobuf::RepeatedPtrField< ::std::string> data_types_;
-  bool info_;
   #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
   friend void  protobuf_AddDesc_loomcomm_2eproto_impl();
   #else
@@ -1032,14 +1035,14 @@ class Data : public ::google::protobuf::MessageLite {
 };
 // -------------------------------------------------------------------
 
-class Info : public ::google::protobuf::MessageLite {
+class Event : public ::google::protobuf::MessageLite {
  public:
-  Info();
-  virtual ~Info();
+  Event();
+  virtual ~Event();
 
-  Info(const Info& from);
+  Event(const Event& from);
 
-  inline Info& operator=(const Info& from) {
+  inline Event& operator=(const Event& from) {
     CopyFrom(from);
     return *this;
   }
@@ -1052,26 +1055,26 @@ class Info : public ::google::protobuf::MessageLite {
     return &_unknown_fields_;
   }
 
-  static const Info& default_instance();
+  static const Event& default_instance();
 
   #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
   // Returns the internal default instance pointer. This function can
   // return NULL thus should not be used by the user. This is intended
   // for Protobuf internal code. Please use default_instance() declared
   // above instead.
-  static inline const Info* internal_default_instance() {
+  static inline const Event* internal_default_instance() {
     return default_instance_;
   }
   #endif
 
-  void Swap(Info* other);
+  void Swap(Event* other);
 
   // implements Message ----------------------------------------------
 
-  Info* New() const;
+  Event* New() const;
   void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
-  void CopyFrom(const Info& from);
-  void MergeFrom(const Info& from);
+  void CopyFrom(const Event& from);
+  void MergeFrom(const Event& from);
   void Clear();
   bool IsInitialized() const;
 
@@ -1091,40 +1094,70 @@ class Info : public ::google::protobuf::MessageLite {
 
   // nested types ----------------------------------------------------
 
+  typedef Event_Type Type;
+  static const Type TASK_START = Event_Type_TASK_START;
+  static const Type TASK_END = Event_Type_TASK_END;
+  static const Type SEND_START = Event_Type_SEND_START;
+  static const Type SEND_END = Event_Type_SEND_END;
+  static inline bool Type_IsValid(int value) {
+    return Event_Type_IsValid(value);
+  }
+  static const Type Type_MIN =
+    Event_Type_Type_MIN;
+  static const Type Type_MAX =
+    Event_Type_Type_MAX;
+  static const int Type_ARRAYSIZE =
+    Event_Type_Type_ARRAYSIZE;
+
   // accessors -------------------------------------------------------
 
-  // required int32 id = 1;
+  // required uint64 time = 1;
+  inline bool has_time() const;
+  inline void clear_time();
+  static const int kTimeFieldNumber = 1;
+  inline ::google::protobuf::uint64 time() const;
+  inline void set_time(::google::protobuf::uint64 value);
+
+  // required .loomcomm.Event.Type type = 2;
+  inline bool has_type() const;
+  inline void clear_type();
+  static const int kTypeFieldNumber = 2;
+  inline ::loomcomm::Event_Type type() const;
+  inline void set_type(::loomcomm::Event_Type value);
+
+  // required int32 id = 3;
   inline bool has_id() const;
   inline void clear_id();
-  static const int kIdFieldNumber = 1;
+  static const int kIdFieldNumber = 3;
   inline ::google::protobuf::int32 id() const;
   inline void set_id(::google::protobuf::int32 value);
 
-  // required string worker = 2;
-  inline bool has_worker() const;
-  inline void clear_worker();
-  static const int kWorkerFieldNumber = 2;
-  inline const ::std::string& worker() const;
-  inline void set_worker(const ::std::string& value);
-  inline void set_worker(const char* value);
-  inline void set_worker(const char* value, size_t size);
-  inline ::std::string* mutable_worker();
-  inline ::std::string* release_worker();
-  inline void set_allocated_worker(::std::string* worker);
+  // optional int32 worker_index = 4;
+  inline bool has_worker_index() const;
+  inline void clear_worker_index();
+  static const int kWorkerIndexFieldNumber = 4;
+  inline ::google::protobuf::int32 worker_index() const;
+  inline void set_worker_index(::google::protobuf::int32 value);
 
-  // @@protoc_insertion_point(class_scope:loomcomm.Info)
+  // @@protoc_insertion_point(class_scope:loomcomm.Event)
  private:
+  inline void set_has_time();
+  inline void clear_has_time();
+  inline void set_has_type();
+  inline void clear_has_type();
   inline void set_has_id();
   inline void clear_has_id();
-  inline void set_has_worker();
-  inline void clear_has_worker();
+  inline void set_has_worker_index();
+  inline void clear_has_worker_index();
 
   ::std::string _unknown_fields_;
 
   ::google::protobuf::uint32 _has_bits_[1];
   mutable int _cached_size_;
-  ::std::string* worker_;
+  ::google::protobuf::uint64 time_;
+  int type_;
   ::google::protobuf::int32 id_;
+  ::google::protobuf::int32 worker_index_;
   #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
   friend void  protobuf_AddDesc_loomcomm_2eproto_impl();
   #else
@@ -1134,7 +1167,7 @@ class Info : public ::google::protobuf::MessageLite {
   friend void protobuf_ShutdownFile_loomcomm_2eproto();
 
   void InitAsDefaultInstance();
-  static Info* default_instance_;
+  static Event* default_instance_;
 };
 // -------------------------------------------------------------------
 
@@ -1320,7 +1353,7 @@ class ClientMessage : public ::google::protobuf::MessageLite {
 
   typedef ClientMessage_Type Type;
   static const Type DATA = ClientMessage_Type_DATA;
-  static const Type INFO = ClientMessage_Type_INFO;
+  static const Type EVENT = ClientMessage_Type_EVENT;
   static const Type ERROR = ClientMessage_Type_ERROR;
   static const Type DICTIONARY = ClientMessage_Type_DICTIONARY;
   static inline bool Type_IsValid(int value) {
@@ -1351,14 +1384,14 @@ class ClientMessage : public ::google::protobuf::MessageLite {
   inline ::loomcomm::DataPrologue* release_data();
   inline void set_allocated_data(::loomcomm::DataPrologue* data);
 
-  // optional .loomcomm.Info info = 3;
-  inline bool has_info() const;
-  inline void clear_info();
-  static const int kInfoFieldNumber = 3;
-  inline const ::loomcomm::Info& info() const;
-  inline ::loomcomm::Info* mutable_info();
-  inline ::loomcomm::Info* release_info();
-  inline void set_allocated_info(::loomcomm::Info* info);
+  // optional .loomcomm.Event event = 3;
+  inline bool has_event() const;
+  inline void clear_event();
+  static const int kEventFieldNumber = 3;
+  inline const ::loomcomm::Event& event() const;
+  inline ::loomcomm::Event* mutable_event();
+  inline ::loomcomm::Event* release_event();
+  inline void set_allocated_event(::loomcomm::Event* event);
 
   // optional .loomcomm.Error error = 4;
   inline bool has_error() const;
@@ -1391,8 +1424,8 @@ class ClientMessage : public ::google::protobuf::MessageLite {
   inline void clear_has_type();
   inline void set_has_data();
   inline void clear_has_data();
-  inline void set_has_info();
-  inline void clear_has_info();
+  inline void set_has_event();
+  inline void clear_has_event();
   inline void set_has_error();
   inline void clear_has_error();
 
@@ -1401,7 +1434,7 @@ class ClientMessage : public ::google::protobuf::MessageLite {
   ::google::protobuf::uint32 _has_bits_[1];
   mutable int _cached_size_;
   ::loomcomm::DataPrologue* data_;
-  ::loomcomm::Info* info_;
+  ::loomcomm::Event* event_;
   ::loomcomm::Error* error_;
   ::google::protobuf::RepeatedPtrField< ::std::string> symbols_;
   int type_;
@@ -1416,6 +1449,109 @@ class ClientMessage : public ::google::protobuf::MessageLite {
   void InitAsDefaultInstance();
   static ClientMessage* default_instance_;
 };
+// -------------------------------------------------------------------
+
+class ClientSubmit : public ::google::protobuf::MessageLite {
+ public:
+  ClientSubmit();
+  virtual ~ClientSubmit();
+
+  ClientSubmit(const ClientSubmit& from);
+
+  inline ClientSubmit& operator=(const ClientSubmit& from) {
+    CopyFrom(from);
+    return *this;
+  }
+
+  inline const ::std::string& unknown_fields() const {
+    return _unknown_fields_;
+  }
+
+  inline ::std::string* mutable_unknown_fields() {
+    return &_unknown_fields_;
+  }
+
+  static const ClientSubmit& default_instance();
+
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  // Returns the internal default instance pointer. This function can
+  // return NULL thus should not be used by the user. This is intended
+  // for Protobuf internal code. Please use default_instance() declared
+  // above instead.
+  static inline const ClientSubmit* internal_default_instance() {
+    return default_instance_;
+  }
+  #endif
+
+  void Swap(ClientSubmit* other);
+
+  // implements Message ----------------------------------------------
+
+  ClientSubmit* New() const;
+  void CheckTypeAndMergeFrom(const ::google::protobuf::MessageLite& from);
+  void CopyFrom(const ClientSubmit& from);
+  void MergeFrom(const ClientSubmit& from);
+  void Clear();
+  bool IsInitialized() const;
+
+  int ByteSize() const;
+  bool MergePartialFromCodedStream(
+      ::google::protobuf::io::CodedInputStream* input);
+  void SerializeWithCachedSizes(
+      ::google::protobuf::io::CodedOutputStream* output) const;
+  void DiscardUnknownFields();
+  int GetCachedSize() const { return _cached_size_; }
+  private:
+  void SharedCtor();
+  void SharedDtor();
+  void SetCachedSize(int size) const;
+  public:
+  ::std::string GetTypeName() const;
+
+  // nested types ----------------------------------------------------
+
+  // accessors -------------------------------------------------------
+
+  // required .loomplan.Plan plan = 1;
+  inline bool has_plan() const;
+  inline void clear_plan();
+  static const int kPlanFieldNumber = 1;
+  inline const ::loomplan::Plan& plan() const;
+  inline ::loomplan::Plan* mutable_plan();
+  inline ::loomplan::Plan* release_plan();
+  inline void set_allocated_plan(::loomplan::Plan* plan);
+
+  // required bool report = 2;
+  inline bool has_report() const;
+  inline void clear_report();
+  static const int kReportFieldNumber = 2;
+  inline bool report() const;
+  inline void set_report(bool value);
+
+  // @@protoc_insertion_point(class_scope:loomcomm.ClientSubmit)
+ private:
+  inline void set_has_plan();
+  inline void clear_has_plan();
+  inline void set_has_report();
+  inline void clear_has_report();
+
+  ::std::string _unknown_fields_;
+
+  ::google::protobuf::uint32 _has_bits_[1];
+  mutable int _cached_size_;
+  ::loomplan::Plan* plan_;
+  bool report_;
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  friend void  protobuf_AddDesc_loomcomm_2eproto_impl();
+  #else
+  friend void  protobuf_AddDesc_loomcomm_2eproto();
+  #endif
+  friend void protobuf_AssignDesc_loomcomm_2eproto();
+  friend void protobuf_ShutdownFile_loomcomm_2eproto();
+
+  void InitAsDefaultInstance();
+  static ClientSubmit* default_instance_;
+};
 // ===================================================================
 
 
@@ -1628,30 +1764,6 @@ inline void Register::set_cpus(::google::protobuf::int32 value) {
   // @@protoc_insertion_point(field_set:loomcomm.Register.cpus)
 }
 
-// optional bool info = 10;
-inline bool Register::has_info() const {
-  return (_has_bits_[0] & 0x00000040u) != 0;
-}
-inline void Register::set_has_info() {
-  _has_bits_[0] |= 0x00000040u;
-}
-inline void Register::clear_has_info() {
-  _has_bits_[0] &= ~0x00000040u;
-}
-inline void Register::clear_info() {
-  info_ = false;
-  clear_has_info();
-}
-inline bool Register::info() const {
-  // @@protoc_insertion_point(field_get:loomcomm.Register.info)
-  return info_;
-}
-inline void Register::set_info(bool value) {
-  set_has_info();
-  info_ = value;
-  // @@protoc_insertion_point(field_set:loomcomm.Register.info)
-}
-
 // -------------------------------------------------------------------
 
 // ServerMessage
@@ -2376,106 +2488,103 @@ inline void Data::set_arg1_u64(::google::protobuf::uint64 value) {
 
 // -------------------------------------------------------------------
 
-// Info
+// Event
 
-// required int32 id = 1;
-inline bool Info::has_id() const {
+// required uint64 time = 1;
+inline bool Event::has_time() const {
   return (_has_bits_[0] & 0x00000001u) != 0;
 }
-inline void Info::set_has_id() {
+inline void Event::set_has_time() {
   _has_bits_[0] |= 0x00000001u;
 }
-inline void Info::clear_has_id() {
+inline void Event::clear_has_time() {
   _has_bits_[0] &= ~0x00000001u;
 }
-inline void Info::clear_id() {
-  id_ = 0;
-  clear_has_id();
+inline void Event::clear_time() {
+  time_ = GOOGLE_ULONGLONG(0);
+  clear_has_time();
 }
-inline ::google::protobuf::int32 Info::id() const {
-  // @@protoc_insertion_point(field_get:loomcomm.Info.id)
-  return id_;
+inline ::google::protobuf::uint64 Event::time() const {
+  // @@protoc_insertion_point(field_get:loomcomm.Event.time)
+  return time_;
 }
-inline void Info::set_id(::google::protobuf::int32 value) {
-  set_has_id();
-  id_ = value;
-  // @@protoc_insertion_point(field_set:loomcomm.Info.id)
+inline void Event::set_time(::google::protobuf::uint64 value) {
+  set_has_time();
+  time_ = value;
+  // @@protoc_insertion_point(field_set:loomcomm.Event.time)
 }
 
-// required string worker = 2;
-inline bool Info::has_worker() const {
+// required .loomcomm.Event.Type type = 2;
+inline bool Event::has_type() const {
   return (_has_bits_[0] & 0x00000002u) != 0;
 }
-inline void Info::set_has_worker() {
+inline void Event::set_has_type() {
   _has_bits_[0] |= 0x00000002u;
 }
-inline void Info::clear_has_worker() {
+inline void Event::clear_has_type() {
   _has_bits_[0] &= ~0x00000002u;
 }
-inline void Info::clear_worker() {
-  if (worker_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-    worker_->clear();
-  }
-  clear_has_worker();
+inline void Event::clear_type() {
+  type_ = 1;
+  clear_has_type();
 }
-inline const ::std::string& Info::worker() const {
-  // @@protoc_insertion_point(field_get:loomcomm.Info.worker)
-  return *worker_;
+inline ::loomcomm::Event_Type Event::type() const {
+  // @@protoc_insertion_point(field_get:loomcomm.Event.type)
+  return static_cast< ::loomcomm::Event_Type >(type_);
 }
-inline void Info::set_worker(const ::std::string& value) {
-  set_has_worker();
-  if (worker_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-    worker_ = new ::std::string;
-  }
-  worker_->assign(value);
-  // @@protoc_insertion_point(field_set:loomcomm.Info.worker)
+inline void Event::set_type(::loomcomm::Event_Type value) {
+  assert(::loomcomm::Event_Type_IsValid(value));
+  set_has_type();
+  type_ = value;
+  // @@protoc_insertion_point(field_set:loomcomm.Event.type)
 }
-inline void Info::set_worker(const char* value) {
-  set_has_worker();
-  if (worker_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-    worker_ = new ::std::string;
-  }
-  worker_->assign(value);
-  // @@protoc_insertion_point(field_set_char:loomcomm.Info.worker)
+
+// required int32 id = 3;
+inline bool Event::has_id() const {
+  return (_has_bits_[0] & 0x00000004u) != 0;
 }
-inline void Info::set_worker(const char* value, size_t size) {
-  set_has_worker();
-  if (worker_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-    worker_ = new ::std::string;
-  }
-  worker_->assign(reinterpret_cast<const char*>(value), size);
-  // @@protoc_insertion_point(field_set_pointer:loomcomm.Info.worker)
+inline void Event::set_has_id() {
+  _has_bits_[0] |= 0x00000004u;
 }
-inline ::std::string* Info::mutable_worker() {
-  set_has_worker();
-  if (worker_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-    worker_ = new ::std::string;
-  }
-  // @@protoc_insertion_point(field_mutable:loomcomm.Info.worker)
-  return worker_;
+inline void Event::clear_has_id() {
+  _has_bits_[0] &= ~0x00000004u;
 }
-inline ::std::string* Info::release_worker() {
-  clear_has_worker();
-  if (worker_ == &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-    return NULL;
-  } else {
-    ::std::string* temp = worker_;
-    worker_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
-    return temp;
-  }
+inline void Event::clear_id() {
+  id_ = 0;
+  clear_has_id();
 }
-inline void Info::set_allocated_worker(::std::string* worker) {
-  if (worker_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
-    delete worker_;
-  }
-  if (worker) {
-    set_has_worker();
-    worker_ = worker;
-  } else {
-    clear_has_worker();
-    worker_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
-  }
-  // @@protoc_insertion_point(field_set_allocated:loomcomm.Info.worker)
+inline ::google::protobuf::int32 Event::id() const {
+  // @@protoc_insertion_point(field_get:loomcomm.Event.id)
+  return id_;
+}
+inline void Event::set_id(::google::protobuf::int32 value) {
+  set_has_id();
+  id_ = value;
+  // @@protoc_insertion_point(field_set:loomcomm.Event.id)
+}
+
+// optional int32 worker_index = 4;
+inline bool Event::has_worker_index() const {
+  return (_has_bits_[0] & 0x00000008u) != 0;
+}
+inline void Event::set_has_worker_index() {
+  _has_bits_[0] |= 0x00000008u;
+}
+inline void Event::clear_has_worker_index() {
+  _has_bits_[0] &= ~0x00000008u;
+}
+inline void Event::clear_worker_index() {
+  worker_index_ = 0;
+  clear_has_worker_index();
+}
+inline ::google::protobuf::int32 Event::worker_index() const {
+  // @@protoc_insertion_point(field_get:loomcomm.Event.worker_index)
+  return worker_index_;
+}
+inline void Event::set_worker_index(::google::protobuf::int32 value) {
+  set_has_worker_index();
+  worker_index_ = value;
+  // @@protoc_insertion_point(field_set:loomcomm.Event.worker_index)
 }
 
 // -------------------------------------------------------------------
@@ -2732,49 +2841,49 @@ inline void ClientMessage::set_allocated_data(::loomcomm::DataPrologue* data) {
   // @@protoc_insertion_point(field_set_allocated:loomcomm.ClientMessage.data)
 }
 
-// optional .loomcomm.Info info = 3;
-inline bool ClientMessage::has_info() const {
+// optional .loomcomm.Event event = 3;
+inline bool ClientMessage::has_event() const {
   return (_has_bits_[0] & 0x00000004u) != 0;
 }
-inline void ClientMessage::set_has_info() {
+inline void ClientMessage::set_has_event() {
   _has_bits_[0] |= 0x00000004u;
 }
-inline void ClientMessage::clear_has_info() {
+inline void ClientMessage::clear_has_event() {
   _has_bits_[0] &= ~0x00000004u;
 }
-inline void ClientMessage::clear_info() {
-  if (info_ != NULL) info_->::loomcomm::Info::Clear();
-  clear_has_info();
+inline void ClientMessage::clear_event() {
+  if (event_ != NULL) event_->::loomcomm::Event::Clear();
+  clear_has_event();
 }
-inline const ::loomcomm::Info& ClientMessage::info() const {
-  // @@protoc_insertion_point(field_get:loomcomm.ClientMessage.info)
+inline const ::loomcomm::Event& ClientMessage::event() const {
+  // @@protoc_insertion_point(field_get:loomcomm.ClientMessage.event)
 #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
-  return info_ != NULL ? *info_ : *default_instance().info_;
+  return event_ != NULL ? *event_ : *default_instance().event_;
 #else
-  return info_ != NULL ? *info_ : *default_instance_->info_;
+  return event_ != NULL ? *event_ : *default_instance_->event_;
 #endif
 }
-inline ::loomcomm::Info* ClientMessage::mutable_info() {
-  set_has_info();
-  if (info_ == NULL) info_ = new ::loomcomm::Info;
-  // @@protoc_insertion_point(field_mutable:loomcomm.ClientMessage.info)
-  return info_;
+inline ::loomcomm::Event* ClientMessage::mutable_event() {
+  set_has_event();
+  if (event_ == NULL) event_ = new ::loomcomm::Event;
+  // @@protoc_insertion_point(field_mutable:loomcomm.ClientMessage.event)
+  return event_;
 }
-inline ::loomcomm::Info* ClientMessage::release_info() {
-  clear_has_info();
-  ::loomcomm::Info* temp = info_;
-  info_ = NULL;
+inline ::loomcomm::Event* ClientMessage::release_event() {
+  clear_has_event();
+  ::loomcomm::Event* temp = event_;
+  event_ = NULL;
   return temp;
 }
-inline void ClientMessage::set_allocated_info(::loomcomm::Info* info) {
-  delete info_;
-  info_ = info;
-  if (info) {
-    set_has_info();
+inline void ClientMessage::set_allocated_event(::loomcomm::Event* event) {
+  delete event_;
+  event_ = event;
+  if (event) {
+    set_has_event();
   } else {
-    clear_has_info();
+    clear_has_event();
   }
-  // @@protoc_insertion_point(field_set_allocated:loomcomm.ClientMessage.info)
+  // @@protoc_insertion_point(field_set_allocated:loomcomm.ClientMessage.event)
 }
 
 // optional .loomcomm.Error error = 4;
@@ -2876,6 +2985,79 @@ ClientMessage::mutable_symbols() {
   return &symbols_;
 }
 
+// -------------------------------------------------------------------
+
+// ClientSubmit
+
+// required .loomplan.Plan plan = 1;
+inline bool ClientSubmit::has_plan() const {
+  return (_has_bits_[0] & 0x00000001u) != 0;
+}
+inline void ClientSubmit::set_has_plan() {
+  _has_bits_[0] |= 0x00000001u;
+}
+inline void ClientSubmit::clear_has_plan() {
+  _has_bits_[0] &= ~0x00000001u;
+}
+inline void ClientSubmit::clear_plan() {
+  if (plan_ != NULL) plan_->::loomplan::Plan::Clear();
+  clear_has_plan();
+}
+inline const ::loomplan::Plan& ClientSubmit::plan() const {
+  // @@protoc_insertion_point(field_get:loomcomm.ClientSubmit.plan)
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  return plan_ != NULL ? *plan_ : *default_instance().plan_;
+#else
+  return plan_ != NULL ? *plan_ : *default_instance_->plan_;
+#endif
+}
+inline ::loomplan::Plan* ClientSubmit::mutable_plan() {
+  set_has_plan();
+  if (plan_ == NULL) plan_ = new ::loomplan::Plan;
+  // @@protoc_insertion_point(field_mutable:loomcomm.ClientSubmit.plan)
+  return plan_;
+}
+inline ::loomplan::Plan* ClientSubmit::release_plan() {
+  clear_has_plan();
+  ::loomplan::Plan* temp = plan_;
+  plan_ = NULL;
+  return temp;
+}
+inline void ClientSubmit::set_allocated_plan(::loomplan::Plan* plan) {
+  delete plan_;
+  plan_ = plan;
+  if (plan) {
+    set_has_plan();
+  } else {
+    clear_has_plan();
+  }
+  // @@protoc_insertion_point(field_set_allocated:loomcomm.ClientSubmit.plan)
+}
+
+// required bool report = 2;
+inline bool ClientSubmit::has_report() const {
+  return (_has_bits_[0] & 0x00000002u) != 0;
+}
+inline void ClientSubmit::set_has_report() {
+  _has_bits_[0] |= 0x00000002u;
+}
+inline void ClientSubmit::clear_has_report() {
+  _has_bits_[0] &= ~0x00000002u;
+}
+inline void ClientSubmit::clear_report() {
+  report_ = false;
+  clear_has_report();
+}
+inline bool ClientSubmit::report() const {
+  // @@protoc_insertion_point(field_get:loomcomm.ClientSubmit.report)
+  return report_;
+}
+inline void ClientSubmit::set_report(bool value) {
+  set_has_report();
+  report_ = value;
+  // @@protoc_insertion_point(field_set:loomcomm.ClientSubmit.report)
+}
+
 
 // @@protoc_insertion_point(namespace_scope)
 
diff --git a/src/proto/loomcomm.proto b/src/proto/loomcomm.proto
index cab85ac86d5713e556fe482b1019128613208adf..55e57efc12146f708d63757cf988cf360db901c8 100644
--- a/src/proto/loomcomm.proto
+++ b/src/proto/loomcomm.proto
@@ -1,6 +1,7 @@
 package loomcomm;
 
 option optimize_for = LITE_RUNTIME;
+import "loomplan.proto";
 
 message Register {
 	enum Type {
@@ -15,9 +16,6 @@ message Register {
 	repeated string task_types = 4;
 	repeated string data_types = 5;
 	optional int32 cpus = 6;
-
-	// Client
-	optional bool info = 10;
 }
 
 message ServerMessage {
@@ -85,9 +83,17 @@ message Data
 	optional uint64 arg1_u64 = 9;
 }
 
-message Info {
-	required int32 id = 1;
-	required string worker = 2;
+message Event {
+	enum Type {
+		TASK_START = 1;
+		TASK_END = 2;
+		SEND_START = 3;
+		SEND_END = 4;
+	}
+	required uint64 time = 1;
+	required Type type = 2;
+	required int32 id = 3;
+	optional int32 worker_index = 4;
 }
 
 message Error {
@@ -99,13 +105,18 @@ message Error {
 message ClientMessage {
 	enum Type {
 		DATA = 1;
-		INFO = 2;
+		EVENT = 2;
 		ERROR = 3;
 		DICTIONARY = 4;
 	}
 	required Type type = 1;
 	optional DataPrologue data = 2;
-	optional Info info = 3;
+	optional Event event = 3;
 	optional Error error = 4;
 	repeated string symbols = 5;
 }
+
+message ClientSubmit {
+	required loomplan.Plan plan = 1;
+	required bool report = 2;
+}
diff --git a/src/proto/loomreport.proto b/src/proto/loomreport.proto
new file mode 100644
index 0000000000000000000000000000000000000000..aa77e126296e7f53b8f16628388b9952a56ec172
--- /dev/null
+++ b/src/proto/loomreport.proto
@@ -0,0 +1,12 @@
+package loomcomm;
+
+option optimize_for = LITE_RUNTIME;
+
+import "loomplan.proto";
+import "loomcomm.proto";
+
+message Report {
+	repeated string symbols = 1;
+	required loomplan.Plan plan = 2;
+	repeated loomcomm.Event events = 3;
+}
diff --git a/src/proto/rebuild.sh b/src/proto/rebuild.sh
index d7c25e7c9f35883998aa62b31d396b1514f27e88..96bf5f340333189201d9bc204d054bbd2b9a4ee6 100644
--- a/src/proto/rebuild.sh
+++ b/src/proto/rebuild.sh
@@ -15,3 +15,4 @@ protoc loomrun.proto --cpp_out=${LIBLOOM_DIR}/tasks
 protoc loomcomm.proto --python_out=${CLIENT_DIR}
 protoc loomplan.proto --python_out=${CLIENT_DIR}
 protoc loomrun.proto --python_out=${CLIENT_DIR}
+protoc loomreport.proto --python_out=${CLIENT_DIR}
diff --git a/src/client/gv.py b/src/rview/gv.py
similarity index 100%
rename from src/client/gv.py
rename to src/rview/gv.py
diff --git a/src/rview/report.py b/src/rview/report.py
new file mode 100644
index 0000000000000000000000000000000000000000..dd930049c8075214aefd59d6583889fa0eeed908
--- /dev/null
+++ b/src/rview/report.py
@@ -0,0 +1,35 @@
+
+# HACK! We need to fix this
+import sys
+import os
+sys.path.insert(0,
+                os.path.join(
+                    os.path.dirname(__file__),
+                    "..",
+                    "client"))
+
+import loomreport_pb2  # noqa
+import gv  # noqas
+
+
+class Report:
+
+    def __init__(self, filename):
+        with open(filename) as f:
+            raw_data = f.read()
+
+        self.report_msg = loomreport_pb2.Report()
+        self.report_msg.ParseFromString(raw_data)
+
+        self.symbols = [s.replace("loom", "L")
+                        for s in self.report_msg.symbols]
+
+    def create_graph(self):
+        graph = gv.Graph()
+        symbols = self.symbols
+        for i, task in enumerate(self.report_msg.plan.tasks):
+            node = graph.node(i)
+            node.label = symbols[task.task_type]
+            for j in task.input_ids:
+                graph.node(j).add_arc(node)
+        return graph
diff --git a/src/rview/rview.py b/src/rview/rview.py
new file mode 100644
index 0000000000000000000000000000000000000000..b78199d083f9b9cbede3204ad1b5faa9751f0082
--- /dev/null
+++ b/src/rview/rview.py
@@ -0,0 +1,52 @@
+
+from report import Report
+import argparse
+import subprocess
+
+
+def parse_args():
+    parser = argparse.ArgumentParser(
+        description="rview -- Loom report inscpector")
+
+    parser.add_argument("report",
+                        metavar="REPORT",
+                        type=str,
+                        help="Path to report")
+
+    parser.add_argument("--show-symbols",
+                        action="store_true")
+
+    parser.add_argument("--show-graph",
+                        action="store_true")
+
+    return parser.parse_args()
+
+
+def run_program(args, stdin=None):
+    p = subprocess.Popen(args, stdin=subprocess.PIPE)
+    p.communicate(input=stdin)
+
+
+def show_symbols(report):
+    for i, symbol in enumerate(report.symbols):
+        print "{}: {}".format(i, symbol)
+
+
+def show_graph(report):
+    dot = report.create_graph().make_dot("Plan")
+    run_program(("xdot", "-"), dot)
+
+
+def main():
+    args = parse_args()
+    report = Report(args.report)
+
+    if args.show_symbols:
+        show_symbols(report)
+
+    if args.show_graph:
+        show_graph(report)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/src/server/clientconn.cpp b/src/server/clientconn.cpp
index 3745df44f664f59a576cf86d792200a2fece663b..6c87109bcd602d3c1223f301a723f7a3ec70df7b 100644
--- a/src/server/clientconn.cpp
+++ b/src/server/clientconn.cpp
@@ -7,8 +7,9 @@
 
 using namespace loom;
 
-ClientConnection::ClientConnection(Server &server, std::unique_ptr<loom::Connection> connection, bool info_flag)
-    : server(server), connection(std::move(connection)), info_flag(info_flag)
+ClientConnection::ClientConnection(Server &server,
+                                   std::unique_ptr<loom::Connection> connection)
+    : server(server), connection(std::move(connection))
 {
     this->connection->set_callback(this);
     llog->info("Client {} connected", this->connection->get_peername());
@@ -36,13 +37,14 @@ ClientConnection::~ClientConnection()
 void ClientConnection::on_message(const char *buffer, size_t size)
 {
     llog->debug("Plan received");
-    loomplan::Plan plan;
-    plan.ParseFromArray(buffer, size);
+    loomcomm::ClientSubmit submit;
+    submit.ParseFromArray(buffer, size);
     auto& task_manager = server.get_task_manager();
 
+    const loomplan::Plan &plan = submit.plan();
     loom::Id id_base = server.new_id(plan.tasks_size());
     task_manager.add_plan(Plan(plan, id_base, server.get_dictionary()));
-    llog->info("Plan submitted tasks={}", plan.tasks_size());
+    llog->info("Plan submitted tasks={} report={}", plan.tasks_size(), submit.report());
 }
 
 void ClientConnection::on_close()
diff --git a/src/server/clientconn.h b/src/server/clientconn.h
index 385ec891ff231b9889657340b85d069aac875b3d..4d6abd57a0935390f4a6a31925e4e32cc8a9bc5f 100644
--- a/src/server/clientconn.h
+++ b/src/server/clientconn.h
@@ -13,7 +13,7 @@ class Server;
 class ClientConnection : public loom::ConnectionCallback {
 public:
     ClientConnection(Server &server,
-                     std::unique_ptr<loom::Connection> connection, bool info_flag);
+                     std::unique_ptr<loom::Connection> connection);
     ~ClientConnection();
     void on_message(const char *buffer, size_t size);
     void on_close();
@@ -22,14 +22,9 @@ public:
         connection->send_buffer(buffer);
     }
 
-    bool has_info_flag() const {
-        return info_flag;
-    }
-
 protected:
     Server &server;
     std::unique_ptr<loom::Connection> connection;
-    bool info_flag;
 };
 
 
diff --git a/src/server/compstate.cpp b/src/server/compstate.cpp
index b3359f5a2acecbb657601a50f1e98968c0dec871..60cd903edd03fa63937566f721c94819f24c375a 100644
--- a/src/server/compstate.cpp
+++ b/src/server/compstate.cpp
@@ -16,6 +16,8 @@ ComputationState::ComputationState(Server &server) : server(server)
    get_task_id = dictionary.find_or_create("loom/base/get");
    dslice_task_id = dictionary.find_or_create("loom/scheduler/dslice");
    dget_task_id = dictionary.find_or_create("loom/scheduler/dget");
+
+   base_time = uv_now(server.get_loop());
 }
 
 void ComputationState::set_plan(Plan &&plan)
diff --git a/src/server/compstate.h b/src/server/compstate.h
index e48373873ea99c1c51e254886c10b855ac02e2ed..dac57cd6b1698029f457535e18b704375f9eaf58 100644
--- a/src/server/compstate.h
+++ b/src/server/compstate.h
@@ -62,12 +62,15 @@ private:
     Plan plan;
     Server &server;
 
+    uint64_t base_time;
+
     loom::Id dslice_task_id;
     loom::Id dget_task_id;
 
     loom::Id slice_task_id;
     loom::Id get_task_id;
 
+
     WorkerConnection *get_best_holder_of_deps(PlanNode *task);
     WorkerConnection *find_best_worker_for_node(PlanNode *task);
 
diff --git a/src/server/freshconn.cpp b/src/server/freshconn.cpp
index c267fa22cf5e249d8e48e2d4b1ea32f71eab8757..421d6b1e2def57177f1056aeafa8486f158886c1 100644
--- a/src/server/freshconn.cpp
+++ b/src/server/freshconn.cpp
@@ -61,10 +61,8 @@ void FreshConnection::on_message(const char *buffer, size_t size)
         return;
     }
     if (msg.type() == loomcomm::Register_Type_REGISTER_CLIENT) {
-        bool info_flag = msg.has_info() && msg.info();
         auto cconn = std::make_unique<ClientConnection>(server,
-                                                        std::move(connection),
-                                                        info_flag);
+                                                        std::move(connection));
         server.add_client_connection(std::move(cconn));
         assert(connection.get() == nullptr);
         server.remove_freshconnection(*this);
diff --git a/src/server/server.cpp b/src/server/server.cpp
index 84deda84fba1f218585698fb2f66d393672f40ea..491e4b524abfaef3117b80f9047b26e6fa789abe 100644
--- a/src/server/server.cpp
+++ b/src/server/server.cpp
@@ -81,9 +81,10 @@ void Server::remove_freshconnection(FreshConnection &conn)
 void Server::on_task_finished(loom::Id id, size_t size, size_t length, WorkerConnection *wc)
 {
     assert(client_connection);
-    if (client_connection->has_info_flag()) {
+    /*
+    if (client_connection->is_report_enabled()) {
         assert(0);
-        /*loomcomm::ClientMessage cmsg;
+        loomcomm::ClientMessage cmsg;
         cmsg.set_type(loomcomm::ClientMessage_Type_INFO);
         loomcomm::Info *info = cmsg.mutable_info();
         info->set_id(task.get_id());
@@ -95,8 +96,8 @@ void Server::on_task_finished(loom::Id id, size_t size, size_t length, WorkerCon
         SendBuffer *buffer = new SendBuffer;
         buffer->add(cmsg);
 
-        client_connection->send_buffer(buffer);*/
-    }
+        client_connection->send_buffer(buffer);
+    }*/
     task_manager.on_task_finished(id, size, length, wc);
 }
 
diff --git a/tests/client/cv_test.py b/tests/client/cv_test.py
index 9fec9889e5cda6fd70ca915dd08ecfd2a42f75ee..65d39cab62ed1ef1a739d11598fa9bdbdc1d4245 100644
--- a/tests/client/cv_test.py
+++ b/tests/client/cv_test.py
@@ -37,7 +37,7 @@ def test_cv_iris(loom_env):
                               [(chunk, "testdata"), (model, "model")])
             predict.append(task)
 
-        results = loom_env.submit(p, predict)
+        results = loom_env.submit(p, predict, report="cv")
 
         assert len(results) == CHUNKS
         for line in results:
diff --git a/tests/client/loomenv.py b/tests/client/loomenv.py
index ef83841222049e134e041cce71a8c998a1ec7771..307d30fdc395820d46e9d01d9402dbdf385c8146 100644
--- a/tests/client/loomenv.py
+++ b/tests/client/loomenv.py
@@ -47,7 +47,6 @@ class Env():
 class LoomEnv(Env):
 
     PORT = 19010
-    info = False
     _client = None
 
     def start(self, workers_count, cpus=1):
@@ -88,11 +87,13 @@ class LoomEnv(Env):
     @property
     def client(self):
         if self._client is None:
-            self._client = client.Client("localhost", self.PORT, self.info)
+            self._client = client.Client("localhost", self.PORT)
         return self._client
 
-    def submit(self, plan, results):
-        return self.client.submit(plan, results)
+    def submit(self, plan, results, report=None):
+        if report:
+            report = os.path.join(LOOM_TEST_BUILD_DIR, report)
+        return self.client.submit(plan, results, report)
 
 
 @pytest.yield_fixture(autouse=True, scope="function")