mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 13:52:43 +08:00
CLI(V1): binary speedup (#11006)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
1231b78aea
commit
6dbbf76231
@ -83,7 +83,6 @@ def check_pyinstaller() -> bool:
|
||||
print(' uv add --dev pyinstaller')
|
||||
return False
|
||||
|
||||
|
||||
def build_executable(
|
||||
spec_file: str = 'openhands-cli.spec',
|
||||
clean: bool = True,
|
||||
@ -274,7 +273,7 @@ def main() -> int:
|
||||
|
||||
# Build the executable
|
||||
if not args.no_build and not build_executable(
|
||||
args.spec, clean=not args.no_clean, install_pyinstaller=args.install_pyinstaller
|
||||
args.spec, clean=not args.no_clean
|
||||
):
|
||||
return 1
|
||||
|
||||
@ -290,5 +289,7 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
||||
63
openhands-cli/hooks/rthook_profile_imports.py
Normal file
63
openhands-cli/hooks/rthook_profile_imports.py
Normal file
@ -0,0 +1,63 @@
|
||||
import atexit, os, sys, time
|
||||
from collections import defaultdict
|
||||
|
||||
ENABLE = os.getenv("IMPORT_PROFILING", "0") not in ("", "0", "false", "False")
|
||||
OUT = "dist/import_profiler.csv"
|
||||
THRESHOLD_MS = float(os.getenv("IMPORT_PROFILING_THRESHOLD_MS", "0"))
|
||||
|
||||
if ENABLE:
|
||||
timings = defaultdict(float) # module -> total seconds (first load only)
|
||||
counts = defaultdict(int) # module -> number of first-loads (should be 1)
|
||||
max_dur = defaultdict(float) # module -> max single load seconds
|
||||
|
||||
try:
|
||||
import importlib._bootstrap as _bootstrap # type: ignore[attr-defined]
|
||||
except Exception:
|
||||
_bootstrap = None
|
||||
|
||||
start_time = time.perf_counter()
|
||||
|
||||
if _bootstrap is not None:
|
||||
_orig_find_and_load = _bootstrap._find_and_load
|
||||
|
||||
def _timed_find_and_load(name, import_):
|
||||
preloaded = name in sys.modules # cache hit?
|
||||
t0 = time.perf_counter()
|
||||
try:
|
||||
return _orig_find_and_load(name, import_)
|
||||
finally:
|
||||
if not preloaded:
|
||||
dt = time.perf_counter() - t0
|
||||
timings[name] += dt
|
||||
counts[name] += 1
|
||||
if dt > max_dur[name]:
|
||||
max_dur[name] = dt
|
||||
|
||||
_bootstrap._find_and_load = _timed_find_and_load
|
||||
|
||||
@atexit.register
|
||||
def _dump_import_profile():
|
||||
def ms(s): return f"{s*1000:.3f}"
|
||||
items = [
|
||||
(name, counts[name], timings[name], max_dur[name])
|
||||
for name in timings
|
||||
if timings[name]*1000 >= THRESHOLD_MS
|
||||
]
|
||||
items.sort(key=lambda x: x[2], reverse=True)
|
||||
try:
|
||||
with open(OUT, "w", encoding="utf-8") as f:
|
||||
f.write("module,count,total_ms,max_ms\n")
|
||||
for name, cnt, tot_s, max_s in items:
|
||||
f.write(f"{name},{cnt},{ms(tot_s)},{ms(max_s)}\n")
|
||||
# brief summary
|
||||
if items:
|
||||
w = max(len(n) for n, *_ in items[:25])
|
||||
sys.stderr.write("\n=== Import Time Profile (first-load only) ===\n")
|
||||
sys.stderr.write(f"{'module'.ljust(w)} count total_ms max_ms\n")
|
||||
for name, cnt, tot_s, max_s in items[:25]:
|
||||
sys.stderr.write(
|
||||
f"{name.ljust(w)} {str(cnt).rjust(5)} {ms(tot_s).rjust(8)} {ms(max_s).rjust(7)}\n"
|
||||
)
|
||||
sys.stderr.write(f"\nImport profile written to: {OUT}\n")
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[import-profiler] failed to write profile: {e}\n")
|
||||
@ -44,7 +44,6 @@ a = Analysis(
|
||||
# Include OpenHands SDK submodules explicitly to avoid resolution issues
|
||||
*collect_submodules('openhands.sdk'),
|
||||
*collect_submodules('openhands.tools'),
|
||||
|
||||
*collect_submodules('tiktoken'),
|
||||
*collect_submodules('tiktoken_ext'),
|
||||
*collect_submodules('litellm'),
|
||||
@ -54,10 +53,14 @@ a = Analysis(
|
||||
'mcp.client',
|
||||
'mcp.server',
|
||||
'mcp.shared',
|
||||
'openhands.tools.execute_bash',
|
||||
'openhands.tools.str_replace_editor',
|
||||
'openhands.tools.task_tracker',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
# runtime_hooks=[str(project_root / "hooks" / "rthook_profile_imports.py")],
|
||||
excludes=[
|
||||
# Exclude unnecessary modules to reduce binary size
|
||||
'tkinter',
|
||||
@ -70,7 +73,13 @@ a = Analysis(
|
||||
'notebook',
|
||||
# Exclude mcp CLI parts that cause issues
|
||||
'mcp.cli',
|
||||
'mcp.cli.cli',
|
||||
'prompt_toolkit.contrib.ssh',
|
||||
'fastmcp.cli',
|
||||
'boto3',
|
||||
'botocore',
|
||||
'posthog',
|
||||
'browser-use',
|
||||
'openhands.tools.browser_use'
|
||||
],
|
||||
noarchive=False,
|
||||
# IMPORTANT: do not use optimize=2 (-OO) because it strips docstrings used by PLY/bashlex grammar
|
||||
|
||||
@ -4,10 +4,9 @@ Simple main entry point for OpenHands CLI.
|
||||
This is a simplified version that demonstrates the TUI functionality.
|
||||
"""
|
||||
|
||||
import traceback
|
||||
|
||||
from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from openhands_cli.agent_chat import run_cli_entry
|
||||
|
||||
|
||||
def main() -> None:
|
||||
@ -19,8 +18,6 @@ def main() -> None:
|
||||
"""
|
||||
try:
|
||||
# Start agent chat directly by default
|
||||
from openhands_cli.agent_chat import run_cli_entry
|
||||
|
||||
run_cli_entry()
|
||||
|
||||
except ImportError as e:
|
||||
@ -37,6 +34,7 @@ def main() -> None:
|
||||
print_formatted_text(HTML("\n<yellow>Goodbye! 👋</yellow>"))
|
||||
except Exception as e:
|
||||
print_formatted_text(HTML(f"<red>Error starting agent chat: {e}</red>"))
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
|
||||
@ -17,8 +17,8 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"openhands-sdk @ git+https://github.com/All-Hands-AI/agent-sdk.git@f8e800a93a3726555a30d1c42a692f4c556187c5#subdirectory=openhands/sdk",
|
||||
"openhands-tools @ git+https://github.com/All-Hands-AI/agent-sdk.git@f8e800a93a3726555a30d1c42a692f4c556187c5#subdirectory=openhands/tools",
|
||||
"openhands-sdk @ git+https://github.com/All-Hands-AI/agent-sdk.git@0e62eeeba193905478187e2f3117846d6cc97718#subdirectory=openhands/sdk",
|
||||
"openhands-tools @ git+https://github.com/All-Hands-AI/agent-sdk.git@0e62eeeba193905478187e2f3117846d6cc97718#subdirectory=openhands/tools",
|
||||
"prompt-toolkit>=3",
|
||||
"typer>=0.17.4",
|
||||
]
|
||||
@ -53,8 +53,8 @@ packages = [ { include = "openhands_cli" } ]
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
prompt-toolkit = "^3.0.0"
|
||||
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "f8e800a93a3726555a30d1c42a692f4c556187c5", subdirectory = "openhands/sdk" }
|
||||
openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "f8e800a93a3726555a30d1c42a692f4c556187c5", subdirectory = "openhands/tools" }
|
||||
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "0e62eeeba193905478187e2f3117846d6cc97718", subdirectory = "openhands/sdk" }
|
||||
openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "0e62eeeba193905478187e2f3117846d6cc97718", subdirectory = "openhands/tools" }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.0.0"
|
||||
@ -115,4 +115,4 @@ disallow_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.uv.sources]
|
||||
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "f8e800a93a3726555a30d1c42a692f4c556187c5" }
|
||||
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "0e62eeeba193905478187e2f3117846d6cc97718" }
|
||||
|
||||
@ -10,56 +10,79 @@ from openhands_cli import simple_main
|
||||
class TestMainEntryPoint:
|
||||
"""Test the main entry point behavior."""
|
||||
|
||||
@patch('openhands_cli.agent_chat.run_cli_entry')
|
||||
@patch('openhands_cli.agent_chat.setup_agent')
|
||||
@patch('openhands_cli.agent_chat.ConversationRunner')
|
||||
@patch('openhands_cli.agent_chat.PromptSession')
|
||||
def test_main_starts_agent_chat_directly(
|
||||
self, mock_run_agent_chat: MagicMock
|
||||
self, mock_prompt_session: MagicMock, mock_runner: MagicMock, mock_setup_agent: MagicMock
|
||||
) -> None:
|
||||
"""Test that main() starts agent chat directly without menu."""
|
||||
mock_run_agent_chat.return_value = None
|
||||
"""Test that main() starts agent chat directly when setup succeeds."""
|
||||
# Mock setup_agent to return a valid conversation
|
||||
mock_conversation = MagicMock()
|
||||
mock_setup_agent.return_value = mock_conversation
|
||||
|
||||
# Mock prompt session to raise KeyboardInterrupt to exit the loop
|
||||
mock_prompt_session.return_value.prompt.side_effect = KeyboardInterrupt()
|
||||
|
||||
# Should complete without raising an exception
|
||||
# Should complete without raising an exception (graceful exit)
|
||||
simple_main.main()
|
||||
|
||||
# Should call run_agent_chat directly
|
||||
mock_run_agent_chat.assert_called_once()
|
||||
# Should call setup_agent
|
||||
mock_setup_agent.assert_called_once()
|
||||
|
||||
@patch('openhands_cli.agent_chat.run_cli_entry')
|
||||
@patch('openhands_cli.simple_main.run_cli_entry')
|
||||
def test_main_handles_import_error(self, mock_run_agent_chat: MagicMock) -> None:
|
||||
"""Test that main() handles ImportError gracefully."""
|
||||
mock_run_agent_chat.side_effect = ImportError('Missing dependency')
|
||||
|
||||
# Should raise ImportError (no longer using sys.exit)
|
||||
# Should raise ImportError (re-raised after handling)
|
||||
with pytest.raises(ImportError) as exc_info:
|
||||
simple_main.main()
|
||||
|
||||
assert str(exc_info.value) == 'Missing dependency'
|
||||
|
||||
@patch('openhands_cli.agent_chat.run_cli_entry')
|
||||
@patch('openhands_cli.agent_chat.setup_agent')
|
||||
@patch('openhands_cli.agent_chat.ConversationRunner')
|
||||
@patch('openhands_cli.agent_chat.PromptSession')
|
||||
def test_main_handles_keyboard_interrupt(
|
||||
self, mock_run_agent_chat: MagicMock
|
||||
self, mock_prompt_session: MagicMock, mock_runner: MagicMock, mock_setup_agent: MagicMock
|
||||
) -> None:
|
||||
"""Test that main() handles KeyboardInterrupt gracefully."""
|
||||
mock_run_agent_chat.side_effect = KeyboardInterrupt()
|
||||
# Mock setup_agent to return a valid conversation
|
||||
mock_conversation = MagicMock()
|
||||
mock_setup_agent.return_value = mock_conversation
|
||||
|
||||
# Mock prompt session to raise KeyboardInterrupt
|
||||
mock_prompt_session.return_value.prompt.side_effect = KeyboardInterrupt()
|
||||
|
||||
# Should complete without raising an exception (graceful exit)
|
||||
simple_main.main()
|
||||
|
||||
@patch('openhands_cli.agent_chat.run_cli_entry')
|
||||
def test_main_handles_eof_error(self, mock_run_agent_chat: MagicMock) -> None:
|
||||
@patch('openhands_cli.agent_chat.setup_agent')
|
||||
@patch('openhands_cli.agent_chat.ConversationRunner')
|
||||
@patch('openhands_cli.agent_chat.PromptSession')
|
||||
def test_main_handles_eof_error(
|
||||
self, mock_prompt_session: MagicMock, mock_runner: MagicMock, mock_setup_agent: MagicMock
|
||||
) -> None:
|
||||
"""Test that main() handles EOFError gracefully."""
|
||||
mock_run_agent_chat.side_effect = EOFError()
|
||||
# Mock setup_agent to return a valid conversation
|
||||
mock_conversation = MagicMock()
|
||||
mock_setup_agent.return_value = mock_conversation
|
||||
|
||||
# Mock prompt session to raise EOFError
|
||||
mock_prompt_session.return_value.prompt.side_effect = EOFError()
|
||||
|
||||
# Should complete without raising an exception (graceful exit)
|
||||
simple_main.main()
|
||||
|
||||
@patch('openhands_cli.agent_chat.run_cli_entry')
|
||||
@patch('openhands_cli.simple_main.run_cli_entry')
|
||||
def test_main_handles_general_exception(
|
||||
self, mock_run_agent_chat: MagicMock
|
||||
) -> None:
|
||||
"""Test that main() handles general exceptions."""
|
||||
mock_run_agent_chat.side_effect = Exception('Unexpected error')
|
||||
|
||||
# Should raise Exception (no longer using sys.exit)
|
||||
# Should raise Exception (re-raised after handling)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
simple_main.main()
|
||||
|
||||
|
||||
8
openhands-cli/uv.lock
generated
8
openhands-cli/uv.lock
generated
@ -1488,8 +1488,8 @@ requires-dist = [
|
||||
{ name = "flake8", marker = "extra == 'dev'", specifier = ">=6" },
|
||||
{ name = "isort", marker = "extra == 'dev'", specifier = ">=5" },
|
||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1" },
|
||||
{ name = "openhands-sdk", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=f8e800a93a3726555a30d1c42a692f4c556187c5" },
|
||||
{ name = "openhands-tools", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=f8e800a93a3726555a30d1c42a692f4c556187c5" },
|
||||
{ name = "openhands-sdk", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=0e62eeeba193905478187e2f3117846d6cc97718" },
|
||||
{ name = "openhands-tools", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=0e62eeeba193905478187e2f3117846d6cc97718" },
|
||||
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7" },
|
||||
{ name = "prompt-toolkit", specifier = ">=3" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7" },
|
||||
@ -1508,7 +1508,7 @@ dev = [
|
||||
[[package]]
|
||||
name = "openhands-sdk"
|
||||
version = "1.0.0"
|
||||
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=f8e800a93a3726555a30d1c42a692f4c556187c5#f8e800a93a3726555a30d1c42a692f4c556187c5" }
|
||||
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=0e62eeeba193905478187e2f3117846d6cc97718#0e62eeeba193905478187e2f3117846d6cc97718" }
|
||||
dependencies = [
|
||||
{ name = "fastmcp" },
|
||||
{ name = "litellm" },
|
||||
@ -1521,7 +1521,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "openhands-tools"
|
||||
version = "1.0.0"
|
||||
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=f8e800a93a3726555a30d1c42a692f4c556187c5#f8e800a93a3726555a30d1c42a692f4c556187c5" }
|
||||
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=0e62eeeba193905478187e2f3117846d6cc97718#0e62eeeba193905478187e2f3117846d6cc97718" }
|
||||
dependencies = [
|
||||
{ name = "bashlex" },
|
||||
{ name = "binaryornot" },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user