From 1437458b13028a4b4c8844e3699a6903c4c04be1 Mon Sep 17 00:00:00 2001 From: Ioannis Papamanoglou Date: Fri, 6 Sep 2024 14:00:27 +0200 Subject: [PATCH] Picker: Perf test; Cache e-series (#41) --- src/faebryk/libs/e_series.py | 12 +++- src/faebryk/libs/picker/jlcpcb/jlcpcb.py | 4 +- src/faebryk/libs/test/times.py | 47 ++++++++++++++ test/core/test_performance.py | 29 +-------- test/libs/picker/test_jlcpcb.py | 81 ++++++++++++++++++++++++ 5 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 src/faebryk/libs/test/times.py diff --git a/src/faebryk/libs/e_series.py b/src/faebryk/libs/e_series.py index a3990712..0a77afd9 100644 --- a/src/faebryk/libs/e_series.py +++ b/src/faebryk/libs/e_series.py @@ -1,3 +1,4 @@ +import copy import logging from math import ceil, floor, log10 from typing import Tuple @@ -429,6 +430,9 @@ def repeat_set_over_base( class ParamNotResolvedError(Exception): ... +_e_series_cache: list[tuple[Parameter, int, set]] = [] + + def e_series_intersect[T: float | Quantity]( value: Parameter[T], e_series: E_SERIES = E_SERIES_VALUES.E_ALL ) -> F.Set[T]: @@ -436,6 +440,10 @@ def e_series_intersect[T: float | Quantity]( value = value.get_most_narrow() + for k, i, v in _e_series_cache: + if k == value and i == id(e_series): + return F.Set(v) + if isinstance(value, F.Constant): value = F.Range(value) elif isinstance(value, F.Set): @@ -476,7 +484,9 @@ def e_series_intersect[T: float | Quantity]( e_series_values = repeat_set_over_base( e_series, 10, range(floor(log10(min_val)), ceil(log10(max_val)) + 1) ) - return value & {e * unit for e in e_series_values} + out = value & {e * unit for e in e_series_values} + _e_series_cache.append((copy.copy(value), id(e_series), out.params)) + return out def e_series_discretize_to_nearest( diff --git a/src/faebryk/libs/picker/jlcpcb/jlcpcb.py b/src/faebryk/libs/picker/jlcpcb/jlcpcb.py index 0ddfddeb..9271c811 100644 --- a/src/faebryk/libs/picker/jlcpcb/jlcpcb.py +++ b/src/faebryk/libs/picker/jlcpcb/jlcpcb.py @@ -670,8 +670,8 @@ def is_db_up_to_date( ) def prompt_db_update(self, prompt: str = "Update JLCPCB database?") -> bool: - ans = input(prompt + " [Y/n]:").lower() - return ans == "y" or ans == "" + ans = input(prompt + " [y/N]:").lower() + return ans == "y" def download( self, diff --git a/src/faebryk/libs/test/times.py b/src/faebryk/libs/test/times.py new file mode 100644 index 00000000..16b1a78f --- /dev/null +++ b/src/faebryk/libs/test/times.py @@ -0,0 +1,47 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +import time +from textwrap import indent + + +class Times: + def __init__(self) -> None: + self.times = {} + self.last_time = time.time() + + def add(self, name: str): + now = time.time() + if name not in self.times: + self.times[name] = now - self.last_time + self.last_time = now + + def _format_val(self, val: float): + return f"{val * 1000:.2f}ms" + + def __repr__(self): + formatted = { + k: self._format_val(v) + for k, v in self.times.items() + if not k.startswith("_") + } + longest_name = max(len(k) for k in formatted) + return "Timings: \n" + indent( + "\n".join(f"{k:>{longest_name}}: {v:<10}" for k, v in formatted.items()), + " " * 4, + ) + + class Context: + def __init__(self, name: str, times: "Times"): + self.name = name + self.times = times + + def __enter__(self): + self.times.add("_" + self.name) + self.start = time.time() + + def __exit__(self, exc_type, exc_value, traceback): + self.times.times[self.name] = time.time() - self.start + + def context(self, name: str): + return Times.Context(name, self) diff --git a/test/core/test_performance.py b/test/core/test_performance.py index 0d378983..090f461c 100644 --- a/test/core/test_performance.py +++ b/test/core/test_performance.py @@ -4,7 +4,6 @@ import time import unittest from itertools import pairwise -from textwrap import indent from typing import Callable import faebryk.library._F as F @@ -13,36 +12,10 @@ from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.node import Node from faebryk.libs.library import L +from faebryk.libs.test.times import Times from faebryk.libs.util import times -class Times: - def __init__(self) -> None: - self.times = {} - self.last_time = time.time() - - def add(self, name: str): - now = time.time() - if name not in self.times: - self.times[name] = now - self.last_time - self.last_time = now - - def _format_val(self, val: float): - return f"{val * 1000:.2f}ms" - - def __repr__(self): - formatted = { - k: self._format_val(v) - for k, v in self.times.items() - if not k.startswith("_") - } - longest_name = max(len(k) for k in formatted) - return "Timings: \n" + indent( - "\n".join(f"{k:>{longest_name}}: {v:<10}" for k, v in formatted.items()), - " " * 4, - ) - - class TestPerformance(unittest.TestCase): def test_get_all(self): def _factory_simple_resistors(count: int): diff --git a/test/libs/picker/test_jlcpcb.py b/test/libs/picker/test_jlcpcb.py index f0fe6765..4e179074 100644 --- a/test/libs/picker/test_jlcpcb.py +++ b/test/libs/picker/test_jlcpcb.py @@ -13,6 +13,7 @@ from faebryk.libs.picker.jlcpcb.jlcpcb import JLCPCB_DB from faebryk.libs.picker.jlcpcb.pickers import add_jlcpcb_pickers from faebryk.libs.picker.picker import DescriptiveProperties, has_part_picked +from faebryk.libs.test.times import Times from faebryk.libs.units import P, Quantity logger = logging.getLogger(__name__) @@ -360,6 +361,86 @@ def tearDown(self): JLCPCB_DB.get().close() +@unittest.skipIf(not JLCPCB_DB.config.db_path.exists(), reason="Requires large db") +class TestPickerPerformanceJLCPCB(unittest.TestCase): + def test_simple_full(self): + # conclusions + # - first pick overall is slow, need to load sqlite into buffer cache + # - first pick of component type is slower than subsequent picks + # (even with different parameters) + # - component type order has no influence + # - component type speed differs a lot (res = 500ms, cap = 100ms) + # (even though both value based) + # e-series speed (query or count), if resistor with E24, 200ms + # still 2x though, maybe total count? + # - e-series intersect 20% execution time + # => optimized with cache + + timings = Times() + + def r_builder(resistance_kohm: float): + return F.Resistor().builder( + lambda r: ( + r.resistance.merge( + F.Range.from_center_rel(resistance_kohm * P.kohm, 0.1) + ), + r.rated_power.merge(F.ANY()), + r.rated_voltage.merge(F.ANY()), + ) + ) + + def c_builder(capacitance_pf: float): + return F.Capacitor().builder( + lambda c: ( + c.capacitance.merge( + F.Range.from_center_rel(capacitance_pf * P.pF, 0.1) + ), + c.rated_voltage.merge(F.ANY()), + c.temperature_coefficient.merge(F.ANY()), + ) + ) + + resistors = [r_builder(5 * (i + 1)) for i in range(5)] + [ + r_builder(5 * (i + 1)) for i in reversed(range(5)) + ] + caps = [c_builder(10 * (i + 1)) for i in range(5)] + [ + c_builder(10 * (i + 1)) for i in reversed(range(5)) + ] + resistors_10k = [r_builder(10) for _ in range(10)] + + mods = resistors + caps + resistors_10k + + for mod in mods: + add_jlcpcb_pickers(mod) + + with timings.context("resistors"): + for i, r in enumerate(resistors): + r.get_trait(F.has_picker).pick() + timings.add( + f"full pick value pick (resistor {i}:" + f" {r.resistance.as_unit_with_tolerance('ohm')})" + ) + + # cache is warm now, but also for non resistors? + with timings.context("capacitors"): + for i, c in enumerate(caps): + c.get_trait(F.has_picker).pick() + timings.add( + f"full pick value pick (capacitor {i}:" + f" {c.capacitance.as_unit_with_tolerance('F')})" + ) + + with timings.context("resistors_10k"): + for i, r in enumerate(resistors_10k): + r.get_trait(F.has_picker).pick() + timings.add( + f"full pick value pick (resistor {i}:" + f" {r.resistance.as_unit_with_tolerance('ohm')})" + ) + + print(timings) + + if __name__ == "__main__": setup_basic_logging() logger.setLevel(logging.DEBUG)