]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
config: Cache lookups of fields
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 27 Mar 2025 10:43:23 +0000 (11:43 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 27 Mar 2025 11:13:10 +0000 (12:13 +0100)
We were calling inspect.signature() > 4000 times because it was evaluated
over and over again in the list comprehensions. Move the fields lookup into
a cached classmethod where possible to make sure we only do it once.

This reduces the time needed to parse the systemd config from history from
1.18s => 188ms on my machine.

mkosi/config.py

index 5d207e99f4b3de01038bc05e7886f76b8a50ae88..f0a9e684314f9f1598d0e8386f3449fc53dafe03 100644 (file)
@@ -1731,9 +1731,14 @@ class Args:
 
         return args
 
+    @classmethod
+    @functools.lru_cache(maxsize=1)
+    def fields(cls) -> set[str]:
+        return {f.name for f in dataclasses.fields(cls)}
+
     @classmethod
     def from_namespace(cls, ns: argparse.Namespace) -> "Args":
-        return cls(**{k: v for k, v in vars(ns).items() if k in inspect.signature(cls).parameters})
+        return cls(**{k: v for k, v in vars(ns).items() if k in cls.fields()})
 
     def to_dict(self) -> dict[str, Any]:
         return dataclasses.asdict(self, dict_factory=dict_with_capitalised_keys_factory)
@@ -1763,7 +1768,7 @@ class Args:
         for k, v in j.items():
             k = key_transformer(k)
 
-            if k not in inspect.signature(cls).parameters and (not isinstance(v, (dict, list, set)) or v):
+            if k not in cls.fields() and (not isinstance(v, (dict, list, set)) or v):
                 die(
                     f"Serialized JSON has unknown field {k} with value {v}",
                     hint="Re-running mkosi once with -f should solve the issue by re-generating the JSON",
@@ -1772,9 +1777,7 @@ class Args:
         value_transformer = json_type_transformer(cls)
         j = {(tk := key_transformer(k)): value_transformer(tk, v) for k, v in j.items()}
 
-        return dataclasses.replace(
-            cls.default(), **{k: v for k, v in j.items() if k in inspect.signature(cls).parameters}
-        )
+        return dataclasses.replace(cls.default(), **{k: v for k, v in j.items() if k in cls.fields()})
 
 
 PACKAGE_GLOBS = (
@@ -1842,9 +1845,8 @@ def make_simple_config_parser(
         for setting in settings:
             finalize_value(config, setting)
 
-        return valtype(
-            **{k: v for k, v in vars(config).items() if k in inspect.signature(valtype).parameters}
-        )
+        parameters = inspect.signature(valtype).parameters
+        return valtype(**{k: v for k, v in vars(config).items() if k in parameters})
 
     return parse_simple_config
 
@@ -2140,9 +2142,14 @@ class Config:
 
         return config
 
+    @classmethod
+    @functools.lru_cache(maxsize=1)
+    def fields(cls) -> set[str]:
+        return {f.name for f in dataclasses.fields(cls)}
+
     @classmethod
     def from_namespace(cls, ns: argparse.Namespace) -> "Config":
-        return cls(**{k: v for k, v in vars(ns).items() if k in inspect.signature(cls).parameters})
+        return cls(**{k: v for k, v in vars(ns).items() if k in cls.fields()})
 
     @property
     def output_with_format(self) -> str:
@@ -2334,7 +2341,7 @@ class Config:
         for k, v in j.items():
             k = key_transformer(k)
 
-            if k not in inspect.signature(cls).parameters and (not isinstance(v, (dict, list, set)) or v):
+            if k not in cls.fields() and (not isinstance(v, (dict, list, set)) or v):
                 die(
                     f"Serialized JSON has unknown field {k} with value {v}",
                     hint="Re-running mkosi once with -f should solve the issue by re-generating the JSON",
@@ -2343,9 +2350,7 @@ class Config:
         value_transformer = json_type_transformer(cls)
         j = {(tk := key_transformer(k)): value_transformer(tk, v) for k, v in j.items()}
 
-        return dataclasses.replace(
-            cls.default(), **{k: v for k, v in j.items() if k in inspect.signature(cls).parameters}
-        )
+        return dataclasses.replace(cls.default(), **{k: v for k, v in j.items() if k in cls.fields()})
 
     def find_binary(self, *names: PathString, tools: bool = True) -> Optional[Path]:
         return find_binary(*names, root=self.tools() if tools else Path("/"), extra=self.extra_search_paths)