diff --git a/.github/workflows/panvimdoc.yml b/.github/workflows/panvimdoc.yml index 13938eaa..d76be516 100644 --- a/.github/workflows/panvimdoc.yml +++ b/.github/workflows/panvimdoc.yml @@ -1,7 +1,9 @@ name: panvimdoc on: - pull_request: + push: + branches-ignore: + - 'main' permissions: contents: write @@ -64,7 +66,8 @@ jobs: shiftheadinglevelby: 0 # Shift heading levels by specified number incrementheadinglevelby: 0 # Increment heading levels by specified number - - uses: stefanzweifel/git-auto-commit-action@v4 + - uses: stefanzweifel/git-auto-commit-action@v6.0.1 with: commit_message: "Auto generate docs" branch: ${{ github.head_ref }} + file_pattern: 'doc/*.txt' diff --git a/Makefile b/Makefile index 4f06bf89..048288b8 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: multitest -DEFAULT_GROUPS=--group dev --group lsp --group mcp +DEFAULT_GROUPS=--group dev --group lsp --group mcp --group debug deps: pdm lock $(DEFAULT_GROUPS) || pdm lock $(DEFAULT_GROUPS) --group legacy; \ diff --git a/doc/VectorCode-cli.txt b/doc/VectorCode-cli.txt index 4b41a7af..66e2f0c3 100644 --- a/doc/VectorCode-cli.txt +++ b/doc/VectorCode-cli.txt @@ -37,6 +37,8 @@ Table of Contents *VectorCode-cli-table-of-contents* - |VectorCode-cli-cleaning-up| - |VectorCode-cli-inspecting-and-manupulating-files-in-an-indexed-project| - |VectorCode-cli-debugging-and-diagnosing| + - |VectorCode-cli-profiling| + - |VectorCode-cli-post-mortem-debugging| - |VectorCode-cli-shell-completion| - |VectorCode-cli-hardware-acceleration| - |VectorCode-cli-for-developers| @@ -605,6 +607,24 @@ For example: Depending on the MCP/LSP client implementation, you may need to take extra steps to make sure the environment variables are captured by VectorCode. +PROFILING + +When you pass `--debug` parameter to the CLI, VectorCode will track the call +stacks with cprofile . The +stats will be saved to the log directory mentioned above. You may use an +external stats viewer (like snakeviz ) +to load the profiling stats for a better viewing experience. + + +POST-MORTEM DEBUGGING + +VectorCode can work with coredumpy + to snapshot an exception so that +developers can inspect the error asynchronously. To use this, you’d need to +install the `vectorcode[debug]` dependency group: `uv tool install +vectorcode[debug]`. + + SHELL COMPLETION*VectorCode-cli-vectorcode-command-line-tool-shell-completion* VectorCode supports shell completion for bash/zsh/tcsh. You can use `vectorcode diff --git a/docs/cli.md b/docs/cli.md index a412e67a..1ab6d914 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -26,6 +26,8 @@ * [Cleaning up](#cleaning-up) * [Inspecting and Manupulating Files in an Indexed Project](#inspecting-and-manupulating-files-in-an-indexed-project) * [Debugging and Diagnosing](#debugging-and-diagnosing) + * [Profiling](#profiling) + * [Post-mortem debugging](#post-mortem-debugging) * [Shell Completion](#shell-completion) * [Hardware Acceleration](#hardware-acceleration) * [For Developers](#for-developers) @@ -551,6 +553,21 @@ VECTORCODE_LOG_LEVEL=INFO vectorcode vectorise file1.py file2.lua > Depending on the MCP/LSP client implementation, you may need to take extra > steps to make sure the environment variables are captured by VectorCode. +#### Profiling + +When you pass `--debug` parameter to the CLI, VectorCode will track the call +stacks with [cprofile](https://docs.python.org/3/library/profile.html). The +stats will be saved to the log directory mentioned above. You may use an +external stats viewer (like [snakeviz](https://jiffyclub.github.io/snakeviz/)) +to load the profiling stats for a better viewing experience. + +#### Post-mortem debugging + +VectorCode can work with [coredumpy](https://github.com/gaogaotiantian/coredumpy) +to snapshot an exception so that developers can inspect the error +asynchronously. To use this, you'd need to install the `vectorcode[debug]` +dependency group: `uv tool install vectorcode[debug]`. + ## Shell Completion VectorCode supports shell completion for bash/zsh/tcsh. You can use `vectorcode -s {bash,zsh,tcsh}` diff --git a/pyproject.toml b/pyproject.toml index dd3e0501..01fd3b28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ omit = [ "./tests/*", "src/vectorcode/_version.py", "src/vectorcode/__init__.py", + "src/vectorcode/debugging.py", "/tmp/*", ] include = ['src/vectorcode/**/*.py'] @@ -63,14 +64,12 @@ write_template = "__version__ = '{}' # pragma: no cover" dev = [ "ipython>=8.31.0", "ruff>=0.9.1", - "viztracer>=1.0.0", "pre-commit>=4.0.1", "pytest>=8.3.4", "pdm-backend>=2.4.3", "coverage>=7.6.12", "pytest-asyncio>=0.25.3", "debugpy>=1.8.12", - "coredumpy>=0.4.1", "basedpyright>=1.29.2", ] @@ -79,6 +78,7 @@ legacy = ["numpy<2.0.0", "torch==2.2.2", "transformers<=4.49.0"] intel = ['optimum[openvino]', 'openvino'] lsp = ['pygls<2.0.0', 'lsprotocol'] mcp = ['mcp<2.0.0', 'pydantic'] +debug = ["coredumpy>=0.4.1"] [tool.basedpyright] typeCheckingMode = "standard" diff --git a/src/vectorcode/cli_utils.py b/src/vectorcode/cli_utils.py index 83e36793..7c49def2 100644 --- a/src/vectorcode/cli_utils.py +++ b/src/vectorcode/cli_utils.py @@ -77,6 +77,7 @@ class FilesAction(StrEnum): @dataclass class Config: + debug: bool = False no_stderr: bool = False recursive: bool = False include_hidden: bool = False @@ -198,6 +199,12 @@ async def merge_from(self, other: "Config") -> "Config": def get_cli_parser(): __default_config = Config() shared_parser = argparse.ArgumentParser(add_help=False) + shared_parser.add_argument( + "--debug", + default=False, + action="store_true", + help="Enable debug mode.", + ) chunking_parser = argparse.ArgumentParser(add_help=False) chunking_parser.add_argument( "--overlap", @@ -423,6 +430,7 @@ async def parse_cli_args(args: Optional[Sequence[str]] = None): "action": CliAction(main_args.action), "project_root": main_args.project_root, "pipe": main_args.pipe, + "debug": main_args.debug, } match main_args.action: diff --git a/src/vectorcode/debugging.py b/src/vectorcode/debugging.py new file mode 100644 index 00000000..7021fb4d --- /dev/null +++ b/src/vectorcode/debugging.py @@ -0,0 +1,65 @@ +import atexit +import cProfile +import logging +import os +import pstats +from datetime import datetime + +__LOG_DIR = os.path.expanduser("~/.local/share/vectorcode/logs/") + +logger = logging.getLogger(name=__name__) + +__profiler: cProfile.Profile | None = None + + +def _ensure_log_dir(): + """Ensure the log directory exists""" + os.makedirs(__LOG_DIR, exist_ok=True) + + +def finish(): + """Clean up profiling and save results""" + if __profiler is not None: + try: + __profiler.disable() + stats_file = os.path.join( + __LOG_DIR, + f"cprofile-{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.stats", + ) + __profiler.dump_stats(stats_file) + print(f"cProfile stats saved to: {stats_file}") + + # Print summary stats + stats = pstats.Stats(__profiler) + stats.sort_stats("cumulative") + stats.print_stats(20) + except Exception as e: + logger.warning(f"Failed to save cProfile output: {e}") + + +def enable(): + """Enable cProfile-based profiling and crash debugging""" + global __profiler + + try: + _ensure_log_dir() + + # Initialize cProfile for comprehensive profiling + __profiler = cProfile.Profile() + __profiler.enable() + atexit.register(finish) + logger.info("cProfile profiling enabled successfully") + + try: + import coredumpy # noqa: F401 + + logger.info("coredumpy crash debugging enabled successfully") + coredumpy.patch_except(directory=__LOG_DIR) + except Exception as e: + logger.warning( + f"Crash debugging will not be available. Failed to import coredumpy: {e}" + ) + + except Exception as e: + logger.error(f"Failed to initialize cProfile: {e}") + logger.warning("Profiling will not be available for this session") diff --git a/src/vectorcode/main.py b/src/vectorcode/main.py index fd14ae14..345aedc5 100644 --- a/src/vectorcode/main.py +++ b/src/vectorcode/main.py @@ -23,6 +23,12 @@ async def async_main(): cli_args = await parse_cli_args() if cli_args.no_stderr: sys.stderr = open(os.devnull, "w") + + if cli_args.debug: + from vectorcode import debugging + + debugging.enable() + logger.info("Collected CLI arguments: %s", cli_args) if cli_args.project_root is None: diff --git a/tests/test_main.py b/tests/test_main.py index 285427c9..b46c9989 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,9 +9,7 @@ @pytest.mark.asyncio async def test_async_main_no_stderr(monkeypatch): - mock_cli_args = MagicMock( - no_stderr=True, project_root=".", action=CliAction.version - ) + mock_cli_args = Config(no_stderr=True, project_root=".", action=CliAction.version) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) @@ -24,9 +22,7 @@ async def test_async_main_no_stderr(monkeypatch): @pytest.mark.asyncio async def test_async_main_default_project_root(monkeypatch): - mock_cli_args = MagicMock( - no_stderr=False, project_root=None, action=CliAction.version - ) + mock_cli_args = Config(no_stderr=False, project_root=None, action=CliAction.version) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) @@ -42,9 +38,7 @@ async def test_async_main_default_project_root(monkeypatch): @pytest.mark.asyncio async def test_async_main_ioerror(monkeypatch): - mock_cli_args = MagicMock( - no_stderr=False, project_root=".", action=CliAction.version - ) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.version) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) @@ -61,7 +55,7 @@ async def test_async_main_ioerror(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_check(monkeypatch): - mock_cli_args = MagicMock(no_stderr=False, project_root=".", action=CliAction.check) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.check) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) @@ -79,7 +73,7 @@ async def test_async_main_cli_action_check(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_init(monkeypatch): - mock_cli_args = MagicMock(no_stderr=False, project_root=".", action=CliAction.init) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.init) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) @@ -94,15 +88,15 @@ async def test_async_main_cli_action_init(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_chunks(monkeypatch): - mock_cli_args = MagicMock( - no_stderr=False, project_root=".", action=CliAction.chunks - ) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.chunks) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) mock_chunks = AsyncMock(return_value=0) monkeypatch.setattr("vectorcode.subcommands.chunks", mock_chunks) - monkeypatch.setattr("vectorcode.main.get_project_config", AsyncMock()) + monkeypatch.setattr( + "vectorcode.main.get_project_config", AsyncMock(return_value=Config()) + ) monkeypatch.setattr("vectorcode.common.try_server", AsyncMock(return_value=True)) return_code = await async_main() @@ -112,9 +106,7 @@ async def test_async_main_cli_action_chunks(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_version(monkeypatch, capsys): - mock_cli_args = MagicMock( - no_stderr=False, project_root=".", action=CliAction.version - ) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.version) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) @@ -127,13 +119,15 @@ async def test_async_main_cli_action_version(monkeypatch, capsys): @pytest.mark.asyncio async def test_async_main_cli_action_prompts(monkeypatch): - mock_cli_args = MagicMock(project_root=".", action=CliAction.prompts) + mock_cli_args = Config(project_root=".", action=CliAction.prompts) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) mock_prompts = MagicMock(return_value=0) monkeypatch.setattr("vectorcode.subcommands.prompts", mock_prompts) - monkeypatch.setattr("vectorcode.main.get_project_config", AsyncMock()) + monkeypatch.setattr( + "vectorcode.main.get_project_config", AsyncMock(return_value=Config()) + ) return_code = await async_main() assert return_code == 0 @@ -142,12 +136,12 @@ async def test_async_main_cli_action_prompts(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_query(monkeypatch): - mock_cli_args = MagicMock(no_stderr=False, project_root=".", action=CliAction.query) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.query) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) - mock_final_configs = MagicMock( - host="test_host", port=1234, action=CliAction.query, pipe=False + mock_final_configs = Config( + db_url="http://test_host:1234", action=CliAction.query, pipe=False ) monkeypatch.setattr( "vectorcode.main.get_project_config", @@ -168,7 +162,7 @@ async def test_async_main_cli_action_query(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_vectorise(monkeypatch): - mock_cli_args = MagicMock( + mock_cli_args = Config( no_stderr=False, project_root=".", action=CliAction.vectorise, @@ -177,8 +171,8 @@ async def test_async_main_cli_action_vectorise(monkeypatch): monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) - mock_final_configs = MagicMock( - host="test_host", port=1234, action=CliAction.vectorise, include_hidden=True + mock_final_configs = Config( + db_url="http://test_host:1234", action=CliAction.vectorise, include_hidden=True ) monkeypatch.setattr( "vectorcode.main.get_project_config", @@ -199,11 +193,11 @@ async def test_async_main_cli_action_vectorise(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_drop(monkeypatch): - mock_cli_args = MagicMock(no_stderr=False, project_root=".", action=CliAction.drop) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.drop) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) - mock_final_configs = MagicMock(host="test_host", port=1234, action=CliAction.drop) + mock_final_configs = Config(db_url="http://test_host:1234", action=CliAction.drop) monkeypatch.setattr( "vectorcode.main.get_project_config", AsyncMock( @@ -223,11 +217,11 @@ async def test_async_main_cli_action_drop(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_ls(monkeypatch): - mock_cli_args = MagicMock(no_stderr=False, project_root=".", action=CliAction.ls) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.ls) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) - mock_final_configs = MagicMock(host="test_host", port=1234, action=CliAction.ls) + mock_final_configs = Config(db_url="http://test_host:1234", action=CliAction.ls) monkeypatch.setattr( "vectorcode.main.get_project_config", AsyncMock( @@ -259,13 +253,11 @@ async def test_async_main_cli_action_files(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_update(monkeypatch): - mock_cli_args = MagicMock( - no_stderr=False, project_root=".", action=CliAction.update - ) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.update) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) - mock_final_configs = MagicMock(host="test_host", port=1234, action=CliAction.update) + mock_final_configs = Config(db_url="http://test_host:1234", action=CliAction.update) monkeypatch.setattr( "vectorcode.main.get_project_config", AsyncMock( @@ -285,11 +277,11 @@ async def test_async_main_cli_action_update(monkeypatch): @pytest.mark.asyncio async def test_async_main_cli_action_clean(monkeypatch): - mock_cli_args = MagicMock(no_stderr=False, project_root=".", action=CliAction.clean) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.clean) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) - mock_final_configs = MagicMock(host="test_host", port=1234, action=CliAction.clean) + mock_final_configs = Config(db_url="http://test_host:1234", action=CliAction.clean) monkeypatch.setattr( "vectorcode.main.get_project_config", AsyncMock( @@ -309,11 +301,11 @@ async def test_async_main_cli_action_clean(monkeypatch): @pytest.mark.asyncio async def test_async_main_exception_handling(monkeypatch): - mock_cli_args = MagicMock(no_stderr=False, project_root=".", action=CliAction.query) + mock_cli_args = Config(no_stderr=False, project_root=".", action=CliAction.query) monkeypatch.setattr( "vectorcode.main.parse_cli_args", AsyncMock(return_value=mock_cli_args) ) - mock_final_configs = MagicMock(host="test_host", port=1234, action=CliAction.query) + mock_final_configs = Config(db_url="http://test_host:1234", action=CliAction.query) monkeypatch.setattr( "vectorcode.main.get_project_config", AsyncMock(