-
Notifications
You must be signed in to change notification settings - Fork 4
Perf/incremental symbol cache #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -61,6 +61,23 @@ class SymbolTable: | |||||||||||||||||||||||||||||||||||||||||||||||||
| symbols: Dict[str, Dict[str, Dict[str, Symbol]]] = field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # Map: global_name -> Symbol (for easy cross-file lookup of exports) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exports: Dict[str, Symbol] = field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # Metadata for caching (Map: file_path -> mtime) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: Dict[str, float] = field(default_factory=dict) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def remove_file(self, file_path: str) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Remove all symbols associated with a specific file.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if file_path not in self.symbols: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for scope, names in self.symbols[file_path].items(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if scope == "global": | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for name, symbol in list(names.items()): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if name in self.exports and self.exports[name].file_path == file_path: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| del self.exports[name] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| del self.symbols[file_path] | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if file_path in self.metadata: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| del self.metadata[file_path] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix stale metadata cleanup in At Line 69, the early return prevents metadata cleanup for files that had no extracted symbols. Deleted symbol-less files can remain forever in Proposed fix def remove_file(self, file_path: str) -> None:
"""Remove all symbols associated with a specific file."""
- if file_path not in self.symbols:
- return
-
- for scope, names in self.symbols[file_path].items():
- if scope == "global":
- for name, symbol in list(names.items()):
- if name in self.exports and self.exports[name].file_path == file_path:
- del self.exports[name]
-
- del self.symbols[file_path]
- if file_path in self.metadata:
- del self.metadata[file_path]
+ if file_path in self.symbols:
+ for scope, names in self.symbols[file_path].items():
+ if scope == "global":
+ for name in list(names):
+ if name in self.exports and self.exports[name].file_path == file_path:
+ del self.exports[name]
+ del self.symbols[file_path]
+ self.metadata.pop(file_path, None)📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.9)[warning] 74-74: Loop control variable Rename unused (B007) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def add_symbol(self, symbol: Symbol) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Add a symbol to the table.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -121,18 +138,50 @@ def __init__(self, cache_dir: Optional[Path] = None): | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def build_for_project(self, project_root: Path) -> SymbolTable: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Scan project and build symbol table.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| updated = False | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.cache_dir: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| cached = self._load_cache() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if cached: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # TODO: Implement incremental update logic here | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return cached | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self.symbol_table = cached | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| all_python_files = list(project_root.rglob("*.py")) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| excluded_dirs = {".git", ".rag", "__pycache__", "venv", ".venv", "env", "node_modules"} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| python_files = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| f for f in all_python_files if not any(excluded in f.parts for excluded in excluded_dirs) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| current_file_paths = set() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| python_files = list(project_root.rglob("*.py")) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for file_path in python_files: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| file_str = str(file_path) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| current_file_paths.add(file_str) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| mtime = file_path.stat().st_mtime | ||||||||||||||||||||||||||||||||||||||||||||||||||
| except OSError: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if file_str in self.symbol_table.metadata and self.symbol_table.metadata[file_str] == mtime: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| # Need to update this file | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if file_str in self.symbol_table.symbols: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self.symbol_table.remove_file(file_str) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| self._analyze_file(file_path) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self.symbol_table.metadata[file_str] = mtime | ||||||||||||||||||||||||||||||||||||||||||||||||||
| updated = True | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
170
to
+172
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t advance metadata when analysis fails. At Line 171, metadata is updated even if Proposed fix- self._analyze_file(file_path)
- self.symbol_table.metadata[file_str] = mtime
+ analyzed = self._analyze_file(file_path)
+ if analyzed:
+ self.symbol_table.metadata[file_str] = mtime
+ else:
+ self.symbol_table.metadata.pop(file_str, None)
updated = True
...
- def _analyze_file(self, file_path: Path) -> None:
+ def _analyze_file(self, file_path: Path) -> bool:
"""Analyze a single file and populate symbols."""
try:
# We use astroid for better inference capabilities later
tree = self.inference_engine.parse_file(str(file_path))
@@
# Walk the tree
self._visit_node(tree, str(file_path), "global")
+ return True
except Exception as e:
logger.warning(f"Failed to build symbol table for {file_path}: {e}")
+ return FalseAlso applies to: 188-199 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if self.cache_dir: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self._save_cache() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check for deleted files | ||||||||||||||||||||||||||||||||||||||||||||||||||
| deleted_files = set(self.symbol_table.symbols.keys()) - current_file_paths | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for file_str in deleted_files: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self.symbol_table.remove_file(file_str) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| updated = True | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if getattr(self, "cache_dir", None) and updated: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| self._save_cache() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning(f"Failed to save cache in build_for_project: {e}") | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return self.symbol_table | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -214,6 +263,7 @@ def _save_cache(self) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||
| for f, scopes in self.symbol_table.symbols.items() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "exports": {n: sym.to_dict() for n, sym in self.symbol_table.exports.items()}, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "metadata": self.symbol_table.metadata, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| with open(cache_file, "w") as f: | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -247,6 +297,8 @@ def _load_cache(self) -> Optional[SymbolTable]: | |||||||||||||||||||||||||||||||||||||||||||||||||
| # Reconstruct exports | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for name, sym_data in data.get("exports", {}).items(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| table.exports[name] = Symbol.from_dict(sym_data) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| table.metadata = data.get("metadata", {}) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return table | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Refactron-ai/Refactron_lib
Length of output: 2427
🏁 Script executed:
cat -n refactron/analysis/symbol_table.py | head -80Repository: Refactron-ai/Refactron_lib
Length of output: 3232
🏁 Script executed:
sed -n '155,170p' refactron/analysis/symbol_table.pyRepository: Refactron-ai/Refactron_lib
Length of output: 647
🏁 Script executed:
sed -n '260,275p' refactron/analysis/symbol_table.pyRepository: Refactron-ai/Refactron_lib
Length of output: 754
🏁 Script executed:
sed -n '295,310p' refactron/analysis/symbol_table.pyRepository: Refactron-ai/Refactron_lib
Length of output: 552
Use
st_mtime_ns(integer nanoseconds) instead ofst_mtime(float seconds) for reliable cache invalidation.The current code uses
st_mtime(float seconds) and compares with==, which can miss rapid file edits on filesystems with coarse timestamp resolution (e.g., 1-second granularity on some systems). Switching tost_mtime_ns(integer nanoseconds) provides sub-microsecond precision and avoids floating-point comparison issues.Update:
metadata: Dict[str, float]toDict[str, int]st_mtimetost_mtime_ns{k: int(v) for k, v in data.get("metadata", {}).items()}🤖 Prompt for AI Agents