Skip to content

Commit 6504034

Browse files
outp1Davidyz
andauthored
feat(debugging): Implement cProfile-based profiling for performance analysis (#271)
* feat(cli): implement debug mode with coredumpy and viztracer * chore(cli): exclude debugging.py from coverage * ci(cli): add debug group to pdm lock * feat(debugging): Implement cProfile-based profiling for performance analysis Since viztracer didn't work (gaogaotiantian/viztracer#606), I decided to replace it with cProfile * chore(dependencies): Remove viztracer from debug dependencies * fix(cli): lazy import * feat(debugging): add log directory creation and crash debugging support * chore(build): revert makefile and ci/cd outdated changes * chore(dependencies): Add coredumpy to development dependencies * fix(debugging): Add noqa comment to suppress linting warning * docs(cli): remove debug dependency from cli help * fix(cli): make sure to enable `coredumpy` when available. * tests(cli): Replace `MagicMock` with `Config` so that `debug` won't be truthy * docs(cli): Add profiling and post-mortem debugging instructions * ci: Update git auto commit action and ignore main branch * Auto generate docs --------- Co-authored-by: Zhe Yu <[email protected]> Co-authored-by: Davidyz <[email protected]>
1 parent 0046e64 commit 6504034

File tree

9 files changed

+154
-43
lines changed

9 files changed

+154
-43
lines changed

.github/workflows/panvimdoc.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
name: panvimdoc
22

33
on:
4-
pull_request:
4+
push:
5+
branches-ignore:
6+
- 'main'
57

68
permissions:
79
contents: write
@@ -64,7 +66,8 @@ jobs:
6466
shiftheadinglevelby: 0 # Shift heading levels by specified number
6567
incrementheadinglevelby: 0 # Increment heading levels by specified number
6668

67-
- uses: stefanzweifel/git-auto-commit-action@v4
69+
- uses: stefanzweifel/git-auto-commit-action@v6.0.1
6870
with:
6971
commit_message: "Auto generate docs"
7072
branch: ${{ github.head_ref }}
73+
file_pattern: 'doc/*.txt'

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.PHONY: multitest
22

3-
DEFAULT_GROUPS=--group dev --group lsp --group mcp
3+
DEFAULT_GROUPS=--group dev --group lsp --group mcp --group debug
44

55
deps:
66
pdm lock $(DEFAULT_GROUPS) || pdm lock $(DEFAULT_GROUPS) --group legacy; \

doc/VectorCode-cli.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Table of Contents *VectorCode-cli-table-of-contents*
3737
- |VectorCode-cli-cleaning-up|
3838
- |VectorCode-cli-inspecting-and-manupulating-files-in-an-indexed-project|
3939
- |VectorCode-cli-debugging-and-diagnosing|
40+
- |VectorCode-cli-profiling|
41+
- |VectorCode-cli-post-mortem-debugging|
4042
- |VectorCode-cli-shell-completion|
4143
- |VectorCode-cli-hardware-acceleration|
4244
- |VectorCode-cli-for-developers|
@@ -605,6 +607,24 @@ For example:
605607
Depending on the MCP/LSP client implementation, you may need to take extra
606608
steps to make sure the environment variables are captured by VectorCode.
607609

610+
PROFILING
611+
612+
When you pass `--debug` parameter to the CLI, VectorCode will track the call
613+
stacks with cprofile <https://docs.python.org/3/library/profile.html>. The
614+
stats will be saved to the log directory mentioned above. You may use an
615+
external stats viewer (like snakeviz <https://jiffyclub.github.io/snakeviz/>)
616+
to load the profiling stats for a better viewing experience.
617+
618+
619+
POST-MORTEM DEBUGGING
620+
621+
VectorCode can work with coredumpy
622+
<https://github.com/gaogaotiantian/coredumpy> to snapshot an exception so that
623+
developers can inspect the error asynchronously. To use this, you’d need to
624+
install the `vectorcode[debug]` dependency group: `uv tool install
625+
vectorcode[debug]`.
626+
627+
608628
SHELL COMPLETION*VectorCode-cli-vectorcode-command-line-tool-shell-completion*
609629

610630
VectorCode supports shell completion for bash/zsh/tcsh. You can use `vectorcode

docs/cli.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
* [Cleaning up](#cleaning-up)
2727
* [Inspecting and Manupulating Files in an Indexed Project](#inspecting-and-manupulating-files-in-an-indexed-project)
2828
* [Debugging and Diagnosing](#debugging-and-diagnosing)
29+
* [Profiling](#profiling)
30+
* [Post-mortem debugging](#post-mortem-debugging)
2931
* [Shell Completion](#shell-completion)
3032
* [Hardware Acceleration](#hardware-acceleration)
3133
* [For Developers](#for-developers)
@@ -551,6 +553,21 @@ VECTORCODE_LOG_LEVEL=INFO vectorcode vectorise file1.py file2.lua
551553
> Depending on the MCP/LSP client implementation, you may need to take extra
552554
> steps to make sure the environment variables are captured by VectorCode.
553555
556+
#### Profiling
557+
558+
When you pass `--debug` parameter to the CLI, VectorCode will track the call
559+
stacks with [cprofile](https://docs.python.org/3/library/profile.html). The
560+
stats will be saved to the log directory mentioned above. You may use an
561+
external stats viewer (like [snakeviz](https://jiffyclub.github.io/snakeviz/))
562+
to load the profiling stats for a better viewing experience.
563+
564+
#### Post-mortem debugging
565+
566+
VectorCode can work with [coredumpy](https://github.com/gaogaotiantian/coredumpy)
567+
to snapshot an exception so that developers can inspect the error
568+
asynchronously. To use this, you'd need to install the `vectorcode[debug]`
569+
dependency group: `uv tool install vectorcode[debug]`.
570+
554571
## Shell Completion
555572

556573
VectorCode supports shell completion for bash/zsh/tcsh. You can use `vectorcode -s {bash,zsh,tcsh}`

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ omit = [
4646
"./tests/*",
4747
"src/vectorcode/_version.py",
4848
"src/vectorcode/__init__.py",
49+
"src/vectorcode/debugging.py",
4950
"/tmp/*",
5051
]
5152
include = ['src/vectorcode/**/*.py']
@@ -63,14 +64,12 @@ write_template = "__version__ = '{}' # pragma: no cover"
6364
dev = [
6465
"ipython>=8.31.0",
6566
"ruff>=0.9.1",
66-
"viztracer>=1.0.0",
6767
"pre-commit>=4.0.1",
6868
"pytest>=8.3.4",
6969
"pdm-backend>=2.4.3",
7070
"coverage>=7.6.12",
7171
"pytest-asyncio>=0.25.3",
7272
"debugpy>=1.8.12",
73-
"coredumpy>=0.4.1",
7473
"basedpyright>=1.29.2",
7574
]
7675

@@ -79,6 +78,7 @@ legacy = ["numpy<2.0.0", "torch==2.2.2", "transformers<=4.49.0"]
7978
intel = ['optimum[openvino]', 'openvino']
8079
lsp = ['pygls<2.0.0', 'lsprotocol']
8180
mcp = ['mcp<2.0.0', 'pydantic']
81+
debug = ["coredumpy>=0.4.1"]
8282

8383
[tool.basedpyright]
8484
typeCheckingMode = "standard"

src/vectorcode/cli_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class FilesAction(StrEnum):
7777

7878
@dataclass
7979
class Config:
80+
debug: bool = False
8081
no_stderr: bool = False
8182
recursive: bool = False
8283
include_hidden: bool = False
@@ -198,6 +199,12 @@ async def merge_from(self, other: "Config") -> "Config":
198199
def get_cli_parser():
199200
__default_config = Config()
200201
shared_parser = argparse.ArgumentParser(add_help=False)
202+
shared_parser.add_argument(
203+
"--debug",
204+
default=False,
205+
action="store_true",
206+
help="Enable debug mode.",
207+
)
201208
chunking_parser = argparse.ArgumentParser(add_help=False)
202209
chunking_parser.add_argument(
203210
"--overlap",
@@ -423,6 +430,7 @@ async def parse_cli_args(args: Optional[Sequence[str]] = None):
423430
"action": CliAction(main_args.action),
424431
"project_root": main_args.project_root,
425432
"pipe": main_args.pipe,
433+
"debug": main_args.debug,
426434
}
427435

428436
match main_args.action:

src/vectorcode/debugging.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import atexit
2+
import cProfile
3+
import logging
4+
import os
5+
import pstats
6+
from datetime import datetime
7+
8+
__LOG_DIR = os.path.expanduser("~/.local/share/vectorcode/logs/")
9+
10+
logger = logging.getLogger(name=__name__)
11+
12+
__profiler: cProfile.Profile | None = None
13+
14+
15+
def _ensure_log_dir():
16+
"""Ensure the log directory exists"""
17+
os.makedirs(__LOG_DIR, exist_ok=True)
18+
19+
20+
def finish():
21+
"""Clean up profiling and save results"""
22+
if __profiler is not None:
23+
try:
24+
__profiler.disable()
25+
stats_file = os.path.join(
26+
__LOG_DIR,
27+
f"cprofile-{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.stats",
28+
)
29+
__profiler.dump_stats(stats_file)
30+
print(f"cProfile stats saved to: {stats_file}")
31+
32+
# Print summary stats
33+
stats = pstats.Stats(__profiler)
34+
stats.sort_stats("cumulative")
35+
stats.print_stats(20)
36+
except Exception as e:
37+
logger.warning(f"Failed to save cProfile output: {e}")
38+
39+
40+
def enable():
41+
"""Enable cProfile-based profiling and crash debugging"""
42+
global __profiler
43+
44+
try:
45+
_ensure_log_dir()
46+
47+
# Initialize cProfile for comprehensive profiling
48+
__profiler = cProfile.Profile()
49+
__profiler.enable()
50+
atexit.register(finish)
51+
logger.info("cProfile profiling enabled successfully")
52+
53+
try:
54+
import coredumpy # noqa: F401
55+
56+
logger.info("coredumpy crash debugging enabled successfully")
57+
coredumpy.patch_except(directory=__LOG_DIR)
58+
except Exception as e:
59+
logger.warning(
60+
f"Crash debugging will not be available. Failed to import coredumpy: {e}"
61+
)
62+
63+
except Exception as e:
64+
logger.error(f"Failed to initialize cProfile: {e}")
65+
logger.warning("Profiling will not be available for this session")

src/vectorcode/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ async def async_main():
2323
cli_args = await parse_cli_args()
2424
if cli_args.no_stderr:
2525
sys.stderr = open(os.devnull, "w")
26+
27+
if cli_args.debug:
28+
from vectorcode import debugging
29+
30+
debugging.enable()
31+
2632
logger.info("Collected CLI arguments: %s", cli_args)
2733

2834
if cli_args.project_root is None:

0 commit comments

Comments
 (0)