Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/framework/stats/__init__.py
1956 views
1
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
4
"""
5
Single threaded producer/consumer for statistics gathering.
6
7
The purpose of this module is to provide primitives for statistics exercises
8
which need a common framework that sets expectations in terms of tests
9
design and results.
10
11
The main components of the module consist of: `Core`, `Producer` and
12
`Consumer`, `ComparisonCriteria`, metadata providers and baselines providers.
13
14
The `Core` is the component which drives the interaction between `Producer`
15
and `Consumer`. The `Producer` goal is to pass raw data to the `Consumer`,
16
while the `Consumer` is responsible for raw data processing and transformation.
17
Metadata and baselines providers are independently used by the `Consumer` to
18
get measurements and statistics definitions relevant in the processing and
19
transformation step. In the end, the processing and transformation step
20
makes use of comparison criteria, present in statistics definitions,
21
which will assert expectations in terms of exercise end result.
22
23
# Test design
24
25
Lets create a test using the above components. The test will answer to two
26
questions:
27
1. What is the sum of 10 randomly generated integers, between 0 and 100,
28
fetched with `randint` module?
29
2. What is the 10th randomly generated integer, between 0 and 100, fetched with
30
`randint` module?
31
32
We can define two exercises from the above questions, so lets call them
33
`10RandomIntsSumExercise` and `10thRandomIntExercise`. The test logic starts
34
with defining raw data producers for both exercises. The producer definition
35
depends on the chosen implementation. We're going to use the `LambdaProducer`.
36
This producer needs a function which produces the raw data.
37
38
```
39
from random import randint
40
from framework.stats.producer import LambdaProducer
41
42
st_prod_func = lambda llimit, ulimit: randint(llimit, ulimit)
43
st_prod = LambdaProducer(
44
func=st_prod_func,
45
func_kwargs={"llimit": 0, "ulimit": 99}
46
)
47
```
48
49
Next up, we need to define consumers for the `st_prod`. For the
50
`10RandomIntsSumExercise`, the consumer must process 10 random integers and
51
sum them up, while for the `10thRandomIntExercise`, the consumer must process
52
the 10th random generated integer and return it. `Consumer`s definitions
53
depend largely on the chosen consumer implementations. We're going to use the
54
`LambdaConsumer`. To define a `LambdaConsumer` we need the following resources:
55
56
1. Measurements definitions: provided through metadata and baselines providers
57
or through the `Consumer`s `set_measurement_def` interface. They can be
58
hardcoded in the test logic or programmatically generated. We're going to
59
use here the programmatic alternative, where measurements definitions
60
will be found in a global config dictionary, processed through programmatic
61
means.
62
2. A function that processes and transforms the data coming from the
63
`st_prod`.
64
65
Let's lay down our measurements definitions first inside the test global
66
configuration dictionary. The dictionary consists from measurements
67
definitions and from baselines, which are going to be used for setting up
68
pass criteria for measurements statistics.
69
```
70
CONFIG = {
71
"measurements": {
72
# This is a map where keys represent the exercise id while the
73
# values represent a map from measurements name to measurements
74
# definition. The values follow the expected `DictProvider` schema.
75
"10RandomIntsSumExercise": {
76
"ints": { # Measurement name.
77
"unit": "none", # We do not have a specific measurement unit.
78
"statistics": [
79
{
80
# By default, the statistic definition name is the
81
# function name.
82
"function": "Sum",
83
"criteria": "LowerThan"
84
}
85
]
86
}
87
},
88
"10thRandomIntExercise": {
89
"int": {
90
"unit": "none", # We do not have a specific measurement unit.
91
"statistics": [
92
{
93
# The function below simply acts like a no-op on top of
94
# the result provided by the `Producer`. It is mainly
95
# useful when consuming statistics results (which do
96
# not need further processing).
97
"function": "ValuePlaceholder",
98
"criteria": "GreaterThan",
99
}
100
]
101
}
102
}
103
},
104
"baselines": {
105
"10RandomIntsSumExercise": {
106
"ints": {
107
# Info about the environment that generated the data.
108
"randint": {
109
"Sum": {
110
"target": 600,
111
}
112
}
113
}
114
},
115
"10thRandomIntExercise": {
116
"int": {
117
"randint": {
118
"value": {
119
"target": 50,
120
}
121
}
122
}
123
}
124
}
125
}
126
```
127
128
We'll continue by implementing the metadata and baseline providers. The
129
measurements definitions from the global configuration dictionary
130
will be processed by the `DictProvider` metadata provider. The measurements
131
definitions schema can be found in the `DictProvider` documentation.
132
```
133
from framework.stats.metadata import DictProvider as DictMetadataProvider
134
from framework.stats.baseline import Provider as BaselineProvider
135
from framework.utils import DictQuery
136
137
# The baseline provider is a requirement for the `DictProvider`.
138
class RandintBaselineProvider(BaselineProvider):
139
def __init__(self, exercise_id, env_id):
140
super().__init__(DictQuery(dict()))
141
if "baselines" in CONFIG:
142
super().__init__(DictQuery(CONFIG["baselines"][exercise_id]))
143
self._tag = "{}/" + env_id + "/{}"
144
def get(self, ms_name: str, st_name: str) -> dict:
145
key = self._tag.format(ms_name, st_name)
146
baseline = self._baselines.get(key)
147
if baseline:
148
target = baseline.get("target")
149
return {
150
"target": target,
151
}
152
return None
153
154
baseline_provider_sum = RandintBaselineProvider(
155
"10RandomIntsSumExercise",
156
"randint")
157
baseline_provider_10th = RandintBaselineProvider(
158
"10thRandomIntExercise",
159
"randint")
160
161
metadata_provider_sum = DictMetadataProvider(
162
CONFIG["measurements"]["10RandomIntsSumExercise"],
163
baseline_provider_sum)
164
metadata_provider_10th = DictMetadataProvider(
165
CONFIG["measurements"]["10thRandomIntExercise"],
166
baseline_provider_10th)
167
```
168
169
The alternative here would be to manually define our measurements and pass
170
them to the `LambdaConsumer` at a later step. Depending on the magnitude of
171
the exercise, this alternative might be preffered over the other. Here's how
172
it can be done:
173
```
174
from framework.utils import DictQuery
175
from framework.stats.function import FunctionFactory
176
from framework.stats.criteria import CriteriaFactory
177
from framework.stats.types import MeasurementDef, StatisticDef
178
179
def baseline(ms_name: str, st_name: str, exercise_id: str):
180
baselines = DictQuery(CONFIG["baselines"][exercise_id])
181
target = baselines.get(f"{ms_name}/randint/{st_name}/target")
182
return {
183
"target": target
184
}
185
186
def measurements(exercise_id: str):
187
ms_list = list()
188
for ms_name in CONFIG["measurements"][exercise_id]:
189
st_list = list()
190
unit = CONFIG["measurements"][exercise_id][ms_name]["unit"]
191
st_defs = CONFIG["measurements"][exercise_id][ms_name]["statistics"]
192
for st_def in st_defs:
193
func_cls_id = st_def["function"]
194
func_cls = FunctionFactory.get(func_cls_id)
195
criteria_cls_id = st_def["criteria"]
196
criteria_cls = CriteriaFactory.get(criteria_cls_id)
197
bs = baseline(ms_name, func_cls.__name__, exercise_id)
198
st_list.append(StatisticDef(func_cls(), criteria_cls(bs)))
199
ms_list.append(MeasurementDef(ms_name, unit, st_list))
200
return ms_list
201
```
202
203
Next, having our measurements definitions layed out, we can start defining
204
`LambdaConsumer`s functions. The functions are strictly related to
205
`LambdaProducer` function, so in our case we need to process an integer
206
coming from the producer.
207
```
208
# The following function is consuming data points, pertaining to measurements
209
# defined above.
210
st_cons_sum_func = lambda cons, res: cons.consume_data("ints", res)
211
212
# Here we consume a statistic value directly. Statistics can be both consumed
213
# or computed based on their measurement data points, consumed via the
214
# `Consumer`s `consume_data` interface.
215
st_cons_10th_func = lambda cons, res: cons.consume_stat("value", "int", res)
216
```
217
218
We can define now our `LambdaConsumer`s for both exercices:
219
220
1. Through the metadata and baseline providers.
221
```
222
from framework.stats.consumer import LambdaConsumer
223
224
st_cons_sum = LambdaConsumer(
225
st_cons_sum_func,
226
metadata_provider=metadata_provider_sum)
227
st_cons_10th = LambdaConsumer(
228
st_cons_10th_func,
229
metadata_provider=metadata_provider_10th)
230
```
231
232
2. By setting the measurements definitions separately:
233
```
234
from framework.stats.consumer import LambdaConsumer
235
from framework.utils import eager_map
236
237
st_cons_sum = LambdaConsumer(st_cons_sum_func)
238
id_sum = "10RandomIntsSumExercise"
239
id_10th = "10thRandomIntExercise"
240
eager_map(st_cons_sum.set_measurement_def, measurements(id_sum))
241
st_cons_10th = LambdaConsumer(st_cons_10th_func)
242
eager_map(st_cons_10th.set_measurement_def, measurements(id_10th))
243
```
244
245
Once we have defined our producers and consumers, we will continue by
246
defining the statistics `Core`.
247
```
248
from framework.stats.core import Core
249
250
# Both exercises require the core to drive both producers and consumers for
251
# 10 iterations to achieve the wanted result.
252
st_core = Core(name="randint_observation", iterations=10)
253
st_core.add_pipe(st_prod, st_cons_sum, tag="10RandomIntsSumExercise")
254
st_core.add_pipe(st_prod, st_cons_10th, tag="10thRandomIntExercise")
255
```
256
257
Let's start the exercise without verifying the criteria:
258
```
259
# Start the exercise without checking the criteria.
260
st_core.run_exercise(check_criteria=False)
261
```
262
263
Output:
264
```
265
{
266
'name': 'randint_observation',
267
'iterations': 10,
268
'results': {
269
'10RandomIntsSumExercise': {
270
'ints': {
271
'_unit': 'none',
272
'Sum': 454
273
}
274
},
275
'10thRandomIntExercise': {
276
'10thRandomIntExercise': {
277
'_unit': 'none',
278
'value': 12
279
}
280
}
281
},
282
'custom': {}
283
}
284
```
285
286
Now, verifying the criteria:
287
```
288
# Start the exercise without checking the criteria.
289
st_core.run_exercise()
290
```
291
292
Output for failure on `10RandomIntsSumExercise`:
293
```
294
Traceback (most recent call last):
295
File "<stdin>", line 1, in <module>
296
File "/Users/iul/iul_fc/tests/framework/statistics/core.py", line 63,
297
in run_exercise
298
assert False, f"Failed on '{tag}': {err.msg}"
299
AssertionError: Failed on '10RandomIntsSumExercise': 'ints/Sum':
300
LowerThan failed. Target: '600 vs Actual: '892'.
301
```
302
303
Output for failure on `10thRandomIntExercise`:
304
```
305
Traceback (most recent call last):
306
File "<stdin>", line 1, in <module>
307
File "/Users/iul/iul_fc/tests/framework/statistics/core.py", line 63,
308
in run_exercise
309
assert False, f"Failed on '{tag}': {err.msg}"
310
AssertionError: Failed on '10thRandomIntExercise': 'int/value': GreaterThan
311
failed. Target: '50 vs Actual: '42'.
312
```
313
314
# Custom producer information
315
316
Important mentions which were not caught in the test design above is the
317
`consume_custom` interface offered by the `Consumer`. Sometimes we
318
need to store per iteration custom information, which might be relevant for
319
analyzing the `Producer` raw data (e.g while debugging). In the above case we
320
might want to produce as well information specific to the PRNG state. Let's
321
modify the producer to do this as well:
322
```
323
import random
324
from framework.stats.producer import LambdaProducer
325
326
def st_prod_func(llimit, ulimit):
327
return {
328
"randint": random.randint(llimit, ulimit),
329
"state": random.getstate()
330
}
331
332
st_prod = LambdaProducer(
333
func=st_prod_func,
334
func_kwargs={"llimit": 0, "ulimit": 99}
335
)
336
```
337
338
Next, let's redefine the consumer to consume the state as custom data. We
339
start again with the `LambdaConsumer` function:
340
```
341
def st_cons_sum_func(cons, res):
342
cons.consume_data("ints", res["randint"])
343
cons.consume_custom("PNGR_state", hash(res["state"]))
344
345
def st_cons_10th_func(cons, res):
346
cons.consume_stat("value", "int", res["randint"])
347
cons.consume_custom("PNGR_state", hash(res["state"]))
348
```
349
350
Next, let's define our consumers, based on metadata providers:
351
```
352
from framework.stats.consumer import LambdaConsumer
353
354
st_cons_sum = LambdaConsumer(
355
st_cons_sum_func,
356
metadata_provider=metadata_provider_sum)
357
st_cons_10th = LambdaConsumer(
358
st_cons_10th_func,
359
metadata_provider=metadata_provider_10th)
360
```
361
362
In the end, we redefine the statistics core:
363
```
364
from framework.stats.core import Core
365
366
# Both exercises require the core to drive both producers and consumers for
367
# 10 iterations to achieve the wanted result.
368
st_core = Core(name="randint_observation", iterations=10)
369
st_core.add_pipe(st_prod, st_cons_sum, tag="10RandomIntsSumExercise")
370
st_core.add_pipe(st_prod, st_cons_10th, tag="10thRandomIntExercise")
371
```
372
373
And run again the exercise:
374
```
375
# Start the exercise without checking the criteria.
376
st_core.run_exercise(check_criteria=False)
377
```
378
379
Output:
380
```
381
{'name': 'randint_observation', 'iterations': 10, 'results': {
382
'10RandomIntsSumExercise': {'ints': {'_unit': 'none', 'Sum': 502}},
383
'10thRandomIntExercise': {'int': {'_unit': 'none', 'value': 93}}},
384
'custom': {
385
'10RandomIntsSumExercise': {0: {'PNGR_state': [-7761051367110439654]},
386
1: {'PNGR_state': [4797715617643311001]},
387
2: {'PNGR_state': [-3343211298676199688]},
388
3: {'PNGR_state': [-1351346424793161009]},
389
4: {'PNGR_state': [-1505689957772366290]},
390
5: {'PNGR_state': [3810535014128659389]},
391
6: {'PNGR_state': [8691056006996621084]},
392
7: {'PNGR_state': [-8394051250601789870]},
393
8: {'PNGR_state': [-3480127558785488400]},
394
9: {'PNGR_state': [-1363822145985393657]}},
395
'10thRandomIntExercise': {0: {'PNGR_state': [1074948021089717094]},
396
1: {'PNGR_state': [-3949202314244540587]},
397
2: {'PNGR_state': [9001501428032987604]},
398
3: {'PNGR_state': [480646194341861131]},
399
4: {'PNGR_state': [8214022971886477930]},
400
5: {'PNGR_state': [-5298632435091237207]},
401
6: {'PNGR_state': [-3177751479450511864]},
402
7: {'PNGR_state': [8940293789185365310]},
403
8: {'PNGR_state': [1072449063189689805]},
404
9: {'PNGR_state': [-6391784864046788756]}}}}
405
```
406
407
"""
408
409
from . import core
410
from . import consumer
411
from . import producer
412
from . import types
413
from . import criteria
414
from . import function
415
from . import baseline
416
from . import metadata
417
418