Skip to content

Commit 18d1151

Browse files
authored
Speed up from_samples metric dispatch (#1408)
Each `from_samples` matches a sample's metric against ~40 `case Metric.X` arms, and every arm evaluates `Metric.X` through the slow `EnumType.__getattribute__`/`_name_map` machinery — once per case, per sample, per component, on every resampling tick. Profiling showed this to be a dominant CPU consumer (~45% of Python CPU at a realistic component count and sample rate). Resolve the matched members once into a plain holder class `_M` and match on `case _M.X`, so the patterns use ordinary (fast) attribute access. This roughly halves steady-state Python CPU in profiling (43 components @ 5 Hz); behaviour is unchanged.
2 parents 75cb7d1 + 388c8b8 commit 18d1151

2 files changed

Lines changed: 99 additions & 69 deletions

File tree

RELEASE_NOTES.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22

33
## Summary
44

5-
<!-- Here goes a general summary of what this release is about -->
6-
7-
## Upgrading
8-
9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
5+
Along with the new `tick_delay` resampler config option, this release also includes some performance improvements in the data pipeline.
106

117
## New Features
128

src/frequenz/sdk/microgrid/_old_component_data.py

Lines changed: 98 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,40 @@
3838

3939
_logger = logging.getLogger(__name__)
4040

41+
42+
class _M:
43+
"""Plain holder for the `Metric` members matched in `from_samples`.
44+
45+
`Metric` is an `Enum`, and `Enum.<MEMBER>` access goes through the slow
46+
`EnumType.__getattribute__`/`_name_map` machinery. The `from_samples`
47+
`match`/`case` blocks evaluate one such access *per case, per sample, per
48+
component, on every resampling tick* — which profiling showed to be a
49+
dominant CPU cost when consuming high-rate telemetry. Resolving the
50+
members once here, into a plain class, lets the `case _M.<MEMBER>`
51+
patterns use ordinary (fast) attribute access instead.
52+
"""
53+
54+
AC_ACTIVE_POWER = Metric.AC_ACTIVE_POWER
55+
AC_ACTIVE_POWER_PHASE_1 = Metric.AC_ACTIVE_POWER_PHASE_1
56+
AC_ACTIVE_POWER_PHASE_2 = Metric.AC_ACTIVE_POWER_PHASE_2
57+
AC_ACTIVE_POWER_PHASE_3 = Metric.AC_ACTIVE_POWER_PHASE_3
58+
AC_REACTIVE_POWER = Metric.AC_REACTIVE_POWER
59+
AC_REACTIVE_POWER_PHASE_1 = Metric.AC_REACTIVE_POWER_PHASE_1
60+
AC_REACTIVE_POWER_PHASE_2 = Metric.AC_REACTIVE_POWER_PHASE_2
61+
AC_REACTIVE_POWER_PHASE_3 = Metric.AC_REACTIVE_POWER_PHASE_3
62+
AC_CURRENT_PHASE_1 = Metric.AC_CURRENT_PHASE_1
63+
AC_CURRENT_PHASE_2 = Metric.AC_CURRENT_PHASE_2
64+
AC_CURRENT_PHASE_3 = Metric.AC_CURRENT_PHASE_3
65+
AC_VOLTAGE_PHASE_1_N = Metric.AC_VOLTAGE_PHASE_1_N
66+
AC_VOLTAGE_PHASE_2_N = Metric.AC_VOLTAGE_PHASE_2_N
67+
AC_VOLTAGE_PHASE_3_N = Metric.AC_VOLTAGE_PHASE_3_N
68+
AC_FREQUENCY = Metric.AC_FREQUENCY
69+
DC_POWER = Metric.DC_POWER
70+
BATTERY_SOC_PCT = Metric.BATTERY_SOC_PCT
71+
BATTERY_CAPACITY = Metric.BATTERY_CAPACITY
72+
BATTERY_TEMPERATURE = Metric.BATTERY_TEMPERATURE
73+
74+
4175
T = TypeVar("T", bound="ComponentData")
4276

4377
PhaseTuple: TypeAlias = tuple[float, float, float]
@@ -314,35 +348,35 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
314348

315349
for sample in samples.metric_samples:
316350
match sample.metric:
317-
case Metric.AC_ACTIVE_POWER:
351+
case _M.AC_ACTIVE_POWER:
318352
self.active_power = sample.as_single_value() or 0.0
319-
case Metric.AC_ACTIVE_POWER_PHASE_1:
353+
case _M.AC_ACTIVE_POWER_PHASE_1:
320354
active_power_per_phase[0] = sample.as_single_value() or 0.0
321-
case Metric.AC_ACTIVE_POWER_PHASE_2:
355+
case _M.AC_ACTIVE_POWER_PHASE_2:
322356
active_power_per_phase[1] = sample.as_single_value() or 0.0
323-
case Metric.AC_ACTIVE_POWER_PHASE_3:
357+
case _M.AC_ACTIVE_POWER_PHASE_3:
324358
active_power_per_phase[2] = sample.as_single_value() or 0.0
325-
case Metric.AC_REACTIVE_POWER_PHASE_1:
359+
case _M.AC_REACTIVE_POWER_PHASE_1:
326360
reactive_power_per_phase[0] = sample.as_single_value() or 0.0
327-
case Metric.AC_REACTIVE_POWER_PHASE_2:
361+
case _M.AC_REACTIVE_POWER_PHASE_2:
328362
reactive_power_per_phase[1] = sample.as_single_value() or 0.0
329-
case Metric.AC_REACTIVE_POWER_PHASE_3:
363+
case _M.AC_REACTIVE_POWER_PHASE_3:
330364
reactive_power_per_phase[2] = sample.as_single_value() or 0.0
331-
case Metric.AC_REACTIVE_POWER:
365+
case _M.AC_REACTIVE_POWER:
332366
self.reactive_power = sample.as_single_value() or 0.0
333-
case Metric.AC_CURRENT_PHASE_1:
367+
case _M.AC_CURRENT_PHASE_1:
334368
current_per_phase[0] = sample.as_single_value() or 0.0
335-
case Metric.AC_CURRENT_PHASE_2:
369+
case _M.AC_CURRENT_PHASE_2:
336370
current_per_phase[1] = sample.as_single_value() or 0.0
337-
case Metric.AC_CURRENT_PHASE_3:
371+
case _M.AC_CURRENT_PHASE_3:
338372
current_per_phase[2] = sample.as_single_value() or 0.0
339-
case Metric.AC_VOLTAGE_PHASE_1_N:
373+
case _M.AC_VOLTAGE_PHASE_1_N:
340374
voltage_per_phase[0] = sample.as_single_value() or 0.0
341-
case Metric.AC_VOLTAGE_PHASE_2_N:
375+
case _M.AC_VOLTAGE_PHASE_2_N:
342376
voltage_per_phase[1] = sample.as_single_value() or 0.0
343-
case Metric.AC_VOLTAGE_PHASE_3_N:
377+
case _M.AC_VOLTAGE_PHASE_3_N:
344378
voltage_per_phase[2] = sample.as_single_value() or 0.0
345-
case Metric.AC_FREQUENCY:
379+
case _M.AC_FREQUENCY:
346380
self.frequency = sample.as_single_value() or 0.0
347381
case unexpected:
348382
_logger.warning(
@@ -495,7 +529,7 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
495529
for sample in samples.metric_samples:
496530
value = sample.as_single_value() or 0.0
497531
match sample.metric:
498-
case Metric.BATTERY_SOC_PCT:
532+
case _M.BATTERY_SOC_PCT:
499533
self.soc = value
500534
if sample.bounds:
501535
# Update power bounds from the SOC metric bounds,
@@ -514,7 +548,7 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
514548
)
515549
self.soc_lower_bound = sample.bounds[0].lower or 0.0
516550
self.soc_upper_bound = sample.bounds[0].upper or 0.0
517-
case Metric.DC_POWER:
551+
case _M.DC_POWER:
518552
(
519553
self.power_inclusion_lower_bound,
520554
self.power_inclusion_upper_bound,
@@ -523,9 +557,9 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
523557
) = _bound_ranges_to_inclusion_exclusion(
524558
sample.bounds, "DC_POWER", sample
525559
)
526-
case Metric.BATTERY_CAPACITY:
560+
case _M.BATTERY_CAPACITY:
527561
self.capacity = value
528-
case Metric.BATTERY_TEMPERATURE:
562+
case _M.BATTERY_TEMPERATURE:
529563
self.temperature = value
530564
case unexpected:
531565
_logger.warning(
@@ -715,7 +749,7 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
715749
for sample in samples.metric_samples:
716750
value = sample.as_single_value() or 0.0
717751
match sample.metric:
718-
case Metric.AC_ACTIVE_POWER:
752+
case _M.AC_ACTIVE_POWER:
719753
self.active_power = value
720754
(
721755
self.active_power_inclusion_lower_bound,
@@ -725,33 +759,33 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
725759
) = _bound_ranges_to_inclusion_exclusion(
726760
sample.bounds, "AC_ACTIVE_POWER", sample
727761
)
728-
case Metric.AC_ACTIVE_POWER_PHASE_1:
762+
case _M.AC_ACTIVE_POWER_PHASE_1:
729763
active_power_per_phase[0] = value
730-
case Metric.AC_ACTIVE_POWER_PHASE_2:
764+
case _M.AC_ACTIVE_POWER_PHASE_2:
731765
active_power_per_phase[1] = value
732-
case Metric.AC_ACTIVE_POWER_PHASE_3:
766+
case _M.AC_ACTIVE_POWER_PHASE_3:
733767
active_power_per_phase[2] = value
734-
case Metric.AC_REACTIVE_POWER:
768+
case _M.AC_REACTIVE_POWER:
735769
self.reactive_power = value
736-
case Metric.AC_REACTIVE_POWER_PHASE_1:
770+
case _M.AC_REACTIVE_POWER_PHASE_1:
737771
reactive_power_per_phase[0] = value
738-
case Metric.AC_REACTIVE_POWER_PHASE_2:
772+
case _M.AC_REACTIVE_POWER_PHASE_2:
739773
reactive_power_per_phase[1] = value
740-
case Metric.AC_REACTIVE_POWER_PHASE_3:
774+
case _M.AC_REACTIVE_POWER_PHASE_3:
741775
reactive_power_per_phase[2] = value
742-
case Metric.AC_CURRENT_PHASE_1:
776+
case _M.AC_CURRENT_PHASE_1:
743777
current_per_phase[0] = value
744-
case Metric.AC_CURRENT_PHASE_2:
778+
case _M.AC_CURRENT_PHASE_2:
745779
current_per_phase[1] = value
746-
case Metric.AC_CURRENT_PHASE_3:
780+
case _M.AC_CURRENT_PHASE_3:
747781
current_per_phase[2] = value
748-
case Metric.AC_VOLTAGE_PHASE_1_N:
782+
case _M.AC_VOLTAGE_PHASE_1_N:
749783
voltage_per_phase[0] = value
750-
case Metric.AC_VOLTAGE_PHASE_2_N:
784+
case _M.AC_VOLTAGE_PHASE_2_N:
751785
voltage_per_phase[1] = value
752-
case Metric.AC_VOLTAGE_PHASE_3_N:
786+
case _M.AC_VOLTAGE_PHASE_3_N:
753787
voltage_per_phase[2] = value
754-
case Metric.AC_FREQUENCY:
788+
case _M.AC_FREQUENCY:
755789
self.frequency = value
756790
case unexpected:
757791
_logger.warning(
@@ -974,7 +1008,7 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
9741008
for sample in samples.metric_samples:
9751009
value = sample.as_single_value() or 0.0
9761010
match sample.metric:
977-
case Metric.AC_ACTIVE_POWER:
1011+
case _M.AC_ACTIVE_POWER:
9781012
self.active_power = value
9791013
(
9801014
self.active_power_inclusion_lower_bound,
@@ -984,33 +1018,33 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
9841018
) = _bound_ranges_to_inclusion_exclusion(
9851019
sample.bounds, "AC_ACTIVE_POWER", sample
9861020
)
987-
case Metric.AC_ACTIVE_POWER_PHASE_1:
1021+
case _M.AC_ACTIVE_POWER_PHASE_1:
9881022
active_power_per_phase[0] = value
989-
case Metric.AC_ACTIVE_POWER_PHASE_2:
1023+
case _M.AC_ACTIVE_POWER_PHASE_2:
9901024
active_power_per_phase[1] = value
991-
case Metric.AC_ACTIVE_POWER_PHASE_3:
1025+
case _M.AC_ACTIVE_POWER_PHASE_3:
9921026
active_power_per_phase[2] = value
993-
case Metric.AC_REACTIVE_POWER:
1027+
case _M.AC_REACTIVE_POWER:
9941028
self.reactive_power = value
995-
case Metric.AC_REACTIVE_POWER_PHASE_1:
1029+
case _M.AC_REACTIVE_POWER_PHASE_1:
9961030
reactive_power_per_phase[0] = value
997-
case Metric.AC_REACTIVE_POWER_PHASE_2:
1031+
case _M.AC_REACTIVE_POWER_PHASE_2:
9981032
reactive_power_per_phase[1] = value
999-
case Metric.AC_REACTIVE_POWER_PHASE_3:
1033+
case _M.AC_REACTIVE_POWER_PHASE_3:
10001034
reactive_power_per_phase[2] = value
1001-
case Metric.AC_CURRENT_PHASE_1:
1035+
case _M.AC_CURRENT_PHASE_1:
10021036
current_per_phase[0] = value
1003-
case Metric.AC_CURRENT_PHASE_2:
1037+
case _M.AC_CURRENT_PHASE_2:
10041038
current_per_phase[1] = value
1005-
case Metric.AC_CURRENT_PHASE_3:
1039+
case _M.AC_CURRENT_PHASE_3:
10061040
current_per_phase[2] = value
1007-
case Metric.AC_VOLTAGE_PHASE_1_N:
1041+
case _M.AC_VOLTAGE_PHASE_1_N:
10081042
voltage_per_phase[0] = value
1009-
case Metric.AC_VOLTAGE_PHASE_2_N:
1043+
case _M.AC_VOLTAGE_PHASE_2_N:
10101044
voltage_per_phase[1] = value
1011-
case Metric.AC_VOLTAGE_PHASE_3_N:
1045+
case _M.AC_VOLTAGE_PHASE_3_N:
10121046
voltage_per_phase[2] = value
1013-
case Metric.AC_FREQUENCY:
1047+
case _M.AC_FREQUENCY:
10141048
self.frequency = value
10151049
case unexpected:
10161050
_logger.warning(
@@ -1234,7 +1268,7 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
12341268
for sample in samples.metric_samples:
12351269
value = sample.as_single_value() or 0.0
12361270
match sample.metric:
1237-
case Metric.AC_ACTIVE_POWER:
1271+
case _M.AC_ACTIVE_POWER:
12381272
self.active_power = value
12391273
(
12401274
self.active_power_inclusion_lower_bound,
@@ -1244,33 +1278,33 @@ def from_samples(cls, samples: ComponentDataSamples) -> Self:
12441278
) = _bound_ranges_to_inclusion_exclusion(
12451279
sample.bounds, "AC_ACTIVE_POWER", sample
12461280
)
1247-
case Metric.AC_ACTIVE_POWER_PHASE_1:
1281+
case _M.AC_ACTIVE_POWER_PHASE_1:
12481282
active_power_per_phase[0] = value
1249-
case Metric.AC_ACTIVE_POWER_PHASE_2:
1283+
case _M.AC_ACTIVE_POWER_PHASE_2:
12501284
active_power_per_phase[1] = value
1251-
case Metric.AC_ACTIVE_POWER_PHASE_3:
1285+
case _M.AC_ACTIVE_POWER_PHASE_3:
12521286
active_power_per_phase[2] = value
1253-
case Metric.AC_REACTIVE_POWER:
1287+
case _M.AC_REACTIVE_POWER:
12541288
self.reactive_power = value
1255-
case Metric.AC_REACTIVE_POWER_PHASE_1:
1289+
case _M.AC_REACTIVE_POWER_PHASE_1:
12561290
reactive_power_per_phase[0] = value
1257-
case Metric.AC_REACTIVE_POWER_PHASE_2:
1291+
case _M.AC_REACTIVE_POWER_PHASE_2:
12581292
reactive_power_per_phase[1] = value
1259-
case Metric.AC_REACTIVE_POWER_PHASE_3:
1293+
case _M.AC_REACTIVE_POWER_PHASE_3:
12601294
reactive_power_per_phase[2] = value
1261-
case Metric.AC_CURRENT_PHASE_1:
1295+
case _M.AC_CURRENT_PHASE_1:
12621296
current_per_phase[0] = value
1263-
case Metric.AC_CURRENT_PHASE_2:
1297+
case _M.AC_CURRENT_PHASE_2:
12641298
current_per_phase[1] = value
1265-
case Metric.AC_CURRENT_PHASE_3:
1299+
case _M.AC_CURRENT_PHASE_3:
12661300
current_per_phase[2] = value
1267-
case Metric.AC_VOLTAGE_PHASE_1_N:
1301+
case _M.AC_VOLTAGE_PHASE_1_N:
12681302
voltage_per_phase[0] = value
1269-
case Metric.AC_VOLTAGE_PHASE_2_N:
1303+
case _M.AC_VOLTAGE_PHASE_2_N:
12701304
voltage_per_phase[1] = value
1271-
case Metric.AC_VOLTAGE_PHASE_3_N:
1305+
case _M.AC_VOLTAGE_PHASE_3_N:
12721306
voltage_per_phase[2] = value
1273-
case Metric.AC_FREQUENCY:
1307+
case _M.AC_FREQUENCY:
12741308
self.frequency = value
12751309
case unexpected:
12761310
_logger.warning(

0 commit comments

Comments
 (0)