Commit 6fb3b11b authored by Paul Graydon's avatar Paul Graydon

amari.kpi: Add support for DRB.UEActive measurement

parent 3a35162b
......@@ -205,9 +205,9 @@ def _read(logm):
# _handle_stats handles next stats xlog entry upon _read request.
@func(LogMeasure)
def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement):
# build Measurement from stats' counters.
# build Measurement from stats' statistical profiles and counters.
#
# we take δ(stats_prev, stat) and process it mapping Amarisoft counters to
# we take δ(stats_prev, stat) and process it, mapping Amarisoft counters to
# 3GPP ones specified by kpi.Measurement. This approach has following limitations:
#
# - for most of the counters there is no direct mapping in between
......@@ -260,6 +260,13 @@ def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement):
# do init/fini correction if there was also third preceding stats message.
m = logm._m.copy() # [stats_prev, stats)
# cell_statt_profile returns the StatT statistical profile value triplet in stats
def cell_statt_profile(cell, statt_profile):
return tuple(
_stats_cell_sp_el(stats, cell, statt_profile + '_' + el)
for el in ['min', 'avg', 'max']
)
# δcc(counter) tells how specified global cumulative counter changed since last stats result.
def δcc(counter):
old = _stats_cc(stats_prev, counter)
......@@ -305,6 +312,30 @@ def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement):
# compute δ for counters.
# any logic error in data will be reported via LogError.
try:
cells = set(stats['cells'].keys()) # NOTE cells are taken only from stats, not from stat_prev
# DRB: number of active UEs
#
# Aggregate statistical profile for all cells.
# While summing the averages is correct, it is impossible to compute
# an accurate value of the aggregate minimum and maximum across all cells.
# Summing them is the best approximation that will produce
# a wider interval containing the correct values, i.e.:
# Σ(minimums) <= min(Σ) and max(Σ) <= Σ(maximums)
# [ min, avg, max]
Σue_active_count = [None, None, None]
for cell in cells:
cell_ue_active_count = cell_statt_profile(cell, 'ue_active_count')
for i, el in enumerate(cell_ue_active_count):
if el is not None:
Σue_active_count[i] = Σue_active_count[i] or 0
Σue_active_count[i] += el
# any None here will map to kpi.NA
m['DRB.UEActive'] = kpi.StatT(*Σue_active_count)
# RRC: connection establishment
#
# Aggregate statistics for all cells because in E-RAB Accessibility we need
......@@ -315,7 +346,6 @@ def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement):
# same whether we do aggregation here or in kpi.Calc.erab_accessibility().
#
# TODO rework to emit per-cell measurements when/if we need per-cell KPIs
cells = set(stats['cells'].keys()) # NOTE cells are taken only from stats, not from stat_prev
δΣcell_rrc_connection_request = 0 # (if a cell disappears its counters stop to be accounted)
δΣcell_rrc_connection_setup_complete = 0
for cell in cells:
......@@ -368,6 +398,15 @@ def _stats_check(stats: xlog.Message):
raise LogError(stats.timestamp, "stats: %s" % e) from None
return
# _stats_cell_sp_el returns specified per-cell element of a statistical profile.
#
# stats is assumed to be already verified by _stats_check.
def _stats_cell_sp_el(stats: xlog.Message, cell: str, stat_profile_el: str):
_ = stats['cells'].get(cell)
if _ is None:
return None # cell is absent in this stats
return _.get(stat_profile_el, None)
# _stats_cc returns specified global cumulative counter from stats result.
#
# stats is assumed to be already verified by _stats_check.
......
......@@ -21,7 +21,7 @@
from __future__ import print_function, division, absolute_import
from xlte.amari.kpi import LogMeasure, LogError, _trace as trace
from xlte.kpi import Measurement, isNA
from xlte.kpi import Measurement, isNA, StatT
from golang import func, defer, b
import io, json, re
......@@ -75,7 +75,8 @@ class tLogMeasure:
def _mok_init(t):
t._mok = Measurement()
# init fields extracted by amari.kpi from stats to 0
# this will be default values to verify against
# this will be default values to verify against.
# all other fields remain NA.
for field in (
'RRC.ConnEstabAtt.sum',
'RRC.ConnEstabSucc.sum',
......@@ -143,7 +144,7 @@ def test_LogMeasure():
_ = t.expect1
# empty stats after first attach
t.xlog( jstats(1, {}) )
t.xlog( jstats(1, {}, {}) )
_('X.Tstart', 0.02)
_('X.δT', 1-0.02)
t.expect_nodata()
......@@ -173,10 +174,10 @@ def test_LogMeasure():
τ_xlog = 1 # timestamp of last emitted xlog entry
τ_logm = τ_xlog-2+1 # timestamp of next measurement to be read from logm
counters_prev = {}
def tstats(counters):
def tstats(stat_profiles, counters):
nonlocal τ_xlog, τ_logm, counters_prev
trace('\n>>> tstats τ_xlog: %s τ_logm: %s' % (τ_xlog, τ_logm))
t.xlog( jstats(τ_xlog+1, counters) ) # xlog τ+1
t.xlog( jstats(τ_xlog+1, stat_profiles, counters) ) # xlog τ+1
t.read() # read+assert M for τ-1
_('X.Tstart', τ_logm+1) # start preparing next expected M at τ
_('X.δT', 1)
......@@ -189,7 +190,7 @@ def test_LogMeasure():
counters = counters_prev.copy()
for k,δv in δcounters.items():
counters[k] = counters.get(k,0) + δv
tstats(counters)
tstats({}, counters)
# tevent is the verb to verify handling of events.
# its logic is similar to tstats.
......@@ -216,10 +217,8 @@ def test_LogMeasure():
trace('\n>>> tdrb_stats τ: %s τ_xlog: %s τ_logm: %s' % (τ, τ_xlog, τ_logm))
t.xlog( jdrb_stats(τ, qci_trx) )
# further empty stats
tstats({})
tstats({}, {})
_('X.Tstart', 1)
_('X.δT', 1)
_('RRC.ConnEstabAtt.sum', 0)
......@@ -232,6 +231,32 @@ def test_LogMeasure():
_('ERAB.EstabAddSuccNbr.sum', 0)
# DRB.UEActive
tstats(
{'C1.ue_active_count_min': 0,
'C1.ue_active_count_avg': 2.73,
'C1.ue_active_count_max': 3},
{}
)
_('DRB.UEActive', StatT(0, 2.73, 3))
# p2
tstats(
{'C1.ue_active_count_min': 1,
'C1.ue_active_count_avg': 6.0,
'C1.ue_active_count_max': 8},
{}
)
_('DRB.UEActive', StatT(1, 6.0, 8))
# p3
tstats(
{'C1.ue_active_count_min': 3,
'C1.ue_active_count_avg': 4.49,
'C1.ue_active_count_max': 7},
{}
)
_('DRB.UEActive', StatT(3, 4.49, 7))
# RRC.ConnEstab
#
# For init/fini correction LogMeasure accounts termination events in the
......@@ -245,42 +270,59 @@ def test_LogMeasure():
# init 0 3 2 5 0
# fini ø ←─── 2 1←─── 2←─── 4←─── 1
# fini' 0 3 ² 2 ² 3 ¹ 0
tstats({'C1.rrc_connection_request': 0,
'C1.rrc_connection_setup_complete': 2}) # completions for previous uncovered period
tstats(
{},
{'C1.rrc_connection_request': 0,
'C1.rrc_connection_setup_complete': 2} # completions for previous uncovered period
)
_('RRC.ConnEstabAtt.sum', 0)
_('RRC.ConnEstabSucc.sum', 0) # not 2
# p2
tstats({'C1.rrc_connection_request': 0 +3, # 3 new initiations
'C1.rrc_connection_setup_complete': 2 +1}) # 1 new completion
tstats(
{},
{'C1.rrc_connection_request': 0 +3, # 3 new initiations
'C1.rrc_connection_setup_complete': 2 +1} # 1 new completion
)
_('RRC.ConnEstabAtt.sum', 3)
_('RRC.ConnEstabSucc.sum', 3) # not 1
# p3
tstats({'C1.rrc_connection_request': 0+3 +2, # 2 new initiations
'C1.rrc_connection_setup_complete': 2+1 +2}) # 2 completions for p2
tstats(
{},
{'C1.rrc_connection_request': 0+3 +2, # 2 new initiations
'C1.rrc_connection_setup_complete': 2+1 +2} # 2 completions for p2
)
_('RRC.ConnEstabAtt.sum', 2)
_('RRC.ConnEstabSucc.sum', 2) # 2, but it is 2 - 2(for_p2) + 2(from_p4)
# p4
tstats({'C1.rrc_connection_request': 0+3+2 +5, # 5 new initiations
'C1.rrc_connection_setup_complete': 2+1+2 +4}) # 2 completions for p3 + 2 new
tstats(
{},
{'C1.rrc_connection_request': 0+3+2 +5, # 5 new initiations
'C1.rrc_connection_setup_complete': 2+1+2 +4} # 2 completions for p3 + 2 new
)
_('RRC.ConnEstabAtt.sum', 5)
_('RRC.ConnEstabSucc.sum', 3)
# p5
tstats({'C1.rrc_connection_request': 0+3+2+5 +0, # no new initiations
'C1.rrc_connection_setup_complete': 2+1+2+4 +1}) # 1 completion for p4
tstats(
{},
{'C1.rrc_connection_request': 0+3+2+5 +0, # no new initiations
'C1.rrc_connection_setup_complete': 2+1+2+4 +1} # 1 completion for p4
)
_('RRC.ConnEstabAtt.sum', 0)
_('RRC.ConnEstabSucc.sum', 0)
# S1SIG.ConnEstab, ERAB.InitEstab
tδstats({'s1_initial_context_setup_request': +3,
's1_initial_context_setup_response': +2})
's1_initial_context_setup_response': +2}
)
_('S1SIG.ConnEstabAtt', 3)
_('S1SIG.ConnEstabSucc', 3) # 2 + 1(from_next)
_('ERAB.EstabInitAttNbr.sum', 3) # currently same as S1SIG.ConnEstab
_('ERAB.EstabInitSuccNbr.sum', 3) # ----//----
tδstats({'s1_initial_context_setup_request': +4,
's1_initial_context_setup_response': +3})
's1_initial_context_setup_response': +3}
)
_('S1SIG.ConnEstabAtt', 4)
_('S1SIG.ConnEstabSucc', 2) # 3 - 1(to_prev)
_('ERAB.EstabInitAttNbr.sum', 4) # currently same as S1SIG.ConnEstab
......@@ -407,10 +449,10 @@ def test_LogMeasure():
tevent("service attach")
t.expect_nodata()
t.xlog( jstats(τ_xlog+1, {i:1000, f:1000}) ) # LogMeasure restarts the queue after data starts to
t.xlog( jstats(τ_xlog+1, {}, {i:1000, f:1000}) ) # LogMeasure restarts the queue after data starts to
τ_xlog += 1 # come in again. Do one t.xlog step manually to
# increase t.read - t.xlog distance back to 2.
tstats({i:1000+2, f:1000+2})
tstats({}, {i:1000+2, f:1000+2})
_(I, 2) # no "extra" events even if counters start with jumped values after reattach
_(F, 2) # and no fini correction going back through detach
......@@ -424,31 +466,69 @@ def test_LogMeasure():
# multiple cells
# TODO emit per-cell measurements instead of accumulating all cells
tstats({})
tstats({}, {})
t.expect_nodata()
tstats({})
tstats({}, {})
_('RRC.ConnEstabAtt.sum', 0)
_('RRC.ConnEstabSucc.sum', 0)
# C1 appears
tstats({'C1.rrc_connection_request': 12, 'C1.rrc_connection_setup_complete': 11})
tstats(
{
'C1.ue_active_count_min': 2,
'C1.ue_active_count_avg': 3.59,
'C1.ue_active_count_max': 4
},
{'C1.rrc_connection_request': 12, 'C1.rrc_connection_setup_complete': 11}
)
_('RRC.ConnEstabAtt.sum', 12)
_('RRC.ConnEstabSucc.sum', 11+1)
_('DRB.UEActive', StatT(2, 3.59, 4))
# C2 appears
tstats({'C1.rrc_connection_request': 12+3, 'C1.rrc_connection_setup_complete': 11+3,
'C2.rrc_connection_request': 22, 'C2.rrc_connection_setup_complete': 21})
tstats(
{
'C1.ue_active_count_min': 1,
'C1.ue_active_count_avg': 2.87,
'C1.ue_active_count_max': 5,
'C2.ue_active_count_min': 1,
'C2.ue_active_count_avg': 1.43,
'C2.ue_active_count_max': 3
},
{'C1.rrc_connection_request': 12+3, 'C1.rrc_connection_setup_complete': 11+3,
'C2.rrc_connection_request': 22, 'C2.rrc_connection_setup_complete': 21}
)
_('RRC.ConnEstabAtt.sum', 3+22)
_('RRC.ConnEstabSucc.sum', -1+3+21+2)
_('DRB.UEActive', StatT(1+1, 2.87+1.43, 5+3))
# C1 and C2 stays
tstats({'C1.rrc_connection_request': 12+3+3, 'C1.rrc_connection_setup_complete': 11+3+3,
'C2.rrc_connection_request': 22+4, 'C2.rrc_connection_setup_complete': 21+4})
tstats(
{
'C1.ue_active_count_min': 3,
'C1.ue_active_count_avg': 3.10,
'C1.ue_active_count_max': 5,
'C2.ue_active_count_min': 0,
'C2.ue_active_count_avg': 0.62,
'C2.ue_active_count_max': 1
},
{'C1.rrc_connection_request': 12+3+3, 'C1.rrc_connection_setup_complete': 11+3+3,
'C2.rrc_connection_request': 22+4, 'C2.rrc_connection_setup_complete': 21+4}
)
_('RRC.ConnEstabAtt.sum', 3+4)
_('RRC.ConnEstabSucc.sum', -2+3+4+2)
_('DRB.UEActive', StatT(3+0, 3.10+0.62, 5+1))
# C1 disappears
tstats({'C2.rrc_connection_request': 22+4+4, 'C2.rrc_connection_setup_complete': 21+4+4})
tstats(
{
'C2.ue_active_count_min': 0,
'C2.ue_active_count_avg': 1.19,
'C2.ue_active_count_max': 3
},
{'C2.rrc_connection_request': 22+4+4, 'C2.rrc_connection_setup_complete': 21+4+4}
)
_('RRC.ConnEstabAtt.sum', 4)
_('RRC.ConnEstabSucc.sum', 4-2)
_('DRB.UEActive', StatT(0, 1.19, 3))
# C2 disappears
tstats({})
tstats({}, {})
_('RRC.ConnEstabAtt.sum', 0)
_('RRC.ConnEstabSucc.sum', 0)
......@@ -467,28 +547,28 @@ def test_LogMeasure_badinput():
CC = 'RRC.ConnEstabAtt.sum'
# initial ok entries
t.xlog( jstats(1, {}) )
t.xlog( jstats(2, {cc: 2}) )
t.xlog( jstats(3, {cc: 2+3}) )
t.xlog( jstats(1, {}, {}) )
t.xlog( jstats(2, {}, {cc: 2}) )
t.xlog( jstats(3, {}, {cc: 2+3}) )
# bad: no counters
t.xlog('{"message":"stats", "utc":21, "counters": {"messages": {}}, "cells": {"1": {}}}')
t.xlog('{"message":"stats", "utc":22, "counters": {"messages": {}}, "cells": {"1": {"counters": {}}}}')
t.xlog('{"message":"stats", "utc":23, "cells": {"1": {"counters": {"messages": {}}}}}')
t.xlog('{"message":"stats", "utc":24, "counters": {}, "cells": {"1": {"counters": {"messages": {}}}}}')
# follow-up ok entries
t.xlog( jstats(31, {cc: 30+4}) )
t.xlog( jstats(32, {cc: 30+4+5}) )
t.xlog( jstats(31, {}, {cc: 30+4}) )
t.xlog( jstats(32, {}, {cc: 30+4+5}) )
# badline 1
t.xlog( "zzzqqqrrr" )
# more ok entries
t.xlog( jstats(41, {cc: 40+6}) )
t.xlog( jstats(42, {cc: 40+6+7}) )
t.xlog( jstats(41, {}, {cc: 40+6}) )
t.xlog( jstats(42, {}, {cc: 40+6+7}) )
# badline 2 + followup event
t.xlog( "hello world" )
t.xlog( '{"meta": {"event": "service attach", "time": 50}}' )
# more ok entries
t.xlog( jstats(51, {cc: 50+8}) )
t.xlog( jstats(52, {cc: 50+8+9}) )
t.xlog( jstats(51, {}, {cc: 50+8}) )
t.xlog( jstats(52, {}, {cc: 50+8+9}) )
def readok(τ, CC_value):
_('X.Tstart', τ)
......@@ -540,11 +620,11 @@ def test_LogMeasure_cc_wraparound():
cc = 'C1.rrc_connection_request'
CC = 'RRC.ConnEstabAtt.sum'
t.xlog( jstats(1, {}) )
t.xlog( jstats(2, {cc: 13}) )
t.xlog( jstats(3, {cc: 12}) ) # cc↓ - should be reported
t.xlog( jstats(4, {cc: 140}) ) # cc↑↑ - should start afresh
t.xlog( jstats(5, {cc: 150}) )
t.xlog( jstats(1, {}, {}) )
t.xlog( jstats(2, {}, {cc: 13}) )
t.xlog( jstats(3, {}, {cc: 12}) ) # cc↓ - should be reported
t.xlog( jstats(4, {}, {cc: 140}) ) # cc↑↑ - should start afresh
t.xlog( jstats(5, {}, {cc: 150}) )
def readok(τ, CC_value):
_('X.Tstart', τ)
......@@ -574,10 +654,10 @@ def test_LogMeasure_sync():
cc = 'C1.rrc_connection_request'
CC = 'RRC.ConnEstabAtt.sum'
t.xlog( jstats(1, {}) )
t.xlog( jstats(2, {cc: 4}) )
t.xlog( jstats(1, {}, {}) )
t.xlog( jstats(2, {}, {cc: 4}) )
t.xlog( '{"meta": {"event": "sync", "time": 2.5, "state": "attached", "reason": "periodic", "generator": "xlog ws://localhost:9001 stats[]/30.0s"}}' )
t.xlog( jstats(3, {cc: 7}) )
t.xlog( jstats(3, {}, {cc: 7}) )
def readok(τ, CC_value):
_('X.Tstart', τ)
......@@ -593,14 +673,29 @@ def test_LogMeasure_sync():
readok(2, 3) # 2-3 jumping over sync
# jstats returns json-encoded stats message corresponding to counters dict.
# jstats returns json-encoded stats message corresponding to
# the given statistical profile and counter dicts.
#
# if a counter goes as "Cxxx.yyy" it is emitted as counter yyy of cell xxx in the output.
# a key formatted as "Cxxx.yyy" is emitted as measurement yyy of cell xxx in the output.
# τ goes directly to stats['utc'] as is.
def jstats(τ, counters): # -> str
def jstats(τ, stat_profiles, counters): # -> str
g_sp = {} # global statistical profiles
g_cc = {} # global cumulative counters
cells = {} # .cells
for sp, value in stat_profiles.items():
_ = re.match(r"^C([^.]+)\.(.+)$", sp)
if _ is not None:
cell = _.group(1)
sp = _.group(2)
# enforce correct cell structure
cells.setdefault(cell, {}) \
.setdefault("counters", {}) \
.setdefault("messages", {})
cells[cell][sp] = value
else:
g_sp[sp] = value
for cc, value in counters.items():
_ = re.match(r"^C([^.]+)\.(.+)$", cc)
if _ is not None:
......@@ -616,6 +711,7 @@ def jstats(τ, counters): # -> str
s = {
"message": "stats",
"utc": τ,
**g_sp,
"cells": cells,
"counters": {"messages": g_cc},
}
......@@ -623,17 +719,29 @@ def jstats(τ, counters): # -> str
return json.dumps(s)
def test_jstats():
assert jstats(0, {}) == '{"message": "stats", "utc": 0, "cells": {}, "counters": {"messages": {}}}'
assert jstats(123.4, {"C1.rrc_x": 1, "s1_y": 2, "C1.rrc_z": 3, "x2_zz": 4}) == \
'{"message": "stats", "utc": 123.4, "cells": {"1": {"counters": {"messages": {"rrc_x": 1, "rrc_z": 3}}}}, "counters": {"messages": {"s1_y": 2, "x2_zz": 4}}}'
# multiple cells
assert jstats(432.1, {"C1.rrc_x": 11, "C2.rrc_y": 22, "C3.xyz": 33, "C1.abc": 111, "xyz": 44}) == \
'{"message": "stats", "utc": 432.1, "cells": {' + \
'"1": {"counters": {"messages": {"rrc_x": 11, "abc": 111}}}, ' + \
'"2": {"counters": {"messages": {"rrc_y": 22}}}, ' + \
'"3": {"counters": {"messages": {"xyz": 33}}}}, ' + \
'"counters": {"messages": {"xyz": 44}}}'
assert jstats(0, {}, {}) == '{"message": "stats", "utc": 0, "cells": {}, "counters": {"messages": {}}}'
# only statistical profiles
assert jstats(1.2, {"C1.ue_x_min": 1, "r1_y": 2, "C1.ue_x_avg": 3, "s2_z": 4, "C1.ue_x_max": 5}, {}) == \
'{"message": "stats", "utc": 1.2, "r1_y": 2, "s2_z": 4, ' + \
'"cells": {"1": {"counters": {"messages": {}}, "ue_x_min": 1, "ue_x_avg": 3, "ue_x_max": 5}}, ' + \
'"counters": {"messages": {}}}'
# only counters
assert jstats(12.34, {}, {"C1.rrc_x": 1, "s1_y": 2, "C1.rrc_z": 3, "x2_zz": 4}) == \
'{"message": "stats", "utc": 12.34, "cells": {"1": {"counters": {"messages": {"rrc_x": 1, "rrc_z": 3}}}}, ' + \
'"counters": {"messages": {"s1_y": 2, "x2_zz": 4}}}'
# multiple cells with both statistical profiles and counters
assert jstats(
432.1,
{"C1.ue_w": 11, "C2.ue_ww": 22, "C3.ue_x": 33, "C1.ue_xx": 44, "rst": 55},
{"C1.rrc_x": 11, "C2.rrc_y": 22, "C3.xyz": 33, "C1.abc": 44, "xyz": 55}
) == '{"message": "stats", "utc": 432.1, "rst": 55, "cells": {' + \
'"1": {"counters": {"messages": {"rrc_x": 11, "abc": 44}}, "ue_w": 11, "ue_xx": 44}, ' + \
'"2": {"counters": {"messages": {"rrc_y": 22}}, "ue_ww": 22}, ' + \
'"3": {"counters": {"messages": {"xyz": 33}}, "ue_x": 33}}, ' + \
'"counters": {"messages": {"xyz": 55}}}'
# jdrb_stats, similarly to jstats, returns json-encoded x.drb_stats message
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment