pyglove 0.5.0.dev202508250811__py3-none-any.whl → 0.5.0.dev202511300809__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglove/core/__init__.py +8 -1
- pyglove/core/geno/base.py +7 -3
- pyglove/core/io/file_system.py +295 -2
- pyglove/core/io/file_system_test.py +291 -0
- pyglove/core/logging.py +45 -1
- pyglove/core/logging_test.py +12 -21
- pyglove/core/monitoring.py +657 -0
- pyglove/core/monitoring_test.py +289 -0
- pyglove/core/symbolic/__init__.py +7 -0
- pyglove/core/symbolic/base.py +89 -35
- pyglove/core/symbolic/base_test.py +3 -3
- pyglove/core/symbolic/dict.py +31 -12
- pyglove/core/symbolic/dict_test.py +49 -0
- pyglove/core/symbolic/list.py +17 -3
- pyglove/core/symbolic/list_test.py +24 -2
- pyglove/core/symbolic/object.py +3 -1
- pyglove/core/symbolic/object_test.py +13 -10
- pyglove/core/symbolic/ref.py +19 -7
- pyglove/core/symbolic/ref_test.py +94 -7
- pyglove/core/symbolic/unknown_symbols.py +147 -0
- pyglove/core/symbolic/unknown_symbols_test.py +100 -0
- pyglove/core/typing/annotation_conversion.py +8 -1
- pyglove/core/typing/annotation_conversion_test.py +14 -19
- pyglove/core/typing/class_schema.py +24 -1
- pyglove/core/typing/json_schema.py +221 -8
- pyglove/core/typing/json_schema_test.py +508 -12
- pyglove/core/typing/type_conversion.py +17 -3
- pyglove/core/typing/type_conversion_test.py +7 -2
- pyglove/core/typing/value_specs.py +5 -1
- pyglove/core/typing/value_specs_test.py +5 -0
- pyglove/core/utils/__init__.py +2 -0
- pyglove/core/utils/contextual.py +9 -4
- pyglove/core/utils/contextual_test.py +10 -0
- pyglove/core/utils/error_utils.py +59 -25
- pyglove/core/utils/json_conversion.py +360 -63
- pyglove/core/utils/json_conversion_test.py +146 -13
- pyglove/core/views/html/controls/tab.py +33 -0
- pyglove/core/views/html/controls/tab_test.py +37 -0
- pyglove/ext/evolution/base_test.py +1 -1
- {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/METADATA +8 -1
- {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/RECORD +44 -40
- {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/WHEEL +0 -0
- {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/licenses/LICENSE +0 -0
- {pyglove-0.5.0.dev202508250811.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# Copyright 2025 The PyGlove Authors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Pluggable metric systems for monitoring.
|
|
15
|
+
|
|
16
|
+
This module allows PyGlove to plugin metrics to monitor the execution of
|
|
17
|
+
programs. There are three common metrics for monitoring: counters, scalars, and
|
|
18
|
+
distributions.
|
|
19
|
+
|
|
20
|
+
* Counters are metrics that track the number of times an event occurs. It's
|
|
21
|
+
monotonically increasing over time.
|
|
22
|
+
|
|
23
|
+
* Scalars are metrics that track a single value at a given time, for example,
|
|
24
|
+
available memory size. It does not accumulate over time like counters.
|
|
25
|
+
|
|
26
|
+
* Distributions are metrics that track the distribution of a numerical value.
|
|
27
|
+
For example, the latency of an operation.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import abc
|
|
31
|
+
import collections
|
|
32
|
+
import contextlib
|
|
33
|
+
import math
|
|
34
|
+
import threading
|
|
35
|
+
import time
|
|
36
|
+
import typing
|
|
37
|
+
from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, Type, TypeVar, Union
|
|
38
|
+
from pyglove.core.utils import error_utils
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
import numpy # pylint: disable=g-import-not-at-top
|
|
42
|
+
except ImportError:
|
|
43
|
+
numpy = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
MetricValueType = TypeVar('MetricValueType')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Metric(Generic[MetricValueType], metaclass=abc.ABCMeta):
|
|
50
|
+
"""Base class for metrics."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
namespace: str,
|
|
55
|
+
name: str,
|
|
56
|
+
description: str,
|
|
57
|
+
parameter_definitions: Dict[str, Type[Union[int, str, bool]]],
|
|
58
|
+
**additional_flags,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Initializes the metric.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
namespace: The namespace of the metric.
|
|
64
|
+
name: The name of the metric.
|
|
65
|
+
description: The description of the metric.
|
|
66
|
+
parameter_definitions: The definitions of the parameters for the metric.
|
|
67
|
+
**additional_flags: Additional flags for the metric.
|
|
68
|
+
"""
|
|
69
|
+
self._namespace = namespace
|
|
70
|
+
self._name = name
|
|
71
|
+
self._description = description
|
|
72
|
+
self._parameter_definitions = parameter_definitions
|
|
73
|
+
self._flags = additional_flags
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def namespace(self) -> str:
|
|
77
|
+
"""Returns the namespace of the metric."""
|
|
78
|
+
return self._namespace
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def name(self) -> str:
|
|
82
|
+
"""Returns the name of the metric."""
|
|
83
|
+
return self._name
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def full_name(self) -> str:
|
|
87
|
+
"""Returns the full name of the metric."""
|
|
88
|
+
return f'{self.namespace}/{self.name}'
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def description(self) -> str:
|
|
92
|
+
"""Returns the description of the metric."""
|
|
93
|
+
return self._description
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def parameter_definitions(self) -> Dict[str, Type[Union[int, str, bool]]]:
|
|
97
|
+
"""Returns the parameter definitions of the metric."""
|
|
98
|
+
return self._parameter_definitions
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def flags(self) -> Dict[str, Any]:
|
|
102
|
+
"""Returns the flags of the metric."""
|
|
103
|
+
return self._flags
|
|
104
|
+
|
|
105
|
+
def _parameters_key(self, **parameters) -> Tuple[Any, ...]:
|
|
106
|
+
"""Returns the parameters tuple for the metric."""
|
|
107
|
+
for k, t in self._parameter_definitions.items():
|
|
108
|
+
v = parameters.get(k)
|
|
109
|
+
if v is None:
|
|
110
|
+
raise KeyError(
|
|
111
|
+
f'Metric {self.full_name!r}: Parameter {k!r} is required but not '
|
|
112
|
+
f'given.'
|
|
113
|
+
)
|
|
114
|
+
if not isinstance(v, t):
|
|
115
|
+
raise TypeError(
|
|
116
|
+
f'Metric {self.full_name!r}: Parameter {k!r} has type '
|
|
117
|
+
f'{type(v)} but expected type {t}.'
|
|
118
|
+
)
|
|
119
|
+
for k in parameters:
|
|
120
|
+
if k not in self._parameter_definitions:
|
|
121
|
+
raise KeyError(
|
|
122
|
+
f'Metric {self.full_name!r}: Parameter {k!r} is not defined but '
|
|
123
|
+
f'provided. Available parameters: '
|
|
124
|
+
f'{list(self._parameter_definitions.keys())}'
|
|
125
|
+
)
|
|
126
|
+
return tuple(parameters[k] for k in self._parameter_definitions)
|
|
127
|
+
|
|
128
|
+
@abc.abstractmethod
|
|
129
|
+
def value(self, **parameters) -> MetricValueType:
|
|
130
|
+
"""Returns the value of the metric for the given parameters.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
**parameters: Parameters for parameterized counters.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
The value of the metric.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Counter(Metric[int]):
|
|
141
|
+
"""Base class for counters.
|
|
142
|
+
|
|
143
|
+
Counters are metrics that track the number of times an event occurs. It's
|
|
144
|
+
monotonically increasing over time.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
@abc.abstractmethod
|
|
148
|
+
def increment(self, delta: int = 1, **parameters) -> int:
|
|
149
|
+
"""Increments the counter by delta and returns the new value.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
delta: The amount to increment the counter by.
|
|
153
|
+
**parameters: Parameters for parameterized counters.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The new value of the counter.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Scalar(Metric[MetricValueType]):
|
|
161
|
+
"""Base class for scalar values.
|
|
162
|
+
|
|
163
|
+
Scalar values are metrics that track a single value at a given time, for
|
|
164
|
+
example, available memory size. It does not accumulate over time like
|
|
165
|
+
counters.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
@abc.abstractmethod
|
|
169
|
+
def set(self, value: MetricValueType, **parameters) -> None:
|
|
170
|
+
"""Sets the value of the scalar.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
value: The value to record.
|
|
174
|
+
**parameters: Parameters for parameterized scalars.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
@abc.abstractmethod
|
|
178
|
+
def increment(
|
|
179
|
+
self,
|
|
180
|
+
delta: MetricValueType = 1, **parameters
|
|
181
|
+
) -> MetricValueType:
|
|
182
|
+
"""Increments the scalar by delta and returns the new value.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
delta: The amount to increment the counter by.
|
|
186
|
+
**parameters: Parameters for parameterized counters.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The new value of the metric.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class DistributionValue(metaclass=abc.ABCMeta):
|
|
194
|
+
"""Base for distribution value."""
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
@abc.abstractmethod
|
|
198
|
+
def count(self) -> int:
|
|
199
|
+
"""Returns the number of samples in the distribution."""
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
@abc.abstractmethod
|
|
203
|
+
def sum(self) -> float:
|
|
204
|
+
"""Returns the sum of the distribution."""
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
@abc.abstractmethod
|
|
208
|
+
def mean(self) -> float:
|
|
209
|
+
"""Returns the mean of the distribution."""
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
@abc.abstractmethod
|
|
213
|
+
def stddev(self) -> float:
|
|
214
|
+
"""Returns the standard deviation of the distribution."""
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def median(self) -> float:
|
|
218
|
+
"""Returns the standard deviation of the distribution."""
|
|
219
|
+
return self.percentile(50)
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
@abc.abstractmethod
|
|
223
|
+
def variance(self) -> float:
|
|
224
|
+
"""Returns the variance of the distribution."""
|
|
225
|
+
|
|
226
|
+
@abc.abstractmethod
|
|
227
|
+
def percentile(self, n: float) -> float:
|
|
228
|
+
"""Returns the median of the distribution.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
n: The percentile to return. Should be in the range [0, 100].
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The n-th percentile of the distribution.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
@abc.abstractmethod
|
|
238
|
+
def fraction_less_than(self, value: float) -> float:
|
|
239
|
+
"""Returns the fraction of values in the distribution less than value."""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class Distribution(Metric[DistributionValue]):
|
|
243
|
+
"""Base class for distributional metrics.
|
|
244
|
+
|
|
245
|
+
Distributions are metrics that track the distribution of a numerical value.
|
|
246
|
+
For example, the latency of an operation.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
@abc.abstractmethod
|
|
250
|
+
def record(self, value: float, **parameters) -> None:
|
|
251
|
+
"""Records a value to the distribution.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
value: The value to record.
|
|
255
|
+
**parameters: Parameters for parameterized distributions.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
@contextlib.contextmanager
|
|
259
|
+
def record_duration(
|
|
260
|
+
self,
|
|
261
|
+
*,
|
|
262
|
+
scale: int = 1000,
|
|
263
|
+
error_parameter: str = 'error',
|
|
264
|
+
**parameters) -> Iterator[None]:
|
|
265
|
+
"""Context manager that records the duration of code block.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
scale: The scale of the duration.
|
|
269
|
+
error_parameter: The parameter name for recording the error. If the name
|
|
270
|
+
is not defined as a parameter for the distribution, the error tag will
|
|
271
|
+
not be recorded.
|
|
272
|
+
**parameters: Parameters for parameterized distributions.
|
|
273
|
+
"""
|
|
274
|
+
start_time = time.time()
|
|
275
|
+
error = None
|
|
276
|
+
try:
|
|
277
|
+
yield
|
|
278
|
+
except BaseException as e:
|
|
279
|
+
error = e
|
|
280
|
+
raise e
|
|
281
|
+
finally:
|
|
282
|
+
duration = (time.time() - start_time) * scale
|
|
283
|
+
if error_parameter in self._parameter_definitions:
|
|
284
|
+
parameters[error_parameter] = (
|
|
285
|
+
error_utils.ErrorInfo.from_exception(error).tag
|
|
286
|
+
if error is not None else ''
|
|
287
|
+
)
|
|
288
|
+
self.record(duration, **parameters)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class MetricCollection(metaclass=abc.ABCMeta):
|
|
292
|
+
"""Base class for counter collections."""
|
|
293
|
+
|
|
294
|
+
_COUNTER_CLASS = Counter
|
|
295
|
+
_SCALAR_CLASS = Scalar
|
|
296
|
+
_DISTRIBUTION_CLASS = Distribution
|
|
297
|
+
|
|
298
|
+
def __init__(
|
|
299
|
+
self,
|
|
300
|
+
namespace: str,
|
|
301
|
+
default_parameters: Optional[
|
|
302
|
+
Dict[str, Type[Union[int, str, bool]]]
|
|
303
|
+
] = None
|
|
304
|
+
):
|
|
305
|
+
"""Initializes the metric collection.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
namespace: The namespace of the metric collection.
|
|
309
|
+
default_parameters: The default parameters used to create metrics
|
|
310
|
+
if not specified.
|
|
311
|
+
"""
|
|
312
|
+
self._namespace = namespace
|
|
313
|
+
self._default_parameter_definitions = default_parameters or {}
|
|
314
|
+
self._metrics = self._metric_container()
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def namespace(self) -> str:
|
|
318
|
+
"""Returns the namespace of the metric collection."""
|
|
319
|
+
return self._namespace
|
|
320
|
+
|
|
321
|
+
def metrics(self) -> List[Metric]:
|
|
322
|
+
"""Returns the names of the metrics."""
|
|
323
|
+
return [m for m in self._metrics.values() if m.namespace == self._namespace]
|
|
324
|
+
|
|
325
|
+
def _metric_container(self) -> dict[str, Metric]:
|
|
326
|
+
"""Returns the container for metrics."""
|
|
327
|
+
return {}
|
|
328
|
+
|
|
329
|
+
def _get_or_create_metric(
|
|
330
|
+
self,
|
|
331
|
+
metric_cls: Type[Metric],
|
|
332
|
+
name: str,
|
|
333
|
+
description: str,
|
|
334
|
+
parameter_definitions: Dict[str, Type[Union[int, str, bool]]],
|
|
335
|
+
**additional_flags,
|
|
336
|
+
) -> Metric:
|
|
337
|
+
"""Gets or creates a metric with the given name."""
|
|
338
|
+
full_name = f'{self._namespace}/{name}'
|
|
339
|
+
metric = self._metrics.get(full_name)
|
|
340
|
+
if metric is not None:
|
|
341
|
+
if not isinstance(metric, metric_cls):
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f'Metric {full_name!r} already exists with a different type '
|
|
344
|
+
f'({type(metric)}).'
|
|
345
|
+
)
|
|
346
|
+
if description != metric.description:
|
|
347
|
+
raise ValueError(
|
|
348
|
+
f'Metric {full_name!r} already exists with a different description '
|
|
349
|
+
f'({metric.description!r}).'
|
|
350
|
+
)
|
|
351
|
+
if parameter_definitions != metric.parameter_definitions:
|
|
352
|
+
raise ValueError(
|
|
353
|
+
f'Metric {full_name!r} already exists with different parameter '
|
|
354
|
+
f'definitions ({metric.parameter_definitions!r}).'
|
|
355
|
+
)
|
|
356
|
+
else:
|
|
357
|
+
metric = metric_cls(
|
|
358
|
+
self.namespace,
|
|
359
|
+
name,
|
|
360
|
+
description,
|
|
361
|
+
parameter_definitions,
|
|
362
|
+
**additional_flags
|
|
363
|
+
)
|
|
364
|
+
self._metrics[full_name] = metric
|
|
365
|
+
return metric
|
|
366
|
+
|
|
367
|
+
def get_counter(
|
|
368
|
+
self,
|
|
369
|
+
name: str,
|
|
370
|
+
description: str,
|
|
371
|
+
parameters: Optional[Dict[str, Type[Union[int, str, bool]]]] = None,
|
|
372
|
+
**additional_flags
|
|
373
|
+
) -> Counter:
|
|
374
|
+
"""Gets or creates a counter with the given name.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
name: The name of the counter.
|
|
378
|
+
description: The description of the counter.
|
|
379
|
+
parameters: The definitions of the parameters for the counter.
|
|
380
|
+
`default_parameters` from the collection will be used if not specified.
|
|
381
|
+
**additional_flags: Additional arguments for creating the counter.
|
|
382
|
+
Subclasses can use these arguments to provide additional information for
|
|
383
|
+
creating the counter.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
The counter with the given name.
|
|
387
|
+
"""
|
|
388
|
+
if parameters is None:
|
|
389
|
+
parameters = self._default_parameter_definitions
|
|
390
|
+
return typing.cast(
|
|
391
|
+
Counter,
|
|
392
|
+
self._get_or_create_metric(
|
|
393
|
+
self._COUNTER_CLASS,
|
|
394
|
+
name,
|
|
395
|
+
description,
|
|
396
|
+
parameters,
|
|
397
|
+
**additional_flags
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def get_scalar(
|
|
402
|
+
self,
|
|
403
|
+
name: str,
|
|
404
|
+
description: str,
|
|
405
|
+
parameters: Optional[Dict[str, Type[Union[int, str, bool]]]] = None,
|
|
406
|
+
value_type: Type[Union[int, float]] = int,
|
|
407
|
+
**additional_flags
|
|
408
|
+
) -> Scalar:
|
|
409
|
+
"""Gets or creates a scalar with the given name.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
name: The name of the counter.
|
|
413
|
+
description: The description of the counter.
|
|
414
|
+
parameters: The definitions of the parameters for the counter.
|
|
415
|
+
`default_parameters` from the collection will be used if not specified.
|
|
416
|
+
value_type: The type of the value for the scalar.
|
|
417
|
+
**additional_flags: Additional arguments for creating the scalar.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
The counter with the given name.
|
|
421
|
+
"""
|
|
422
|
+
if parameters is None:
|
|
423
|
+
parameters = self._default_parameter_definitions
|
|
424
|
+
return typing.cast(
|
|
425
|
+
Scalar,
|
|
426
|
+
self._get_or_create_metric(
|
|
427
|
+
self._SCALAR_CLASS,
|
|
428
|
+
name,
|
|
429
|
+
description,
|
|
430
|
+
parameters,
|
|
431
|
+
value_type=value_type,
|
|
432
|
+
**additional_flags
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def get_distribution(
|
|
437
|
+
self,
|
|
438
|
+
name: str,
|
|
439
|
+
description: str,
|
|
440
|
+
parameters: Optional[Dict[str, Type[Union[int, str, bool]]]] = None,
|
|
441
|
+
**additional_flags
|
|
442
|
+
) -> Distribution:
|
|
443
|
+
"""Gets or creates a distribution with the given name.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
name: The name of the distribution.
|
|
447
|
+
description: The description of the distribution.
|
|
448
|
+
parameters: The definitions of the parameters for the distribution.
|
|
449
|
+
`default_parameters` from the collection will be used if not specified.
|
|
450
|
+
**additional_flags: Additional arguments for creating the distribution.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
The distribution with the given name.
|
|
454
|
+
"""
|
|
455
|
+
if parameters is None:
|
|
456
|
+
parameters = self._default_parameter_definitions
|
|
457
|
+
return typing.cast(
|
|
458
|
+
Distribution,
|
|
459
|
+
self._get_or_create_metric(
|
|
460
|
+
self._DISTRIBUTION_CLASS,
|
|
461
|
+
name,
|
|
462
|
+
description,
|
|
463
|
+
parameters,
|
|
464
|
+
**additional_flags
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
#
|
|
469
|
+
# InMemoryMetricCollection.
|
|
470
|
+
#
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class _InMemoryCounter(Counter):
|
|
474
|
+
"""In-memory counter."""
|
|
475
|
+
|
|
476
|
+
def __init__(self, *args, **kwargs):
|
|
477
|
+
super().__init__(*args, **kwargs)
|
|
478
|
+
self._counter = collections.defaultdict(int)
|
|
479
|
+
self._lock = threading.Lock()
|
|
480
|
+
|
|
481
|
+
def increment(self, delta: int = 1, **parameters) -> int:
|
|
482
|
+
"""Increments the counter by delta and returns the new value."""
|
|
483
|
+
parameters_key = self._parameters_key(**parameters)
|
|
484
|
+
with self._lock:
|
|
485
|
+
value = self._counter[parameters_key]
|
|
486
|
+
value += delta
|
|
487
|
+
self._counter[parameters_key] = value
|
|
488
|
+
return value
|
|
489
|
+
|
|
490
|
+
def value(self, **parameters) -> int:
|
|
491
|
+
"""Returns the value of the counter based on the given parameters."""
|
|
492
|
+
return self._counter[self._parameters_key(**parameters)]
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class _InMemoryScalar(Scalar[MetricValueType]):
|
|
496
|
+
"""In-memory scalar."""
|
|
497
|
+
|
|
498
|
+
def __init__(self, *args, **kwargs):
|
|
499
|
+
super().__init__(*args, **kwargs)
|
|
500
|
+
self._values = collections.defaultdict(self.flags['value_type'])
|
|
501
|
+
self._lock = threading.Lock()
|
|
502
|
+
|
|
503
|
+
def increment(
|
|
504
|
+
self,
|
|
505
|
+
delta: MetricValueType = 1,
|
|
506
|
+
**parameters
|
|
507
|
+
) -> MetricValueType:
|
|
508
|
+
"""Increments the scalar by delta and returns the new value."""
|
|
509
|
+
parameters_key = self._parameters_key(**parameters)
|
|
510
|
+
with self._lock:
|
|
511
|
+
value = self._values[parameters_key]
|
|
512
|
+
value += delta
|
|
513
|
+
self._values[parameters_key] = value
|
|
514
|
+
return value
|
|
515
|
+
|
|
516
|
+
def set(self, value: MetricValueType, **parameters) -> None:
|
|
517
|
+
"""Sets the value of the scalar."""
|
|
518
|
+
parameters_key = self._parameters_key(**parameters)
|
|
519
|
+
with self._lock:
|
|
520
|
+
self._values[parameters_key] = value
|
|
521
|
+
|
|
522
|
+
def value(self, **parameters) -> MetricValueType:
|
|
523
|
+
"""Returns the distribution of the scalar."""
|
|
524
|
+
return self._values[self._parameters_key(**parameters)]
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class _InMemoryDistribution(Distribution):
|
|
528
|
+
"""In-memory distribution."""
|
|
529
|
+
|
|
530
|
+
def __init__(self, *args, window_size: int = 1024 * 1024, **kwargs):
|
|
531
|
+
super().__init__(*args, **kwargs)
|
|
532
|
+
self._window_size = window_size
|
|
533
|
+
self._distributions = collections.defaultdict(
|
|
534
|
+
lambda: _InMemoryDistributionValue(self._window_size)
|
|
535
|
+
)
|
|
536
|
+
self._lock = threading.Lock()
|
|
537
|
+
|
|
538
|
+
def record(self, value: Any, **parameters) -> None:
|
|
539
|
+
"""Records a value to the scalar."""
|
|
540
|
+
parameters_key = self._parameters_key(**parameters)
|
|
541
|
+
self._distributions[parameters_key].add(value)
|
|
542
|
+
|
|
543
|
+
def value(self, **parameters) -> DistributionValue:
|
|
544
|
+
"""Returns the distribution of the scalar."""
|
|
545
|
+
return self._distributions[self._parameters_key(**parameters)]
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class _InMemoryDistributionValue(DistributionValue):
|
|
549
|
+
"""In memory distribution value."""
|
|
550
|
+
|
|
551
|
+
def __init__(self, window_size: int = 1024 * 1024):
|
|
552
|
+
self._window_size = window_size
|
|
553
|
+
self._data = []
|
|
554
|
+
self._sum = 0.0
|
|
555
|
+
self._square_sum = 0.0
|
|
556
|
+
self._count = 0
|
|
557
|
+
self._lock = threading.Lock()
|
|
558
|
+
|
|
559
|
+
def add(self, value: int):
|
|
560
|
+
"""Adds a value to the distribution."""
|
|
561
|
+
with self._lock:
|
|
562
|
+
if len(self._data) == self._window_size:
|
|
563
|
+
x = self._data.pop(0)
|
|
564
|
+
self._sum -= x
|
|
565
|
+
self._count -= 1
|
|
566
|
+
self._square_sum -= x ** 2
|
|
567
|
+
|
|
568
|
+
self._data.append(value)
|
|
569
|
+
self._count += 1
|
|
570
|
+
self._sum += value
|
|
571
|
+
self._square_sum += value ** 2
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def count(self) -> int:
|
|
575
|
+
"""Returns the number of samples in the distribution."""
|
|
576
|
+
return self._count
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def sum(self) -> float:
|
|
580
|
+
"""Returns the sum of the distribution."""
|
|
581
|
+
return self._sum
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def mean(self) -> float:
|
|
585
|
+
"""Returns the mean of the distribution."""
|
|
586
|
+
return self._sum / self._count if self._count else 0.0
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
def stddev(self) -> float:
|
|
590
|
+
"""Returns the standard deviation of the distribution."""
|
|
591
|
+
return math.sqrt(self.variance)
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def variance(self) -> float:
|
|
595
|
+
"""Returns the variance of the distribution."""
|
|
596
|
+
if self._count < 2:
|
|
597
|
+
return 0.0
|
|
598
|
+
return self._square_sum / self._count - self.mean ** 2
|
|
599
|
+
|
|
600
|
+
def percentile(self, n: float) -> float:
|
|
601
|
+
"""Returns the median of the distribution."""
|
|
602
|
+
if n < 0 or n > 100:
|
|
603
|
+
raise ValueError(f'Percentile {n} is not in the range [0, 100].')
|
|
604
|
+
|
|
605
|
+
if self._count == 0:
|
|
606
|
+
return 0.0
|
|
607
|
+
|
|
608
|
+
if numpy is not None:
|
|
609
|
+
return numpy.percentile(self._data, n) # pytype: disable=attribute-error
|
|
610
|
+
|
|
611
|
+
sorted_data = sorted(self._data)
|
|
612
|
+
index = (n / 100) * (len(sorted_data) - 1)
|
|
613
|
+
if index % 1 == 0:
|
|
614
|
+
return sorted_data[int(index)]
|
|
615
|
+
else:
|
|
616
|
+
# Interpolate the value at the given percentile.
|
|
617
|
+
lower_index = int(index)
|
|
618
|
+
fraction = index - lower_index
|
|
619
|
+
|
|
620
|
+
# Get the values at the two surrounding integer indices
|
|
621
|
+
lower_value = sorted_data[lower_index]
|
|
622
|
+
upper_value = sorted_data[lower_index + 1]
|
|
623
|
+
return lower_value + fraction * (upper_value - lower_value)
|
|
624
|
+
|
|
625
|
+
def fraction_less_than(self, value: float) -> float:
|
|
626
|
+
"""Returns the fraction of values in the distribution less than value."""
|
|
627
|
+
if self._count == 0:
|
|
628
|
+
return 0.0
|
|
629
|
+
with self._lock:
|
|
630
|
+
return len([x for x in self._data if x < value]) / self._count
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
class InMemoryMetricCollection(MetricCollection):
|
|
634
|
+
"""In-memory counter."""
|
|
635
|
+
|
|
636
|
+
_COUNTER_CLASS = _InMemoryCounter
|
|
637
|
+
_SCALAR_CLASS = _InMemoryScalar
|
|
638
|
+
_DISTRIBUTION_CLASS = _InMemoryDistribution
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
_METRIC_COLLECTION_CLS = InMemoryMetricCollection # pylint: disable=invalid-name
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def metric_collection(namespace: str, **kwargs) -> MetricCollection:
|
|
645
|
+
"""Creates a metric collection."""
|
|
646
|
+
return _METRIC_COLLECTION_CLS(namespace, **kwargs)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def set_default_metric_collection_cls(cls: Type[MetricCollection]) -> None:
|
|
650
|
+
"""Sets the default metric collection class."""
|
|
651
|
+
global _METRIC_COLLECTION_CLS
|
|
652
|
+
_METRIC_COLLECTION_CLS = cls
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def default_metric_collection_cls() -> Type[MetricCollection]:
|
|
656
|
+
"""Returns the default metric collection class."""
|
|
657
|
+
return _METRIC_COLLECTION_CLS
|