#!/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, math 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 spaceplot: "Total times without instrumentation." name = "space" def __init__(self, fp): self.fixes = [] self.fp = fp def header(self, session): self.fp.write("# Position uncertainty, %s, %s, %ds cycle\n" % \ (title, session.gps_id, session.cycle)) def formatter(self, session): self.fixes.append((session.latitude, session.longitude)) return True def plot(self, file, title, session): if len(self.fixes) == 0: sys.stderr.write("No fixes collected, can't estimate accuracy.") sys.exit(1) else: centroid = (sum(map(lambda x:x[0], self.fixes))/len(self.fixes), sum(map(lambda x:x[1], self.fixes))/len(self.fixes)) # Sort fixes by distance from centroid def d(a, b): return math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2) self.fixes.sort(lambda x, y: cmp(d(centroid, x), d(centroid, y))) # Compute CEP(50%) cep_meters = gps.EarthDistance(centroid, self.fixes[len(self.fixes)/2]) # Convert fixes to offsets from centroid in meters recentered = map(lambda fix: gps.MeterOffset(centroid, fix), self.fixes) for (lat, lon) in recentered: self.fp.write("%f %f\n" % (lat, lon)) self.fp.flush() if centroid[0] < 0: latstring = "%fS" % -centroid[0] elif centroid[0] == 0: latstring = "0" else: latstring = "%fN" % centroid[0] if centroid[1] < 0: lonstring = "%fW" % -centroid[1] elif centroid[1] == 0: lonstring = "0" else: lonstring = "%fE" % centroid[1] fmt = "" fmt += "set autoscale\n" fmt += 'set key below\n' fmt += 'set key title "%s"\n' % time.asctime() fmt += 'set size -1\n' fmt += 'set style line 3 pt 2 # Looks good on X11\n' fmt += 'set xlabel "Meters east from %s"\n' % lonstring fmt += 'set ylabel "Meters north from %s"\n' % latstring fmt += 'cep=%f\n' % d((0,0), recentered[len(self.fixes)/2]) fmt += 'set parametric\n' fmt += 'set trange [0:2*pi]\n' fmt += 'cx(t, r) = sin(t)*r\n' fmt += 'cy(t, r) = cos(t)*r\n' fmt += 'chlen = cep/20\n' fmt += "set arrow from -chlen,0 to chlen,0 nohead\n" fmt += "set arrow from 0,-chlen to 0,chlen nohead\n" fmt += 'plot cx(t, cep),cy(t, cep) title "CEP (50%%) = %f meters", "%s" using 1:2 with points ls 3 title "%d GPS fixes"\n' % (cep_meters, file, len(self.fixes)) return fmt class uninstrumented: "Total times without instrumentation." name = "uninstrumented" def __init__(self, fp): self.fp = fp def header(self, session, fp): fp.write("# Uninstrumented total latency, %s, %s, %dN%d, cycle %ds\n" % \ (title, session.gps_id, 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, %s, %dN%d, cycle %ds" plot "%s" using 0:1 title "Total time" with impulses ''' return fmt % (title, session.gps_id, session.baudrate, session.stopbits, session.cycle, file) class rawplot: "All measurement, no deductions." name = "raw" def __init__(self, fp): self.fp = fp def header(self, session): self.fp.write("# Raw latency data, %s, %s, %dN%d, cycle %ds\n" % \ (title, session.gps_id, session.baudrate, session.stopbits, session.cycle)) self.fp.write("#\t") for hn in ("T1", "E1", "D1", "W", "E2", "T2", "D2"): self.fp.write("%8s\t" % hn) self.fp.write("tag\n#-\t") for i in range(0, 7): self.fp.write("--------\t") self.fp.write("--------\n") def formatter(self, session): self.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, %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.gps_id, 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, fp): self.found = {} self.fp = fp def header(self, session): self.fp.write("# Split latency data, %s, %s, %dN%d, cycle %ds\n" % \ (title, session.gps_id, session.baudrate, session.stopbits, session.cycle)) self.fp.write("#") for s in splitplot.sentences: self.fp.write("%8s\t" % s) for hn in ("T1", "D1", "W", "E2", "T2", "D2", "length"): self.fp.write("%8s\t" % hn) self.fp.write("tag\n# ") for s in splitplot.sentences + ("T1", "D1", "W", "E2", "T2", "D2", "length"): self.fp.write("---------\t") self.fp.write("--------\n") def formatter(self, session): for s in splitplot.sentences: if s == session.tag: self.fp.write("%2.6f\t"% session.d_xmit_time) self.found[s] = True else: self.fp.write("- \t") self.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, %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.gps_id, 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 = (spaceplot, rawplot, splitplot, uninstrumented) def timeplot(await, fname, file, speed, threshold, title): "Return a string containing a GNUplot script " if file: out = open(file, "w") elif fname == None: out = sys.stdout else: out = tempfile.NamedTemporaryFile() if fname: for formatter in formatters: if formatter.name == fname: plotter = formatter(out) break else: sys.stderr.write("gpsprof: no such formatter.\n") sys.exit(1) try: session = gps.gps() except socket.error: sys.stderr.write("gpsprof: gpsd unreachable.\n") sys.exit(1) try: if speed: session.query("b=%d" % speed) if session.baudrate != speed: sys.stderr.write("gpsprof: baud rate change failed.\n") session.query("w+bci") if formatter not in (spaceplot, uninstrumented): session.query("z+") #session.set_raw_hook(lambda x: sys.stderr.write(`x`+"\n")) plotter.header(session) baton = Baton("gpsprof: looking for fix", "done") countdown = await while countdown > 0: if session.poll() == None: sys.stderr.write("gpsprof: gpsd has vanished.\n") sys.exit(1) baton.twirl() if session.status == gps.STATUS_NO_FIX: continue if 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): countdown -= 1 baton.end() finally: session.query("w-z-") out.flush() command = plotter.plot(out.name, title, session) del session return (out, command) if __name__ == '__main__': try: (options, arguments) = getopt.getopt(sys.argv[1:], "f:hm:n:o:rs:t:T:") formatter = "split" raw = False file = None speed = 0 terminal = None title = time.ctime() threshold = 0 await = 100 for (switch, val) in options: if (switch == '-f'): formatter = val 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) (out, gnuplot) = timeplot(await,formatter,file,speed,threshold,title) if not raw: if terminal: gnuplot = "set terminal " + terminal + "\n" + gnuplot pfp = os.popen("gnuplot -persist", "w") pfp.write(gnuplot) pfp.close() out.close() except KeyboardInterrupt: pass