| #!/usr/bin/env python |
| |
| """report: make a table summarizing output from one or more runs of `sample`.""" |
| |
| from collections import Counter |
| import csv |
| import os |
| import re |
| import sys |
| |
| import ifstats |
| import iperf |
| import options |
| |
| optspec = """ |
| report [options...] <journal> |
| -- |
| r,report_dir= path to a single report directory to be parsed |
| """ |
| |
| NFILE = 'n-datarates.tsv' |
| nrates = {} |
| |
| CHANNELFILE = 'channels.tsv' |
| channels = {} |
| |
| |
| def _Resource(name): |
| return os.path.join(os.path.dirname(os.path.abspath(__file__)), name) |
| |
| |
| def LoadNRates(): |
| """Loads 802.11n coding and data rates into a global variable.""" |
| if nrates: return |
| |
| raw = [] |
| |
| with open(_Resource(NFILE), 'rb') as csvfile: |
| reader = csv.reader(csvfile, delimiter='\t') |
| next(reader) # skip header row when reading by machine |
| for mcs, width, gi, rate in reader: |
| raw.append([int(mcs), int(width), int(gi), float(rate)]) |
| |
| # Load global table, computing MCS 8-31 statistics from MCS 0-7. |
| for mcs, width, gi, rate in raw: |
| for i in range(4): |
| nrates[(8*i + mcs, width, gi)] = rate * (i + 1) |
| |
| |
| def LoadChannels(): |
| """Load 802.11n channels and frequencies into a global variable.""" |
| if channels: return |
| |
| with open(_Resource(CHANNELFILE), 'rb') as csvfile: |
| reader = csv.reader(csvfile, delimiter='\t') |
| next(reader) |
| |
| for channel, freq in reader: |
| channels[int(channel)] = int(freq) |
| |
| |
| def ParseMCSFile(outfile, width=20): |
| """Extract MCS and PHY rate statistics from an MCS report file.""" |
| LoadNRates() |
| |
| # assume long guard interval |
| guard = 800 |
| |
| counter = Counter() |
| for line in outfile: |
| for tok in line.split(): |
| if tok == '.': continue |
| |
| mcs = int(tok) |
| counter[mcs] += 1 |
| |
| phy = 0.0 |
| alltimes = 0 |
| for mcs, times in counter.iteritems(): |
| phy += nrates[(mcs, width, guard)] * times |
| alltimes += times |
| |
| return counter.most_common()[0][0], phy / alltimes |
| |
| |
| def Channel(text_channel): |
| """Given a text channel spec like 149,+1 return the central freq and width.""" |
| LoadChannels() |
| |
| if ',' in text_channel: |
| base, offset = text_channel.split(',') |
| freq = channels[int(base)] |
| offset = int(offset) |
| return (2 * freq + offset * 20) / 2, 40 |
| else: |
| return channels[int(text_channel)], 20 |
| |
| |
| def Overlap(c1, w1, c2, w2): |
| """Return True if two WiFi channels overlap, or False otherwise.""" |
| # TODO(willangley): replace with code from Waveguide |
| b1 = c1 - w1 / 2 |
| t1 = c1 + w1 / 2 |
| b2 = c2 - w2 / 2 |
| t2 = c2 + w2 / 2 |
| |
| return ((b1 <= b2 <= t1) or (b2 <= b1 <= t2) |
| or (b1 <= t2 <= t1) or (b2 <= t1 <= t2)) |
| |
| |
| def ReportLine(report_dir): |
| """Condense the output of a sample.py run into a one-line summary report.""" |
| _, _, steps = os.path.basename(report_dir).split('-') |
| line = [int(steps)] |
| |
| system, cache = ifstats.Restore(report_dir) |
| result = ifstats.Parse(system, cache) |
| if system == 'Darwin': |
| airport = result.get('link') |
| channel, width = Channel(airport['channel']) |
| shared = 0 |
| overlap = 0 |
| |
| scan = result.get('scan') |
| if len(scan) > 1: |
| for row in scan[1:]: |
| oc, ow = Channel(row[3]) |
| if channel == oc and width == ow: |
| shared += 1 |
| if Overlap(channel, width, oc, ow): |
| overlap += 1 |
| |
| rssi = airport['agrCtlRSSI'] |
| noise = airport['agrCtlNoise'] |
| line += [channel, width, rssi, noise, shared, overlap - shared] |
| elif system == 'Linux': |
| iwlink = result.get('link') |
| signal = int(iwlink.get('signal', '0 dBm').split()[0]) |
| channel = int(iwlink.get('freq')) |
| width = 20 |
| m = re.search(r'(\d+)MHz', iwlink.get('tx bitrate'), flags=re.I) |
| if m: |
| width = int(m.group(1)) |
| |
| # Noise and contention not yet gathered in samples run on Linux systems. |
| line += [channel, width, signal, None, None, None] |
| |
| mpath = os.path.join(report_dir, 'mcs') |
| if os.path.exists(mpath): |
| with open(mpath) as mf: |
| line += ParseMCSFile(mf, width) |
| else: |
| line += [None, None] |
| |
| # If the initial ping test fails, we won't collect performance information. |
| # deal with this gracefully. |
| ips = iperf.Restore(report_dir) |
| for perf in [iperf.ParseIperfTCP(ips.get('iperf', '')), |
| iperf.ParseIperfUDP(ips.get('iperfu', ''))]: |
| line += [perf.get('bandwidth'), perf.get('bandwidth_unit')] |
| |
| return line |
| |
| |
| def main(): |
| o = options.Options(optspec) |
| (opt, _, extra) = o.parse(sys.argv[1:]) |
| if len(extra) > 1: |
| o.fatal('expected at most one journal name.') |
| |
| LoadNRates() |
| LoadChannels() |
| |
| lines = [] |
| if opt.report_dir: |
| lines += [ReportLine(opt.report_dir)] |
| |
| if extra: |
| for jname in extra[:1]: |
| jname = os.path.realpath(jname) |
| with open(jname) as journal: |
| for line in journal: |
| lines += [ReportLine(os.path.join(os.path.dirname(jname), |
| line.strip()))] |
| |
| if len(lines) < 1: |
| o.fatal("didn't find any samples. did you supply at least one report dir" |
| ' or journal?') |
| |
| header = ['Steps', 'Channel', 'Width', 'RSSI', 'Noise', 'Shared', |
| 'Interfering', 'MCS', 'PHY', 'TCP BW', '(Units)', 'UDP BW', |
| '(Units)'] |
| writer = csv.writer(sys.stdout, delimiter='\t', quoting=csv.QUOTE_MINIMAL) |
| writer.writerow(header) |
| writer.writerows(lines) |
| |
| |
| if __name__ == '__main__': |
| main() |