Skip to content

Commit 72a41a5

Browse files
authored
Merge pull request #2055 from codeflash-ai/perf/defer-cli-imports
perf: defer cli.py imports for 7.7x faster --help
2 parents 93810f8 + 381d131 commit 72a41a5

27 files changed

Lines changed: 604 additions & 108 deletions

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ jobs:
199199
run: |
200200
uv run ruff check --fix . || true
201201
uv run ruff format .
202+
# uv-dynamic-versioning rewrites version.py on every `uv run` — discard those changes
203+
git checkout HEAD -- codeflash/version.py codeflash-benchmark/codeflash_benchmark/version.py 2>/dev/null || true
202204
203205
- name: Commit and push fixes
204206
run: |

benchmarks/__init__.py

Whitespace-only changes.

benchmarks/bench_cli_startup.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Benchmark CLI startup latency for codeflash compare --script mode.
2+
3+
Run from a worktree root. Installs deps via uv sync, then times several
4+
CLI entry points and writes a JSON file mapping command names to median
5+
wall-clock seconds.
6+
7+
Usage:
8+
codeflash compare main codeflash/optimize \
9+
--script "python benchmarks/bench_cli_startup.py" \
10+
--script-output benchmarks/results.json
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import json
16+
import os
17+
import subprocess
18+
import time
19+
from pathlib import Path
20+
21+
WARMUP = 3
22+
RUNS = 30
23+
OUTPUT = os.environ.get("BENCH_OUTPUT", "benchmarks/results.json")
24+
25+
COMMANDS: dict[str, list[str]] = {
26+
"version": ["uv", "run", "codeflash", "--version"],
27+
"help": ["uv", "run", "codeflash", "--help"],
28+
"auth_status": ["uv", "run", "codeflash", "auth", "status"],
29+
"compare_help": ["uv", "run", "codeflash", "compare", "--help"],
30+
}
31+
32+
33+
def measure(cmd: list[str], warmup: int = WARMUP, runs: int = RUNS) -> float:
34+
"""Return median wall-clock seconds for *cmd* over *runs* iterations."""
35+
env = {**os.environ, "CODEFLASH_API_KEY": "bench_dummy_key"}
36+
for _ in range(warmup):
37+
subprocess.run(cmd, capture_output=True, check=False, env=env)
38+
39+
times: list[float] = []
40+
for _ in range(runs):
41+
t0 = time.perf_counter()
42+
subprocess.run(cmd, capture_output=True, check=False, env=env)
43+
times.append(time.perf_counter() - t0)
44+
45+
times.sort()
46+
mid = len(times) // 2
47+
return times[mid] if len(times) % 2 else (times[mid - 1] + times[mid]) / 2
48+
49+
50+
def main() -> None:
51+
# Ensure deps are installed in the worktree
52+
subprocess.run(["uv", "sync"], check=True, capture_output=True)
53+
54+
results: dict[str, float] = {}
55+
for name, cmd in COMMANDS.items():
56+
print(f" {name}: ", end="", flush=True)
57+
median = measure(cmd)
58+
results[name] = round(median, 4)
59+
print(f"{median * 1000:.0f} ms")
60+
61+
# Total = sum of medians (useful for a single summary number)
62+
results["__total__"] = round(sum(results.values()), 4)
63+
64+
output_path = Path(OUTPUT)
65+
output_path.parent.mkdir(parents=True, exist_ok=True)
66+
with output_path.open("w") as f:
67+
json.dump(results, f, indent=2)
68+
print(f"\nResults written to {OUTPUT}")
69+
70+
71+
if __name__ == "__main__":
72+
main()

codeflash/benchmarking/instrument_codeflash_trace.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import libcst as cst
66

7+
import codeflash.code_utils._libcst_cache # noqa: F401
78
from codeflash.code_utils.formatter import sort_imports
89

910
if TYPE_CHECKING:

codeflash/cli_cmds/cli.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,6 @@
55
from functools import lru_cache
66
from pathlib import Path
77

8-
from codeflash.cli_cmds import logging_config
9-
from codeflash.cli_cmds.console import apologize_and_exit, logger
10-
from codeflash.code_utils import env_utils
11-
from codeflash.code_utils.code_utils import exit_with_message, normalize_ignore_paths
12-
from codeflash.code_utils.config_parser import parse_config_file
13-
from codeflash.languages.test_framework import set_current_test_framework
14-
from codeflash.lsp.helpers import is_LSP_enabled
15-
from codeflash.version import __version__ as version
16-
178

189
def parse_args() -> Namespace:
1910
parser = _build_parser()
@@ -30,12 +21,17 @@ def parse_args() -> Namespace:
3021

3122

3223
def process_and_validate_cmd_args(args: Namespace) -> Namespace:
24+
from codeflash.cli_cmds import logging_config
25+
from codeflash.cli_cmds.console import logger
26+
from codeflash.code_utils import env_utils
27+
from codeflash.code_utils.code_utils import exit_with_message
3328
from codeflash.code_utils.git_utils import (
3429
check_running_in_git_repo,
3530
confirm_proceeding_with_no_git_repo,
3631
get_repo_owner_and_name,
3732
)
3833
from codeflash.code_utils.github_utils import require_github_app_or_exit
34+
from codeflash.version import __version__ as version
3935

