Skip to content

Commit

Permalink
NET0260-a evaluate latency of calls to SP's /flights endpoint for dis…
Browse files Browse the repository at this point in the history
…playing flights in a given area
  • Loading branch information
Shastick committed Aug 16, 2023
1 parent 9cf1821 commit d729a19
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 26 deletions.
18 changes: 18 additions & 0 deletions monitoring/monitorlib/rid.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,24 @@ def dp_data_resp_percentile99_s(self) -> float:
else:
raise ValueError("Unsupported RID version '{}'".format(self))

@property
def sp_data_resp_percentile95_s(self) -> float:
if self == RIDVersion.f3411_19:
return v19.constants.NetSpDataResponseTime95thPercentileSeconds
elif self == RIDVersion.f3411_22a:
return v22a.constants.NetSpDataResponseTime95thPercentileSeconds
else:
raise ValueError("Unsupported RID version '{}'".format(self))

@property
def sp_data_resp_percentile99_s(self) -> float:
if self == RIDVersion.f3411_19:
return v19.constants.NetSpDataResponseTime99thPercentileSeconds
elif self == RIDVersion.f3411_22a:
return v22a.constants.NetSpDataResponseTime99thPercentileSeconds
else:
raise ValueError("Unsupported RID version '{}'".format(self))

