service-capacity-modeling 0.3.106__py3-none-any.whl → 0.3.108__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.
- service_capacity_modeling/models/plan_comparison.py +523 -0
- {service_capacity_modeling-0.3.106.dist-info → service_capacity_modeling-0.3.108.dist-info}/METADATA +1 -1
- {service_capacity_modeling-0.3.106.dist-info → service_capacity_modeling-0.3.108.dist-info}/RECORD +7 -6
- {service_capacity_modeling-0.3.106.dist-info → service_capacity_modeling-0.3.108.dist-info}/WHEEL +0 -0
- {service_capacity_modeling-0.3.106.dist-info → service_capacity_modeling-0.3.108.dist-info}/entry_points.txt +0 -0
- {service_capacity_modeling-0.3.106.dist-info → service_capacity_modeling-0.3.108.dist-info}/licenses/LICENSE +0 -0
- {service_capacity_modeling-0.3.106.dist-info → service_capacity_modeling-0.3.108.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""Plan comparison utilities and types for capacity planning.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to compare two capacity plans and determine
|
|
4
|
+
if they are roughly equivalent, with detailed explanations of any differences.
|
|
5
|
+
|
|
6
|
+
Example usage::
|
|
7
|
+
|
|
8
|
+
from service_capacity_modeling.models.plan_comparison import (
|
|
9
|
+
compare_plans,
|
|
10
|
+
ignore_resource,
|
|
11
|
+
lte,
|
|
12
|
+
plus_or_minus,
|
|
13
|
+
ResourceTolerances,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Get recommendation from planner
|
|
17
|
+
recommendations = planner.plan_certain(
|
|
18
|
+
model_name="org.netflix.cassandra",
|
|
19
|
+
region="us-east-1",
|
|
20
|
+
desires=desires,
|
|
21
|
+
)
|
|
22
|
+
recommendation = recommendations[0]
|
|
23
|
+
|
|
24
|
+
# Get current deployment as baseline using CapacityPlanner
|
|
25
|
+
# This uses model-specific cost methods for accurate comparison
|
|
26
|
+
baseline = planner.extract_baseline_plan(
|
|
27
|
+
model_name="org.netflix.cassandra",
|
|
28
|
+
region="us-east-1",
|
|
29
|
+
desires=desires, # must have current_clusters populated
|
|
30
|
+
extra_model_arguments={"copies_per_region": 3},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Compare with custom tolerances
|
|
34
|
+
result = compare_plans(
|
|
35
|
+
baseline,
|
|
36
|
+
recommendation,
|
|
37
|
+
tolerances=ResourceTolerances(
|
|
38
|
+
annual_cost=ignore_resource(), # Don't care about cost
|
|
39
|
+
cpu=lte(1.05), # CPU can be at most 5% over baseline
|
|
40
|
+
disk=plus_or_minus(0.10), # Storage within ±10%
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if result.is_equivalent:
|
|
45
|
+
print("Current capacity is sufficient")
|
|
46
|
+
else:
|
|
47
|
+
print("Capacity adjustments needed:")
|
|
48
|
+
for diff in result.get_out_of_tolerance():
|
|
49
|
+
print(f" - {diff}")
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from functools import lru_cache
|
|
53
|
+
from itertools import chain
|
|
54
|
+
|
|
55
|
+
from pydantic import ConfigDict
|
|
56
|
+
|
|
57
|
+
from service_capacity_modeling.enum_utils import enum_docstrings
|
|
58
|
+
from service_capacity_modeling.enum_utils import StrEnum
|
|
59
|
+
|
|
60
|
+
from service_capacity_modeling.interface import (
|
|
61
|
+
CapacityPlan,
|
|
62
|
+
default_reference_shape,
|
|
63
|
+
ExcludeUnsetModel,
|
|
64
|
+
Instance,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@enum_docstrings
|
|
69
|
+
class ResourceType(StrEnum):
|
|
70
|
+
"""Resource types that can be compared between capacity plans."""
|
|
71
|
+
|
|
72
|
+
annual_cost = "annual_cost"
|
|
73
|
+
"""Annual cost in dollars"""
|
|
74
|
+
|
|
75
|
+
cpu = "cpu"
|
|
76
|
+
"""CPU cores required"""
|
|
77
|
+
|
|
78
|
+
mem_gib = "mem_gib"
|
|
79
|
+
"""Memory in GiB"""
|
|
80
|
+
|
|
81
|
+
disk_gib = "disk_gib"
|
|
82
|
+
"""Disk storage in GiB"""
|
|
83
|
+
|
|
84
|
+
network_mbps = "network_mbps"
|
|
85
|
+
"""Network bandwidth in Mbps"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Tolerance for floating-point comparisons (e.g., exact_match)
|
|
89
|
+
_FLOAT_TOLERANCE = 1e-9
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Tolerance(ExcludeUnsetModel):
|
|
93
|
+
"""Tolerance bounds for comparison / baseline ratio.
|
|
94
|
+
|
|
95
|
+
The bounds define the acceptable range for the ratio:
|
|
96
|
+
- ratio > 1.0 = comparison exceeds baseline (need more)
|
|
97
|
+
- ratio < 1.0 = baseline exceeds comparison (have extra capacity)
|
|
98
|
+
- ratio = 1.0 = exact match
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
Tolerance(lower=0.0, upper=1.1) # lte(1.1) - ratio ≤ 1.1
|
|
102
|
+
Tolerance(lower=0.9, upper=float('inf')) # gte(0.9) - ratio ≥ 0.9
|
|
103
|
+
Tolerance(lower=0.9, upper=1.1) # tolerance(0.9, 1.1)
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
lower: float = 1.0
|
|
107
|
+
"""Lower bound for ratio (< 1.0 means can have extra capacity)"""
|
|
108
|
+
|
|
109
|
+
upper: float = 1.0
|
|
110
|
+
"""Upper bound for ratio (> 1.0 means requirement can exceed baseline)"""
|
|
111
|
+
|
|
112
|
+
model_config = ConfigDict(frozen=True)
|
|
113
|
+
|
|
114
|
+
def __contains__(self, ratio: float) -> bool:
|
|
115
|
+
"""Check if a ratio is within tolerance bounds.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
ratio: comparison / baseline
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if within bounds (equivalent), False otherwise
|
|
122
|
+
"""
|
|
123
|
+
return self.lower <= ratio <= self.upper
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# -----------------------------------------------------------------------------
|
|
127
|
+
# Tolerance helper functions
|
|
128
|
+
# -----------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@lru_cache(256)
|
|
132
|
+
def tolerance(lower: float, upper: float) -> Tolerance:
|
|
133
|
+
"""Create a tolerance with explicit ratio bounds.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
lower: Lower bound for ratio (e.g., 0.9 means comparison ≥ 0.9× baseline)
|
|
137
|
+
upper: Upper bound for ratio (e.g., 1.1 means comparison ≤ 1.1× baseline)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Tolerance with specified bounds
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> t = tolerance(0.9, 1.1) # Allow 0.9× to 1.1× baseline
|
|
144
|
+
>>> 0.95 in t # 0.95× baseline: True
|
|
145
|
+
>>> 1.05 in t # 1.05× baseline: True
|
|
146
|
+
>>> 1.15 in t # 1.15× baseline: False
|
|
147
|
+
"""
|
|
148
|
+
return Tolerance(lower=lower, upper=upper)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def lte(upper: float) -> Tolerance:
|
|
152
|
+
"""Ratio must be ≤ upper bound.
|
|
153
|
+
|
|
154
|
+
Use this to limit how much the requirement can exceed the baseline.
|
|
155
|
+
Any amount of extra capacity (ratio < 1.0) is acceptable.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
upper: Upper bound as a ratio (e.g., 1.1 means comparison ≤ 1.1× baseline).
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
With baseline=100:
|
|
162
|
+
|
|
163
|
+
>>> tol = lte(1.1) # ratio must be ≤ 1.1
|
|
164
|
+
|
|
165
|
+
comparison=110 → ratio = 1.1 → OK (at boundary)
|
|
166
|
+
comparison=111 → ratio = 1.11 → NOT OK
|
|
167
|
+
comparison=90 → ratio = 0.9 → OK (extra capacity)
|
|
168
|
+
"""
|
|
169
|
+
return tolerance(0.0, upper)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def gte(lower: float) -> Tolerance:
|
|
173
|
+
"""Ratio must be ≥ lower bound.
|
|
174
|
+
|
|
175
|
+
Use this to limit how much extra capacity is acceptable.
|
|
176
|
+
Any amount of requirement exceeding baseline (ratio > 1.0) is acceptable.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
lower: Lower bound as a ratio (e.g., 0.9 means comparison ≥ 0.9× baseline).
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
With baseline=100:
|
|
183
|
+
|
|
184
|
+
>>> tol = gte(0.9) # ratio must be ≥ 0.9
|
|
185
|
+
|
|
186
|
+
comparison=90 → ratio = 0.9 → OK (at boundary)
|
|
187
|
+
comparison=89 → ratio = 0.89 → NOT OK (too much extra)
|
|
188
|
+
comparison=110 → ratio = 1.1 → OK (need more)
|
|
189
|
+
"""
|
|
190
|
+
return tolerance(lower, float("inf"))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def plus_or_minus(percent: float) -> Tolerance:
|
|
194
|
+
"""Create a symmetric tolerance of ±percent around 1.0.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
percent: The tolerance as a decimal (e.g., 0.10 for ±10%).
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
>>> tol = plus_or_minus(0.10) # ratio must be 0.9 to 1.1
|
|
201
|
+
|
|
202
|
+
comparison=110, baseline=100 → ratio = 1.1 → OK (at boundary)
|
|
203
|
+
comparison=89, baseline=100 → ratio = 0.89 → NOT OK
|
|
204
|
+
"""
|
|
205
|
+
return tolerance(1.0 - percent, 1.0 + percent)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def exact_match() -> Tolerance:
|
|
209
|
+
"""Create a tolerance requiring exact match (ratio = 1.0)."""
|
|
210
|
+
return tolerance(1.0 - _FLOAT_TOLERANCE, 1.0 + _FLOAT_TOLERANCE)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def ignore_resource() -> Tolerance:
|
|
214
|
+
"""Create a tolerance that ignores a resource (any ratio acceptable)."""
|
|
215
|
+
return tolerance(0.0, float("inf"))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class ResourceTolerances(ExcludeUnsetModel):
|
|
219
|
+
"""Per-resource tolerance configuration for plan comparison.
|
|
220
|
+
|
|
221
|
+
Set specific tolerances to override for individual resources.
|
|
222
|
+
Unset resources fall back to `default`.
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> tolerances = ResourceTolerances(
|
|
226
|
+
... default=lte(1.1), # comparison ≤ 1.1× baseline
|
|
227
|
+
... cpu=lte(1.05), # Stricter for CPU
|
|
228
|
+
... annual_cost=ignore_resource(), # Don't care about cost
|
|
229
|
+
... )
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
default: Tolerance = plus_or_minus(0.10)
|
|
233
|
+
"""Default tolerance: ±10% (ratio must be 0.9× to 1.1× baseline)"""
|
|
234
|
+
|
|
235
|
+
annual_cost: Tolerance | None = None
|
|
236
|
+
"""Tolerance for annual cost comparison"""
|
|
237
|
+
|
|
238
|
+
cpu: Tolerance | None = None
|
|
239
|
+
"""Tolerance for CPU cores comparison"""
|
|
240
|
+
|
|
241
|
+
memory: Tolerance | None = None
|
|
242
|
+
"""Tolerance for memory (GiB) comparison"""
|
|
243
|
+
|
|
244
|
+
disk: Tolerance | None = None
|
|
245
|
+
"""Tolerance for disk (GiB) comparison"""
|
|
246
|
+
|
|
247
|
+
network: Tolerance | None = None
|
|
248
|
+
"""Tolerance for network (Mbps) comparison"""
|
|
249
|
+
|
|
250
|
+
def get_tolerance(self, resource: ResourceType) -> Tolerance:
|
|
251
|
+
"""Get tolerance for a resource type, falling back to default."""
|
|
252
|
+
match resource:
|
|
253
|
+
case ResourceType.annual_cost:
|
|
254
|
+
return self.annual_cost or self.default
|
|
255
|
+
case ResourceType.cpu:
|
|
256
|
+
return self.cpu or self.default
|
|
257
|
+
case ResourceType.mem_gib:
|
|
258
|
+
return self.memory or self.default
|
|
259
|
+
case ResourceType.disk_gib:
|
|
260
|
+
return self.disk or self.default
|
|
261
|
+
case ResourceType.network_mbps:
|
|
262
|
+
return self.network or self.default
|
|
263
|
+
case _:
|
|
264
|
+
raise ValueError(f"Unknown resource type: {resource}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class ResourceComparison(ExcludeUnsetModel):
|
|
268
|
+
"""Represents a comparison of a resource between baseline and comparison plans.
|
|
269
|
+
|
|
270
|
+
Properties:
|
|
271
|
+
- is_equivalent: ratio is within tolerance bounds
|
|
272
|
+
- exceeds_upper_bound: ratio > upper bound (need more than tolerance allows)
|
|
273
|
+
- exceeds_lower_bound: ratio < lower bound (have more extra than tolerance allows)
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
resource: ResourceType
|
|
277
|
+
"""Resource type being compared"""
|
|
278
|
+
|
|
279
|
+
baseline_value: float
|
|
280
|
+
"""Value from the current deployment"""
|
|
281
|
+
|
|
282
|
+
comparison_value: float
|
|
283
|
+
"""Value from the recommendation"""
|
|
284
|
+
|
|
285
|
+
tolerance: Tolerance
|
|
286
|
+
"""The tolerance bounds that were applied for this comparison"""
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def ratio(self) -> float:
|
|
290
|
+
"""Ratio: comparison / baseline.
|
|
291
|
+
|
|
292
|
+
- ratio > 1.0 = comparison exceeds baseline (need more)
|
|
293
|
+
- ratio < 1.0 = baseline exceeds comparison (have extra capacity)
|
|
294
|
+
- ratio = 1.0 = exact match
|
|
295
|
+
|
|
296
|
+
Examples:
|
|
297
|
+
- Baseline=100, Comparison=110 → ratio = 1.1 (need 10% more)
|
|
298
|
+
- Baseline=100, Comparison=90 → ratio = 0.9 (have 10% extra)
|
|
299
|
+
"""
|
|
300
|
+
if self.baseline_value == 0:
|
|
301
|
+
if self.comparison_value == 0:
|
|
302
|
+
return 1.0 # Both zero = exact match
|
|
303
|
+
return float("inf") # comparison > 0, baseline = 0
|
|
304
|
+
return self.comparison_value / self.baseline_value
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def is_equivalent(self) -> bool:
|
|
308
|
+
"""True if the ratio is within tolerance bounds."""
|
|
309
|
+
return self.ratio in self.tolerance
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def exceeds_lower_bound(self) -> bool:
|
|
313
|
+
"""True if ratio < lower bound (too much extra capacity)."""
|
|
314
|
+
return self.ratio < self.tolerance.lower
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def exceeds_upper_bound(self) -> bool:
|
|
318
|
+
"""True if ratio > upper bound (requirement exceeds tolerance)."""
|
|
319
|
+
return self.ratio > self.tolerance.upper
|
|
320
|
+
|
|
321
|
+
def __str__(self) -> str:
|
|
322
|
+
"""Human-readable explanation of the comparison."""
|
|
323
|
+
if self.tolerance.upper == float("inf"):
|
|
324
|
+
bounds_str = f"≥ {self.tolerance.lower:.2f}×"
|
|
325
|
+
elif self.tolerance.lower == 0.0:
|
|
326
|
+
bounds_str = f"≤ {self.tolerance.upper:.2f}×"
|
|
327
|
+
else:
|
|
328
|
+
bounds_str = f"{self.tolerance.lower:.2f}× to {self.tolerance.upper:.2f}×"
|
|
329
|
+
|
|
330
|
+
if self.is_equivalent:
|
|
331
|
+
return (
|
|
332
|
+
f"{self.resource.value}: {self.ratio:.2f}× "
|
|
333
|
+
f"(within tolerance: {bounds_str})"
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
bound = "lower" if self.exceeds_lower_bound else "upper"
|
|
337
|
+
return (
|
|
338
|
+
f"{self.resource.value}: exceeds {bound} bound, "
|
|
339
|
+
f"ratio={self.ratio:.2f}× (baseline={self.baseline_value:.2f}, "
|
|
340
|
+
f"comparison={self.comparison_value:.2f}, tolerance: {bounds_str})"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class PlanComparisonResult(ExcludeUnsetModel):
|
|
345
|
+
"""Result of comparing two capacity plans for equivalence."""
|
|
346
|
+
|
|
347
|
+
is_equivalent: bool
|
|
348
|
+
"""True if plans are within tolerance, False if significant differences"""
|
|
349
|
+
|
|
350
|
+
comparisons: dict[ResourceType, ResourceComparison] = {}
|
|
351
|
+
"""Resource comparisons keyed by resource type"""
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def cpu(self) -> ResourceComparison:
|
|
355
|
+
"""Get CPU comparison result."""
|
|
356
|
+
return self.comparisons[ResourceType.cpu]
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def memory(self) -> ResourceComparison:
|
|
360
|
+
"""Get memory comparison result."""
|
|
361
|
+
return self.comparisons[ResourceType.mem_gib]
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def disk(self) -> ResourceComparison:
|
|
365
|
+
"""Get disk comparison result."""
|
|
366
|
+
return self.comparisons[ResourceType.disk_gib]
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def network(self) -> ResourceComparison:
|
|
370
|
+
"""Get network comparison result."""
|
|
371
|
+
return self.comparisons[ResourceType.network_mbps]
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def annual_cost(self) -> ResourceComparison:
|
|
375
|
+
"""Get annual cost comparison result."""
|
|
376
|
+
return self.comparisons[ResourceType.annual_cost]
|
|
377
|
+
|
|
378
|
+
def get_out_of_tolerance(self) -> list[ResourceComparison]:
|
|
379
|
+
"""Get only comparisons that exceed tolerance bounds."""
|
|
380
|
+
return [c for c in self.comparisons.values() if not c.is_equivalent]
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def to_reference_cores(core_count: float, instance: Instance) -> float:
|
|
384
|
+
"""Convert instance cores to reference-equivalent cores.
|
|
385
|
+
|
|
386
|
+
This is the inverse of normalize_cores() from models.common. While
|
|
387
|
+
normalize_cores answers "how many target cores to match N reference cores",
|
|
388
|
+
this answers "how many reference cores is N instance cores equivalent to".
|
|
389
|
+
|
|
390
|
+
Mathematically equivalent to:
|
|
391
|
+
normalize_cores(core_count, target=default_reference_shape, reference=instance)
|
|
392
|
+
|
|
393
|
+
but returns float instead of ceiling int. We need float precision for
|
|
394
|
+
accurate ratio comparisons - ceiling would distort ratios by up to ~3%.
|
|
395
|
+
|
|
396
|
+
See normalize_cores() in models/common.py for the original implementation.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
core_count: Number of cores on the instance
|
|
400
|
+
instance: The instance shape (with cpu_ghz and cpu_ipc_scale)
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Equivalent cores on default_reference_shape (2.3 GHz, IPC 1.0)
|
|
404
|
+
|
|
405
|
+
Example:
|
|
406
|
+
# 32 cores on a 2.4 GHz instance = 33.4 reference cores
|
|
407
|
+
to_reference_cores(32, instance_at_2_4_ghz) # → 33.39
|
|
408
|
+
"""
|
|
409
|
+
instance_speed = instance.cpu_ghz * instance.cpu_ipc_scale
|
|
410
|
+
ref_speed = default_reference_shape.cpu_ghz * default_reference_shape.cpu_ipc_scale
|
|
411
|
+
return core_count * (instance_speed / ref_speed)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _aggregate_resources(plan: CapacityPlan) -> dict[ResourceType, float]:
|
|
415
|
+
"""Aggregate resource values from a plan, normalizing CPU to reference shape.
|
|
416
|
+
|
|
417
|
+
CPU is computed from candidate_clusters and normalized to default_reference_shape
|
|
418
|
+
using IPC and frequency factors. This ensures consistent comparison even when
|
|
419
|
+
plans use different instance types with varying CPU performance characteristics.
|
|
420
|
+
|
|
421
|
+
Memory, disk, and network are summed from requirements (no normalization needed).
|
|
422
|
+
"""
|
|
423
|
+
totals: dict[ResourceType, float] = {
|
|
424
|
+
ResourceType.cpu: 0.0,
|
|
425
|
+
ResourceType.mem_gib: 0.0,
|
|
426
|
+
ResourceType.disk_gib: 0.0,
|
|
427
|
+
ResourceType.network_mbps: 0.0,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# CPU: compute from candidate_clusters, normalized to reference shape
|
|
431
|
+
for cluster in chain(
|
|
432
|
+
plan.candidate_clusters.zonal, plan.candidate_clusters.regional
|
|
433
|
+
):
|
|
434
|
+
totals[ResourceType.cpu] += to_reference_cores(
|
|
435
|
+
cluster.instance.cpu * cluster.count, cluster.instance
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Other resources: sum from requirements
|
|
439
|
+
for req in chain(plan.requirements.zonal, plan.requirements.regional):
|
|
440
|
+
totals[ResourceType.mem_gib] += req.mem_gib.mid
|
|
441
|
+
totals[ResourceType.disk_gib] += req.disk_gib.mid
|
|
442
|
+
totals[ResourceType.network_mbps] += req.network_mbps.mid
|
|
443
|
+
|
|
444
|
+
return totals
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def compare_plans(
|
|
448
|
+
baseline: CapacityPlan,
|
|
449
|
+
comparison: CapacityPlan,
|
|
450
|
+
tolerances: ResourceTolerances | None = None,
|
|
451
|
+
) -> PlanComparisonResult:
|
|
452
|
+
"""Compare two capacity plans to determine if they are roughly equivalent.
|
|
453
|
+
|
|
454
|
+
This function compares plans across multiple dimensions (cost, CPU, memory,
|
|
455
|
+
disk, network) and determines if the differences are significant based on
|
|
456
|
+
the provided tolerance bounds.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
baseline: The reference plan (e.g., current production deployment)
|
|
460
|
+
comparison: The plan to compare against baseline (e.g., new recommendation)
|
|
461
|
+
tolerances: Per-resource tolerance configuration. If None, uses defaults.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
PlanComparisonResult containing:
|
|
465
|
+
- is_equivalent: True if all resources within tolerance, False otherwise
|
|
466
|
+
- differences: Dict of ResourceDifference for ALL resources (use
|
|
467
|
+
get_out_of_tolerance() to filter to only problematic ones)
|
|
468
|
+
|
|
469
|
+
Example:
|
|
470
|
+
>>> result = compare_plans(baseline, recommended)
|
|
471
|
+
>>> if result.cpu.exceeds_lower_bound:
|
|
472
|
+
... print("Current has excess CPU capacity")
|
|
473
|
+
>>> for diff in result.get_out_of_tolerance():
|
|
474
|
+
... print(diff) # Human-readable explanation
|
|
475
|
+
"""
|
|
476
|
+
if tolerances is None:
|
|
477
|
+
tolerances = ResourceTolerances()
|
|
478
|
+
|
|
479
|
+
baseline_cost = float(baseline.candidate_clusters.total_annual_cost)
|
|
480
|
+
comparison_cost = float(comparison.candidate_clusters.total_annual_cost)
|
|
481
|
+
baseline_resources = _aggregate_resources(baseline)
|
|
482
|
+
comparison_resources = _aggregate_resources(comparison)
|
|
483
|
+
|
|
484
|
+
def make_comparison(
|
|
485
|
+
resource: ResourceType, baseline_val: float, comparison_val: float
|
|
486
|
+
) -> ResourceComparison:
|
|
487
|
+
return ResourceComparison(
|
|
488
|
+
resource=resource,
|
|
489
|
+
baseline_value=baseline_val,
|
|
490
|
+
comparison_value=comparison_val,
|
|
491
|
+
tolerance=tolerances.get_tolerance(resource),
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
comparisons = {
|
|
495
|
+
ResourceType.annual_cost: make_comparison(
|
|
496
|
+
ResourceType.annual_cost, baseline_cost, comparison_cost
|
|
497
|
+
),
|
|
498
|
+
ResourceType.cpu: make_comparison(
|
|
499
|
+
ResourceType.cpu,
|
|
500
|
+
baseline_resources[ResourceType.cpu],
|
|
501
|
+
comparison_resources[ResourceType.cpu],
|
|
502
|
+
),
|
|
503
|
+
ResourceType.mem_gib: make_comparison(
|
|
504
|
+
ResourceType.mem_gib,
|
|
505
|
+
baseline_resources[ResourceType.mem_gib],
|
|
506
|
+
comparison_resources[ResourceType.mem_gib],
|
|
507
|
+
),
|
|
508
|
+
ResourceType.disk_gib: make_comparison(
|
|
509
|
+
ResourceType.disk_gib,
|
|
510
|
+
baseline_resources[ResourceType.disk_gib],
|
|
511
|
+
comparison_resources[ResourceType.disk_gib],
|
|
512
|
+
),
|
|
513
|
+
ResourceType.network_mbps: make_comparison(
|
|
514
|
+
ResourceType.network_mbps,
|
|
515
|
+
baseline_resources[ResourceType.network_mbps],
|
|
516
|
+
comparison_resources[ResourceType.network_mbps],
|
|
517
|
+
),
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return PlanComparisonResult(
|
|
521
|
+
is_equivalent=all(c.is_equivalent for c in comparisons.values()),
|
|
522
|
+
comparisons=comparisons,
|
|
523
|
+
)
|
{service_capacity_modeling-0.3.106.dist-info → service_capacity_modeling-0.3.108.dist-info}/RECORD
RENAMED
|
@@ -54,6 +54,7 @@ service_capacity_modeling/hardware/profiles/shapes/aws/manual_services.json,sha2
|
|
|
54
54
|
service_capacity_modeling/models/__init__.py,sha256=MbnmdVfxDJFVtS5d6GK567RSa5V086oEDX-tJtB68WA,15494
|
|
55
55
|
service_capacity_modeling/models/common.py,sha256=6a2ar_0lrrXKZtBAFKl7ETmd2Vp7lneH2C9eXUy0TBM,37713
|
|
56
56
|
service_capacity_modeling/models/headroom_strategy.py,sha256=rGo_d7nxkQDjx0_hIAXKKZAWnQDBtqZhc0eTMouVh8s,682
|
|
57
|
+
service_capacity_modeling/models/plan_comparison.py,sha256=3Ed4hcB6dVOmkUFtPFLJokW-b4TmI80QgvxbXItvmWQ,18096
|
|
57
58
|
service_capacity_modeling/models/utils.py,sha256=WlBaU9l11V5atThMTWDV9-FT1uf0FJS_2iyu8RF5NIk,2665
|
|
58
59
|
service_capacity_modeling/models/org/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
60
|
service_capacity_modeling/models/org/netflix/__init__.py,sha256=keaBt7dk6DB2VuRINdo8wRfsobK655Gfw3hYjruacJs,2553
|
|
@@ -84,9 +85,9 @@ service_capacity_modeling/tools/fetch_pricing.py,sha256=fO84h77cqiiIHF4hZt490Rwb
|
|
|
84
85
|
service_capacity_modeling/tools/generate_missing.py,sha256=F7YqvMJAV4nZc20GNrlIsnQSF8_77sLgwYZqc5k4LDg,3099
|
|
85
86
|
service_capacity_modeling/tools/instance_families.py,sha256=e5RuYkCLUITvsAazDH12B6KjX_PaBsv6Ne3mj0HK_sQ,9223
|
|
86
87
|
service_capacity_modeling/tools/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
|
-
service_capacity_modeling-0.3.
|
|
88
|
-
service_capacity_modeling-0.3.
|
|
89
|
-
service_capacity_modeling-0.3.
|
|
90
|
-
service_capacity_modeling-0.3.
|
|
91
|
-
service_capacity_modeling-0.3.
|
|
92
|
-
service_capacity_modeling-0.3.
|
|
88
|
+
service_capacity_modeling-0.3.108.dist-info/licenses/LICENSE,sha256=nl_Lt5v9VvJ-5lWJDT4ddKAG-VZ-2IaLmbzpgYDz2hU,11343
|
|
89
|
+
service_capacity_modeling-0.3.108.dist-info/METADATA,sha256=wF3U0J15kzsj0ZjgN5-7DjaM3qKxRZWmAq5w_Ip8_1k,10367
|
|
90
|
+
service_capacity_modeling-0.3.108.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
91
|
+
service_capacity_modeling-0.3.108.dist-info/entry_points.txt,sha256=ZsjzpG5SomWpT1zCE19n1uSXKH2gTI_yc33sdl0vmJg,146
|
|
92
|
+
service_capacity_modeling-0.3.108.dist-info/top_level.txt,sha256=H8XjTCLgR3enHq5t3bIbxt9SeUkUT8HT_SDv2dgIT_A,26
|
|
93
|
+
service_capacity_modeling-0.3.108.dist-info/RECORD,,
|
{service_capacity_modeling-0.3.106.dist-info → service_capacity_modeling-0.3.108.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|