#!/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()