1 # Copyright (c) 2023 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Module defining MeasurementResult class."""
16 from dataclasses import dataclass
20 class MeasurementResult:
21 """Structure defining the result of a single trial measurement.
23 There are few primary (required) quantities. Various secondary (derived)
24 quantities are calculated and can be queried.
26 The constructor allows broader argument types,
27 the post init function converts to the stricter types.
29 Integer quantities (counts) are preferred, as float values
30 can suffer from rounding errors, and sometimes they are measured
31 at unknown (possibly very limited) precision and accuracy.
33 There are relations between the counts (e.g. offered count
34 should be equal to a sum of forwarding count and loss count).
35 This implementation does not perform consistency checks, but uses them
36 for computing quantities the caller left unspecified.
38 In some cases, the units of intended load are different from units
39 of loss count (e.g. load in transactions but loss in packets).
40 Quantities with relative_ prefix can be used to get load candidates
41 from forwarding results.
43 Sometimes, the measurement provider is unable to reach the intended load,
44 and it can react by spending longer than intended duration
45 to reach its intended count. To signal irregular situations like this,
46 several optional fields can be given, and various secondary quantities
47 are populated, so the measurement consumer can query the quantity
48 it wants to rely on in these irregular situations.
50 The current implementation intentionally limits the secondary quantities
51 to the few that proved useful in practice.
54 # Required primary quantities.
55 intended_duration: float
56 """Intended trial measurement duration [s]."""
58 """Intended load [tps]. If bidirectional (or multi-port) traffic is used,
59 most users will put unidirectional (single-port) value here,
60 as bandwidth and pps limits are usually per-port."""
61 # Two of the next three primary quantities are required.
62 offered_count: int = None
63 """Number of packets actually transmitted (transactions attempted).
64 This should be the aggregate (bidirectional, multi-port) value,
65 so that asymmetric trafic profiles are supported."""
66 loss_count: int = None
67 """Number of packets transmitted but not received (transactions failed)."""
68 forwarding_count: int = None
69 """Number of packets successfully forwarded (transactions succeeded)."""
70 # Optional primary quantities.
71 offered_duration: float = None
72 """Estimate of the time [s] the trial was actually transmitting traffic."""
73 duration_with_overheads: float = None
74 """Estimate of the time [s] it took to get the trial result
75 since the measurement started."""
76 intended_count: int = None
77 """Expected number of packets to transmit. If not known,
78 the value of offered_count is used."""
80 def __post_init__(self) -> None:
81 """Convert types, compute missing values.
84 A failing assumption looks like a conversion error.
85 Negative counts are allowed, which can lead to errors later.
87 self.intended_duration = float(self.intended_duration)
88 if self.offered_duration is None:
89 self.offered_duration = self.intended_duration
91 self.offered_duration = float(self.offered_duration)
92 if self.duration_with_overheads is None:
93 self.duration_with_overheads = self.offered_duration
95 self.duration_with_overheads = float(self.duration_with_overheads)
96 self.intended_load = float(self.intended_load)
97 if self.forwarding_count is None:
98 self.forwarding_count = int(self.offered_count) - int(
102 self.forwarding_count = int(self.forwarding_count)
103 if self.offered_count is None:
104 self.offered_count = self.forwarding_count + int(self.loss_count)
106 self.offered_count = int(self.offered_count)
107 if self.loss_count is None:
108 self.loss_count = self.offered_count - self.forwarding_count
110 self.loss_count = int(self.loss_count)
111 if self.intended_count is None:
112 self.intended_count = self.offered_count
114 self.intended_count = int(self.intended_count)
115 # TODO: Handle (somehow) situations where offered > intended?
118 def unsent_count(self) -> int:
119 """How many packets were not transmitted (transactions not started).
121 :return: Intended count minus offered count.
124 return self.intended_count - self.offered_count
127 def loss_ratio(self) -> float:
128 """Bad count divided by overall count, zero if the latter is zero.
130 The bad count includes not only loss count, but also unsent count.
131 If unsent count is negative, its absolute value is used.
132 The overall count is intended count or offered count,
135 Together, the resulting formula tends to increase loss ratio
136 (but not above 100%) in irregular situations,
137 thus guiding search algorithms towards lower loads
138 where there should be less irregularities.
139 The zero default is there to prevent search algorithms from
140 getting stuck on a too low intended load.
142 :returns: Bad count divided by overall count.
145 overall = max(self.offered_count, self.intended_count)
146 bad = abs(self.loss_count) + abs(self.unsent_count)
147 return bad / overall if overall else 0.0
150 def relative_forwarding_rate(self) -> float:
151 """Forwarding rate in load units as if duration and load was intended.
153 The result is based purely on intended load and loss ratio.
154 While the resulting value may be far from what really happened,
155 it has nice behavior with respect to common assumptions
156 of search algorithms.
158 :returns: Forwarding rate in load units estimated from loss ratio.
161 return self.intended_load * (1.0 - self.loss_ratio)