#!/usr/bin/env python
#
# Collect and plot latency-profiling data from a running gpsd.
# Requires gnuplot.
#
import sys, os, time, getopt, gps, tempfile, time, socket

class Baton:
    "Ship progress indication to stderr."
    def __init__(self, prompt, endmsg=None):
        self.stream = sys.stderr
        self.stream.write(prompt + "... \010")
        self.stream.flush()
        self.count = 0
        self.endmsg = endmsg
        self.time = time.time()
        return

    def twirl(self, ch=None):
        if self.stream is None:
            return
        if ch:
            self.stream.write(ch)
        else:
            self.stream.write("-/|\\"[self.count % 4])
            self.stream.write("\010")
        self.count = self.count + 1
        self.stream.flush()
        return

    def end(self, msg=None):
        if msg == None:
            msg = self.endmsg
        if self.stream:
            self.stream.write("...(%2.2f sec) %s.\n" % (time.time() - self.time, msg))
        return

class uninstrumented:
    "Total times without instrumentation."
    name = "uninstrumented"
    def header(self, fp):
        fp.write("# Uninstrumented total latency, %s, %dN%d, cycle %ds\n" % \
                 (title, session.baudrate, session.stopbits, session.cycle))
    def formatter(self, session, fp):
        fp.write("%2.6lf\n" % (time.time() - gps.isotime(session.utc),))
        return True
    def plot(self, file, title, session):
        fmt = '''
set autoscale
set key below
set key title "Uninstrumented total latency, %s, %dN%d, cycle %ds"
plot "%s" using 0:1 title "Total time" with impulses
'''
        return fmt % (title,
                      session.baudrate, session.stopbits, session.cycle,
                      file)

class rawplot:
    "All measurement, no deductions."
    name = "raw"
    def header(self, fp):
        fp.write("# Raw latency data, %s, %dN%d, cycle %ds\n" % \
                 (title, session.baudrate, session.stopbits, session.cycle))
        fp.write("#\t")
        for hn in ("T1", "E1", "D1", "W", "E2", "T2", "D2"):
            fp.write("%8s\t" % hn)
        fp.write("tag\n#-\t")
        for i in range(0, 7):
            fp.write("--------\t")
        fp.write("--------\n")

    def formatter(self, session, fp):
        fp.write("%2d	%2.6f	%2.6f	%2.6f	%2.6f	%2.6f	%2.6f	%2.6f  	# %s\n" \
              % (session.length,
                 session.d_xmit_time, 
                 session.d_recv_time,
                 session.d_decode_time,
                 session.poll_time,
                 session.emit_time,
                 session.c_recv_time,
                 session.c_decode_time,
                 session.tag))
        return True
    def plot(self, file, title, session):
        fmt = '''
set autoscale
set key below
set key title "Raw latency data, %s, %dN%d, cycle %ds"
plot \
     "%s" using 0:8 title "D2 = Client decode time" with impulses, \
     "%s" using 0:7 title "T2 =     TCP/IP latency" with impulses, \
     "%s" using 0:6 title "E2 = Daemon encode time" with impulses, \
     "%s" using 0:5 title "W  =     Poll wait time" with impulses, \
     "%s" using 0:4 title "D1 = Daemon decode time" with impulses, \
     "%s" using 0:3 title "T1 =         RS232 time" with impulses, \
     "%s" using 0:2 title "E1 =        GPS latency" with impulses
'''
        return fmt % (title,
                      session.baudrate, session.stopbits, session.cycle,
                      file, file, file, file, file, file, file)

