blob: 67719b2232abc4540fc34b2b5afc1d47aa73a0ff [file] [log] [blame]
#!/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()