Speed up startup: defer heavy imports and cut redundant class setup work#956
Open
Carreau wants to merge 3 commits into
Open
Speed up startup: defer heavy imports and cut redundant class setup work#956Carreau wants to merge 3 commits into
Carreau wants to merge 3 commits into
Conversation
Optimize both import-time and runtime startup cost, which matters for applications (IPython, Jupyter, etc.) that import traitlets and build/ instantiate many HasTraits/Configurable/Application classes at startup. Import-time: traitlets and its core utils imported inspect (which pulls in ast, dis, tokenize, linecache), pathlib, and ast at module top level, and traitlets.config additionally imported logging.config (pulling logging.handlers, socket, pickle, dataclasses), pprint and json eagerly. All of these are only needed on cold paths (string parsing, filesystem Path/CRegExp traits, help/config-dump output, logging configuration at Application startup). Because the modules use `from __future__ import annotations`, annotations referring to these names are never evaluated at runtime, so the imports can be deferred to their actual (rare) use sites. inspect.isclass(x) is replaced with isinstance(x, type) and inspect.currentframe() with sys._getframe() to avoid needing inspect at all on the class-definition hot path. Runtime: - MetaHasDescriptors.setup_class already walks the full class namespace via getmembers(cls); it now returns that result so MetaHasTraits. setup_class can reuse it instead of doing a second dir()+getattr walk over every class. Same (name, value) pairs, identical semantics. - traits()/class_traits() normalize their metadata filters once instead of rebuilding a _SimpleTest wrapper for every trait on every call. Measured (Python 3.11): import traitlets ~28ms -> ~16ms, import traitlets.config ~47ms -> ~28ms, class definition ~118us -> ~96us/class, class_traits(config=True) ~7.5us -> ~4.2us/call. Full test suite (582 tests), mypy, and ruff all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaKDJ3fpGf7anQeYeBsJbk
Targets the runtime cost of constructing an Application and its many Configurable sub-components at startup (profiled on a synthetic 12-component, 373-trait app modeled on Jupyter/IPython scale). Three changes: 1. Memoize metadata-filtered class_traits()/traits() results per class. `config=True` filtering was the single largest startup cost (~25-30%), recomputed from scratch ~45x per startup (Application._classes_with_config_traits, KVArgParseConfigLoader._add_arguments, and each Configurable._load_config) for results that are static per class. class_traits/traits now delegate to a shared classmethod that caches the filtered dict per class and returns a .copy() (the existing "fresh dict" contract is preserved — the cached dict never escapes by reference). cls._traits is frozen after class creation, so the only staleness vector is a post-hoc metadata mutation via tag()/set_metadata(); those bump a module-level generation counter and stale cache entries are recomputed. Only constant (non-callable, hashable) filters are cached; callable predicates stay on the uncached path. class_traits(config=True): ~8.3us -> ~1.3us/call. 2. Skip the redundant re-validate pass in HasTraits.__init__. Constructor kwargs were validated twice: once via setattr in the fast loop, then again via _cross_validate + set_trait. The second pass is only needed for traits that actually have a cross-validator; for the common case with none it re-ran validate() on an already-validated value. Now guarded on the same condition _cross_validate itself uses, recording the coerced stored value for the notification. Instantiation with kwargs (no cross-validators): ~1.2-1.4x. 3. Configurable._load_config early-returns when no config applies to the instance, skipping traits(config=True) and an empty hold_trait_notifications() block for the many leaf Configurables that carry no matching config. Also removes a dead `section_names = self.section_names()` local that was computed (twice per instance) but never used. Measured on the synthetic app (Python 3.11): construct+initialize ~2196us -> ~1740us (~21%). Full test suite (583, incl. a new cache-invalidation regression test), mypy, and ruff all pass. Pickling, instance/subclass observers and validators, add_traits, and end-to-end Application config loading verified. Note for reviewers: the class_traits/traits cache is invalidated by tag() and set_metadata(), the supported post-construction metadata-mutation APIs. Mutating trait.metadata as a raw dict after the class has already been queried is not reflected until the next generation bump; this pattern is not used in traitlets and is vanishingly rare in practice, but is called out for awareness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaKDJ3fpGf7anQeYeBsJbk
Collapse the MetaHasTraits.setup_class signature onto a single line to satisfy the ruff-format pre-commit hook (Test Lint CI job). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VaKDJ3fpGf7anQeYeBsJbk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Optimize both import-time and runtime startup cost, which matters for
applications (IPython, Jupyter, etc.) that import traitlets and build/
instantiate many HasTraits/Configurable/Application classes at startup.
Import-time: traitlets and its core utils imported inspect (which pulls
in ast, dis, tokenize, linecache), pathlib, and ast at module top level,
and traitlets.config additionally imported logging.config (pulling
logging.handlers, socket, pickle, dataclasses), pprint and json eagerly.
All of these are only needed on cold paths (string parsing, filesystem
Path/CRegExp traits, help/config-dump output, logging configuration at
Application startup). Because the modules use
from __future__ import annotations, annotations referring to these names are never evaluated atruntime, so the imports can be deferred to their actual (rare) use sites.
inspect.isclass(x) is replaced with isinstance(x, type) and
inspect.currentframe() with sys._getframe() to avoid needing inspect at
all on the class-definition hot path.
Runtime:
via getmembers(cls); it now returns that result so MetaHasTraits.
setup_class can reuse it instead of doing a second dir()+getattr walk
over every class. Same (name, value) pairs, identical semantics.
of rebuilding a _SimpleTest wrapper for every trait on every call.
Measured (Python 3.11): import traitlets ~28ms -> ~16ms, import
traitlets.config ~47ms -> ~28ms, class definition ~118us -> ~96us/class,
class_traits(config=True) ~7.5us -> ~4.2us/call. Full test suite (582
tests), mypy, and ruff all pass.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01VaKDJ3fpGf7anQeYeBsJbk