class splitplot:
    "Discard base time, use color to indicate different tags."
    name = "split"
    sentences = ("GPGGA", "GPRMC", "GPGLL")
    def __init__(self):
        self.found = {}
    def header(self, fp):
        fp.write("# Split latency data, %s, %dN%d, cycle %ds\n" % \
                 (title, session.baudrate, session.stopbits, session.cycle))
        fp.write("#")
        for s in splitplot.sentences:
            fp.write("%8s\t" % s)
        for hn in ("T1", "D1", "W", "E2", "T2", "D2", "length"):
            fp.write("%8s\t" % hn)
        fp.write("tag\n# ")
        for s in splitplot.sentences + ("T1", "D1", "W", "E2", "T2", "D2", "length"):
            fp.write("---------\t")
        fp.write("--------\n")
    def formatter(self, session, fp):
        for s in splitplot.sentences:
            if s == session.tag:
                fp.write("%2.6f\t"% session.d_xmit_time)
                self.found[s] = True
            else:
                fp.write("-       \t")
        fp.write("%2.6f	%2.6f	%2.6f	%2.6f	%2.6f	%2.6f	%8d	# %s\n" \
                 % (session.d_recv_time,
                    session.d_decode_time,
                    session.poll_time,
                    session.emit_time,
                    session.c_recv_time,
                    session.c_decode_time,
                    session.length,
                    session.tag))
        return True
    def plot(self, file, title, session):
        fixed = '''
set autoscale
set key below
set key title "Filtered latency data, %s, %dN%d, cycle %ds"
plot \\
     "%s" using 0:%d title "D2 = Client decode time" with impulses, \
     "%s" using 0:%d title "T2 = TCP/IP latency" with impulses, \
     "%s" using 0:%d title "E2 = Daemon encode time" with impulses, \
     "%s" using 0:%d title "W  = Poll wait time" with impulses, \
     "%s" using 0:%d title "D1 = Daemon decode time" with impulses, \
     "%s" using 0:%d title "T1 = RS3232 time" with impulses, \
'''
        sc = len(splitplot.sentences)
        fmt = fixed % (title,
                       session.baudrate, session.stopbits, session.cycle,
                       file, sc+6,
                       file, sc+5,
                       file, sc+4,
                       file, sc+3,
                       file, sc+2,
                       file, sc+1)
        for i in range(sc):
            if splitplot.sentences[i] in self.found:
                fmt += '     "%s" using 0:%d title "%s" with impulses, \\\n' % \
                       (file, i+1, splitplot.sentences[i])
        return fmt[:-4] + "\n"

formatters = (rawplot, splitplot, uninstrumented)

if __name__ == '__main__':
    (options, arguments) = getopt.getopt(sys.argv[1:], "f:hm:n:o:rs:t:T:")
    formatter = splitplot
    raw = False
    file = None
    speed = 0
    terminal = None
    title = time.ctime()
    threshold = 0
    await = 100
    for (switch, val) in options:
	if (switch == '-f'):
            for formatter in formatters:
                if formatter.name == val:
                    break
            else:
                sys.stderr.write("gpsprof: no such formatter.\n")
                sys.exit(1)
	elif (switch == '-m'):
	    threshold = int(val)
	elif (switch == '-n'):
	    await = int(val)
	elif (switch == '-o'):
	    file = val
	elif (switch == '-r'):
	    raw = True
        elif (switch == '-s'):
            speed = int(val)
	elif (switch == '-t'):
	    title = val
        elif (switch == '-T'):
	    terminal = val
        elif (switch == '-h'):
            sys.stderr.write(\
                "usage: gpsprof [-h] [-r] [-m threshold] [-n samplecount] \n"
                 + "\t[-f {" + "|".join(map(lambda x: x.name, formatters)) + "}] [-s speed] [-t title] [-o file]\n")
            sys.exit(0)
    plotter = formatter()
    if file:
        out = open(file, "w")
    elif raw:
        out = sys.stdout
    else:
        out = tempfile.NamedTemporaryFile()

    try:
        session = gps.gps()
    except socket.error:
        sys.stderr.write("gpsprof: gpsd unreachable.\n")
        sys.exit(0)
    try:
        if speed:
            session.query("b=%d" % speed)
            if session.baudrate != speed:
                sys.stderr.write("gpsprof: baud rate change failed.\n")
        session.query("w+bc")
        if formatter != uninstrumented:
            session.query("z+")
        #session.set_raw_hook(lambda x: sys.stdout.write(x))
        plotter.header(out)
        baton = Baton("gpsprof: looking for fix", "done")
        countdown = await
        while countdown > 0:
            session.poll()
            baton.twirl()
            # If timestamp is no good, skip it.
            if session.utc == "?":
                continue
            if session.status and countdown == await:
                sys.stderr.write("gathering samples...")
            # We can get some funky artifacts at start of session apparently
            # due to RS232 buffering effects. Ignore them.
            if threshold and session.c_decode_time > session.cycle * threshold:
                continue
            if plotter.formatter(session, out):
                countdown -= 1
        baton.end()
    finally:
        session.query("w-z-")
        
    out.flush()
    if not raw:
        command = plotter.plot(out.name, title, session)
        if terminal:
            command = "set terminal " + terminal + "\n" + command
        pfp = os.popen("gnuplot -persist", "w")
        pfp.write(command)
        pfp.close()
    del session
    out.close()

