#!/usr/bin/env python3
# VICIfast Research, Report 01: The Two Bottlenecks Inside a Busy VICIdial Server
# Load rig (§7.2). FOR A THROWAWAY TEST SERVER, NOT A LIVE FLOOR. Read it before you run it.
# Per minute, from Asterisk's verbose log: answered calls (the optimize AGI), redirects that reached AMD (8369), lost ones,
# and the delay from the AGI to AMD.
# Changes the server? No.
# redir.py <screenlog> : per minute, answered calls (optimize AGI), redirects that reached 8369, and the delay.
import re, sys, collections, datetime
OPT = re.compile(r'^\[(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3})\].*Executing \[1383\d\d@default:1\].*agi-VDAD_local_optimize\.agi,(V\d+)')
INS = re.compile(r"'Redirect','(V\d+)','Channel: (PJSIP/loadsink-[0-9a-f]+)'")
V83 = re.compile(r'^\[(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3})\].*Executing \[8369@default:1\].*AGI\("(PJSIP/loadsink-[0-9a-f]+)"')
NOD = re.compile(r'NO DestChannel for callid (V\d+)')
ANSI = re.compile(r'\x1b\[[0-9;]*m')
ts = lambda s: datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S.%f').timestamp()
opt, chan, at8369, nodest = {}, {}, {}, set()
for line in open(sys.argv[1], errors='replace'):
    line = ANSI.sub('', line)
    m = OPT.match(line)
    if m: opt.setdefault(m[2], ts(m[1])); continue
    m = INS.search(line)
    if m: chan[m[1]] = m[2]; continue
    m = V83.match(line)
    if m: at8369.setdefault(m[2], ts(m[1])); continue
    m = NOD.search(line)
    if m: nodest.add(m[1])
per = collections.defaultdict(lambda: {'ans': 0, 'nodest': 0, 'ok': 0, 'lost': 0, 'd': []})
for cid, t in opt.items():
    k = datetime.datetime.fromtimestamp(t).strftime('%H:%M')
    p = per[k]; p['ans'] += 1
    if cid in nodest: p['nodest'] += 1; continue
    c = chan.get(cid)
    if c and c in at8369:
        p['ok'] += 1; p['d'].append(at8369[c] - t)
    else:
        p['lost'] += 1
def q(a, f):
    a = sorted(a); return round(a[min(len(a) - 1, int(f * len(a)))], 2) if a else None
print('minute ans nodest redirect_lost lost% | delay p50 p95 max (s, optimize start -> 8369)')
for k in sorted(per):
    p = per[k]
    print(k, p['ans'], p['nodest'], p['lost'], round(100 * (p['lost'] + p['nodest']) / max(1, p['ans']), 1), '|', q(p['d'], .5), q(p['d'], .95), round(max(p['d']), 2) if p['d'] else None)
