Commit eb9f1fa9 authored by Paul Graydon's avatar Paul Graydon

amari.kpi: Add DRB.UEActive measurement

parent 7164e99c
...@@ -29,6 +29,8 @@ from xlte import kpi ...@@ -29,6 +29,8 @@ from xlte import kpi
from xlte.amari import xlog from xlte.amari import xlog
from golang import func from golang import func
from math import floor
# LogMeasure takes enb.xlog (TODO and enb.log) as input, and produces kpi.Measurements on output. # LogMeasure takes enb.xlog (TODO and enb.log) as input, and produces kpi.Measurements on output.
# #
...@@ -205,10 +207,14 @@ def _read(logm): ...@@ -205,10 +207,14 @@ def _read(logm):
# _handle_stats handles next stats xlog entry upon _read request. # _handle_stats handles next stats xlog entry upon _read request.
@func(LogMeasure) @func(LogMeasure)
def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement): def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement):
# build Measurement from stats' counters. # build Measurement from stats' values, by mapping Amarisoft non-counters
# (non-cumulative) and counters to their respective 3GPP value specified by
# kpi.Measurement.
#
# for non-counters, simply map the value to the corresponding 3GPP value.
# #
# we take δ(stats_prev, stat) and process it mapping Amarisoft counters to # for cumulative counters, we take δ(stats_prev, stat) and process it
# 3GPP ones specified by kpi.Measurement. This approach has following limitations: # before performing the mapping. This approach has following limitations:
# #
# - for most of the counters there is no direct mapping in between # - for most of the counters there is no direct mapping in between
# Amarisoft and 3GPP. For example we currently use s1_erab_setup_request for # Amarisoft and 3GPP. For example we currently use s1_erab_setup_request for
...@@ -302,6 +308,17 @@ def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement): ...@@ -302,6 +308,17 @@ def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement):
if m[fini] > m[init]: if m[fini] > m[init]:
m[fini] = m[init] m[fini] = m[init]
cells = set(stats['cells'].keys()) # NOTE cells are taken only from stats, not from stat_prev
# map non-counter values, which are independent between periods
# and do not need any special processing.
Σue_active_count_avg = 0
for cell in cells:
Σue_active_count_avg += _stats_cell_nc(stats, cell, 'ue_active_count_avg')
# flooring the sum of averages is more accurate to the 3GPP spec than summing the floors.
Σue_active_count_avg = int(floor(Σue_active_count_avg))
m['DRB.UEActive'] = Σue_active_count_avg
# compute δ for counters. # compute δ for counters.
# any logic error in data will be reported via LogError. # any logic error in data will be reported via LogError.
try: try:
...@@ -315,7 +332,6 @@ def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement): ...@@ -315,7 +332,6 @@ def _handle_stats(logm, stats: xlog.Message, m_prev: kpi.Measurement):
# same whether we do aggregation here or in kpi.Calc.erab_accessibility(). # 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 # 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_request = 0 # (if a cell disappears its counters stop to be accounted)
δΣcell_rrc_connection_setup_complete = 0 δΣcell_rrc_connection_setup_complete = 0
for cell in cells: for cell in cells:
...@@ -368,12 +384,25 @@ def _stats_check(stats: xlog.Message): ...@@ -368,12 +384,25 @@ def _stats_check(stats: xlog.Message):
raise LogError(stats.timestamp, "stats: %s" % e) from None raise LogError(stats.timestamp, "stats: %s" % e) from None
return return
# _stats_nc returns specified global non-counter from stats result.
#
# stats is assumed to be already verified by _stats_check.
def _stats_nc(stats: xlog.Message, non_counter: str):
return stats.get(non_counter, 0.0)
# _stats_cc returns specified global cumulative counter from stats result. # _stats_cc returns specified global cumulative counter from stats result.
# #
# stats is assumed to be already verified by _stats_check. # stats is assumed to be already verified by _stats_check.
def _stats_cc(stats: xlog.Message, counter: str): def _stats_cc(stats: xlog.Message, counter: str):
return stats['counters']['messages'].get(counter, 0) return stats['counters']['messages'].get(counter, 0)
# _stats_cell_nc is like _stats_nc but returns specified per-cell non-counter from stats result.
def _stats_cell_nc(stats: xlog.Message, cell: str, non_counter: str):
_ = stats['cells'].get(cell)
if _ is None:
return 0.0 # cell is absent in this stats
return _.get(non_counter, 0.0)
# _stats_cell_cc is like _stats_cc but returns specified per-cell cumulative counter from stats result. # _stats_cell_cc is like _stats_cc but returns specified per-cell cumulative counter from stats result.
def _stats_cell_cc(stats: xlog.Message, cell: str, counter: str): def _stats_cell_cc(stats: xlog.Message, cell: str, counter: str):
_ = stats['cells'].get(cell) _ = stats['cells'].get(cell)
......
...@@ -85,6 +85,7 @@ class tLogMeasure: ...@@ -85,6 +85,7 @@ class tLogMeasure:
'ERAB.EstabInitSuccNbr.sum', 'ERAB.EstabInitSuccNbr.sum',
'ERAB.EstabAddAttNbr.sum', 'ERAB.EstabAddAttNbr.sum',
'ERAB.EstabAddSuccNbr.sum', 'ERAB.EstabAddSuccNbr.sum',
'DRB.UEActive'
): ):
t._mok[field] = 0 t._mok[field] = 0
...@@ -143,7 +144,7 @@ def test_LogMeasure(): ...@@ -143,7 +144,7 @@ def test_LogMeasure():
_ = t.expect1 _ = t.expect1
# empty stats after first attach # empty stats after first attach
t.xlog( jstats(1, {}) ) t.xlog( jstats(1, {}, {}) )
_('X.Tstart', 0.02) _('X.Tstart', 0.02)
_('X.δT', 1-0.02) _('X.δT', 1-0.02)
t.expect_nodata() t.expect_nodata()
...@@ -152,7 +153,7 @@ def test_LogMeasure(): ...@@ -152,7 +153,7 @@ def test_LogMeasure():
# tstats is the verb to check handling of stats message. # tstats is the verb to check handling of stats message.
# #
# it xlogs next stats(counters) and reads back new measurement via t.read(). # it xlogs next stats(non_counters, counters) and reads back new measurement via t.read().
# #
# NOTE t.read goes 2 steps behind corresponding t.xlog call. This is on # NOTE t.read goes 2 steps behind corresponding t.xlog call. This is on
# purpose to sync emitting xlog entries with corresponding checks in test # purpose to sync emitting xlog entries with corresponding checks in test
...@@ -166,17 +167,17 @@ def test_LogMeasure(): ...@@ -166,17 +167,17 @@ def test_LogMeasure():
# #
# As the result it allows to write testing code as: # As the result it allows to write testing code as:
# #
# tstats(counters) # tstats(non_counters, counters)
# _(...) # verify effect on Measurements returned with period # _(...) # verify effect on Measurements returned with period
# _(...) # ending by timestamp of the above stats call. # _(...) # ending by timestamp of the above stats call.
# _(...) # i.e. Measurement₁ if tstats call corresponds to xlog₂. # _(...) # i.e. Measurement₁ if tstats call corresponds to xlog₂.
τ_xlog = 1 # timestamp of last emitted xlog entry τ_xlog = 1 # timestamp of last emitted xlog entry
τ_logm = τ_xlog-2+1 # timestamp of next measurement to be read from logm τ_logm = τ_xlog-2+1 # timestamp of next measurement to be read from logm
counters_prev = {} counters_prev = {}
def tstats(counters): def tstats(non_counters, counters):
nonlocal τ_xlog, τ_logm, counters_prev nonlocal τ_xlog, τ_logm, counters_prev
trace('\n>>> tstats τ_xlog: %s τ_logm: %s' % (τ_xlog, τ_logm)) trace('\n>>> tstats τ_xlog: %s τ_logm: %s' % (τ_xlog, τ_logm))
t.xlog( jstats(τ_xlog+1, counters) ) # xlog τ+1 t.xlog( jstats(τ_xlog+1, non_counters, counters) ) # xlog τ+1
t.read() # read+assert M for τ-1 t.read() # read+assert M for τ-1
_('X.Tstart', τ_logm+1) # start preparing next expected M at τ _('X.Tstart', τ_logm+1) # start preparing next expected M at τ
_('X.δT', 1) _('X.δT', 1)
...@@ -189,7 +190,7 @@ def test_LogMeasure(): ...@@ -189,7 +190,7 @@ def test_LogMeasure():
counters = counters_prev.copy() counters = counters_prev.copy()
for k,δv in δcounters.items(): for k,δv in δcounters.items():
counters[k] = counters.get(k,0) + δv counters[k] = counters.get(k,0) + δv
tstats(counters) tstats({}, counters)
# tevent is the verb to verify handling of events. # tevent is the verb to verify handling of events.
# its logic is similar to tstats. # its logic is similar to tstats.
...@@ -217,9 +218,8 @@ def test_LogMeasure(): ...@@ -217,9 +218,8 @@ def test_LogMeasure():
t.xlog( jdrb_stats(τ, qci_trx) ) t.xlog( jdrb_stats(τ, qci_trx) )
# further empty stats # further empty stats
tstats({}) tstats({}, {})
_('X.Tstart', 1) _('X.Tstart', 1)
_('X.δT', 1) _('X.δT', 1)
_('RRC.ConnEstabAtt.sum', 0) _('RRC.ConnEstabAtt.sum', 0)
...@@ -230,6 +230,21 @@ def test_LogMeasure(): ...@@ -230,6 +230,21 @@ def test_LogMeasure():
_('ERAB.EstabInitSuccNbr.sum', 0) _('ERAB.EstabInitSuccNbr.sum', 0)
_('ERAB.EstabAddAttNbr.sum', 0) _('ERAB.EstabAddAttNbr.sum', 0)
_('ERAB.EstabAddSuccNbr.sum', 0) _('ERAB.EstabAddSuccNbr.sum', 0)
_('DRB.UEActive', 0)
# DRB.UEActive
#
# For non-counter statistics, simply check that
# the value of each period is correct
tstats({'C1.ue_active_count_avg': 0}, {})
_('DRB.UEActive', 0)
tstats({'C1.ue_active_count_avg': 0.287}, {})
_('DRB.UEActive', 0)
tstats({'C1.ue_active_count_avg': 1.089}, {})
_('DRB.UEActive', 1)
# RRC.ConnEstab # RRC.ConnEstab
...@@ -245,28 +260,28 @@ def test_LogMeasure(): ...@@ -245,28 +260,28 @@ def test_LogMeasure():
# init 0 3 2 5 0 # init 0 3 2 5 0
# fini ø ←─── 2 1←─── 2←─── 4←─── 1 # fini ø ←─── 2 1←─── 2←─── 4←─── 1
# fini' 0 3 ² 2 ² 3 ¹ 0 # fini' 0 3 ² 2 ² 3 ¹ 0
tstats({'C1.rrc_connection_request': 0, tstats({}, {'C1.rrc_connection_request': 0,
'C1.rrc_connection_setup_complete': 2}) # completions for previous uncovered period 'C1.rrc_connection_setup_complete': 2}) # completions for previous uncovered period
_('RRC.ConnEstabAtt.sum', 0) _('RRC.ConnEstabAtt.sum', 0)
_('RRC.ConnEstabSucc.sum', 0) # not 2 _('RRC.ConnEstabSucc.sum', 0) # not 2
# p2 # p2
tstats({'C1.rrc_connection_request': 0 +3, # 3 new initiations tstats({}, {'C1.rrc_connection_request': 0 +3, # 3 new initiations
'C1.rrc_connection_setup_complete': 2 +1}) # 1 new completion 'C1.rrc_connection_setup_complete': 2 +1}) # 1 new completion
_('RRC.ConnEstabAtt.sum', 3) _('RRC.ConnEstabAtt.sum', 3)
_('RRC.ConnEstabSucc.sum', 3) # not 1 _('RRC.ConnEstabSucc.sum', 3) # not 1
# p3 # p3
tstats({'C1.rrc_connection_request': 0+3 +2, # 2 new initiations tstats({}, {'C1.rrc_connection_request': 0+3 +2, # 2 new initiations
'C1.rrc_connection_setup_complete': 2+1 +2}) # 2 completions for p2 'C1.rrc_connection_setup_complete': 2+1 +2}) # 2 completions for p2
_('RRC.ConnEstabAtt.sum', 2) _('RRC.ConnEstabAtt.sum', 2)
_('RRC.ConnEstabSucc.sum', 2) # 2, but it is 2 - 2(for_p2) + 2(from_p4) _('RRC.ConnEstabSucc.sum', 2) # 2, but it is 2 - 2(for_p2) + 2(from_p4)
# p4 # p4
tstats({'C1.rrc_connection_request': 0+3+2 +5, # 5 new initiations 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 'C1.rrc_connection_setup_complete': 2+1+2 +4}) # 2 completions for p3 + 2 new
_('RRC.ConnEstabAtt.sum', 5) _('RRC.ConnEstabAtt.sum', 5)
_('RRC.ConnEstabSucc.sum', 3) _('RRC.ConnEstabSucc.sum', 3)
# p5 # p5
tstats({'C1.rrc_connection_request': 0+3+2+5 +0, # no new initiations 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 'C1.rrc_connection_setup_complete': 2+1+2+4 +1}) # 1 completion for p4
_('RRC.ConnEstabAtt.sum', 0) _('RRC.ConnEstabAtt.sum', 0)
_('RRC.ConnEstabSucc.sum', 0) _('RRC.ConnEstabSucc.sum', 0)
...@@ -407,10 +422,10 @@ def test_LogMeasure(): ...@@ -407,10 +422,10 @@ def test_LogMeasure():
tevent("service attach") tevent("service attach")
t.expect_nodata() 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 τ_xlog += 1 # come in again. Do one t.xlog step manually to
# increase t.read - t.xlog distance back to 2. # 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 _(I, 2) # no "extra" events even if counters start with jumped values after reattach
_(F, 2) # and no fini correction going back through detach _(F, 2) # and no fini correction going back through detach
...@@ -424,31 +439,41 @@ def test_LogMeasure(): ...@@ -424,31 +439,41 @@ def test_LogMeasure():
# multiple cells # multiple cells
# TODO emit per-cell measurements instead of accumulating all cells # TODO emit per-cell measurements instead of accumulating all cells
tstats({}) tstats({}, {})
t.expect_nodata() t.expect_nodata()
tstats({}) tstats({}, {})
_('DRB.UEActive', 0)
_('RRC.ConnEstabAtt.sum', 0) _('RRC.ConnEstabAtt.sum', 0)
_('RRC.ConnEstabSucc.sum', 0) _('RRC.ConnEstabSucc.sum', 0)
# C1 appears # C1 appears
tstats({'C1.rrc_connection_request': 12, 'C1.rrc_connection_setup_complete': 11}) tstats({'C1.ue_active_count_avg': 3.59},
{'C1.rrc_connection_request': 12, 'C1.rrc_connection_setup_complete': 11})
_('DRB.UEActive', 3) # floor(3.59)
_('RRC.ConnEstabAtt.sum', 12) _('RRC.ConnEstabAtt.sum', 12)
_('RRC.ConnEstabSucc.sum', 11+1) _('RRC.ConnEstabSucc.sum', 11+1)
# C2 appears # C2 appears
tstats({'C1.rrc_connection_request': 12+3, 'C1.rrc_connection_setup_complete': 11+3, tstats({'C1.ue_active_count_avg': 2.87, 'C2.ue_active_count_avg': 1.43},
'C2.rrc_connection_request': 22, 'C2.rrc_connection_setup_complete': 21}) {'C1.rrc_connection_request': 12+3, 'C1.rrc_connection_setup_complete': 11+3,
'C2.rrc_connection_request': 22, 'C2.rrc_connection_setup_complete': 21})
_('DRB.UEActive', 4) # floor(2.87+1.43)
_('RRC.ConnEstabAtt.sum', 3+22) _('RRC.ConnEstabAtt.sum', 3+22)
_('RRC.ConnEstabSucc.sum', -1+3+21+2) _('RRC.ConnEstabSucc.sum', -1+3+21+2)
# C1 and C2 stays # C1 and C2 stays
tstats({'C1.rrc_connection_request': 12+3+3, 'C1.rrc_connection_setup_complete': 11+3+3, tstats({'C1.ue_active_count_avg': 3.10, 'C2.ue_active_count_avg': 0.62},
'C2.rrc_connection_request': 22+4, 'C2.rrc_connection_setup_complete': 21+4}) {'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})
_('DRB.UEActive', 3) # floor(3.10+0.62)
_('RRC.ConnEstabAtt.sum', 3+4) _('RRC.ConnEstabAtt.sum', 3+4)
_('RRC.ConnEstabSucc.sum', -2+3+4+2) _('RRC.ConnEstabSucc.sum', -2+3+4+2)
# C1 disappears # C1 disappears
tstats({'C2.rrc_connection_request': 22+4+4, 'C2.rrc_connection_setup_complete': 21+4+4}) tstats({'C2.ue_active_count_avg': 1.19},
{'C2.rrc_connection_request': 22+4+4, 'C2.rrc_connection_setup_complete': 21+4+4})
_('DRB.UEActive', 1) # floor(1.19)
_('RRC.ConnEstabAtt.sum', 4) _('RRC.ConnEstabAtt.sum', 4)
_('RRC.ConnEstabSucc.sum', 4-2) _('RRC.ConnEstabSucc.sum', 4-2)
# C2 disappears # C2 disappears
tstats({}) tstats({}, {})
_('DRB.UEActive', 0)
_('RRC.ConnEstabAtt.sum', 0) _('RRC.ConnEstabAtt.sum', 0)
_('RRC.ConnEstabSucc.sum', 0) _('RRC.ConnEstabSucc.sum', 0)
...@@ -467,28 +492,28 @@ def test_LogMeasure_badinput(): ...@@ -467,28 +492,28 @@ def test_LogMeasure_badinput():
CC = 'RRC.ConnEstabAtt.sum' CC = 'RRC.ConnEstabAtt.sum'
# initial ok entries # initial ok entries
t.xlog( jstats(1, {}) ) t.xlog( jstats(1, {}, {}) )
t.xlog( jstats(2, {cc: 2}) ) t.xlog( jstats(2, {}, {cc: 2}) )
t.xlog( jstats(3, {cc: 2+3}) ) t.xlog( jstats(3, {}, {cc: 2+3}) )
# bad: no counters # bad: no counters
t.xlog('{"message":"stats", "utc":21, "counters": {"messages": {}}, "cells": {"1": {}}}') 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":22, "counters": {"messages": {}}, "cells": {"1": {"counters": {}}}}')
t.xlog('{"message":"stats", "utc":23, "cells": {"1": {"counters": {"messages": {}}}}}') t.xlog('{"message":"stats", "utc":23, "cells": {"1": {"counters": {"messages": {}}}}}')
t.xlog('{"message":"stats", "utc":24, "counters": {}, "cells": {"1": {"counters": {"messages": {}}}}}') t.xlog('{"message":"stats", "utc":24, "counters": {}, "cells": {"1": {"counters": {"messages": {}}}}}')
# follow-up ok entries # follow-up ok entries
t.xlog( jstats(31, {cc: 30+4}) ) t.xlog( jstats(31, {}, {cc: 30+4}) )
t.xlog( jstats(32, {cc: 30+4+5}) ) t.xlog( jstats(32, {}, {cc: 30+4+5}) )
# badline 1 # badline 1
t.xlog( "zzzqqqrrr" ) t.xlog( "zzzqqqrrr" )
# more ok entries # more ok entries
t.xlog( jstats(41, {cc: 40+6}) ) t.xlog( jstats(41, {}, {cc: 40+6}) )
t.xlog( jstats(42, {cc: 40+6+7}) ) t.xlog( jstats(42, {}, {cc: 40+6+7}) )
# badline 2 + followup event # badline 2 + followup event
t.xlog( "hello world" ) t.xlog( "hello world" )
t.xlog( '{"meta": {"event": "service attach", "time": 50}}' ) t.xlog( '{"meta": {"event": "service attach", "time": 50}}' )
# more ok entries # more ok entries
t.xlog( jstats(51, {cc: 50+8}) ) t.xlog( jstats(51, {}, {cc: 50+8}) )
t.xlog( jstats(52, {cc: 50+8+9}) ) t.xlog( jstats(52, {}, {cc: 50+8+9}) )
def readok(τ, CC_value): def readok(τ, CC_value):
_('X.Tstart', τ) _('X.Tstart', τ)
...@@ -540,11 +565,11 @@ def test_LogMeasure_cc_wraparound(): ...@@ -540,11 +565,11 @@ def test_LogMeasure_cc_wraparound():
cc = 'C1.rrc_connection_request' cc = 'C1.rrc_connection_request'
CC = 'RRC.ConnEstabAtt.sum' CC = 'RRC.ConnEstabAtt.sum'
t.xlog( jstats(1, {}) ) t.xlog( jstats(1, {}, {}) )
t.xlog( jstats(2, {cc: 13}) ) t.xlog( jstats(2, {}, {cc: 13}) )
t.xlog( jstats(3, {cc: 12}) ) # cc↓ - should be reported t.xlog( jstats(3, {}, {cc: 12}) ) # cc↓ - should be reported
t.xlog( jstats(4, {cc: 140}) ) # cc↑↑ - should start afresh t.xlog( jstats(4, {}, {cc: 140}) ) # cc↑↑ - should start afresh
t.xlog( jstats(5, {cc: 150}) ) t.xlog( jstats(5, {}, {cc: 150}) )
def readok(τ, CC_value): def readok(τ, CC_value):
_('X.Tstart', τ) _('X.Tstart', τ)
...@@ -574,10 +599,10 @@ def test_LogMeasure_sync(): ...@@ -574,10 +599,10 @@ def test_LogMeasure_sync():
cc = 'C1.rrc_connection_request' cc = 'C1.rrc_connection_request'
CC = 'RRC.ConnEstabAtt.sum' CC = 'RRC.ConnEstabAtt.sum'
t.xlog( jstats(1, {}) ) t.xlog( jstats(1, {}, {}) )
t.xlog( jstats(2, {cc: 4}) ) 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( '{"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): def readok(τ, CC_value):
_('X.Tstart', τ) _('X.Tstart', τ)
...@@ -593,29 +618,49 @@ def test_LogMeasure_sync(): ...@@ -593,29 +618,49 @@ def test_LogMeasure_sync():
readok(2, 3) # 2-3 jumping over 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 non-counters and counters dicts.
# #
# if a counter goes as "Cxxx.yyy" it is emitted as counter yyy of cell xxx in the output. # if a non-counter or counter goes as "Cxxx.yyy" it is emitted as value yyy of cell xxx in the output.
# τ goes directly to stats['utc'] as is. # τ goes directly to stats['utc'] as is.
def jstats(τ, counters): # -> str def jstats(τ, non_counters, counters): # -> str
g_nc = {} # global non-counters
g_cc = {} # global cumulative counters g_cc = {} # global cumulative counters
cells = {} # .cells cells = {} # .cells
for cc, value in counters.items(): for nc, value in non_counters.items():
_ = re.match(r"^C([^.]+)\.(.+)$", cc) _ = re.match(r"^C([^.]+)\.(.+)$", nc)
if _ is not None: if _ is not None:
cell = _.group(1) cell = _.group(1)
cc = _.group(2) nc = _.group(2)
cells.setdefault(cell, {}) \ cells.setdefault(cell, {}) \
.setdefault("counters", {}) \ [nc] = value
.setdefault("messages", {}) \
[cc] = value
else: else:
g_cc[cc] = value g_nc[nc] = value
# Keep a correct structure in every cell
# in which a non-counter value is present
for cell in cells:
cells.setdefault(cell, {}) \
.setdefault("counters", {}) \
.setdefault("messages", {})
if counters:
for cc, value in counters.items():
_ = re.match(r"^C([^.]+)\.(.+)$", cc)
if _ is not None:
cell = _.group(1)
cc = _.group(2)
cells.setdefault(cell, {}) \
.setdefault("counters", {}) \
.setdefault("messages", {}) \
[cc] = value
else:
g_cc[cc] = value
s = { s = {
"message": "stats", "message": "stats",
"utc": τ, "utc": τ,
**g_nc,
"cells": cells, "cells": cells,
"counters": {"messages": g_cc}, "counters": {"messages": g_cc},
} }
...@@ -623,17 +668,32 @@ def jstats(τ, counters): # -> str ...@@ -623,17 +668,32 @@ def jstats(τ, counters): # -> str
return json.dumps(s) return json.dumps(s)
def test_jstats(): def test_jstats():
assert jstats(0, {}) == '{"message": "stats", "utc": 0, "cells": {}, "counters": {"messages": {}}}' 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}}}' # only non-counters
assert jstats(1.2, {"C1.ue_x": 1, "r1_y": 2, "C1.ue_z": 3, "s2_zz": 4}, {}) == \
'{"message": "stats", "utc": 1.2, "r1_y": 2, "s2_zz": 4, "cells": {"1": {"ue_x": 1, "ue_z": 3, "counters": {"messages": {}}}}, "counters": {"messages": {}}}'
# only counters
assert jstats(12.3, {}, {"C1.rrc_x": 1, "s1_y": 2, "C1.rrc_z": 3, "x2_zz": 4}) == \
'{"message": "stats", "utc": 12.3, "cells": {"1": {"counters": {"messages": {"rrc_x": 1, "rrc_z": 3}}}}, "counters": {"messages": {"s1_y": 2, "x2_zz": 4}}}'
# both non-counters and counters
assert jstats(123.4, {"C1.ue_w": 1, "r1_ww": 2, "C1.ue_x": 3, "s2_xx": 4}, {"C1.rrc_y": 5, "s1_yy": 6, "C1.rrc_z": 7, "x2_zz": 8}) == \
'{"message": "stats", "utc": 123.4, ' + \
'"r1_ww": 2, "s2_xx": 4, ' + \
'"cells": {"1": {"ue_w": 1, "ue_x": 3, "counters": {"messages": {"rrc_y": 5, "rrc_z": 7}}}}, ' + \
'"counters": {"messages": {"s1_yy": 6, "x2_zz": 8}}}'
# multiple cells # multiple cells
assert jstats(432.1, {"C1.rrc_x": 11, "C2.rrc_y": 22, "C3.xyz": 33, "C1.abc": 111, "xyz": 44}) == \ assert jstats(432.1,
'{"message": "stats", "utc": 432.1, "cells": {' + \ {"C1.ue_w": 11, "C2.ue_ww": 22, "C3.ue_x": 33, "C1.ue_xx": 111, "rst": 44},
'"1": {"counters": {"messages": {"rrc_x": 11, "abc": 111}}}, ' + \ {"C1.rrc_y": 55, "C2.rrc_yy": 66, "C3.xyz": 77, "C1.abc": 222, "xyz": 88}) == \
'"2": {"counters": {"messages": {"rrc_y": 22}}}, ' + \ '{"message": "stats", "utc": 432.1, "rst": 44, "cells": {' + \
'"3": {"counters": {"messages": {"xyz": 33}}}}, ' + \ '"1": {"ue_w": 11, "ue_xx": 111, "counters": {"messages": {"rrc_y": 55, "abc": 222}}}, ' + \
'"counters": {"messages": {"xyz": 44}}}' '"2": {"ue_ww": 22, "counters": {"messages": {"rrc_yy": 66}}}, ' + \
'"3": {"ue_x": 33, "counters": {"messages": {"xyz": 77}}}}, ' + \
'"counters": {"messages": {"xyz": 88}}}'
# jdrb_stats, similarly to jstats, returns json-encoded x.drb_stats message # jdrb_stats, similarly to jstats, returns json-encoded x.drb_stats message
......
...@@ -179,6 +179,9 @@ class Measurement(np.void): ...@@ -179,6 +179,9 @@ class Measurement(np.void):
('DRB.PdcpSduBitrateUl.QCI', np.float64),# bit/s 4.4.1.1 NOTE not kbit/s ('DRB.PdcpSduBitrateUl.QCI', np.float64),# bit/s 4.4.1.1 NOTE not kbit/s
('DRB.PdcpSduBitrateDl.QCI', np.float64),# bit/s 4.4.1.2 NOTE not kbit/s ('DRB.PdcpSduBitrateDl.QCI', np.float64),# bit/s 4.4.1.2 NOTE not kbit/s
('DRB.UEActive', np.int32), # 1 4.4.2.4 36.314:4.1.3.3
# XXX mean is not good for our model # XXX mean is not good for our model
# TODO mean -> total + npkt? # TODO mean -> total + npkt?
#('DRB.IPLatDl.QCI', Ttime), # s 4.4.5.1 32.450:6.3.2 NOTE not ms #('DRB.IPLatDl.QCI', Ttime), # s 4.4.5.1 32.450:6.3.2 NOTE not ms
......
...@@ -53,6 +53,8 @@ def test_Measurement(): ...@@ -53,6 +53,8 @@ def test_Measurement():
assert m['S1SIG.ConnEstabAtt'] == 123 assert m['S1SIG.ConnEstabAtt'] == 123
m['RRC.ConnEstabAtt.sum'] = 17 m['RRC.ConnEstabAtt.sum'] = 17
assert m['RRC.ConnEstabAtt.sum'] == 17 assert m['RRC.ConnEstabAtt.sum'] == 17
m['DRB.UEActive'] = 6
assert m['DRB.UEActive'] == 6
m['DRB.IPVolDl.QCI'][:] = 0 m['DRB.IPVolDl.QCI'][:] = 0
m['DRB.IPVolDl.5'] = 55 m['DRB.IPVolDl.5'] = 55
m['DRB.IPVolDl.7'] = NA(m['DRB.IPVolDl.7'].dtype) m['DRB.IPVolDl.7'] = NA(m['DRB.IPVolDl.7'].dtype)
...@@ -67,13 +69,14 @@ def test_Measurement(): ...@@ -67,13 +69,14 @@ def test_Measurement():
assert m['DRB.IPVolDl.QCI'][k] == 0 assert m['DRB.IPVolDl.QCI'][k] == 0
# str/repr # str/repr
assert repr(m) == "Measurement(RRC.ConnEstabAtt.sum=17, DRB.IPVolDl.QCI={5:55 7:ø 9:99}, S1SIG.ConnEstabAtt=123)" assert repr(m) == "Measurement(RRC.ConnEstabAtt.sum=17, DRB.UEActive=6, DRB.IPVolDl.QCI={5:55 7:ø 9:99}, S1SIG.ConnEstabAtt=123)"
s = str(m) s = str(m)
assert s[0] == '(' assert s[0] == '('
assert s[-1] == ')' assert s[-1] == ')'
v = s[1:-1].split(', ') v = s[1:-1].split(', ')
vok = ['ø'] * len(m._dtype0.names) vok = ['ø'] * len(m._dtype0.names)
vok[m.dtype.names.index("RRC.ConnEstabAtt.sum")] = "17" vok[m.dtype.names.index("RRC.ConnEstabAtt.sum")] = "17"
vok[m.dtype.names.index("DRB.UEActive")] = "6"
vok[m.dtype.names.index("S1SIG.ConnEstabAtt")] = "123" vok[m.dtype.names.index("S1SIG.ConnEstabAtt")] = "123"
vok[m.dtype.names.index("DRB.IPVolDl.QCI")] = "{5:55 7:ø 9:99}" vok[m.dtype.names.index("DRB.IPVolDl.QCI")] = "{5:55 7:ø 9:99}"
assert v == vok assert v == vok
......
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