Skip to content

perf: defer imports + runtime micro-optimizations in hot paths#13

Open
KRRT7 wants to merge 3 commits intoperf/architectural-winsfrom
perf/runtime-wins-2
Open

perf: defer imports + runtime micro-optimizations in hot paths#13
KRRT7 wants to merge 3 commits intoperf/architectural-winsfrom
perf/runtime-wins-2

Conversation

@KRRT7
Copy link
Copy Markdown
Owner

@KRRT7 KRRT7 commented Apr 9, 2026

Stacked on #12.

E2E Import Time (hyperfine, 30+ runs, Standard_D2s_v5)

CPython 3.12 (typing imports re — deferral doesn't help)

Import master + #12 + this PR
Console 79.1 ± 0.8ms 37.6 ± 1.3ms 37.5 ± 0.5ms
RichHandler 100.3 ± 3.6ms 39.0 ± 0.5ms 39.6 ± 0.5ms

CPython 3.13 (typing no longer imports re — full benefit)

Import master + #12 (est.) + this PR Speedup vs master
Console 67.9 ± 0.7ms ~36.8ms 33.6 ± 0.5ms 2.02x
RichHandler ~38.4ms 37.5 ± 0.4ms

On Python ≥3.13, typing no longer imports re, so deferring all re.compile() calls eliminates re (+ _sre, re._compiler, re._parser, re._constants) from the Console import chain entirely, saving ~3ms.

Runtime Micro-benchmarks (Standard_D2s_v5, Python 3.13.13)

Benchmark Before After Speedup
Style.__eq__ (identity) 114ns/call 62ns/call 1.84x
Style.__eq__ (equal) 113ns/call 129ns/call ~same
Style.combine (3 styles) 579ns/call 433ns/call 1.34x
Style.combine (2 styles) 604ns/call 550ns/call 1.10x
Style.chain (3 styles) 959ns/call 878ns/call 1.09x
Segment.simplify (identity) 1269ns/call 931ns/call 1.36x
Segment.simplify (equal) 1269ns/call 1128ns/call 1.12x
E2E Console.print 173.7µs/call 171.6µs/call ~1.01x

Identity comparisons (same object) are the common case in the render pipeline — cached styles are reused across segments. The E2E improvement is modest (~1%) because these hot paths are a fraction of total Console.print cost.

What this adds (on top of #12)

color.py — deferred imports

  • from __future__ import annotations
  • Defer colorsys, _palettes, terminal_theme to first use in get_truecolor() / downgrade() (both LRU-cached)
  • Move Result to TYPE_CHECKING

Defer re module from Console import chain (7 files)

  • color.py: RE_COLOR compiled lazily in Color.parse() (LRU-cached)
  • text.py: _re_whitespace compiled lazily on first rstrip_end(); inline import re in highlight_regex(), highlight_words(), split(), detect_indentation(), with_indent_guides()
  • markup.py: RE_TAGS via _compile_tags(), RE_HANDLER lazy in render(), escape regex lazy in escape(); add from __future__ import annotations, move Match to TYPE_CHECKING
  • _emoji_replace.py: regex default arg → lazy _EMOJI_SUB global; move Match to TYPE_CHECKING
  • _wrap.py: re_word → lazy _re_word in words()
  • highlighter.py: import re moved inside JSONHighlighter.highlight()
  • default_styles.py: 3 rgb(...) color strings → Color.from_rgb() to avoid regex-based Color.parse() at import time

Runtime micro-optimizations in hot paths

  • style.py Style.__eq__/__ne__: add identity shortcut (is) before hash comparison — skips hash computation when comparing the same cached object (very common in the render pipeline)
  • style.py Style.combine/chain: use _add (LRU-cached) directly instead of sum() which routes through __add__ + redundant .copy() check per iteration
  • segment.py Segment.simplify: check is before == for style comparison — adjacent segments very often share the exact same Style object reference

Testing

  • pytest tests/ — 952 passed, 25 skipped
  • Updated tests/test_markup.py to use _compile_tags() instead of removed RE_TAGS module constant

KRRT7 added 3 commits April 9, 2026 02:21
- Add `from __future__ import annotations` to color.py
- Move `from colorsys import rgb_to_hls` to inline in downgrade()
- Move `from ._palettes import ...` to inline in get_truecolor() and
  downgrade() — palette data only needed at runtime, not import time
- Move `from .terminal_theme import DEFAULT_TERMINAL_THEME` to inline
  in get_truecolor() — only needed as default arg fallback
- Move `Result` to TYPE_CHECKING (annotation-only with future annotations)

These are all loaded on cache misses in LRU-cached methods, so the
inline import overhead is negligible after warmup.
Remove `re` from module-level imports in all files on the
Console import path (color.py, text.py, markup.py, _wrap.py,
_emoji_replace.py, highlighter.py). Regex patterns are now
compiled lazily on first use via module-level sentinels.

Also move `typing.Match` and `typing.Pattern` (which trigger
`re` import) to TYPE_CHECKING blocks, and replace three
`rgb(...)` color strings in default_styles.py with
`Color.from_rgb()` to avoid regex-based Color.parse().

This eliminates `re` (and its `_sre`, `re._compiler`,
`re._parser`, `re._constants` dependencies) from the
`from rich.console import Console` import chain entirely.
- Style.__eq__/__ne__: add identity shortcut (`is`) before hash comparison
- Style.combine/chain: use _add (LRU-cached) directly instead of sum()
  which goes through __add__ + redundant .copy() check per iteration
- Segment.simplify: check `is` before `==` for style comparison since
  adjacent segments very often share the exact same Style object
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant