diff --git a/src/toad/preferences.py b/src/toad/preferences.py index 9f6dd0bd1..bb58eb564 100644 --- a/src/toad/preferences.py +++ b/src/toad/preferences.py @@ -474,26 +474,33 @@ def practice_opening(cls, files: FilesRequest, directory: Path) -> None: if path.exists(): FieldCodec.decode(ToadSettings, FieldCodec.parse(path.read_text("utf-8"))) - def saved_key(self, path: DottedFieldPath) -> str: - owner = DeclaredField(self.state, path).owner_type - return next(key for declaration, key in FieldCodec.record_fields(owner) - if declaration.name == path.field_name) + def saved_keys(self, path: str) -> list[tuple[DottedFieldPath, str]]: + """Each setting or group directly below `path`, with its key in the file. + + They share one owner record, so its class is resolved once for the level: resolving + it again per field cost every launch 0.04 s of value resolution for the keys alone. + """ + below = self.state.direct_parameter_paths(path) + if not below: + return [] + owner = DeclaredField(self.state, below[0]).owner_type + keys = {declaration.name: key for declaration, key in FieldCodec.record_fields(owner)} + return [(field, keys[field.field_name]) for field in below] def keys_held(self, path: str, raw: object) -> list[DottedFieldPath]: """The settings and groups a document read from the file has a key for.""" if not isinstance(raw, dict): return [] held = [] - for below in self.state.direct_parameter_paths(path): - if (key := self.saved_key(below)) in raw: + for below, key in self.saved_keys(path): + if key in raw: held += [below, *self.keys_held(below.value, raw[key])] return held def kept(self, path: str) -> dict[str, JsonValue]: """The part of the whole encoded tree that belongs in the file.""" document: dict[str, JsonValue] = {} - for below in self.state.direct_parameter_paths(path): - key = self.saved_key(below) + for below, key in self.saved_keys(path): if self.state.has_parameter_descendants(below.value): part = self.kept(below.value) if part or below in self.user_set: diff --git a/tests/support/roots.py b/tests/support/roots.py index 333234599..170b7743b 100644 --- a/tests/support/roots.py +++ b/tests/support/roots.py @@ -108,5 +108,9 @@ def environment(self, inherited: dict[str, str]) -> dict[str, str]: "TERM": "xterm-256color", "COLORTERM": "truecolor", "FORCE_COLOR": "1", + # A test app publishes every 1 ms instead of 1/144 s. Each input still gets its + # own frame; a wheel walk that waited one 7 ms tick per event no longer does + # (the saved-history scroll test went from 36.5 s to 26.5 s). + "TEXTUAL_FPS": "1000", }) return env diff --git a/tests/support/template_server.py b/tests/support/template_server.py index 7285dab12..68ea99e3b 100644 --- a/tests/support/template_server.py +++ b/tests/support/template_server.py @@ -8,7 +8,9 @@ from dataclasses import dataclass import gc +import importlib import json +import pkgutil import os import socket import sys @@ -17,6 +19,7 @@ import traceback import pytest +import toad import support.workspace # noqa: F401 (the app, its fixtures and the comms package) from agent_comms.storage.locked_store import RootFile @@ -85,6 +88,10 @@ def serve() -> None: """Warm what every app test pays for, then fork each test and its helper spawner.""" # Every agent_comms module once, so a forked helper imports and walks nothing. RootFile.load_declarations() + # Every toad module once: the app imports its screens and widgets as it mounts them + # (51 modules, 0.21 s cold), and every test paid for the ones it reached. + for module in pkgutil.walk_packages(toad.__path__, "toad."): + importlib.import_module(module.name) # The file-preview terminal probe: 0.15 s of every app's start. Neither this process nor # a forked test has a terminal, so both detect the same answer. FileKind.prepare_terminal()