starlink-grpc-tools/dishHistoryStats.py
sparky8512 a5036db9e0 Don't allow a sample to be both unscheduled and obstructed
Doesn't ever seem to happen, but in case it does in the future, treat that case as just unscheduled. This way, the unclassified ping loss (AKA "Beta downtime") can be computed from the totals.
2020-12-30 13:09:24 -08:00

190 lines
6.3 KiB
Python

#!/usr/bin/python3
######################################################################
#
# Equivalent script to parseJsonHistory.py, except integrating the
# gRPC calls, instead of relying on separatate invocation of grpcurl.
#
# This script examines the most recent samples from the history data
# and computes several different metrics related to packet loss. By
# default, it will print the results in CSV format.
#
######################################################################
import grpc
import spacex.api.device.device_pb2
import spacex.api.device.device_pb2_grpc
import datetime
import sys
import getopt
from itertools import chain
fArgError = False
try:
opts, args = getopt.getopt(sys.argv[1:], "ahrs:vH")
except getopt.GetoptError as err:
print(str(err))
fArgError = True
# Default to 1 hour worth of data samples.
parseSamples = 3600
fUsage = False
fVerbose = False
fParseAll = False
fHeader = False
fRunLengths = False
if not fArgError:
if len(args) > 0:
fArgError = True
else:
for opt, arg in opts:
if opt == "-a":
fParseAll = True
elif opt == "-h":
fUsage = True
elif opt == "-r":
fRunLengths = True
elif opt == "-s":
parseSamples = int(arg)
elif opt == "-v":
fVerbose = True
elif opt == "-H":
fHeader = True
if fUsage or fArgError:
print("Usage: "+sys.argv[0]+" [options...]")
print("Options:")
print(" -a: Parse all valid samples")
print(" -h: Be helpful")
print(" -r: Include ping drop run length stats")
print(" -s <num>: Parse <num> data samples, default: "+str(parseSamples))
print(" -v: Be verbose")
print(" -H: print CSV header instead of parsing file")
sys.exit(1 if fArgError else 0)
if fHeader:
header = "datetimestamp_utc,samples,total_ping_drop,count_full_ping_drop,count_obstructed,total_obstructed_ping_drop,count_full_obstructed_ping_drop,count_unscheduled,total_unscheduled_ping_drop,count_full_unscheduled_ping_drop"
if fRunLengths:
header+= ",init_run_fragment,final_run_fragment,"
header += ",".join("run_seconds_" + str(x) for x in range(1, 61)) + ","
header += ",".join("run_minutes_" + str(x) for x in range(1, 60))
header += ",run_minutes_60_or_greater"
print(header)
sys.exit(0)
with grpc.insecure_channel('192.168.100.1:9200') as channel:
stub = spacex.api.device.device_pb2_grpc.DeviceStub(channel)
response = stub.Handle(spacex.api.device.device_pb2.Request(get_history={}))
timestamp = datetime.datetime.utcnow()
historyData = response.dish_get_history
# 'current' is the count of data samples written to the ring buffer,
# irrespective of buffer wrap.
current = int(historyData.current)
nSamples = len(historyData.pop_ping_drop_rate)
if fVerbose:
print("current: " + str(current))
print("All samples: " + str(nSamples))
nSamples = min(nSamples,current)
if fVerbose:
print("Valid samples: " + str(nSamples))
# This is ring buffer offset, so both index to oldest data sample and
# index to next data sample after the newest one.
offset = current % nSamples
tot = 0
totOne = 0
totUnsched = 0
totUnschedD = 0
totUnschedOne = 0
totObstruct = 0
totObstructD = 0
totObstructOne = 0
secondRuns = [0] * 60
minuteRuns = [0] * 60
runLength = 0
initRun = None
if fParseAll or nSamples < parseSamples:
parseSamples = nSamples
# Parse the most recent parseSamples-sized set of samples. This will
# iterate samples in order from oldest to newest, although that's not
# actually required for the current set of stats being computed below.
if parseSamples <= offset:
sampleRange = range(offset - parseSamples, offset)
else:
sampleRange = chain(range(nSamples + offset - parseSamples, nSamples), range(0, offset))
for i in sampleRange:
d = historyData.pop_ping_drop_rate[i]
tot += d
if d >= 1:
totOne += d
runLength += 1
elif runLength > 0:
if initRun is None:
initRun = runLength
else:
if runLength <= 60:
secondRuns[runLength-1] += runLength
else:
minuteRuns[min((runLength-1)//60-1, 59)] += runLength
runLength = 0
elif initRun is None:
initRun = 0
if not historyData.scheduled[i]:
totUnsched += 1
totUnschedD += d
if d >= 1:
totUnschedOne += d
# scheduled=false and obstructed=true do not ever appear to overlap,
# but in case they do in the future, treat that as just unscheduled
# in order to avoid double-counting it.
elif historyData.obstructed[i]:
totObstruct += 1
totObstructD += d
if d >= 1:
totObstructOne += d
# If the entire sample set is one big drop run, it will be both initial
# fragment (continued from prior sample range) and final one (continued
# to next sample range), but to avoid double-reporting, just call it
# the initial run.
if initRun is None:
initRun = runLength
runLength = 0
if fVerbose:
print("Parsed samples: " + str(parseSamples))
print("Total ping drop: " + str(tot))
print("Count of drop == 1: " + str(totOne))
print("Obstructed: " + str(totObstruct))
print("Obstructed ping drop: " + str(totObstructD))
print("Obstructed drop == 1: " + str(totObstructOne))
print("Unscheduled: " + str(totUnsched))
print("Unscheduled ping drop: " + str(totUnschedD))
print("Unscheduled drop == 1: " + str(totUnschedOne))
if fRunLengths:
print("Initial drop run fragment: " + str(initRun))
print("Final drop run fragment: " + str(runLength))
print("Per-second drop runs: " + ", ".join(str(x) for x in secondRuns))
print("Per-minute drop runs: " + ", ".join(str(x) for x in minuteRuns))
else:
# NOTE: When changing data output format, also change the -H header printing above.
csvData = timestamp.replace(microsecond=0).isoformat() + "," + ",".join(str(x) for x in [parseSamples, tot, totOne, totObstruct, totObstructD, totObstructOne, totUnsched, totUnschedD, totUnschedOne])
if fRunLengths:
csvData += "," + ",".join(str(x) for x in chain([initRun, runLength], secondRuns, minuteRuns))
print(csvData)