Commits

ksbuble committed 2634d5f Merge

Addressed merge from MF April3,2013 commit to KSB local changes April3,2013.

Comments (0)

Files changed (3)

 * Comprehensive testing
 * Logging (print debug messages in debug mode for checking network problems)
 * Profile performance.
+* Auto-launch client from the server.
+
+  * The user defines the plotting code in a (picklable) class, and provides the
+    class to the Server() constructor.  The Server passes the pickled Plotter to
+    the client when the client process is launched in a new python interpreter.
+    The python interpreter command line options for the client are passed to the
+    Server constructor at the time of instantiation. Default values are:
+    (ex. ``["ipython", "--pylab=osx", "-c"]``)
+
 * Configuration (hostname, port specification etc.)
 
 I think that these have been dealt with, but they need testing:

asyncplot/client.py

     def get_data(self):
         tic = time.time()
         sock = self.connect()
-        datasz = -1
-        data = []
-        while datasz != 0:
-            data.append(sock.recv(self._BUF_SIZE))
-            datasz = len(data[-1])
-            #print "tmpdata from Server is %i bytes long." % (datasz,)
+        try:
+            datasz = -1
+            data = []
+            while datasz != 0:
+                data.append(sock.recv(self._BUF_SIZE))
+                datasz = len(data[-1])
+                #print "tmpdata from Server is %i bytes long." % (datasz,)
 
-        #print "close client socket and shutdown. "
-        sock.close()
-        #sock.shutdown(1)
+        finally:
+            #print "close client socket and shutdown. "
+            sock.close()
+            #sock.shutdown(1)
 
         res = ''.join(data)
 #        print("Getting data %g" % (time.time() - tic,))
         try:
             self.is_alive = True
             while self.is_alive:
-                data = self.get_data()            
+                data = self.get_data()
                 tic = time.time()
                 v, kw = cPickle.loads(data)
                 print("Loading pickle %g" % (time.time() - tic,))

asyncplot/server.py

-r"""Threaded Socket Server"""
+r"""Threaded Socket Server
+
+This server listens for connections, and then sends plot data as requested.
+Each connection request is treated as a single request -- multiple requests
+require multiple connections.
+
+To Do
+-----
+* Allow the server to respond to a limited set of requests, such as a query for
+  information about the process running etc.  (Each server should provide a bit
+  of human-readable meta-data so that clients can select from the appropriate
+  server if a machine is running multiple servers on multiple ports.
+* Ensure that a simple default blocking mode works.
+* Choose the next avialable port if the default port is blocked.
+"""
 
 import time
 import cPickle
 import SocketServer
 import client
 
-# import mmf
-
 
 def get_port(port=None):
     r"""Return `port` or the default value if `port` is `None`."""
 
     Call :meth:`update` to update the plot data.
 
+    Parameters
+    ----------
+    continuous : bool
+       If `True`, then the data is pickled *every time* a client requests the
+       data, otherwise, it is only pickled after :meth:`update` is called.  It
+       might be slightly dangerous to use this mode because the computational
+       (main) thread could be modifying the data while the pickle is being
+       created.  (I.e. the main thread does not use locks.)  This can be useful,
+       however, if the main computation updates the data directly because plot
+       clients can update as fast as possible -- even if the server only calls
+       :meth:`update` infrequently.
+
+       Setting `continuous` to `False` will prevent unecessary pickling from
+       occuring, but then the clients will only see new data after
+       :meth:`update` is called.
+
     To Do
     -----
+    * Send the `_id` with the pickle to the clients.  In return, the client
+      should sent the `_id` of the last request so that new data is sent only if
+      it has been updated.
     * Add timeouts.
     """
     _DEFAULT_PORT = 9999
     _DEFAULT_HOST = ''
 
-    def __init__(self, host=None, port=None):
+    def __init__(self, host=None, continuous=True, port=None):
         self.lock = threading.Lock()  # RLock()??
         self.have_data = threading.Event()
+        self.continuous = continuous
+
         self.data = None
+        self._id = 0               # Each time update is called, _id changes
+        self._pickle_cache = None  # Pickle cache to prevent repickling
+
         self.daemon_threads = True       # Don't keep python alive.
         self.allow_reuse_address = True  # Allow port reuse after termination.
-        SocketServer.TCPServer.__init__(self, (get_host(host), get_port(port)),
-                                        PlotRequestHandler)
+        try:
+            SocketServer.TCPServer.__init__(
+                self, (get_host(host), get_port(port)), PlotRequestHandler)
+        except Exception:
+            # To Do: If this fails because the port is in use, then try a
+            # different port and tell the user.  Probably do this with a for
+            # loop over a few ports.
+            raise
 
-    def _get_data(self, block=True):
+    def _get_data(self):
         r"""The object is pickled here."""
         self.have_data.wait()
         with self.lock:
-            return cPickle.dumps(self.data)
+            if (self._pickle_cache is None 
+                or self._pickle_cache[0] != self._id
+                or self.continuous):
+                self._pickle_cache = (
+                    self._id, cPickle.dumps(self.data))
+            return self._pickle_cache[1]
 
     def update(self, *v, **kw):
         with self.lock:
             self.data = (v, kw)
+            self._id += 1
         self.have_data.set()    # This is very slow!
 
 
 class Server(threading.Thread):
-    r"""If you pass `spawn_client_command`, then a plot client will be spawned
-    in a subprocess.
+    r"""If you pass `client_cmd`, then a plot client will be spawned in a
+    subprocess.
+
+    Notes
+    -----
+    This server acts as both a `Thread`, and a context manager.  The thread part
+    spawns the socket server that listens for connections, creates pickles, and
+    sends them.  The context manager provides a mechanism for ensuring
+    everything is shut down.  Typical usage is as follows::
+
+       with Server() as s:
+           # Start of the server context...
+           for n in xrange(iterations):
+               <do stuff, computing x, y, data etc.>
+               s.update(x, y, data=data, <all args required by Plotter.draw>)
+           # ... end of the server context.  Server will be shutdown when
+           # leaving the context.
+
+    To Do
+    -----
+    * Make launching client much more robust!
     """
     def __init__(self,
                  Plotter=None,