4036
if args.server:
4137
os.environ["CODEFLASH_AIS_SERVER"] = args.server
@@ -85,6 +81,12 @@ def process_and_validate_cmd_args(args: Namespace) -> Namespace:
8581

8682

8783
def process_pyproject_config(args: Namespace) -> Namespace:
84+
from codeflash.code_utils import env_utils
85+
from codeflash.code_utils.code_utils import exit_with_message, normalize_ignore_paths
86+
from codeflash.code_utils.config_parser import parse_config_file
87+
from codeflash.languages.test_framework import set_current_test_framework
88+
from codeflash.lsp.helpers import is_LSP_enabled
89+
8890
try:
8991
pyproject_config, pyproject_file_path = parse_config_file(args.config_file)
9092
except ValueError as e:
@@ -222,6 +224,9 @@ def project_root_from_module_root(module_root: Path, pyproject_file_path: Path)
222224

223225

224226
def handle_optimize_all_arg_parsing(args: Namespace) -> Namespace:
227+
from codeflash.cli_cmds.console import apologize_and_exit, logger
228+
from codeflash.code_utils.code_utils import exit_with_message
229+
225230
if hasattr(args, "all") or (hasattr(args, "file") and args.file):
226231
no_pr = getattr(args, "no_pr", False)
227232

codeflash/cli_cmds/cmd_auth.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22

33
import os
44

5-
import click
6-
7-
from codeflash.cli_cmds.console import console
8-
from codeflash.cli_cmds.oauth_handler import perform_oauth_signin
9-
from codeflash.code_utils.env_utils import get_codeflash_api_key
10-
from codeflash.code_utils.shell_utils import save_api_key_to_rc
11-
from codeflash.either import is_successful
12-
135

146
def auth_login() -> None:
157
"""Perform OAuth login and save the API key."""
8+
import click
9+
10+
from codeflash.cli_cmds.console import console
11+
from codeflash.cli_cmds.oauth_handler import perform_oauth_signin
12+
from codeflash.code_utils.env_utils import get_codeflash_api_key
13+
from codeflash.code_utils.shell_utils import save_api_key_to_rc
14+
from codeflash.either import is_successful
15+
1616
try:
1717
existing_api_key = get_codeflash_api_key()
1818
except OSError:
@@ -41,6 +41,9 @@ def auth_login() -> None:
4141

4242
def auth_status() -> None:
4343
"""Check and display current authentication status."""
44+
from codeflash.cli_cmds.console import console
45+
from codeflash.code_utils.env_utils import get_codeflash_api_key
46+
4447
try:
4548
api_key = get_codeflash_api_key()
4649
except OSError:
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Cache libcst visitor dispatch table construction.
2+
3+
libcst's ``MatcherDecoratableTransformer`` and
4+
``MatcherDecoratableVisitor`` rebuild visitor dispatch tables on
5+
every instantiation by iterating ``dir(self)`` (~600 attributes)
6+
and calling ``getattr`` + ``inspect.ismethod`` on each. The
7+
results depend only on the class, not the instance, so caching
8+
by ``type(obj)`` is safe.
9+
10+
Import this module before any libcst visitors are instantiated
11+
to install the cache.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from typing import Any
17+
18+
import libcst.matchers._visitors as _mv
19+
20+
_visit_cache: dict[type, Any] = {}
21+
_leave_cache: dict[type, Any] = {}
22+
_matchers_cache: dict[type, Any] = {}
23+
24+
_original_visit = _mv._gather_constructed_visit_funcs # noqa: SLF001
25+
_original_leave = _mv._gather_constructed_leave_funcs # noqa: SLF001
26+
_original_matchers = _mv._gather_matchers # noqa: SLF001
27+
28+
29+
def _cached_visit(obj: object) -> Any:
30+
"""Return cached visit-function dispatch table for the object's class."""
31+
cls = type(obj)
32+
try:
33+
return _visit_cache[cls]
34+
except KeyError:
35+
result = _original_visit(obj)
36+
_visit_cache[cls] = result
37+
return result
38+
39+
40+
def _cached_leave(obj: object) -> Any:
41+
"""Return cached leave-function dispatch table for the object's class."""
42+
cls = type(obj)
43+
try:
44+
return _leave_cache[cls]
45+
except KeyError:
46+
result = _original_leave(obj)
47+
_leave_cache[cls] = result
48+
return result
49+
50+
51+
def _cached_matchers(obj: object) -> Any:
52+
"""Return cached matcher dispatch table for the object's class."""
53+
cls = type(obj)
54+
try:
55+
return dict(_matchers_cache[cls])
56+
except KeyError:
57+
result = _original_matchers(obj)
58+
_matchers_cache[cls] = result
59+
return dict(result)
60+
61+
62+
_mv._gather_constructed_visit_funcs = _cached_visit # noqa: SLF001
63+
_mv._gather_constructed_leave_funcs = _cached_leave # noqa: SLF001
64+
_mv._gather_matchers = _cached_matchers # noqa: SLF001

