codeboarding 0.13.4__tar.gz → 0.13.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codeboarding-0.13.4/codeboarding.egg-info → codeboarding-0.13.5}/PKG-INFO +1 -1
- {codeboarding-0.13.4 → codeboarding-0.13.5/codeboarding.egg-info}/PKG-INFO +1 -1
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding.egg-info/SOURCES.txt +2 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/pyproject.toml +1 -1
- {codeboarding-0.13.4 → codeboarding-0.13.5}/repo_utils/ignore.py +15 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/__init__.py +18 -3
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/csharp_adapter.py +5 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/call_graph_builder.py +28 -16
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/edge_build_context.py +4 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/edge_builder.py +15 -2
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/language_adapter.py +12 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/lsp_client.py +32 -1
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/lsp_constants.py +12 -0
- codeboarding-0.13.5/static_analyzer/engine/lsp_recycler.py +142 -0
- codeboarding-0.13.5/static_analyzer/engine/process_memory.py +104 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/source_inspector.py +54 -13
- {codeboarding-0.13.4 → codeboarding-0.13.5}/LICENSE +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/PYPI.md +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/README.md +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/abstraction_agent.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/agent.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/agent_responses.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/change_status.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/cluster_ids.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/cluster_methods_mixin.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/constants.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/content_hash.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/dependency_discovery.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/details_agent.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/file_index_models.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/incremental_agent.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/incremental_results.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/llm_config.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/llm_errors.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/meta_agent.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/model_capabilities.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/planner_agent.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/abstract_prompt_factory.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/claude_prompts.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/deepseek_prompts.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/gemini_flash_prompts.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/glm_prompts.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/gpt_prompts.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/kimi_prompts.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/prompts/prompt_factory.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/relation_edges.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/repair.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/retry.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/scope_ids.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/base.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/component_bridge_edges.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/get_external_deps.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/get_method_invocations.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/list_git_changes.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/read_cfg.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/read_docs.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/read_file.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/read_file_structure.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/read_packages.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/read_source.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/read_structure.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/tools/toolkit.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/agents/validation.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/caching/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/caching/cache.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/caching/details_cache.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/caching/meta_cache.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding.egg-info/dependency_links.txt +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding.egg-info/entry_points.txt +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding.egg-info/requires.txt +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding.egg-info/top_level.txt +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_cli/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_cli/bootstrap.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_cli/commands/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_cli/commands/full_analysis.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_cli/commands/incremental_analysis.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_cli/commands/partial_analysis.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_cli/view_instructions.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_workflows/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_workflows/analysis.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_workflows/orchestration.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_workflows/rendering.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_workflows/sources/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_workflows/sources/local.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/codeboarding_workflows/sources/remote.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/constants.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/core/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/core/plugin_loader.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/core/protocols.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/core/registry.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/analysis_json.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/cluster_delta.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/cluster_snapshot.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/diagram_generator.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/exceptions.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/file_coverage.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/file_index.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/io_utils.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/run_context.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/run_mode.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/scope_plan.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/diagram_analysis/tree_shape.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/github_action.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/circular_deps.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/cohesion.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/coupling.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/function_size.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/god_class.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/inheritance.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/instability.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/checks/unused_code_diagnostics.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/config.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/models.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/health/runner.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/install.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/logging_config.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/main.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/monitoring/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/monitoring/callbacks.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/monitoring/context.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/monitoring/mixin.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/monitoring/paths.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/monitoring/stats.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/monitoring/writers.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/output_generators/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/output_generators/html.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/output_generators/html_template.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/output_generators/markdown.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/output_generators/mdx.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/output_generators/sphinx.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/repo_utils/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/repo_utils/change_detector.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/repo_utils/errors.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/repo_utils/fingerprint_diff.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/repo_utils/git_ops.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/repo_utils/path_utils.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/setup.cfg +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/analysis_cache.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/analysis_result.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/cluster_helpers.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/cluster_relations.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/constants.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/csharp_config_scanner.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/dotnet_sdk.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/go_adapter.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/java_adapter.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/php_adapter.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/python_adapter.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/rust_adapter.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/typescript_adapter.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/hierarchy_builder.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/models.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/progress.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/protocols.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/result_converter.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/symbol_table.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/utils.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/graph.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/incremental_orchestrator.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/internal_references.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/java_config_scanner.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/java_utils.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/language_results.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/leiden_utils.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/lsp_client/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/lsp_client/diagnostics.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/method_cluster_paths.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/node.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/programming_language.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/reference_resolver.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/scanner.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/typescript_config_scanner.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/telemetry/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/telemetry/device_id.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/telemetry/events.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/telemetry/schemas.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/telemetry/service.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_cli_parser.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_github_action.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_install.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_logging_config.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_main.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_pyproject_packages.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_registry_coverage.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_telemetry_events.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_telemetry_service.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_tool_registry.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_user_config.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_view_instructions.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_vscode_constants.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_windows_compatibility.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tests/test_windows_encoding.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tool_registry/__init__.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tool_registry/installers.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tool_registry/manifest.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tool_registry/paths.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/tool_registry/registry.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/user_config.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/utils.py +0 -0
- {codeboarding-0.13.4 → codeboarding-0.13.5}/vscode_constants.py +0 -0
|
@@ -160,7 +160,9 @@ static_analyzer/engine/hierarchy_builder.py
|
|
|
160
160
|
static_analyzer/engine/language_adapter.py
|
|
161
161
|
static_analyzer/engine/lsp_client.py
|
|
162
162
|
static_analyzer/engine/lsp_constants.py
|
|
163
|
+
static_analyzer/engine/lsp_recycler.py
|
|
163
164
|
static_analyzer/engine/models.py
|
|
165
|
+
static_analyzer/engine/process_memory.py
|
|
164
166
|
static_analyzer/engine/progress.py
|
|
165
167
|
static_analyzer/engine/protocols.py
|
|
166
168
|
static_analyzer/engine/result_converter.py
|
|
@@ -174,6 +174,7 @@ class RepoIgnoreManager:
|
|
|
174
174
|
|
|
175
175
|
def reload(self):
|
|
176
176
|
"""Reload ignore patterns from .gitignore and .codeboardingignore."""
|
|
177
|
+
self._checked_paths: dict[str, bool] = {}
|
|
177
178
|
gitignore_patterns = self._load_gitignore_patterns()
|
|
178
179
|
codeboardingignore_patterns = self._load_codeboardingignore_patterns()
|
|
179
180
|
|
|
@@ -227,7 +228,21 @@ class RepoIgnoreManager:
|
|
|
227
228
|
"""Check if a given path should be ignored.
|
|
228
229
|
|
|
229
230
|
Handles both absolute paths and paths relative to repo_root.
|
|
231
|
+
|
|
232
|
+
Records the result for each checked path until ``reload`` because callers
|
|
233
|
+
ask about the same files during discovery and again while converting
|
|
234
|
+
symbols and references.
|
|
230
235
|
"""
|
|
236
|
+
path_key = str(path)
|
|
237
|
+
should_ignore = self._checked_paths.get(path_key)
|
|
238
|
+
if should_ignore is not None:
|
|
239
|
+
return should_ignore
|
|
240
|
+
|
|
241
|
+
should_ignore = self._compute_should_ignore(path)
|
|
242
|
+
self._checked_paths[path_key] = should_ignore
|
|
243
|
+
return should_ignore
|
|
244
|
+
|
|
245
|
+
def _compute_should_ignore(self, path: Path) -> bool:
|
|
231
246
|
try:
|
|
232
247
|
# Convert to relative path if absolute
|
|
233
248
|
if path.is_absolute():
|
|
@@ -232,6 +232,17 @@ class StaticAnalyzer:
|
|
|
232
232
|
|
|
233
233
|
for engine_config in self._engine_configs:
|
|
234
234
|
adapter, project_path = engine_config.adapter, engine_config.project_path
|
|
235
|
+
# Resolve file membership before paying for the server. A language
|
|
236
|
+
# the scanner detected from a handful of stray files still costs a
|
|
237
|
+
# full LSP process (tsserver + its node workers run ~350MB) that
|
|
238
|
+
# then analyzes nothing. Also reused by ``_run_full_analysis``, so
|
|
239
|
+
# the directory walk happens once per engine rather than twice.
|
|
240
|
+
if not engine_config.source_files:
|
|
241
|
+
engine_config.source_files = adapter.discover_source_files(project_path, self.ignore_manager)
|
|
242
|
+
if not engine_config.source_files:
|
|
243
|
+
logger.info(f"No {adapter.language} source files under {project_path}; skipping its LSP server.")
|
|
244
|
+
continue
|
|
245
|
+
|
|
235
246
|
attempted.append(adapter.language)
|
|
236
247
|
engine_client: LSPClient | None = None
|
|
237
248
|
try:
|
|
@@ -289,12 +300,16 @@ class StaticAnalyzer:
|
|
|
289
300
|
f"Error shutting down partially-started {adapter.language} client during cleanup"
|
|
290
301
|
)
|
|
291
302
|
|
|
303
|
+
if not attempted:
|
|
304
|
+
logger.info(f"No source files for any detected language in {self.repository_path}; no LSP clients started.")
|
|
305
|
+
self._engine_clients = []
|
|
306
|
+
self._clients_started = True
|
|
307
|
+
return
|
|
308
|
+
|
|
292
309
|
if not started:
|
|
293
310
|
self._clients_started = False
|
|
294
311
|
details = f"; failures: {'; '.join(failed_details)}" if failed_details else ""
|
|
295
|
-
raise RuntimeError(
|
|
296
|
-
f"Failed to start any engine LSP client (attempted: {', '.join(attempted) or 'none'}){details}"
|
|
297
|
-
)
|
|
312
|
+
raise RuntimeError(f"Failed to start any engine LSP client (attempted: {', '.join(attempted)}){details}")
|
|
298
313
|
|
|
299
314
|
if failed_languages:
|
|
300
315
|
details = f" Details: {'; '.join(failed_details)}." if failed_details else ""
|
{codeboarding-0.13.4 → codeboarding-0.13.5}/static_analyzer/engine/adapters/csharp_adapter.py
RENAMED
|
@@ -168,6 +168,11 @@ class CSharpAdapter(LanguageAdapter):
|
|
|
168
168
|
"""csharp-ls loads all files from the .sln — didOpen before workspace load kills it."""
|
|
169
169
|
return True
|
|
170
170
|
|
|
171
|
+
@property
|
|
172
|
+
def workspace_owns_documents(self) -> bool:
|
|
173
|
+
"""csharp-ls answers position queries from the loaded solution, opened or not."""
|
|
174
|
+
return True
|
|
175
|
+
|
|
171
176
|
def get_lsp_default_timeout(self) -> int:
|
|
172
177
|
"""csharp-ls needs extra time to load Roslyn workspace for large solutions."""
|
|
173
178
|
return 120
|
|
@@ -13,6 +13,7 @@ from static_analyzer.engine.hierarchy_builder import HierarchyBuilder
|
|
|
13
13
|
from static_analyzer.engine.language_adapter import LanguageAdapter
|
|
14
14
|
from static_analyzer.engine.lsp_client import LSPClient
|
|
15
15
|
from static_analyzer.engine.lsp_constants import DID_OPEN_BATCH_SIZE, EdgeStrategy
|
|
16
|
+
from static_analyzer.engine.lsp_recycler import LSPRecycler
|
|
16
17
|
from static_analyzer.engine.models import CallFlowGraph, LanguageAnalysisResult
|
|
17
18
|
from static_analyzer.engine.source_inspector import SourceInspector
|
|
18
19
|
from static_analyzer.engine.symbol_table import SymbolTable
|
|
@@ -60,7 +61,9 @@ class CallGraphBuilder:
|
|
|
60
61
|
t_indices_done = time.monotonic()
|
|
61
62
|
logger.info("Build indices: %.1fs", t_indices_done - t_symbols_done)
|
|
62
63
|
|
|
63
|
-
ctx = EdgeBuildContext(
|
|
64
|
+
ctx = EdgeBuildContext(
|
|
65
|
+
self._lsp, self._symbol_table, self._source_inspector, recycler=self._build_recycler(source_files)
|
|
66
|
+
)
|
|
64
67
|
edge_set = self._build_edges(ctx, source_files)
|
|
65
68
|
edge_set = self._postprocess_edges(edge_set)
|
|
66
69
|
t_edges_done = time.monotonic()
|
|
@@ -118,28 +121,37 @@ class CallGraphBuilder:
|
|
|
118
121
|
source_files=abs_files,
|
|
119
122
|
)
|
|
120
123
|
|
|
124
|
+
def _build_recycler(self, source_files: list[Path]) -> LSPRecycler | None:
|
|
125
|
+
"""A recycler for servers whose memory the references phase would otherwise grow without bound."""
|
|
126
|
+
if not self._adapter.workspace_owns_documents or not source_files:
|
|
127
|
+
return None
|
|
128
|
+
return LSPRecycler(self._lsp, source_files[0], self._probe_timeout(len(source_files)))
|
|
129
|
+
|
|
121
130
|
def _build_edges(self, ctx: EdgeBuildContext, source_files: list[Path]) -> EdgeMap:
|
|
122
131
|
"""Dispatch to the edge-building strategy specified by the adapter."""
|
|
123
132
|
if self._adapter.edge_strategy == EdgeStrategy.DEFINITIONS:
|
|
124
133
|
return build_edges_via_definitions(self._adapter, ctx, source_files)
|
|
125
134
|
return build_edges_via_references(self._adapter, ctx, source_files)
|
|
126
135
|
|
|
127
|
-
def
|
|
128
|
-
"""
|
|
129
|
-
total = len(source_files)
|
|
136
|
+
def _probe_timeout(self, total_files: int) -> int:
|
|
137
|
+
"""Seconds to allow a synchronization probe to block on LSP indexing.
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# to load a Roslyn workspace).
|
|
139
|
+
Scales linearly with file count (no OS branching); the per-file ceiling
|
|
140
|
+
is picked conservatively to cover gopls on AV-heavy Windows and APFS
|
|
141
|
+
macOS without being loose enough to let a hung LSP waste CI minutes.
|
|
142
|
+
Adapters can raise the floor via ``get_probe_timeout_minimum()`` (e.g.
|
|
143
|
+
csharp-ls needs extra time to load a Roslyn workspace).
|
|
144
|
+
"""
|
|
138
145
|
_PROBE_STARTUP_BASE = 60 # seconds — LSP startup independent of file count
|
|
139
146
|
_PROBE_PER_FILE = 2.0 # seconds — ceiling across OSes (Linux observed ~0.35s/file)
|
|
140
147
|
_PROBE_MAX_TIMEOUT = 1800 # seconds — hard cap
|
|
141
|
-
probe_timeout = int(min(_PROBE_STARTUP_BASE +
|
|
142
|
-
|
|
148
|
+
probe_timeout = int(min(_PROBE_STARTUP_BASE + total_files * _PROBE_PER_FILE, _PROBE_MAX_TIMEOUT))
|
|
149
|
+
return max(probe_timeout, self._adapter.get_probe_timeout_minimum())
|
|
150
|
+
|
|
151
|
+
def _discover_symbols(self, source_files: list[Path]) -> None:
|
|
152
|
+
"""Phase 0+1: Synchronize with the server, open files, and extract symbols."""
|
|
153
|
+
total = len(source_files)
|
|
154
|
+
probe_timeout = self._probe_timeout(total)
|
|
143
155
|
|
|
144
156
|
interleave_open = self._adapter.interleave_did_open_with_symbols
|
|
145
157
|
|
|
@@ -164,7 +176,7 @@ class CallGraphBuilder:
|
|
|
164
176
|
# Interleaved adapters deliberately query again after didOpen so
|
|
165
177
|
# that each overlay notification has a response barrier.
|
|
166
178
|
should_reuse_probe = idx == 1 and not interleave_open
|
|
167
|
-
if should_reuse_probe
|
|
179
|
+
if should_reuse_probe:
|
|
168
180
|
symbols = probe_result
|
|
169
181
|
elif interleave_open:
|
|
170
182
|
symbols = self._lsp.document_symbol(file_path, timeout=probe_timeout)
|
|
@@ -193,9 +205,9 @@ class CallGraphBuilder:
|
|
|
193
205
|
pbar.finish()
|
|
194
206
|
logger.info("did_open %d files: %.1fs", total, time.monotonic() - t_open_start)
|
|
195
207
|
|
|
196
|
-
def _send_sync_probe(self, source_files: list[Path], probe_timeout: int) -> list[dict]
|
|
208
|
+
def _send_sync_probe(self, source_files: list[Path], probe_timeout: int) -> list[dict]:
|
|
197
209
|
"""Send a documentSymbol probe to wait for the LSP server to finish indexing."""
|
|
198
|
-
probe_result: list[dict]
|
|
210
|
+
probe_result: list[dict] = []
|
|
199
211
|
logger.info("Waiting for LSP server indexing (timeout=%ds)...", probe_timeout)
|
|
200
212
|
t_probe = time.monotonic()
|
|
201
213
|
if source_files:
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
7
|
from static_analyzer.engine.lsp_client import LSPClient
|
|
8
|
+
from static_analyzer.engine.lsp_recycler import LSPRecycler
|
|
8
9
|
from static_analyzer.engine.source_inspector import SourceInspector
|
|
9
10
|
from static_analyzer.engine.symbol_table import SymbolTable
|
|
10
11
|
|
|
@@ -16,3 +17,6 @@ class EdgeBuildContext:
|
|
|
16
17
|
lsp: LSPClient
|
|
17
18
|
symbol_table: SymbolTable
|
|
18
19
|
source_inspector: SourceInspector
|
|
20
|
+
# Set only for servers that may be restarted mid-phase; None means the
|
|
21
|
+
# strategy runs against a single server process for the whole phase.
|
|
22
|
+
recycler: LSPRecycler | None = None
|
|
@@ -85,8 +85,13 @@ def build_edges_via_references(
|
|
|
85
85
|
skipped_positions = 0
|
|
86
86
|
|
|
87
87
|
pbar = ProgressLogger("Phase 2 (edges)", total_unique, unit="pos")
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
batch_start = 0
|
|
89
|
+
while batch_start < total_unique:
|
|
90
|
+
# The recycler samples the server here — it decides both whether to
|
|
91
|
+
# restart it and how many queries this batch may carry.
|
|
92
|
+
size = batch_size if ctx.recycler is None else ctx.recycler.before_batch(batch_size)
|
|
93
|
+
batch_positions = unique_positions[batch_start : batch_start + size]
|
|
94
|
+
batch_start += len(batch_positions)
|
|
90
95
|
|
|
91
96
|
# Filter out positions from files that already produced LSP errors
|
|
92
97
|
filtered_positions: list[tuple[str, int, int]] = []
|
|
@@ -140,6 +145,14 @@ def build_edges_via_references(
|
|
|
140
145
|
refs_total,
|
|
141
146
|
(1 - refs_call_sites / max(refs_total, 1)) * 100,
|
|
142
147
|
)
|
|
148
|
+
if ctx.recycler is not None and ctx.recycler.recycle_count:
|
|
149
|
+
# Worth one line at the end: a recycled run answered some queries from a
|
|
150
|
+
# different server process than the one it started with.
|
|
151
|
+
logger.info(
|
|
152
|
+
"Phase 2 (edges): the language server was recycled %d time(s); %d batches ran shrunk under memory pressure",
|
|
153
|
+
ctx.recycler.recycle_count,
|
|
154
|
+
ctx.recycler.shrunk_batches,
|
|
155
|
+
)
|
|
143
156
|
return edge_set
|
|
144
157
|
|
|
145
158
|
|
|
@@ -191,6 +191,18 @@ class LanguageAdapter(ABC):
|
|
|
191
191
|
"""If True, probe first, then pair each didOpen with its documentSymbol request."""
|
|
192
192
|
return False
|
|
193
193
|
|
|
194
|
+
@property
|
|
195
|
+
def workspace_owns_documents(self) -> bool:
|
|
196
|
+
"""If True, the server reads documents from the project, not from our didOpen.
|
|
197
|
+
|
|
198
|
+
Such a server answers position queries for files we never opened, which
|
|
199
|
+
makes a restarted process equivalent to the one it replaced — so the
|
|
200
|
+
references phase may recycle it to bound memory (see ``LSPRecycler``).
|
|
201
|
+
Servers that only know the documents we pushed must not be recycled:
|
|
202
|
+
they would come back empty.
|
|
203
|
+
"""
|
|
204
|
+
return False
|
|
205
|
+
|
|
194
206
|
def get_probe_timeout_minimum(self) -> int:
|
|
195
207
|
"""Return the minimum probe timeout in seconds for initial indexing.
|
|
196
208
|
|
|
@@ -234,6 +234,36 @@ class LSPClient:
|
|
|
234
234
|
self._opened_uris.clear()
|
|
235
235
|
self._doc_versions.clear()
|
|
236
236
|
|
|
237
|
+
def restart(self) -> None:
|
|
238
|
+
"""Replace the server process with a fresh one, same configuration.
|
|
239
|
+
|
|
240
|
+
Used to release memory a server accumulated and will not give back (see
|
|
241
|
+
``LSPRecycler``). Diagnostics already collected are kept — they describe
|
|
242
|
+
the source, not the process. Open-document state is not: the new server
|
|
243
|
+
has nothing open, so only callers whose server reads documents from the
|
|
244
|
+
project itself may restart mid-analysis. Readiness is the caller's to
|
|
245
|
+
re-establish; this client has no view of what ready means per server.
|
|
246
|
+
"""
|
|
247
|
+
self.shutdown()
|
|
248
|
+
self._shutdown_event.clear()
|
|
249
|
+
self._server_ready.clear()
|
|
250
|
+
self._server_health = None
|
|
251
|
+
self._server_health_message = None
|
|
252
|
+
self._work_done_progress_titles.clear()
|
|
253
|
+
self._work_done_progress_failures.clear()
|
|
254
|
+
self._init_failed = False
|
|
255
|
+
# Drop responses the dead server never got to be asked about, so a
|
|
256
|
+
# stale id cannot satisfy a pending request on the new connection.
|
|
257
|
+
self._msg_queue = queue.Queue()
|
|
258
|
+
self.start()
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def pid(self) -> int | None:
|
|
262
|
+
"""OS pid of the running server process, or None when it is not running."""
|
|
263
|
+
if self._process is None or self._process.poll() is not None:
|
|
264
|
+
return None
|
|
265
|
+
return self._process.pid
|
|
266
|
+
|
|
237
267
|
# ---- Document management ----
|
|
238
268
|
|
|
239
269
|
def did_open(self, file_path: Path, language_id: str) -> None:
|
|
@@ -299,7 +329,7 @@ class LSPClient:
|
|
|
299
329
|
return result
|
|
300
330
|
return []
|
|
301
331
|
|
|
302
|
-
def references(self, file_path: Path, line: int, character: int) -> list[dict]:
|
|
332
|
+
def references(self, file_path: Path, line: int, character: int, timeout: int | None = None) -> list[dict]:
|
|
303
333
|
"""Find all references to the symbol at the given position."""
|
|
304
334
|
result = self._send_request(
|
|
305
335
|
"textDocument/references",
|
|
@@ -308,6 +338,7 @@ class LSPClient:
|
|
|
308
338
|
"position": {"line": line, "character": character},
|
|
309
339
|
"context": {"includeDeclaration": True},
|
|
310
340
|
},
|
|
341
|
+
timeout=timeout,
|
|
311
342
|
)
|
|
312
343
|
if isinstance(result, list):
|
|
313
344
|
return result
|
|
@@ -25,6 +25,18 @@ CALLABLE_KINDS: set[int] = {
|
|
|
25
25
|
# Batch size for did_open to avoid overwhelming LSP servers
|
|
26
26
|
DID_OPEN_BATCH_SIZE = 50
|
|
27
27
|
|
|
28
|
+
# Share of RAM the language server may hold before it gets recycled. This
|
|
29
|
+
# leaves room for Python, other language servers, and the operating system.
|
|
30
|
+
MEMORY_BUDGET_FRACTION = 0.4
|
|
31
|
+
MIN_MEMORY_BUDGET = 2 * 1024**3
|
|
32
|
+
MAX_MEMORY_BUDGET = 12 * 1024**3
|
|
33
|
+
|
|
34
|
+
# Reduce reference-query concurrency before reaching the recycle threshold.
|
|
35
|
+
PRESSURE_FRACTION = 0.5
|
|
36
|
+
PRESSURED_BATCH_DIVISOR = 10
|
|
37
|
+
|
|
38
|
+
MEMORY_BUDGET_ENV_VAR = "CODEBOARDING_LSP_MEMORY_BUDGET_MB"
|
|
39
|
+
|
|
28
40
|
|
|
29
41
|
class EdgeStrategy(StrEnum):
|
|
30
42
|
"""Edge-building strategy selection for Phase 2."""
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Bounds language-server memory during the references phase by restarting it.
|
|
2
|
+
|
|
3
|
+
Answering ``textDocument/references`` across a large workspace makes Roslyn (and
|
|
4
|
+
comparable engines) materialize a compilation per project and keep every one.
|
|
5
|
+
Growth is linear in positions queried: on bitwarden/server csharp-ls climbed
|
|
6
|
+
past 11GB and the run was OOM-killed on a 16GB runner. GC tuning halves the
|
|
7
|
+
slope but the line still has no ceiling, so a big enough repo always wins.
|
|
8
|
+
|
|
9
|
+
Restarting the server between batches drops the whole accumulation and costs one
|
|
10
|
+
workspace reload. It is only sound for servers that read documents from the
|
|
11
|
+
project itself rather than from our ``didOpen`` overlays -- those answer position
|
|
12
|
+
queries for files we never opened, so a fresh process is equivalent to the old
|
|
13
|
+
one. Adapters declare that via ``LanguageAdapter.workspace_owns_documents``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from static_analyzer.engine.lsp_client import LSPClient
|
|
24
|
+
from static_analyzer.engine.lsp_constants import (
|
|
25
|
+
MAX_MEMORY_BUDGET,
|
|
26
|
+
MEMORY_BUDGET_ENV_VAR,
|
|
27
|
+
MEMORY_BUDGET_FRACTION,
|
|
28
|
+
MIN_MEMORY_BUDGET,
|
|
29
|
+
PRESSURE_FRACTION,
|
|
30
|
+
PRESSURED_BATCH_DIVISOR,
|
|
31
|
+
)
|
|
32
|
+
from static_analyzer.engine.process_memory import format_bytes, physical_memory_bytes, process_tree_rss
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def default_memory_budget() -> int:
|
|
38
|
+
"""Bytes a language server may occupy before it gets recycled."""
|
|
39
|
+
override = os.environ.get(MEMORY_BUDGET_ENV_VAR, "").strip()
|
|
40
|
+
if override:
|
|
41
|
+
try:
|
|
42
|
+
return int(float(override) * 1024**2)
|
|
43
|
+
except ValueError:
|
|
44
|
+
logger.warning("Ignoring %s=%r: not a number of megabytes", MEMORY_BUDGET_ENV_VAR, override)
|
|
45
|
+
physical = physical_memory_bytes()
|
|
46
|
+
if not physical:
|
|
47
|
+
return MIN_MEMORY_BUDGET
|
|
48
|
+
return int(min(max(physical * MEMORY_BUDGET_FRACTION, MIN_MEMORY_BUDGET), MAX_MEMORY_BUDGET))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LSPRecycler:
|
|
52
|
+
"""Restarts a language server whose process tree outgrows the budget."""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
lsp: LSPClient,
|
|
57
|
+
probe_file: Path,
|
|
58
|
+
probe_timeout: int,
|
|
59
|
+
budget_bytes: int = 0,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._lsp = lsp
|
|
62
|
+
self._probe_file = probe_file
|
|
63
|
+
self._probe_timeout = probe_timeout
|
|
64
|
+
self._budget = budget_bytes or default_memory_budget()
|
|
65
|
+
self.recycle_count = 0
|
|
66
|
+
self.shrunk_batches = 0
|
|
67
|
+
self._disarmed = False
|
|
68
|
+
logger.info("LSP recycler armed at a %s budget", format_bytes(self._budget))
|
|
69
|
+
|
|
70
|
+
def before_batch(self, full_batch_size: int) -> int:
|
|
71
|
+
"""Sample the server, restart it if over budget, and size the next batch.
|
|
72
|
+
|
|
73
|
+
One sample per batch, not per N batches: a single batch of solution-wide
|
|
74
|
+
reference queries can allocate gigabytes, so a coarser interval sails
|
|
75
|
+
past the budget between checks. Reading the process table costs
|
|
76
|
+
milliseconds against a batch's own LSP round-trip.
|
|
77
|
+
|
|
78
|
+
Returns how many positions the next batch may carry. A batch is one
|
|
79
|
+
round-trip we cannot interrupt, so its own growth is the floor under any
|
|
80
|
+
bound we can offer; near the ceiling we carry fewer queries at once to
|
|
81
|
+
keep that floor low.
|
|
82
|
+
"""
|
|
83
|
+
if self._disarmed:
|
|
84
|
+
return full_batch_size
|
|
85
|
+
used = self._server_rss()
|
|
86
|
+
if used >= self._budget:
|
|
87
|
+
self._recycle(used)
|
|
88
|
+
return full_batch_size
|
|
89
|
+
if used < self._budget * PRESSURE_FRACTION:
|
|
90
|
+
return full_batch_size
|
|
91
|
+
self.shrunk_batches += 1
|
|
92
|
+
return max(1, full_batch_size // PRESSURED_BATCH_DIVISOR)
|
|
93
|
+
|
|
94
|
+
def _recycle(self, used: int) -> None:
|
|
95
|
+
logger.warning(
|
|
96
|
+
"LSP holding %s (budget %s) — restarting it to release cached compilations",
|
|
97
|
+
format_bytes(used),
|
|
98
|
+
format_bytes(self._budget),
|
|
99
|
+
)
|
|
100
|
+
t_restart = time.monotonic()
|
|
101
|
+
self._lsp.restart()
|
|
102
|
+
# Blocks until the workspace is loaded again: a workspace-backed server
|
|
103
|
+
# cannot answer a documentSymbol until it has read the project files.
|
|
104
|
+
self._lsp.document_symbol(self._probe_file, timeout=self._probe_timeout)
|
|
105
|
+
# A loaded workspace is not a warm reference index — that is built on the
|
|
106
|
+
# first references query. Pay for it here, on a request we can give a long
|
|
107
|
+
# timeout, rather than losing a whole batch of real positions to the
|
|
108
|
+
# per-request deadline. Measured: recycling without this tripled the
|
|
109
|
+
# "Timeout waiting for references request" warnings.
|
|
110
|
+
try:
|
|
111
|
+
self._lsp.references(self._probe_file, 0, 0, timeout=self._probe_timeout)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
logger.debug("Post-restart references warmup failed (non-fatal): %s", exc)
|
|
114
|
+
self.recycle_count += 1
|
|
115
|
+
reloaded = self._server_rss()
|
|
116
|
+
logger.info(
|
|
117
|
+
"LSP restarted in %.0fs: %s -> %s (recycle #%d)",
|
|
118
|
+
time.monotonic() - t_restart,
|
|
119
|
+
format_bytes(used),
|
|
120
|
+
format_bytes(reloaded),
|
|
121
|
+
self.recycle_count,
|
|
122
|
+
)
|
|
123
|
+
if reloaded >= self._budget:
|
|
124
|
+
# A workspace whose freshly-loaded footprint already exceeds the
|
|
125
|
+
# budget cannot be helped by restarting: every batch would trigger
|
|
126
|
+
# another reload and the phase would make no progress. Say so and
|
|
127
|
+
# get out of the way — the budget is a fraction of RAM, so running
|
|
128
|
+
# on may still fit, and a livelock certainly will not.
|
|
129
|
+
self._disarmed = True
|
|
130
|
+
logger.warning(
|
|
131
|
+
"Loading this workspace alone needs %s, at or above the %s budget — memory can no longer be bounded. "
|
|
132
|
+
"Raise %s or analyze on a host with more RAM.",
|
|
133
|
+
format_bytes(reloaded),
|
|
134
|
+
format_bytes(self._budget),
|
|
135
|
+
MEMORY_BUDGET_ENV_VAR,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _server_rss(self) -> int:
|
|
139
|
+
pid = self._lsp.pid
|
|
140
|
+
if pid is None:
|
|
141
|
+
return 0
|
|
142
|
+
return process_tree_rss(pid)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Process-tree memory helpers used to enforce the LSP budget."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
_PAGE_SIZE = os.sysconf("SC_PAGE_SIZE") if hasattr(os, "sysconf") else 4096
|
|
8
|
+
_IS_LINUX = sys.platform.startswith("linux")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def physical_memory_bytes() -> int:
|
|
12
|
+
"""Return total physical memory, or zero when it cannot be determined."""
|
|
13
|
+
try:
|
|
14
|
+
return os.sysconf("SC_PHYS_PAGES") * os.sysconf("SC_PAGE_SIZE")
|
|
15
|
+
except (ValueError, OSError, AttributeError):
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def format_bytes(num_bytes: float) -> str:
|
|
20
|
+
"""Render a byte count as a short human-readable string."""
|
|
21
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
22
|
+
if abs(num_bytes) < 1024.0:
|
|
23
|
+
return f"{num_bytes:.0f}{unit}" if unit == "B" else f"{num_bytes:.1f}{unit}"
|
|
24
|
+
num_bytes /= 1024.0
|
|
25
|
+
return f"{num_bytes:.1f}TB"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _linux_process_table() -> dict[int, tuple[int, int]]:
|
|
29
|
+
"""Return each readable Linux process as ``{pid: (ppid, rss_bytes)}``."""
|
|
30
|
+
table: dict[int, tuple[int, int]] = {}
|
|
31
|
+
try:
|
|
32
|
+
entries = os.listdir("/proc")
|
|
33
|
+
except OSError:
|
|
34
|
+
return table
|
|
35
|
+
|
|
36
|
+
for entry in entries:
|
|
37
|
+
if not entry.isdigit():
|
|
38
|
+
continue
|
|
39
|
+
pid = int(entry)
|
|
40
|
+
try:
|
|
41
|
+
with open(f"/proc/{pid}/stat", "rb") as handle:
|
|
42
|
+
stat = handle.read().decode("utf-8", errors="replace")
|
|
43
|
+
except OSError:
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
close_paren = stat.rfind(")")
|
|
47
|
+
if close_paren == -1:
|
|
48
|
+
continue
|
|
49
|
+
fields = stat[close_paren + 2 :].split()
|
|
50
|
+
if len(fields) < 22:
|
|
51
|
+
continue
|
|
52
|
+
try:
|
|
53
|
+
table[pid] = (int(fields[1]), int(fields[21]) * _PAGE_SIZE)
|
|
54
|
+
except ValueError:
|
|
55
|
+
continue
|
|
56
|
+
return table
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _ps_process_table() -> dict[int, tuple[int, int]]:
|
|
60
|
+
"""Return each readable macOS/BSD process as ``{pid: (ppid, rss_bytes)}``."""
|
|
61
|
+
table: dict[int, tuple[int, int]] = {}
|
|
62
|
+
try:
|
|
63
|
+
output = subprocess.run(
|
|
64
|
+
["ps", "-axo", "pid=,ppid=,rss="],
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
timeout=20,
|
|
68
|
+
).stdout
|
|
69
|
+
except (OSError, subprocess.SubprocessError):
|
|
70
|
+
return table
|
|
71
|
+
|
|
72
|
+
for line in output.splitlines():
|
|
73
|
+
parts = line.split()
|
|
74
|
+
if len(parts) != 3:
|
|
75
|
+
continue
|
|
76
|
+
try:
|
|
77
|
+
pid, ppid, rss_kb = int(parts[0]), int(parts[1]), int(parts[2])
|
|
78
|
+
except ValueError:
|
|
79
|
+
continue
|
|
80
|
+
table[pid] = (ppid, rss_kb * 1024)
|
|
81
|
+
return table
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def process_tree_rss(root_pid: int) -> int:
|
|
85
|
+
"""Return resident memory held by a process and all its descendants."""
|
|
86
|
+
table = _linux_process_table() if _IS_LINUX else _ps_process_table()
|
|
87
|
+
if root_pid not in table:
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
children_by_parent: dict[int, list[int]] = {}
|
|
91
|
+
for pid, (parent_pid, _) in table.items():
|
|
92
|
+
children_by_parent.setdefault(parent_pid, []).append(pid)
|
|
93
|
+
|
|
94
|
+
total = 0
|
|
95
|
+
stack = [root_pid]
|
|
96
|
+
seen: set[int] = set()
|
|
97
|
+
while stack:
|
|
98
|
+
pid = stack.pop()
|
|
99
|
+
if pid in seen:
|
|
100
|
+
continue
|
|
101
|
+
seen.add(pid)
|
|
102
|
+
total += table[pid][1]
|
|
103
|
+
stack.extend(children_by_parent.get(pid, []))
|
|
104
|
+
return total
|