guidellm 0.3.1__py3-none-any.whl → 0.6.0a5__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.
- guidellm/__init__.py +5 -2
- guidellm/__main__.py +524 -255
- guidellm/backends/__init__.py +33 -0
- guidellm/backends/backend.py +109 -0
- guidellm/backends/openai.py +340 -0
- guidellm/backends/response_handlers.py +428 -0
- guidellm/benchmark/__init__.py +69 -39
- guidellm/benchmark/benchmarker.py +160 -316
- guidellm/benchmark/entrypoints.py +560 -127
- guidellm/benchmark/outputs/__init__.py +24 -0
- guidellm/benchmark/outputs/console.py +633 -0
- guidellm/benchmark/outputs/csv.py +721 -0
- guidellm/benchmark/outputs/html.py +473 -0
- guidellm/benchmark/outputs/output.py +169 -0
- guidellm/benchmark/outputs/serialized.py +69 -0
- guidellm/benchmark/profiles.py +718 -0
- guidellm/benchmark/progress.py +553 -556
- guidellm/benchmark/scenarios/__init__.py +40 -0
- guidellm/benchmark/scenarios/chat.json +6 -0
- guidellm/benchmark/scenarios/rag.json +6 -0
- guidellm/benchmark/schemas/__init__.py +66 -0
- guidellm/benchmark/schemas/base.py +402 -0
- guidellm/benchmark/schemas/generative/__init__.py +55 -0
- guidellm/benchmark/schemas/generative/accumulator.py +841 -0
- guidellm/benchmark/schemas/generative/benchmark.py +163 -0
- guidellm/benchmark/schemas/generative/entrypoints.py +381 -0
- guidellm/benchmark/schemas/generative/metrics.py +927 -0
- guidellm/benchmark/schemas/generative/report.py +158 -0
- guidellm/data/__init__.py +34 -4
- guidellm/data/builders.py +541 -0
- guidellm/data/collators.py +16 -0
- guidellm/data/config.py +120 -0
- guidellm/data/deserializers/__init__.py +49 -0
- guidellm/data/deserializers/deserializer.py +141 -0
- guidellm/data/deserializers/file.py +223 -0
- guidellm/data/deserializers/huggingface.py +94 -0
- guidellm/data/deserializers/memory.py +194 -0
- guidellm/data/deserializers/synthetic.py +246 -0
- guidellm/data/entrypoints.py +52 -0
- guidellm/data/loaders.py +190 -0
- guidellm/data/preprocessors/__init__.py +27 -0
- guidellm/data/preprocessors/formatters.py +410 -0
- guidellm/data/preprocessors/mappers.py +196 -0
- guidellm/data/preprocessors/preprocessor.py +30 -0
- guidellm/data/processor.py +29 -0
- guidellm/data/schemas.py +175 -0
- guidellm/data/utils/__init__.py +6 -0
- guidellm/data/utils/dataset.py +94 -0
- guidellm/extras/__init__.py +4 -0
- guidellm/extras/audio.py +220 -0
- guidellm/extras/vision.py +242 -0
- guidellm/logger.py +2 -2
- guidellm/mock_server/__init__.py +8 -0
- guidellm/mock_server/config.py +84 -0
- guidellm/mock_server/handlers/__init__.py +17 -0
- guidellm/mock_server/handlers/chat_completions.py +280 -0
- guidellm/mock_server/handlers/completions.py +280 -0
- guidellm/mock_server/handlers/tokenizer.py +142 -0
- guidellm/mock_server/models.py +510 -0
- guidellm/mock_server/server.py +238 -0
- guidellm/mock_server/utils.py +302 -0
- guidellm/scheduler/__init__.py +69 -26
- guidellm/scheduler/constraints/__init__.py +49 -0
- guidellm/scheduler/constraints/constraint.py +325 -0
- guidellm/scheduler/constraints/error.py +411 -0
- guidellm/scheduler/constraints/factory.py +182 -0
- guidellm/scheduler/constraints/request.py +312 -0
- guidellm/scheduler/constraints/saturation.py +722 -0
- guidellm/scheduler/environments.py +252 -0
- guidellm/scheduler/scheduler.py +137 -368
- guidellm/scheduler/schemas.py +358 -0
- guidellm/scheduler/strategies.py +617 -0
- guidellm/scheduler/worker.py +413 -419
- guidellm/scheduler/worker_group.py +712 -0
- guidellm/schemas/__init__.py +65 -0
- guidellm/schemas/base.py +417 -0
- guidellm/schemas/info.py +188 -0
- guidellm/schemas/request.py +235 -0
- guidellm/schemas/request_stats.py +349 -0
- guidellm/schemas/response.py +124 -0
- guidellm/schemas/statistics.py +1018 -0
- guidellm/{config.py → settings.py} +31 -24
- guidellm/utils/__init__.py +71 -8
- guidellm/utils/auto_importer.py +98 -0
- guidellm/utils/cli.py +132 -5
- guidellm/utils/console.py +566 -0
- guidellm/utils/encoding.py +778 -0
- guidellm/utils/functions.py +159 -0
- guidellm/utils/hf_datasets.py +1 -2
- guidellm/utils/hf_transformers.py +4 -4
- guidellm/utils/imports.py +9 -0
- guidellm/utils/messaging.py +1118 -0
- guidellm/utils/mixins.py +115 -0
- guidellm/utils/random.py +3 -4
- guidellm/utils/registry.py +220 -0
- guidellm/utils/singleton.py +133 -0
- guidellm/utils/synchronous.py +159 -0
- guidellm/utils/text.py +163 -50
- guidellm/utils/typing.py +41 -0
- guidellm/version.py +2 -2
- guidellm-0.6.0a5.dist-info/METADATA +364 -0
- guidellm-0.6.0a5.dist-info/RECORD +109 -0
- guidellm/backend/__init__.py +0 -23
- guidellm/backend/backend.py +0 -259
- guidellm/backend/openai.py +0 -708
- guidellm/backend/response.py +0 -136
- guidellm/benchmark/aggregator.py +0 -760
- guidellm/benchmark/benchmark.py +0 -837
- guidellm/benchmark/output.py +0 -997
- guidellm/benchmark/profile.py +0 -409
- guidellm/benchmark/scenario.py +0 -104
- guidellm/data/prideandprejudice.txt.gz +0 -0
- guidellm/dataset/__init__.py +0 -22
- guidellm/dataset/creator.py +0 -213
- guidellm/dataset/entrypoints.py +0 -42
- guidellm/dataset/file.py +0 -92
- guidellm/dataset/hf_datasets.py +0 -62
- guidellm/dataset/in_memory.py +0 -132
- guidellm/dataset/synthetic.py +0 -287
- guidellm/objects/__init__.py +0 -18
- guidellm/objects/pydantic.py +0 -89
- guidellm/objects/statistics.py +0 -953
- guidellm/preprocess/__init__.py +0 -3
- guidellm/preprocess/dataset.py +0 -374
- guidellm/presentation/__init__.py +0 -28
- guidellm/presentation/builder.py +0 -27
- guidellm/presentation/data_models.py +0 -232
- guidellm/presentation/injector.py +0 -66
- guidellm/request/__init__.py +0 -18
- guidellm/request/loader.py +0 -284
- guidellm/request/request.py +0 -79
- guidellm/request/types.py +0 -10
- guidellm/scheduler/queues.py +0 -25
- guidellm/scheduler/result.py +0 -155
- guidellm/scheduler/strategy.py +0 -495
- guidellm-0.3.1.dist-info/METADATA +0 -329
- guidellm-0.3.1.dist-info/RECORD +0 -62
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/WHEEL +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/entry_points.txt +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/licenses/LICENSE +0 -0
- {guidellm-0.3.1.dist-info → guidellm-0.6.0a5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Statistical distribution analysis and summary calculations for benchmark metrics.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive statistical analysis tools including percentile calculations,
|
|
5
|
+
summary statistics, and status-based distributions. Supports value distributions,
|
|
6
|
+
time-based rate and concurrency distributions with weighted sampling, and probability
|
|
7
|
+
density functions for analyzing benchmark performance metrics and request patterns
|
|
8
|
+
across different status categories (successful, incomplete, errored).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
from collections.abc import Callable, Sequence
|
|
15
|
+
from typing import Literal, TypeVar
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from pydantic import Field
|
|
19
|
+
|
|
20
|
+
from guidellm.schemas.base import StandardBaseModel, StatusBreakdown
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DistributionSummary",
|
|
24
|
+
"FunctionObjT",
|
|
25
|
+
"Percentiles",
|
|
26
|
+
"StatusDistributionSummary",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
FunctionObjT = TypeVar("FunctionObjT")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Percentiles(StandardBaseModel):
|
|
33
|
+
"""
|
|
34
|
+
Standard percentile values for probability distributions.
|
|
35
|
+
|
|
36
|
+
Captures key percentile points from 0.1th to 99.9th percentile for comprehensive
|
|
37
|
+
distribution analysis, enabling assessment of central tendency, spread, and tail
|
|
38
|
+
behavior in benchmark metrics.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
p001: float = Field(description="0.1th percentile value")
|
|
42
|
+
p01: float = Field(description="1st percentile value")
|
|
43
|
+
p05: float = Field(description="5th percentile value")
|
|
44
|
+
p10: float = Field(description="10th percentile value")
|
|
45
|
+
p25: float = Field(description="25th percentile value")
|
|
46
|
+
p50: float = Field(description="50th percentile (median) value")
|
|
47
|
+
p75: float = Field(description="75th percentile value")
|
|
48
|
+
p90: float = Field(description="90th percentile value")
|
|
49
|
+
p95: float = Field(description="95th percentile value")
|
|
50
|
+
p99: float = Field(description="99th percentile value")
|
|
51
|
+
p999: float = Field(description="99.9th percentile value")
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_pdf(
|
|
55
|
+
cls, pdf: np.ndarray, epsilon: float = 1e-6, validate: bool = True
|
|
56
|
+
) -> Percentiles:
|
|
57
|
+
"""
|
|
58
|
+
Create percentiles from a probability density function.
|
|
59
|
+
|
|
60
|
+
:param pdf: 2D array (N, 2) with values in column 0 and probabilities in
|
|
61
|
+
column 1
|
|
62
|
+
:param epsilon: Tolerance for probability sum validation
|
|
63
|
+
:param validate: Whether to validate probabilities sum to 1 and are
|
|
64
|
+
non-negative
|
|
65
|
+
:return: Percentiles object with computed values
|
|
66
|
+
:raises ValueError: If PDF shape is invalid, probabilities are negative,
|
|
67
|
+
or probabilities don't sum to 1
|
|
68
|
+
"""
|
|
69
|
+
expected_shape = (None, 2)
|
|
70
|
+
|
|
71
|
+
if len(pdf.shape) != len(expected_shape) or pdf.shape[1] != expected_shape[1]:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"PDF must be a 2D array of shape (N, 2) where first column is values "
|
|
74
|
+
f"and second column is probabilities. Got {pdf.shape} instead."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
percentile_probs = {
|
|
78
|
+
"p001": 0.001,
|
|
79
|
+
"p01": 0.01,
|
|
80
|
+
"p05": 0.05,
|
|
81
|
+
"p10": 0.1,
|
|
82
|
+
"p25": 0.25,
|
|
83
|
+
"p50": 0.5,
|
|
84
|
+
"p75": 0.75,
|
|
85
|
+
"p90": 0.9,
|
|
86
|
+
"p95": 0.95,
|
|
87
|
+
"p99": 0.99,
|
|
88
|
+
"p999": 0.999,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if pdf.shape[0] == 0:
|
|
92
|
+
return Percentiles(**dict.fromkeys(percentile_probs.keys(), 0.0))
|
|
93
|
+
|
|
94
|
+
probabilities = pdf[:, 1]
|
|
95
|
+
|
|
96
|
+
if validate:
|
|
97
|
+
if np.any(probabilities < 0):
|
|
98
|
+
raise ValueError("Probabilities must be non-negative.")
|
|
99
|
+
|
|
100
|
+
prob_sum = np.sum(probabilities)
|
|
101
|
+
if abs(prob_sum - 1.0) > epsilon:
|
|
102
|
+
raise ValueError(f"Probabilities must sum to 1, got {prob_sum}.")
|
|
103
|
+
|
|
104
|
+
cdf_probs = np.cumsum(probabilities)
|
|
105
|
+
|
|
106
|
+
return Percentiles(
|
|
107
|
+
**{
|
|
108
|
+
key: pdf[np.searchsorted(cdf_probs, value, side="left"), 0].item()
|
|
109
|
+
for key, value in percentile_probs.items()
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class DistributionSummary(StandardBaseModel):
|
|
115
|
+
"""
|
|
116
|
+
Comprehensive statistical summary of a probability distribution.
|
|
117
|
+
|
|
118
|
+
Captures central tendency (mean, median, mode), spread (variance, std_dev),
|
|
119
|
+
extrema (min, max), and percentile information with optional probability density
|
|
120
|
+
function. Supports creation from raw values, PDFs, or time-based event data for
|
|
121
|
+
rate and concurrency analysis in benchmark metrics.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
mean: float = Field(description="Mean/average value")
|
|
125
|
+
median: float = Field(description="Median (50th percentile) value")
|
|
126
|
+
mode: float = Field(description="Mode (most probable) value")
|
|
127
|
+
variance: float = Field(description="Variance of the distribution")
|
|
128
|
+
std_dev: float = Field(description="Standard deviation")
|
|
129
|
+
min: float = Field(description="Minimum value")
|
|
130
|
+
max: float = Field(description="Maximum value")
|
|
131
|
+
count: int = Field(description="Number of observations")
|
|
132
|
+
total_sum: float = Field(description="Sum of all values")
|
|
133
|
+
percentiles: Percentiles = Field(description="Standard percentile values")
|
|
134
|
+
pdf: list[tuple[float, float]] | None = Field(
|
|
135
|
+
description="Probability density function as (value, probability) pairs",
|
|
136
|
+
default=None,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_pdf(
|
|
141
|
+
cls,
|
|
142
|
+
pdf: np.ndarray,
|
|
143
|
+
count: int | None = None,
|
|
144
|
+
include_pdf: bool | int = False,
|
|
145
|
+
epsilon: float = 1e-6,
|
|
146
|
+
validate: bool = True,
|
|
147
|
+
) -> DistributionSummary:
|
|
148
|
+
"""
|
|
149
|
+
Create distribution summary from a probability density function.
|
|
150
|
+
|
|
151
|
+
:param pdf: 2D array (N, 2) with values in column 0 and probabilities in
|
|
152
|
+
column 1
|
|
153
|
+
:param count: Number of original observations; defaults to PDF length
|
|
154
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
155
|
+
:param epsilon: Tolerance for probability validation
|
|
156
|
+
:param validate: Whether to validate probabilities sum to 1 and are non-negative
|
|
157
|
+
:return: Complete distribution summary with statistics
|
|
158
|
+
:raises ValueError: If PDF shape is invalid or probabilities are invalid
|
|
159
|
+
"""
|
|
160
|
+
expected_shape = (None, 2)
|
|
161
|
+
|
|
162
|
+
if len(pdf.shape) != len(expected_shape) or pdf.shape[1] != expected_shape[1]:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"PDF must be a 2D array of shape (N, 2) where first column is values "
|
|
165
|
+
f"and second column is probabilities. Got {pdf.shape} instead."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if pdf.shape[0] == 0:
|
|
169
|
+
return DistributionSummary(
|
|
170
|
+
mean=0.0,
|
|
171
|
+
median=0.0,
|
|
172
|
+
mode=0.0,
|
|
173
|
+
variance=0.0,
|
|
174
|
+
std_dev=0.0,
|
|
175
|
+
min=0.0,
|
|
176
|
+
max=0.0,
|
|
177
|
+
count=0 if count is None else count,
|
|
178
|
+
total_sum=0.0,
|
|
179
|
+
percentiles=Percentiles.from_pdf(pdf, epsilon=epsilon),
|
|
180
|
+
pdf=None if include_pdf is False else [],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Calculate stats
|
|
184
|
+
values = pdf[:, 0]
|
|
185
|
+
probabilities = pdf[:, 1]
|
|
186
|
+
|
|
187
|
+
if validate:
|
|
188
|
+
# Fail if probabilities don't sum to 1 or are negative
|
|
189
|
+
if np.any(probabilities < 0):
|
|
190
|
+
raise ValueError("Probabilities must be non-negative.")
|
|
191
|
+
|
|
192
|
+
prob_sum = np.sum(probabilities)
|
|
193
|
+
if not np.isclose(prob_sum, 1.0, atol=epsilon):
|
|
194
|
+
raise ValueError(f"Probabilities must sum to 1.0 (sum={prob_sum}).")
|
|
195
|
+
|
|
196
|
+
# Fail if values are not sorted
|
|
197
|
+
if not np.all(values[:-1] <= values[1:]):
|
|
198
|
+
raise ValueError("Values in PDF must be sorted in ascending order.")
|
|
199
|
+
|
|
200
|
+
percentiles = Percentiles.from_pdf(pdf, epsilon=epsilon, validate=False)
|
|
201
|
+
median = percentiles.p50
|
|
202
|
+
mean = np.sum(values * probabilities).item()
|
|
203
|
+
mode = values[np.argmax(probabilities)].item()
|
|
204
|
+
variance = np.sum((values - mean) ** 2 * probabilities).item()
|
|
205
|
+
std_dev = math.sqrt(variance)
|
|
206
|
+
minimum = values[0].item()
|
|
207
|
+
maximum = values[-1].item()
|
|
208
|
+
|
|
209
|
+
if count is None:
|
|
210
|
+
count = len(pdf)
|
|
211
|
+
|
|
212
|
+
total_sum = mean * count
|
|
213
|
+
sampled_pdf = cls._sample_pdf(pdf, include_pdf)
|
|
214
|
+
|
|
215
|
+
return DistributionSummary(
|
|
216
|
+
mean=mean,
|
|
217
|
+
median=median,
|
|
218
|
+
mode=mode,
|
|
219
|
+
variance=variance,
|
|
220
|
+
std_dev=std_dev,
|
|
221
|
+
min=minimum,
|
|
222
|
+
max=maximum,
|
|
223
|
+
count=count,
|
|
224
|
+
total_sum=total_sum,
|
|
225
|
+
percentiles=percentiles,
|
|
226
|
+
pdf=sampled_pdf,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def _sample_pdf(
|
|
231
|
+
cls, pdf: np.ndarray, include_pdf: bool | int
|
|
232
|
+
) -> list[tuple[float, float]] | None:
|
|
233
|
+
"""
|
|
234
|
+
Sample PDF based on include_pdf parameter.
|
|
235
|
+
|
|
236
|
+
:param pdf: PDF array to sample
|
|
237
|
+
:param include_pdf: False for None, True for full, int for sampled size
|
|
238
|
+
:return: Sampled PDF as list of tuples or None
|
|
239
|
+
"""
|
|
240
|
+
if include_pdf is False:
|
|
241
|
+
return None
|
|
242
|
+
if include_pdf is True:
|
|
243
|
+
return pdf.tolist()
|
|
244
|
+
if isinstance(include_pdf, int) and include_pdf > 0:
|
|
245
|
+
if len(pdf) <= include_pdf:
|
|
246
|
+
return pdf.tolist()
|
|
247
|
+
sample_indices = np.linspace(0, len(pdf) - 1, include_pdf, dtype=int)
|
|
248
|
+
return pdf[sample_indices].tolist()
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def from_values(
|
|
253
|
+
cls,
|
|
254
|
+
values: Sequence[float | tuple[float, float]] | np.ndarray,
|
|
255
|
+
count: int | None = None,
|
|
256
|
+
include_pdf: bool | int = False,
|
|
257
|
+
epsilon: float = 1e-6,
|
|
258
|
+
) -> DistributionSummary:
|
|
259
|
+
"""
|
|
260
|
+
Create distribution summary from raw values with optional weights.
|
|
261
|
+
|
|
262
|
+
:param values: Values or (value, weight) tuples, or numpy array
|
|
263
|
+
:param count: Number of original observations; defaults to sum of weights
|
|
264
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
265
|
+
:param epsilon: Tolerance for probability validation
|
|
266
|
+
:return: Distribution summary computed from the values
|
|
267
|
+
:raises ValueError: If total weight is zero or invalid
|
|
268
|
+
"""
|
|
269
|
+
np_values = cls._to_weighted_ndarray(values, num_values_per_item=2)
|
|
270
|
+
|
|
271
|
+
if np_values.shape[0] == 0:
|
|
272
|
+
return DistributionSummary.from_pdf(
|
|
273
|
+
pdf=np.empty((0, 2)), count=0, include_pdf=include_pdf, epsilon=epsilon
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if count is None:
|
|
277
|
+
count = round(np.sum(np_values[:, 1]).item())
|
|
278
|
+
|
|
279
|
+
# Sort values and weights by values
|
|
280
|
+
sort_ind = np.argsort(np_values[:, 0])
|
|
281
|
+
sorted_values = np_values[sort_ind, 0]
|
|
282
|
+
sorted_weights = np_values[sort_ind, 1]
|
|
283
|
+
|
|
284
|
+
# Combine any duplicate values by summing their weights
|
|
285
|
+
unique_values, inverse_indices = np.unique(sorted_values, return_inverse=True)
|
|
286
|
+
combined_weights = np.zeros_like(unique_values, dtype=float)
|
|
287
|
+
np.add.at(combined_weights, inverse_indices, sorted_weights)
|
|
288
|
+
|
|
289
|
+
# Remove any values with zero weight
|
|
290
|
+
nonzero_mask = combined_weights > 0
|
|
291
|
+
final_values = unique_values[nonzero_mask]
|
|
292
|
+
final_weights = combined_weights[nonzero_mask]
|
|
293
|
+
|
|
294
|
+
# Create PDF by normalizing weights and stacking
|
|
295
|
+
total_weight = np.sum(final_weights)
|
|
296
|
+
if total_weight <= epsilon:
|
|
297
|
+
# No valid weights to create PDF, overwrite to uniform distribution
|
|
298
|
+
final_weights = np.ones_like(final_values)
|
|
299
|
+
total_weight = np.sum(final_weights)
|
|
300
|
+
|
|
301
|
+
probabilities = final_weights / total_weight
|
|
302
|
+
pdf = np.column_stack((final_values, probabilities))
|
|
303
|
+
|
|
304
|
+
return DistributionSummary.from_pdf(
|
|
305
|
+
pdf=pdf,
|
|
306
|
+
count=count,
|
|
307
|
+
include_pdf=include_pdf,
|
|
308
|
+
epsilon=epsilon,
|
|
309
|
+
validate=False,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def rate_distribution_from_timings(
|
|
314
|
+
cls,
|
|
315
|
+
event_times: Sequence[float | tuple[float, float]] | np.ndarray,
|
|
316
|
+
start_time: float | None = None,
|
|
317
|
+
end_time: float | None = None,
|
|
318
|
+
threshold: float | None = 1e-4, # 1/10th of a millisecond
|
|
319
|
+
include_pdf: bool | int = False,
|
|
320
|
+
epsilon: float = 1e-6,
|
|
321
|
+
) -> DistributionSummary:
|
|
322
|
+
"""
|
|
323
|
+
Create rate distribution from event timestamps.
|
|
324
|
+
|
|
325
|
+
Computes event rates over time intervals weighted by interval duration for
|
|
326
|
+
analyzing request throughput patterns.
|
|
327
|
+
|
|
328
|
+
:param event_times: Event timestamps or (timestamp, weight) tuples
|
|
329
|
+
:param start_time: Analysis window start; filters earlier events
|
|
330
|
+
:param end_time: Analysis window end; filters later events
|
|
331
|
+
:param threshold: Time threshold for merging nearby events; 1/10th millisecond
|
|
332
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
333
|
+
:param epsilon: Tolerance for probability validation
|
|
334
|
+
:return: Distribution summary of event rates over time
|
|
335
|
+
"""
|
|
336
|
+
weighted_times = cls._to_weighted_ndarray(event_times, num_values_per_item=2)
|
|
337
|
+
|
|
338
|
+
if start_time is not None:
|
|
339
|
+
# Filter out any times before start, insert start time with 0 weight
|
|
340
|
+
weighted_times = np.insert(
|
|
341
|
+
weighted_times[weighted_times[:, 0] >= start_time],
|
|
342
|
+
0,
|
|
343
|
+
[start_time, 0.0],
|
|
344
|
+
axis=0,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if end_time is not None:
|
|
348
|
+
# Filter out any times after end, insert end time with 0 weight
|
|
349
|
+
weighted_times = np.append(
|
|
350
|
+
weighted_times[weighted_times[:, 0] <= end_time],
|
|
351
|
+
[[end_time, 0.0]],
|
|
352
|
+
axis=0,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Sort by time for merging, merge any times within threshold
|
|
356
|
+
sort_ind = np.argsort(weighted_times[:, 0])
|
|
357
|
+
weighted_times = weighted_times[sort_ind]
|
|
358
|
+
weighted_times = cls._merge_sorted_times_with_weights(weighted_times, threshold)
|
|
359
|
+
|
|
360
|
+
if len(weighted_times) <= 1:
|
|
361
|
+
# No data to calculate rates from (need at least two times)
|
|
362
|
+
return cls.from_values(
|
|
363
|
+
[],
|
|
364
|
+
count=len(weighted_times),
|
|
365
|
+
include_pdf=include_pdf,
|
|
366
|
+
epsilon=epsilon,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
times = weighted_times[:, 0]
|
|
370
|
+
occurrences = weighted_times[:, 1]
|
|
371
|
+
|
|
372
|
+
# Calculate local duration for each event: ((times[i+1] - times[i-1])) / 2
|
|
373
|
+
midpoints = (times[1:] + times[:-1]) / 2
|
|
374
|
+
durations = np.empty_like(times)
|
|
375
|
+
durations[0] = midpoints[0] - times[0]
|
|
376
|
+
durations[1:-1] = midpoints[1:] - midpoints[:-1]
|
|
377
|
+
durations[-1] = np.clip(times[-1] - midpoints[-1], epsilon, None)
|
|
378
|
+
|
|
379
|
+
# Calculate rate at each interval: occurences[i] / duration[i]
|
|
380
|
+
rates = occurrences / durations
|
|
381
|
+
count = round(np.sum(occurrences).item())
|
|
382
|
+
|
|
383
|
+
return cls.from_values(
|
|
384
|
+
np.column_stack((rates, durations)),
|
|
385
|
+
count=count,
|
|
386
|
+
include_pdf=include_pdf,
|
|
387
|
+
epsilon=epsilon,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
@classmethod
|
|
391
|
+
def concurrency_distribution_from_timings(
|
|
392
|
+
cls,
|
|
393
|
+
event_intervals: (
|
|
394
|
+
Sequence[tuple[float, float] | tuple[float, float, float]] | np.ndarray
|
|
395
|
+
),
|
|
396
|
+
start_time: float | None = None,
|
|
397
|
+
end_time: float | None = None,
|
|
398
|
+
threshold: float | None = 1e-4, # 1/10th of a millisecond
|
|
399
|
+
include_pdf: bool | int = False,
|
|
400
|
+
epsilon: float = 1e-6,
|
|
401
|
+
) -> DistributionSummary:
|
|
402
|
+
"""
|
|
403
|
+
Create concurrency distribution from event time intervals.
|
|
404
|
+
|
|
405
|
+
Tracks overlapping events to compute concurrency levels over time for analyzing
|
|
406
|
+
request processing patterns and resource utilization.
|
|
407
|
+
|
|
408
|
+
:param event_intervals: Event (start, end) or (start, end, weight) tuples
|
|
409
|
+
:param start_time: Analysis window start
|
|
410
|
+
:param end_time: Analysis window end
|
|
411
|
+
:param threshold: Time threshold for merging nearby transitions;
|
|
412
|
+
1/10th millisecond
|
|
413
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
414
|
+
:param epsilon: Tolerance for probability validation
|
|
415
|
+
:return: Distribution summary of concurrency levels over time
|
|
416
|
+
"""
|
|
417
|
+
weighted_intervals = cls._to_weighted_ndarray(
|
|
418
|
+
event_intervals, num_values_per_item=3
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# If start_time, filter any intervals that end before start_time
|
|
422
|
+
if start_time is not None:
|
|
423
|
+
keep_mask = weighted_intervals[:, 1] >= start_time
|
|
424
|
+
weighted_intervals = weighted_intervals[keep_mask]
|
|
425
|
+
|
|
426
|
+
# If end_time, filter any intervals that start after end_time
|
|
427
|
+
if end_time is not None:
|
|
428
|
+
keep_mask = weighted_intervals[:, 0] <= end_time
|
|
429
|
+
weighted_intervals = weighted_intervals[keep_mask]
|
|
430
|
+
|
|
431
|
+
count = len(weighted_intervals)
|
|
432
|
+
|
|
433
|
+
# Convert to concurrency changes at each time
|
|
434
|
+
add_occurences = (
|
|
435
|
+
np.stack(
|
|
436
|
+
(
|
|
437
|
+
weighted_intervals[:, 0],
|
|
438
|
+
weighted_intervals[:, 2],
|
|
439
|
+
),
|
|
440
|
+
axis=1,
|
|
441
|
+
)
|
|
442
|
+
if len(weighted_intervals) > 0
|
|
443
|
+
else np.empty((0, 2))
|
|
444
|
+
)
|
|
445
|
+
remove_occurences = (
|
|
446
|
+
np.stack(
|
|
447
|
+
(
|
|
448
|
+
weighted_intervals[:, 1],
|
|
449
|
+
-1 * weighted_intervals[:, 2],
|
|
450
|
+
),
|
|
451
|
+
axis=1,
|
|
452
|
+
)
|
|
453
|
+
if len(weighted_intervals) > 0
|
|
454
|
+
else np.empty((0, 2))
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Combine add and remove occurences into weighted times
|
|
458
|
+
weighted_times = np.vstack((add_occurences, remove_occurences))
|
|
459
|
+
|
|
460
|
+
# Sort by the times and merge any times within threshold
|
|
461
|
+
weighted_times = weighted_times[np.argsort(weighted_times[:, 0])]
|
|
462
|
+
weighted_times = cls._merge_sorted_times_with_weights(weighted_times, threshold)
|
|
463
|
+
|
|
464
|
+
# If start_time, ensure included (if any before, add final concurrency at start)
|
|
465
|
+
if start_time is not None and len(weighted_times) > 0:
|
|
466
|
+
start_ind = np.searchsorted(weighted_times[:, 0], start_time, side="left")
|
|
467
|
+
prior_delta = (
|
|
468
|
+
np.sum(weighted_times[:start_ind, 1]) if start_ind > 0 else 0.0
|
|
469
|
+
)
|
|
470
|
+
weighted_times = np.insert(
|
|
471
|
+
weighted_times[start_ind:], 0, [start_time, prior_delta], axis=0
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# If end_time, ensure included (if any after, filter out)
|
|
475
|
+
if end_time is not None and len(weighted_times) > 0:
|
|
476
|
+
end_ind = np.searchsorted(weighted_times[:, 0], end_time, side="right")
|
|
477
|
+
weighted_times = np.append(
|
|
478
|
+
weighted_times[:end_ind], [[end_time, 0.0]], axis=0
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Calculate concurrency from cumulative sum of changes over time
|
|
482
|
+
concurrencies = np.clip(np.cumsum(weighted_times[:, 1]), 0, None)
|
|
483
|
+
|
|
484
|
+
if len(concurrencies) <= 1:
|
|
485
|
+
# No data to calculate concurrency from
|
|
486
|
+
return cls.from_values(
|
|
487
|
+
[] if count == 0 else [concurrencies[0].item()],
|
|
488
|
+
include_pdf=include_pdf,
|
|
489
|
+
epsilon=epsilon,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Calculate durations equal to times[i+1] - times[i]
|
|
493
|
+
# The last concurrency level is not used since no following time point
|
|
494
|
+
durations = np.clip(np.diff(weighted_times[:, 0]), 0, None)
|
|
495
|
+
values = np.column_stack((concurrencies[:-1], durations))
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
cls.from_values(
|
|
499
|
+
values,
|
|
500
|
+
count=count,
|
|
501
|
+
include_pdf=include_pdf,
|
|
502
|
+
epsilon=epsilon,
|
|
503
|
+
)
|
|
504
|
+
if np.any(durations > 0)
|
|
505
|
+
else cls.from_values(
|
|
506
|
+
[],
|
|
507
|
+
count=count,
|
|
508
|
+
include_pdf=include_pdf,
|
|
509
|
+
epsilon=epsilon,
|
|
510
|
+
)
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
@classmethod
|
|
514
|
+
def _to_weighted_ndarray(
|
|
515
|
+
cls,
|
|
516
|
+
inputs: (
|
|
517
|
+
Sequence[float | tuple[float, float] | tuple[float, float, float]]
|
|
518
|
+
| np.ndarray
|
|
519
|
+
),
|
|
520
|
+
num_values_per_item: Literal[2, 3],
|
|
521
|
+
) -> np.ndarray:
|
|
522
|
+
if not isinstance(inputs, np.ndarray):
|
|
523
|
+
# Convert list to structured numpy array with dims (N, num_dimensions)
|
|
524
|
+
# Fill in missing weights with 1.0
|
|
525
|
+
return cls._sequence_to_weighted_ndarray(inputs, num_values_per_item)
|
|
526
|
+
|
|
527
|
+
if len(inputs.shape) == 1:
|
|
528
|
+
# 1D array: reshape to (N, 1) and add weights column
|
|
529
|
+
inputs = inputs.reshape(-1, 1)
|
|
530
|
+
weights = np.ones((inputs.shape[0], 1), dtype=float)
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
np.hstack((inputs, weights))
|
|
534
|
+
if num_values_per_item == 2 # noqa: PLR2004
|
|
535
|
+
else np.hstack((inputs, inputs, weights))
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if len(inputs.shape) == 2 and inputs.shape[1] == num_values_per_item - 1: # noqa: PLR2004
|
|
539
|
+
# Add weights column of 1.0
|
|
540
|
+
weights = np.ones((inputs.shape[0], 1), dtype=float)
|
|
541
|
+
|
|
542
|
+
return np.hstack((inputs, weights))
|
|
543
|
+
|
|
544
|
+
if len(inputs.shape) == 2 and inputs.shape[1] == num_values_per_item: # noqa: PLR2004
|
|
545
|
+
return inputs
|
|
546
|
+
|
|
547
|
+
raise ValueError(
|
|
548
|
+
"inputs must be a numpy array of shape (N,), "
|
|
549
|
+
f"(N, {num_values_per_item - 1}), or (N, {num_values_per_item}). "
|
|
550
|
+
f"Got shape {inputs.shape}."
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
@classmethod
|
|
554
|
+
def _sequence_to_weighted_ndarray(
|
|
555
|
+
cls,
|
|
556
|
+
inputs: Sequence[float | tuple[float, float] | tuple[float, float, float]],
|
|
557
|
+
num_values_per_item: Literal[2, 3],
|
|
558
|
+
) -> np.ndarray:
|
|
559
|
+
ndarray = np.empty((len(inputs), num_values_per_item), dtype=float)
|
|
560
|
+
scalar_types: tuple[type, ...] = (int, float, np.integer, np.floating)
|
|
561
|
+
|
|
562
|
+
for ind, val in enumerate(inputs):
|
|
563
|
+
if isinstance(val, scalar_types):
|
|
564
|
+
ndarray[ind, :] = (
|
|
565
|
+
(val, 1.0) if num_values_per_item == 2 else (val, val, 1.0) # noqa: PLR2004
|
|
566
|
+
)
|
|
567
|
+
elif isinstance(val, tuple) and len(val) == num_values_per_item:
|
|
568
|
+
ndarray[ind, :] = val
|
|
569
|
+
elif isinstance(val, tuple) and len(val) == num_values_per_item - 1:
|
|
570
|
+
ndarray[ind, :] = (
|
|
571
|
+
(val[0], 1.0) if num_values_per_item == 2 else (val[0], val[1], 1.0) # noqa: PLR2004
|
|
572
|
+
)
|
|
573
|
+
else:
|
|
574
|
+
raise ValueError(
|
|
575
|
+
"Each item must be a float or a tuple of "
|
|
576
|
+
f"{num_values_per_item} or {num_values_per_item - 1} "
|
|
577
|
+
"elements."
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
return ndarray
|
|
581
|
+
|
|
582
|
+
@classmethod
|
|
583
|
+
def _merge_sorted_times_with_weights(
|
|
584
|
+
cls, weighted_times: np.ndarray, threshold: float | None
|
|
585
|
+
) -> np.ndarray:
|
|
586
|
+
# First remove any exact duplicate times and sum their weights
|
|
587
|
+
unique_times, inverse = np.unique(weighted_times[:, 0], return_inverse=True)
|
|
588
|
+
unique_weights = np.zeros_like(unique_times, dtype=float)
|
|
589
|
+
np.add.at(unique_weights, inverse, weighted_times[:, 1])
|
|
590
|
+
weighted_times = np.column_stack((unique_times, unique_weights))
|
|
591
|
+
|
|
592
|
+
if threshold is None or threshold <= 0.0:
|
|
593
|
+
return weighted_times
|
|
594
|
+
|
|
595
|
+
# Loop to merge times within threshold until no more merges possible
|
|
596
|
+
# (loop due to possible overlapping merge groups)
|
|
597
|
+
while weighted_times.shape[0] > 1:
|
|
598
|
+
times = weighted_times[:, 0]
|
|
599
|
+
weights = weighted_times[:, 1]
|
|
600
|
+
|
|
601
|
+
# Find diffs between consecutive times, create mask for within-threshold
|
|
602
|
+
diffs = np.diff(times)
|
|
603
|
+
within = diffs <= threshold
|
|
604
|
+
if not np.any(within):
|
|
605
|
+
break
|
|
606
|
+
|
|
607
|
+
# Start indices are marked by the transition from 0 to 1 in the mask
|
|
608
|
+
# End indices found by searching for last time within threshold from start
|
|
609
|
+
starts = np.where(np.diff(np.insert(within.astype(int), 0, 0)) == 1)[0]
|
|
610
|
+
start_end_times = times[starts] + threshold
|
|
611
|
+
ends = np.searchsorted(times, start_end_times, side="right") - 1
|
|
612
|
+
|
|
613
|
+
# Collapse overlapping or chained merge groups
|
|
614
|
+
if len(starts) > 1:
|
|
615
|
+
valid_mask = np.concatenate([[True], starts[1:] > ends[:-1]])
|
|
616
|
+
starts, ends = starts[valid_mask], ends[valid_mask]
|
|
617
|
+
|
|
618
|
+
# Update weights at start indices to sum of merged weights
|
|
619
|
+
cumsum = np.concatenate(([0.0], np.cumsum(weights)))
|
|
620
|
+
weighted_times[starts, 1] = cumsum[ends + 1] - cumsum[starts]
|
|
621
|
+
|
|
622
|
+
# Calculate vectorized mask for removing merged entries
|
|
623
|
+
merged_events = np.zeros(len(weighted_times) + 1, dtype=int)
|
|
624
|
+
np.add.at(merged_events, starts, 1)
|
|
625
|
+
np.add.at(merged_events, ends + 1, -1)
|
|
626
|
+
remove_mask = np.cumsum(merged_events[:-1]) > 0
|
|
627
|
+
remove_mask[starts] = False # Keep start indices
|
|
628
|
+
|
|
629
|
+
# Remove merged entries, update weighted_times
|
|
630
|
+
weights = weights[~remove_mask]
|
|
631
|
+
times = times[~remove_mask]
|
|
632
|
+
weighted_times = np.column_stack((times, weights))
|
|
633
|
+
|
|
634
|
+
return weighted_times
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class StatusDistributionSummary(
|
|
638
|
+
StatusBreakdown[
|
|
639
|
+
DistributionSummary,
|
|
640
|
+
DistributionSummary,
|
|
641
|
+
DistributionSummary,
|
|
642
|
+
DistributionSummary,
|
|
643
|
+
]
|
|
644
|
+
):
|
|
645
|
+
"""
|
|
646
|
+
Distribution summaries broken down by request status categories.
|
|
647
|
+
|
|
648
|
+
Provides separate statistical analysis for successful, incomplete, and errored
|
|
649
|
+
requests with total aggregate statistics. Enables status-aware performance analysis
|
|
650
|
+
and SLO validation across different request outcomes in benchmark results.
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
@property
|
|
654
|
+
def count(self) -> int:
|
|
655
|
+
"""
|
|
656
|
+
:return: Total count of samples across all status categories
|
|
657
|
+
"""
|
|
658
|
+
return self.total.count
|
|
659
|
+
|
|
660
|
+
@property
|
|
661
|
+
def total_sum(self) -> float:
|
|
662
|
+
"""
|
|
663
|
+
:return: Total sum of values across all status categories
|
|
664
|
+
"""
|
|
665
|
+
return self.total.total_sum
|
|
666
|
+
|
|
667
|
+
@classmethod
|
|
668
|
+
def from_values(
|
|
669
|
+
cls,
|
|
670
|
+
successful: Sequence[float | tuple[float, float]] | np.ndarray,
|
|
671
|
+
incomplete: Sequence[float | tuple[float, float]] | np.ndarray,
|
|
672
|
+
errored: Sequence[float | tuple[float, float]] | np.ndarray,
|
|
673
|
+
include_pdf: bool | int = False,
|
|
674
|
+
epsilon: float = 1e-6,
|
|
675
|
+
) -> StatusDistributionSummary:
|
|
676
|
+
"""
|
|
677
|
+
Create status-broken-down distribution from values by status category.
|
|
678
|
+
|
|
679
|
+
:param successful: Values or (value, weight) tuples for successful requests
|
|
680
|
+
:param incomplete: Values or (value, weight) tuples for incomplete requests
|
|
681
|
+
:param errored: Values or (value, weight) tuples for errored requests
|
|
682
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
683
|
+
:param epsilon: Tolerance for probability validation
|
|
684
|
+
:return: Status breakdown of distribution summaries
|
|
685
|
+
"""
|
|
686
|
+
total, successful_arr, incomplete_arr, errored_arr = cls._combine_status_arrays(
|
|
687
|
+
successful, incomplete, errored, num_values_per_item=2
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
return StatusDistributionSummary(
|
|
691
|
+
total=DistributionSummary.from_values(
|
|
692
|
+
total, include_pdf=include_pdf, epsilon=epsilon
|
|
693
|
+
),
|
|
694
|
+
successful=DistributionSummary.from_values(
|
|
695
|
+
successful_arr, include_pdf=include_pdf, epsilon=epsilon
|
|
696
|
+
),
|
|
697
|
+
incomplete=DistributionSummary.from_values(
|
|
698
|
+
incomplete_arr, include_pdf=include_pdf, epsilon=epsilon
|
|
699
|
+
),
|
|
700
|
+
errored=DistributionSummary.from_values(
|
|
701
|
+
errored_arr, include_pdf=include_pdf, epsilon=epsilon
|
|
702
|
+
),
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
@classmethod
|
|
706
|
+
def from_values_function(
|
|
707
|
+
cls,
|
|
708
|
+
function: Callable[
|
|
709
|
+
[FunctionObjT],
|
|
710
|
+
float | tuple[float, float] | Sequence[float | tuple[float, float]] | None,
|
|
711
|
+
],
|
|
712
|
+
successful: Sequence[FunctionObjT],
|
|
713
|
+
incomplete: Sequence[FunctionObjT],
|
|
714
|
+
errored: Sequence[FunctionObjT],
|
|
715
|
+
include_pdf: bool | int = False,
|
|
716
|
+
epsilon: float = 1e-6,
|
|
717
|
+
) -> StatusDistributionSummary:
|
|
718
|
+
"""
|
|
719
|
+
Create distribution summary by extracting values from objects via function.
|
|
720
|
+
|
|
721
|
+
:param function: Function to extract value(s) from each object
|
|
722
|
+
:param successful: Successful request objects
|
|
723
|
+
:param incomplete: Incomplete request objects
|
|
724
|
+
:param errored: Errored request objects
|
|
725
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
726
|
+
:param epsilon: Tolerance for probability validation
|
|
727
|
+
:return: Status breakdown of distribution summaries
|
|
728
|
+
"""
|
|
729
|
+
|
|
730
|
+
def _extract_values(
|
|
731
|
+
_objs: Sequence[FunctionObjT],
|
|
732
|
+
) -> Sequence[float | tuple[float, float]]:
|
|
733
|
+
_outputs: list[float | tuple[float, float]] = []
|
|
734
|
+
for _obj in _objs:
|
|
735
|
+
if (_result := function(_obj)) is None:
|
|
736
|
+
continue
|
|
737
|
+
if isinstance(_result, Sequence) and not isinstance(_result, tuple):
|
|
738
|
+
_outputs.extend(_result)
|
|
739
|
+
else:
|
|
740
|
+
_outputs.append(_result)
|
|
741
|
+
return _outputs
|
|
742
|
+
|
|
743
|
+
return cls.from_values(
|
|
744
|
+
successful=_extract_values(successful),
|
|
745
|
+
incomplete=_extract_values(incomplete),
|
|
746
|
+
errored=_extract_values(errored),
|
|
747
|
+
include_pdf=include_pdf,
|
|
748
|
+
epsilon=epsilon,
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
@classmethod
|
|
752
|
+
def rate_distribution_from_timings(
|
|
753
|
+
cls,
|
|
754
|
+
successful: Sequence[float | tuple[float, float]] | np.ndarray,
|
|
755
|
+
incomplete: Sequence[float | tuple[float, float]] | np.ndarray,
|
|
756
|
+
errored: Sequence[float | tuple[float, float]] | np.ndarray,
|
|
757
|
+
start_time: float | None = None,
|
|
758
|
+
end_time: float | None = None,
|
|
759
|
+
threshold: float | None = 1e-4,
|
|
760
|
+
include_pdf: bool | int = False,
|
|
761
|
+
epsilon: float = 1e-6,
|
|
762
|
+
) -> StatusDistributionSummary:
|
|
763
|
+
"""
|
|
764
|
+
Create status-broken-down rate distribution from event timestamps.
|
|
765
|
+
|
|
766
|
+
:param successful: Timestamps for successful request events
|
|
767
|
+
:param incomplete: Timestamps for incomplete request events
|
|
768
|
+
:param errored: Timestamps for errored request events
|
|
769
|
+
:param start_time: Analysis window start
|
|
770
|
+
:param end_time: Analysis window end
|
|
771
|
+
:param threshold: Time threshold for merging nearby events
|
|
772
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
773
|
+
:param epsilon: Tolerance for probability validation
|
|
774
|
+
:return: Status breakdown of rate distribution summaries
|
|
775
|
+
"""
|
|
776
|
+
total, successful_arr, incomplete_arr, errored_arr = cls._combine_status_arrays(
|
|
777
|
+
successful, incomplete, errored, num_values_per_item=2
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
return StatusDistributionSummary(
|
|
781
|
+
total=DistributionSummary.rate_distribution_from_timings(
|
|
782
|
+
total,
|
|
783
|
+
start_time=start_time,
|
|
784
|
+
end_time=end_time,
|
|
785
|
+
threshold=threshold,
|
|
786
|
+
include_pdf=include_pdf,
|
|
787
|
+
epsilon=epsilon,
|
|
788
|
+
),
|
|
789
|
+
successful=DistributionSummary.rate_distribution_from_timings(
|
|
790
|
+
successful_arr,
|
|
791
|
+
start_time=start_time,
|
|
792
|
+
end_time=end_time,
|
|
793
|
+
threshold=threshold,
|
|
794
|
+
include_pdf=include_pdf,
|
|
795
|
+
epsilon=epsilon,
|
|
796
|
+
),
|
|
797
|
+
incomplete=DistributionSummary.rate_distribution_from_timings(
|
|
798
|
+
incomplete_arr,
|
|
799
|
+
start_time=start_time,
|
|
800
|
+
end_time=end_time,
|
|
801
|
+
threshold=threshold,
|
|
802
|
+
include_pdf=include_pdf,
|
|
803
|
+
epsilon=epsilon,
|
|
804
|
+
),
|
|
805
|
+
errored=DistributionSummary.rate_distribution_from_timings(
|
|
806
|
+
errored_arr,
|
|
807
|
+
start_time=start_time,
|
|
808
|
+
end_time=end_time,
|
|
809
|
+
threshold=threshold,
|
|
810
|
+
include_pdf=include_pdf,
|
|
811
|
+
epsilon=epsilon,
|
|
812
|
+
),
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
@classmethod
|
|
816
|
+
def rate_distribution_from_timings_function(
|
|
817
|
+
cls,
|
|
818
|
+
function: Callable[
|
|
819
|
+
[FunctionObjT],
|
|
820
|
+
float | tuple[float, float] | Sequence[float | tuple[float, float]] | None,
|
|
821
|
+
],
|
|
822
|
+
successful: Sequence[FunctionObjT],
|
|
823
|
+
incomplete: Sequence[FunctionObjT],
|
|
824
|
+
errored: Sequence[FunctionObjT],
|
|
825
|
+
start_time: float | None = None,
|
|
826
|
+
end_time: float | None = None,
|
|
827
|
+
threshold: float | None = 1e-4,
|
|
828
|
+
include_pdf: bool | int = False,
|
|
829
|
+
epsilon: float = 1e-6,
|
|
830
|
+
) -> StatusDistributionSummary:
|
|
831
|
+
"""
|
|
832
|
+
Create rate distribution by extracting timestamps from objects via function.
|
|
833
|
+
|
|
834
|
+
:param function: Function to extract timestamp(s) from each object
|
|
835
|
+
:param successful: Successful request objects
|
|
836
|
+
:param incomplete: Incomplete request objects
|
|
837
|
+
:param errored: Errored request objects
|
|
838
|
+
:param start_time: Analysis window start
|
|
839
|
+
:param end_time: Analysis window end
|
|
840
|
+
:param threshold: Time threshold for merging nearby events
|
|
841
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
842
|
+
:param epsilon: Tolerance for probability validation
|
|
843
|
+
:return: Status breakdown of rate distribution summaries
|
|
844
|
+
"""
|
|
845
|
+
|
|
846
|
+
def _extract_values(
|
|
847
|
+
_objs: Sequence[FunctionObjT],
|
|
848
|
+
) -> Sequence[float | tuple[float, float]]:
|
|
849
|
+
_outputs: list[float | tuple[float, float]] = []
|
|
850
|
+
for _obj in _objs:
|
|
851
|
+
if (_result := function(_obj)) is None:
|
|
852
|
+
continue
|
|
853
|
+
if isinstance(_result, Sequence) and not isinstance(_result, tuple):
|
|
854
|
+
_outputs.extend(_result)
|
|
855
|
+
else:
|
|
856
|
+
_outputs.append(_result)
|
|
857
|
+
return _outputs
|
|
858
|
+
|
|
859
|
+
return cls.rate_distribution_from_timings(
|
|
860
|
+
successful=_extract_values(successful),
|
|
861
|
+
incomplete=_extract_values(incomplete),
|
|
862
|
+
errored=_extract_values(errored),
|
|
863
|
+
start_time=start_time,
|
|
864
|
+
end_time=end_time,
|
|
865
|
+
threshold=threshold,
|
|
866
|
+
include_pdf=include_pdf,
|
|
867
|
+
epsilon=epsilon,
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
@classmethod
|
|
871
|
+
def concurrency_distribution_from_timings(
|
|
872
|
+
cls,
|
|
873
|
+
successful: Sequence[tuple[float, float] | tuple[float, float, float]]
|
|
874
|
+
| np.ndarray,
|
|
875
|
+
incomplete: Sequence[tuple[float, float] | tuple[float, float, float]]
|
|
876
|
+
| np.ndarray,
|
|
877
|
+
errored: Sequence[tuple[float, float] | tuple[float, float, float]]
|
|
878
|
+
| np.ndarray,
|
|
879
|
+
start_time: float | None = None,
|
|
880
|
+
end_time: float | None = None,
|
|
881
|
+
threshold: float | None = 1e-4,
|
|
882
|
+
include_pdf: bool | int = False,
|
|
883
|
+
epsilon: float = 1e-6,
|
|
884
|
+
) -> StatusDistributionSummary:
|
|
885
|
+
"""
|
|
886
|
+
Create status-broken-down concurrency distribution from event intervals.
|
|
887
|
+
|
|
888
|
+
:param successful: Event intervals for successful requests
|
|
889
|
+
:param incomplete: Event intervals for incomplete requests
|
|
890
|
+
:param errored: Event intervals for errored requests
|
|
891
|
+
:param start_time: Analysis window start
|
|
892
|
+
:param end_time: Analysis window end
|
|
893
|
+
:param threshold: Time threshold for merging nearby transitions
|
|
894
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
895
|
+
:param epsilon: Tolerance for probability validation
|
|
896
|
+
:return: Status breakdown of concurrency distribution summaries
|
|
897
|
+
"""
|
|
898
|
+
total, successful_arr, incomplete_arr, errored_arr = cls._combine_status_arrays(
|
|
899
|
+
successful, incomplete, errored, num_values_per_item=3
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
return StatusDistributionSummary(
|
|
903
|
+
total=DistributionSummary.concurrency_distribution_from_timings(
|
|
904
|
+
total,
|
|
905
|
+
start_time=start_time,
|
|
906
|
+
end_time=end_time,
|
|
907
|
+
threshold=threshold,
|
|
908
|
+
include_pdf=include_pdf,
|
|
909
|
+
epsilon=epsilon,
|
|
910
|
+
),
|
|
911
|
+
successful=DistributionSummary.concurrency_distribution_from_timings(
|
|
912
|
+
successful_arr,
|
|
913
|
+
start_time=start_time,
|
|
914
|
+
end_time=end_time,
|
|
915
|
+
threshold=threshold,
|
|
916
|
+
include_pdf=include_pdf,
|
|
917
|
+
epsilon=epsilon,
|
|
918
|
+
),
|
|
919
|
+
incomplete=DistributionSummary.concurrency_distribution_from_timings(
|
|
920
|
+
incomplete_arr,
|
|
921
|
+
start_time=start_time,
|
|
922
|
+
end_time=end_time,
|
|
923
|
+
threshold=threshold,
|
|
924
|
+
include_pdf=include_pdf,
|
|
925
|
+
epsilon=epsilon,
|
|
926
|
+
),
|
|
927
|
+
errored=DistributionSummary.concurrency_distribution_from_timings(
|
|
928
|
+
errored_arr,
|
|
929
|
+
start_time=start_time,
|
|
930
|
+
end_time=end_time,
|
|
931
|
+
threshold=threshold,
|
|
932
|
+
include_pdf=include_pdf,
|
|
933
|
+
epsilon=epsilon,
|
|
934
|
+
),
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
@classmethod
|
|
938
|
+
def concurrency_distribution_from_timings_function(
|
|
939
|
+
cls,
|
|
940
|
+
function: Callable[
|
|
941
|
+
[FunctionObjT],
|
|
942
|
+
tuple[float, float]
|
|
943
|
+
| tuple[float, float, float]
|
|
944
|
+
| Sequence[tuple[float, float] | tuple[float, float, float]]
|
|
945
|
+
| None,
|
|
946
|
+
],
|
|
947
|
+
successful: Sequence[FunctionObjT],
|
|
948
|
+
incomplete: Sequence[FunctionObjT],
|
|
949
|
+
errored: Sequence[FunctionObjT],
|
|
950
|
+
start_time: float | None = None,
|
|
951
|
+
end_time: float | None = None,
|
|
952
|
+
threshold: float | None = 1e-4,
|
|
953
|
+
include_pdf: bool | int = False,
|
|
954
|
+
epsilon: float = 1e-6,
|
|
955
|
+
) -> StatusDistributionSummary:
|
|
956
|
+
"""
|
|
957
|
+
Create concurrency distribution by extracting intervals from objects.
|
|
958
|
+
|
|
959
|
+
:param function: Function to extract time interval(s) from each object
|
|
960
|
+
:param successful: Successful request objects
|
|
961
|
+
:param incomplete: Incomplete request objects
|
|
962
|
+
:param errored: Errored request objects
|
|
963
|
+
:param start_time: Analysis window start
|
|
964
|
+
:param end_time: Analysis window end
|
|
965
|
+
:param threshold: Time threshold for merging nearby transitions
|
|
966
|
+
:param include_pdf: Whether to include PDF; True for full, int for sampled size
|
|
967
|
+
:param epsilon: Tolerance for probability validation
|
|
968
|
+
:return: Status breakdown of concurrency distribution summaries
|
|
969
|
+
"""
|
|
970
|
+
|
|
971
|
+
def _extract_values(
|
|
972
|
+
_objs: Sequence[FunctionObjT],
|
|
973
|
+
) -> Sequence[tuple[float, float] | tuple[float, float, float]]:
|
|
974
|
+
_outputs: list[tuple[float, float] | tuple[float, float, float]] = []
|
|
975
|
+
for _obj in _objs:
|
|
976
|
+
if (_result := function(_obj)) is None:
|
|
977
|
+
continue
|
|
978
|
+
if isinstance(_result, Sequence) and not isinstance(_result, tuple):
|
|
979
|
+
_outputs.extend(_result)
|
|
980
|
+
else:
|
|
981
|
+
_outputs.append(_result)
|
|
982
|
+
return _outputs
|
|
983
|
+
|
|
984
|
+
return cls.concurrency_distribution_from_timings(
|
|
985
|
+
successful=_extract_values(successful),
|
|
986
|
+
incomplete=_extract_values(incomplete),
|
|
987
|
+
errored=_extract_values(errored),
|
|
988
|
+
start_time=start_time,
|
|
989
|
+
end_time=end_time,
|
|
990
|
+
threshold=threshold,
|
|
991
|
+
include_pdf=include_pdf,
|
|
992
|
+
epsilon=epsilon,
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
@classmethod
|
|
996
|
+
def _combine_status_arrays(
|
|
997
|
+
cls,
|
|
998
|
+
successful: Sequence[float | tuple[float, float] | tuple[float, float, float]]
|
|
999
|
+
| np.ndarray,
|
|
1000
|
+
incomplete: Sequence[float | tuple[float, float] | tuple[float, float, float]]
|
|
1001
|
+
| np.ndarray,
|
|
1002
|
+
errored: Sequence[float | tuple[float, float] | tuple[float, float, float]]
|
|
1003
|
+
| np.ndarray,
|
|
1004
|
+
num_values_per_item: Literal[2, 3],
|
|
1005
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
1006
|
+
successful_array = DistributionSummary._to_weighted_ndarray( # noqa: SLF001
|
|
1007
|
+
successful, num_values_per_item=num_values_per_item
|
|
1008
|
+
)
|
|
1009
|
+
incomplete_array = DistributionSummary._to_weighted_ndarray( # noqa: SLF001
|
|
1010
|
+
incomplete, num_values_per_item=num_values_per_item
|
|
1011
|
+
)
|
|
1012
|
+
errored_array = DistributionSummary._to_weighted_ndarray( # noqa: SLF001
|
|
1013
|
+
errored, num_values_per_item=num_values_per_item
|
|
1014
|
+
)
|
|
1015
|
+
total_array = np.concatenate(
|
|
1016
|
+
(successful_array, incomplete_array, errored_array), axis=0
|
|
1017
|
+
)
|
|
1018
|
+
return total_array, successful_array, incomplete_array, errored_array
|