def flights_url_of(self, base_url: str) -> str:
if self == RIDVersion.f3411_19:
return base_url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ net_rid:
service_providers:
- participant_id: uss1
injection_base_url: http://host.docker.internal:8071/ridsp/injection
uss_base_urls: [ http://host.docker.internal:8071 ]
netrid_service_providers_v22a:
$content_schema: monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json
resource_type: resources.netrid.NetRIDServiceProviders
Expand All @@ -26,6 +27,7 @@ net_rid:
service_providers:
- participant_id: uss1
injection_base_url: http://host.docker.internal:8081/ridsp/injection
uss_base_urls: [ http://host.docker.internal:8081 ]
netrid_observers_v19:
$content_schema: monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json
resource_type: resources.netrid.NetRIDObserversResource
Expand All @@ -35,6 +37,7 @@ net_rid:
observers:
- participant_id: uss2
observation_base_url: http://host.docker.internal:8073/riddp/observation
uss_base_urls: [ http://host.docker.internal:8073 ]
netrid_observers_v22a:
$content_schema: monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json
resource_type: resources.netrid.NetRIDObserversResource
Expand All @@ -44,6 +47,7 @@ net_rid:
observers:
- participant_id: uss2
observation_base_url: http://host.docker.internal:8083/riddp/observation
uss_base_urls: [ http://host.docker.internal:8083 ]
netrid_dss_instances_v19:
$content_schema: monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json
resource_type: resources.astm.f3411.DSSInstancesResource
Expand Down
77 changes: 77 additions & 0 deletions monitoring/uss_qualifier/report_analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import sys
import json

from implicitdict import ImplicitDict

from monitoring.uss_qualifier.reports.report import (
TestRunReport,
TestSuiteReport,
TestScenarioReport,
)


def parse_report(path: str) -> TestRunReport:
with open(path, "r") as f:
report = json.load(f)
return ImplicitDict.parse(report, TestRunReport)


def look_at_test_suite(ts: TestSuiteReport):
print("test-suite: " + ts.name)


def look_at_scenario(ts: TestScenarioReport):
print("Looking at test scenario: ", ts.name)
print("Has #cases: ", len(ts.cases))
for tcr in ts.cases:
print(" Test case report: ", tcr.name)
print(" has #steps: ", len(tcr.steps))
for step in tcr.steps:
print(" step: ", step.name)
print(" has #queries: ", len(step.queries)) if step.get(
"queries"
) is not None else print(" has #queries: 0")
for q in step.queries if step.get("queries") is not None else []:
print(f" {q.response.elapsed_s} - {q.request.url}")


def main():
"""
Print some infos about a report's content.
Usage: python report_analyzer.py <path-to-report.json> <test_scenario_name>
Eg: python report_analyzer.py output/report_netrid_v22a.json "ASTM NetRID nominal behavior"
"""
if len(sys.argv) < 2:
print(
"Usage: python report_analyzer.py <path-to-report.json> <test_scenario_name>"
)
return 1

r = parse_report(sys.argv[1])

for a in r.report.test_suite.actions:
print("Types of actions (test_suite, test_scenario, action_generator): ")
print(a._get_applicable_report())

suite_reports = {
r.test_suite.name: r.test_suite
for r in r.report.test_suite.actions
if r.get("test_suite") is not None
}
scenario_reports = {
r.test_scenario.name: r.test_scenario
for r in r.report.test_suite.actions
if r.get("test_scenario") is not None
}

print("Available suite reports: ", suite_reports.keys())
print("Available scenario reports: ", scenario_reports.keys())

if len(sys.argv) > 2:
look_at_scenario(scenario_reports[sys.argv[2]])
return 0


if __name__ == "__main__":
sys.exit(main())
33 changes: 20 additions & 13 deletions monitoring/uss_qualifier/resources/netrid/observers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,39 @@
from monitoring.uss_qualifier.resources.communications import AuthAdapterResource


class ObserverConfiguration(ImplicitDict):
participant_id: str
"""Participant ID of the observer providing a view of RID data in the system"""

observation_base_url: str
"""Base URL for the observer's implementation of the interfaces/automated-testing/rid/observation.yaml API"""

uss_base_urls: List[str]
"""Base URLS where to reach this particular USS. We allow multiple ones as nothing prevents a
USS to advertise multiple different endpoints where to be contacted."""


class NetRIDObserversSpecification(ImplicitDict):
observers: List[ObserverConfiguration]


class RIDSystemObserver(object):
participant_id: str
base_url: str
session: infrastructure.UTMClientSession
original_configuration: NetRIDObserversSpecification

def __init__(
self,
participant_id: str,
base_url: str,
auth_adapter: infrastructure.AuthAdapter,
original_configuration: NetRIDObserversSpecification,
):
self.session = UTMClientSession(base_url, auth_adapter)
self.participant_id = participant_id
self.base_url = base_url
self.original_configuration = original_configuration

# TODO: Change observation API to use an InterUSS scope rather than re-using an ASTM scope
self.rid_version = RIDVersion.f3411_19
Expand Down Expand Up @@ -74,18 +93,6 @@ def observe_flight_details(
return result, query


class ObserverConfiguration(ImplicitDict):
participant_id: str
"""Participant ID of the observer providing a view of RID data in the system"""

observation_base_url: str
"""Base URL for the observer's implementation of the interfaces/automated-testing/rid/observation.yaml API"""


class NetRIDObserversSpecification(ImplicitDict):
observers: List[ObserverConfiguration]


class NetRIDObserversResource(Resource[NetRIDObserversSpecification]):
observers: List[RIDSystemObserver]

Expand All @@ -96,7 +103,7 @@ def __init__(
):
self.observers = [
RIDSystemObserver(
o.participant_id, o.observation_base_url, auth_adapter.adapter
o.participant_id, o.observation_base_url, auth_adapter.adapter, o
)
for o in specification.observers
]
17 changes: 16 additions & 1 deletion monitoring/uss_qualifier/resources/netrid/service_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class ServiceProviderConfiguration(ImplicitDict):
injection_base_url: str
"""Base URL for the Service Provider's implementation of the interfaces/automated-testing/rid/injection.yaml API"""

uss_base_urls: List[str]
"""Base URLS where to reach this particular USS. We allow multiple ones as nothing prevents a
USS to advertise multiple different endpoints where to be contacted."""

def __init__(self, *args, **kwargs):
super().__init__(**kwargs)
try:
Expand All @@ -29,6 +33,14 @@ def __init__(self, *args, **kwargs):
"ServiceProviderConfiguration.injection_base_url must be a URL"
)

for uss_base_url in self.uss_base_urls:
try:
urlparse(uss_base_url)
except ValueError:
raise ValueError(
f"ServiceProviderConfiguration.uss_base_urls must contain valid URLs. Was: {uss_base_url}",
)


class NetRIDServiceProvidersSpecification(ImplicitDict):
service_providers: List[ServiceProviderConfiguration]
Expand All @@ -38,16 +50,19 @@ class NetRIDServiceProvider(object):
participant_id: str
base_url: str
client: infrastructure.UTMClientSession
original_configuration: ServiceProviderConfiguration

def __init__(
self,
participant_id: str,
base_url: str,
auth_adapter: infrastructure.AuthAdapter,
original_configuration: ServiceProviderConfiguration,
):
self.participant_id = participant_id
self.base_url = base_url
self.client = infrastructure.UTMClientSession(base_url, auth_adapter)
self.original_configuration = original_configuration

def submit_test(self, request: CreateTestParameters, test_id: str) -> fetch.Query:
return fetch.query_and_describe(
Expand Down Expand Up @@ -77,7 +92,7 @@ def __init__(
):
self.service_providers = [
NetRIDServiceProvider(
s.participant_id, s.injection_base_url, auth_adapter.adapter
s.participant_id, s.injection_base_url, auth_adapter.adapter, s
)
for s in specification.service_providers
]
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re
from typing import List, Dict

import urllib.parse

from monitoring.monitorlib import fetch
from monitoring.monitorlib.fetch import evaluation
from monitoring.monitorlib.rid import RIDVersion
Expand Down Expand Up @@ -41,13 +43,24 @@ def __init__(
self._service_providers = service_providers.service_providers
self._observers = observers.observers

# identify SPs and observers by their base URL
self._participants_by_base_url.update(
{sp.base_url: sp.participant_id for sp in self._service_providers}
)
self._participants_by_base_url.update(
{dp.base_url: dp.participant_id for dp in self._observers}
)
# identify SPs and observers by their hostnames + ports
for sp in self._service_providers:
for base_url in sp.original_configuration.uss_base_urls:
prev_mapping = self._participants_by_base_url.get(base_url)
if prev_mapping is not None and prev_mapping != sp.participant_id:
raise ValueError(
f"Invalid configuration detected: same uss base url is defined for mutiple participants. Url: {base_url}, participants: {prev_mapping}, {sp.participant_id}"
)
self._participants_by_base_url[base_url] = sp.participant_id

for obs in self._observers:
for base_url in obs.original_configuration.uss_base_urls:
prev_mapping = self._participants_by_base_url.get(base_url)
if prev_mapping is not None and prev_mapping != obs.participant_id:
raise ValueError(
f"Invalid configuration detected: same uss base url is defined for mutiple participants. Url: {base_url}, participants: {prev_mapping}, {obs.participant_id}"
)
self._participants_by_base_url[base_url] = obs.participant_id

# collect and classify queries by participant
self._queries_by_participant = {
Expand All @@ -61,19 +74,72 @@ def __init__(
break

def run(self):

self.begin_test_scenario()
self.record_note("participants", str(self._participants_by_base_url))
self.record_note("nb_queries", str(len(self._queries)))

# DP performance
self.begin_test_case("Performance of Display Providers requests")

self.begin_test_step("Performance of /display_data requests")

self._dp_display_data_times_step()

self.end_test_step()
self.end_test_case()

# SP performance
self.begin_test_case("Performance of Service Providers requests")
self.begin_test_step("Performance of /flights?view requests")

self._sp_flights_area_times_step()

self.end_test_step()
self.end_test_case()

self.end_test_scenario()

def _sp_flights_area_times_step(self):
pattern = re.compile(r"/flights\?view=")
for participant, all_queries in self._queries_by_participant.items():
# identify successful flights queries
relevant_queries: List[fetch.Query] = list()
for query in all_queries:
match = pattern.search(query.request.url)
if match is not None and query.status_code == 200:
relevant_queries.append(query)

if len(relevant_queries) == 0:
# this may be a display provider
self.record_note(
f"{participant}/flights", "skipped check: no relevant queries"
)

continue

# Collect query durations
durations = [query.response.elapsed_s for query in relevant_queries]
(p95, p99) = evaluation.compute_percentiles(durations, [95, 99])

with self.check(
"Performance for replies to requested flights in an area", [participant]
) as check:
if p95 > self._rid_version.sp_data_resp_percentile95_s:
check.record_failed(
f"95th percentile of /flights?view requests is {p95} s, "
f"expected less than {self._rid_version.sp_data_resp_percentile95_s} s"
)
if p99 > self._rid_version.sp_data_resp_percentile99_s:
check.record_failed(
f"99th percentile of /flights?view requests is {p99} s, "
f"expected less than {self._rid_version.sp_data_resp_percentile99_s} s"
)

self.record_note(
f"{participant}/flights",
f"percentiles on {len(relevant_queries)} relevant queries: 95th: {p95}; 99th: {p99}",
)

def _dp_display_data_times_step(self):
"""
:return: the query durations of respectively the initial queries and the subsequent ones
Expand Down Expand Up @@ -119,7 +185,7 @@ def _dp_display_data_times_step(self):
summary=f"95th percentile of durations for initial DP display_data queries is higher than threshold",
severity=Severity.Medium,
participants=[participant],
details=f"threshold: {self._rid_version.dp_init_resp_percentile95_s}, 95th percentile: {init_95th}",
details=f"threshold: {self._rid_version.sp_init_resp_percentile95_s}, 95th percentile: {init_95th}",
)
if init_99th > self._rid_version.dp_init_resp_percentile99_s:
check.record_failed(
Expand Down
Loading

0 comments on commit d729a19

Please sign in to comment.