codeflash/code_utils/env_utils.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,16 @@
99
from pathlib import Path
1010
from typing import Any, Optional
1111

12-
from codeflash.cli_cmds.console import logger
13-
from codeflash.code_utils.code_utils import exit_with_message
14-
from codeflash.code_utils.formatter import format_code
1512
from codeflash.code_utils.shell_utils import read_api_key_from_shell_config, save_api_key_to_rc
16-
from codeflash.languages.registry import get_language_support_by_common_formatters
17-
from codeflash.lsp.helpers import is_LSP_enabled
1813

1914

2015
def check_formatter_installed(
2116
formatter_cmds: list[str], exit_on_failure: bool = True, language: str = "python"
2217
) -> bool:
18+
from codeflash.cli_cmds.console import logger
19+
from codeflash.code_utils.formatter import format_code
20+
from codeflash.languages.registry import get_language_support_by_common_formatters
21+
2322
if not formatter_cmds or formatter_cmds[0] == "disabled":
2423
return True
2524
first_cmd = formatter_cmds[0]
@@ -69,6 +68,8 @@ def check_formatter_installed(
6968

7069
@lru_cache(maxsize=1)
7170
def get_codeflash_api_key() -> str:
71+
from codeflash.cli_cmds.console import logger
72+
7273
# Check environment variable first
7374
env_api_key = os.environ.get("CODEFLASH_API_KEY")
7475
shell_api_key = read_api_key_from_shell_config()
@@ -96,7 +97,8 @@ def get_codeflash_api_key() -> str:
9697
# Prefer the shell configuration over environment variables for lsp,
9798
# as the API key may change in the RC file during lsp runtime. Since the LSP client (extension) can restart
9899
# within the same process, the environment variable could become outdated.
99-
api_key = shell_api_key or env_api_key if is_LSP_enabled() else env_api_key or shell_api_key
100+
is_lsp = os.getenv("CODEFLASH_LSP", default="false").lower() == "true"
101+
api_key = shell_api_key or env_api_key if is_lsp else env_api_key or shell_api_key
100102

101103
api_secret_docs_message = "For more information, refer to the documentation at [https://docs.codeflash.ai/optimizing-with-codeflash/codeflash-github-actions#manual-setup]." # noqa
102104
if not api_key:
@@ -106,6 +108,8 @@ def get_codeflash_api_key() -> str:
106108
f"{api_secret_docs_message}"
107109
)
108110
if is_repo_a_fork():
111+
from codeflash.code_utils.code_utils import exit_with_message
112+
109113
msg = (
110114
"Codeflash API key not detected in your environment. It appears you're running Codeflash from a GitHub fork.\n"
111115
"For external contributors, please ensure you've added your own API key to your fork's repository secrets and set it as the CODEFLASH_API_KEY environment variable.\n"
@@ -124,6 +128,8 @@ def get_codeflash_api_key() -> str:
124128

125129

126130
def ensure_codeflash_api_key() -> bool:
131+
from codeflash.cli_cmds.console import logger
132+
127133
try:
128134
get_codeflash_api_key()
129135
except OSError:

codeflash/code_utils/instrument_existing_tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import libcst as cst
99

10+
import codeflash.code_utils._libcst_cache # noqa: F401
1011
from codeflash.cli_cmds.console import logger
1112
from codeflash.code_utils.code_utils import get_run_tmp_file, module_name_from_file_path
1213
from codeflash.code_utils.formatter import sort_imports

codeflash/code_utils/shell_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from pathlib import Path
99
from typing import TYPE_CHECKING, Optional
1010

11-
from codeflash.cli_cmds.console import logger
1211
from codeflash.code_utils.compat import LF
1312
from codeflash.either import Failure, Success
1413

@@ -41,6 +40,8 @@ def is_powershell() -> bool:
4140
2. COMSPEC pointing to powershell.exe
4241
3. TERM_PROGRAM indicating Windows Terminal (often uses PowerShell)
4342
"""
43+
from codeflash.cli_cmds.console import logger
44+
4445
if os.name != "nt":
4546
return False
4647

@@ -72,6 +73,8 @@ def is_powershell() -> bool:
7273

7374
def read_api_key_from_shell_config() -> Optional[str]:
7475
"""Read API key from shell configuration file."""
76+
from codeflash.cli_cmds.console import logger
77+
7578
shell_rc_path = get_shell_rc_path()
7679
# Ensure shell_rc_path is a Path object for consistent handling
7780
if not isinstance(shell_rc_path, Path):
@@ -127,6 +130,8 @@ def get_api_key_export_line(api_key: str) -> str:
127130

128131
def save_api_key_to_rc(api_key: str) -> Result[str, str]:
129132
"""Save API key to the appropriate shell configuration file."""
133+
from codeflash.cli_cmds.console import logger
134+
130135
shell_rc_path = get_shell_rc_path()
131136
# Ensure shell_rc_path is a Path object for consistent handling
132137
if not isinstance(shell_rc_path, Path):

0 commit comments

Comments
 (0)