perf(theme): defer heavy imports until user actions need them#668
Open
thomwebb wants to merge 7 commits into
Open
perf(theme): defer heavy imports until user actions need them#668thomwebb wants to merge 7 commits into
thomwebb wants to merge 7 commits into
Conversation
added 4 commits
July 23, 2026 09:39
The theme plugin's register_callbacks.py eagerly imported eight sibling
modules at the top of the file — including .picker (prompt_toolkit
widgets), .themes (728 lines), and .prompt_toolkit_theme. None were
needed to satisfy the startup callback, whose only job is to apply the
default theme on a fresh install.
Move all sibling imports into the functions that actually use them.
The prompt_toolkit_style callback needed a small shim because the
callback registry captures the callable at registration time.
Verified via sys.modules snapshot: themes.py, picker.py,
prompt_toolkit_theme.py, and bundled_palettes.py no longer appear in
sys.modules after plugins.load_plugin_callbacks() completes.
Wall-clock impact is modest:
- Isolated register_callbacks import: ~30ms cold, ~10ms warm
(down from ~82ms cold / ~21ms warm)
- End-to-end --version: within measurement noise (~30ms)
The bigger win is architectural: the theme package no longer
transitively requires prompt_toolkit at plugin-discovery time, and
this establishes the lazy-import pattern for other plugins to follow.
Tests: retargeted white-box patches from register_callbacks.X to the
actual source modules (config.get_value, osc_palette.get_saved_palette,
themes.apply, etc.). All 121 theme-related tests pass.
Report: docs/STARTUP_PERFORMANCE.md documents the full analysis and
remaining priorities (P2 — skip plugin discovery on --version/--help —
is now clearly the biggest lever).
…h profiling
The original priorities were scored against `--version` timing, which
turned out to be a bad proxy for what humans actually experience.
Interactive-launch profiling shows:
- pypi.org version check is the biggest lever (0.8s median, up to 13-18s
when pypi is unreachable through the Walmart sysproxy). Elevated from
P5 to P1.
- The 'skip plugins on --version' fix (was P2) does NOT help interactive
users at all. Downgraded to P4.
- Per-plugin isolated-import profiling surfaced two bugs:
* claude_code_oauth pulls in the entire OpenAI SDK (557 modules)
* Multiple plugins (aws_bedrock, claude_code_oauth) transitively
load playwright via the tools registry
- pydantic_ai (~52 modules per plugin) is the highest-leverage single
target for lazy provider-SDK loading.
Adds:
- Revised priority table at the top of the report, superseding the old
P1-P5 ordering below.
- Per-plugin isolated-import profile with heavy-library breakdown.
- Documented methodology for interactive-launch and per-plugin
measurement.
The old sections are kept for context but marked as superseded.
CI caught one `with patch(...) as ...` block that ruff format wanted collapsed onto a single line. Local `ruff format .` before the initial commit missed this file because the format ran before the patch retargets landed.
The perf-plan doc is being moved out of this PR. The theme lazy-import code change (ccd59cb) is the only substantive change; key findings and follow-up context are now inlined in the PR description instead.
added 3 commits
July 23, 2026 10:29
The note referenced docs/STARTUP_PERFORMANCE.md (dropped from this PR) and quoted the ~267ms figure that was later corrected as measurement noise. Replace with a self-contained explanation that describes what the pattern does and why (avoid pulling in prompt_toolkit at plugin discovery + short-circuit on already-configured installs).
These two user-facing strings had gotten converted to a \U0001f3a8 Unicode escape sequence during the earlier refactor pass. Semantically identical at runtime, but gratuitously different from the upstream source style. Restore the literal character to keep this PR's diff focused on the actual import motions.
Fixes uncovered by an adversarial review pass: * Correct the module NOTE comment. The prior version claimed the theme plugin "no longer transitively requires prompt_toolkit at plugin discovery," which is factually false — theme/__init__.py eagerly imports .content_styles / .osc_palette / .rich_themes and calls reapply_from_config() on each, which drags in ~115 prompt_toolkit modules via code_puppy.messaging.rich_renderer. The rewrite scopes the win honestly (.picker + .themes + .prompt_toolkit_theme stay off the discovery path; "never loaded on the common launch path" softened to reflect that REPL renders still trigger them on first paint) and flags the __init__.py deferral as a follow-up. * Tighten _prompt_toolkit_style shim signature. The previous '*args, **kwargs' shape silently swallowed bogus calls that would have TypeError'd against the real callable. Replaced with an explicit '(style: BaseStyle | None = None) -> BaseStyle' signature using TYPE_CHECKING so no runtime import cost is added. Also preserves the real symbol name via __name__ assignment so code_puppy.callbacks failure logs remain useful. * Push imports in _apply_default_theme_on_first_run below the early-return check so the common case (theme already configured) doesn't bind unnecessary symbols. Consistent with the PR's philosophy. * Add two new unit tests: one verifying the shim delegates its single argument to merge_with_active_style with no silent widening, and one confirming __name__ still reports the real callback symbol.
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.
Summary
Move eight sibling imports in
code_puppy/plugins/theme/register_callbacks.pyfrom module top-level into the functions that actually use them, and
replace one eagerly-registered callback with a lazy shim.
The theme plugin's startup callback only needs to apply a default theme
on first run. Everything else —
.picker,.themes,.prompt_toolkit_theme,.bundled_palettes, plus a handful of heaviercode_puppyinternals — is only needed when the user runs/themeortriggers a render.
Measured impact
Honest numbers — this is a modest win:
register_callbacksisolated import (warm)register_callbacksisolated import (cold)--version(warm)The end-to-end saving is within measurement noise (~30 ms). Verified via a
sys.modulessnapshot afterplugins.load_plugin_callbacks()completes:Scope caveat (honest correction)
An earlier draft of this PR claimed the theme plugin "no longer
transitively pulls in
prompt_toolkitat plugin-discovery time." Thatclaim was false and has been retracted from the in-file comment and
this description.
Repro:
code_puppy/plugins/theme/__init__.pyeagerly imports.content_styles,.osc_palette, and.rich_themes, and callsreapply_from_config()oneach at package-import time. That path transitively pulls in
code_puppy.messaging.rich_renderer, which loads ~115prompt_toolkitmodules. Nothing this PR does to
register_callbacks.pycan shortcutthat — those modules are already resident by the time
register_callbacksis imported.
The measured 10-30 ms savings this PR delivers therefore come from
deferring
.picker(TUI widgets),.themes(large color-menu tables),and
.prompt_toolkit_theme— not from avoidingprompt_toolkitaltogether. A future PR could defer the
__init__.pyreapply sweep toa startup callback, which would recover the
prompt_toolkitcost for--version/--helpinvocations that skip startup. Out of scope here.Why we did it anyway
register_callbacks.X(fragile — patching a re-importpoint instead of the source). They now target the actual source
modules, which is the healthier shape.
_prompt_toolkit_stylelazy shim with a strict signature(
(style: BaseStyle | None) -> BaseStyle) somerge_with_active_stylegets called on-demand rather than requiringits module at registration time.
Tests
test_theme_plugin.py+test_prompt_toolkit_theme.py).register_callbacks.X(which nolonger exists at module scope after this change) to the actual source
modules (
code_puppy.config.get_value,code_puppy.plugins.theme.osc_palette.get_saved_palette, etc.)._prompt_toolkit_styleshim: oneverifies it delegates its single argument to
merge_with_active_style(regression guard against silent signaturewidening), and one confirms the shim preserves
__name__for errorlogging.
ruff checkandruff format --checkclean.Broader startup-perf context
While profiling this I measured the import cost of each plugin's
register_callbacksin isolation (snapshotsys.modulesbefore/after,bucket the delta by heavy library). A few results worth passing along:
claude_code_oauththeme(before this PR)aws_bedrockobsidian_agentTakeaways for future PRs (not part of this change):
pydantic_ai(~52 modules) loads per-plugin. A single root importpoint is the leverage lever — likely via the type imports referenced
at module scope in the agent-registration API.
playwright(aws_bedrock,claude_code_oauth). Neither does browser automation. Likely acode_puppy.tools-registry-level bug where registering browser toolsrequires importing playwright.
claude_code_oauthpulls in the entire OpenAI SDK (557 modules).A plugin for Claude authentication should not need OpenAI's Python
client.
version check — ~0.8 s median on a healthy network, up to
10-plus seconds when pypi.org is slow or blocked by a corporate
proxy. Kicking it to a background task is the single biggest UX lever
and doesn't require touching plugin structure.
Happy to file follow-up issues on this repo for any of the above if
they'd be useful — I've been tracking them internally.
Files touched
code_puppy/plugins/theme/register_callbacks.py— 8 sibling importsmoved into functions;
_prompt_toolkit_stylelazy shim added; NOTEcomment rewritten for accuracy.
tests/plugins/test_theme_plugin.py— patch targets retargeted fromregister_callbacks.Xto source modules; two new tests for theshim added.