mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-23 06:28:34 +00:00
perf(sandbox): speed up should_ignore_name in glob/grep walks (#3657)
should_ignore_name runs once per directory entry during glob/grep tree walks and looped over ~57 IGNORE_PATTERNS calling fnmatch per pattern. Precompute the literal names into a frozenset (O(1)) and the few glob patterns into one combined compiled regex via fnmatch.translate; per name it's now one normcase + a set lookup + at most one regex match. os.path.normcase preserves fnmatch's platform case behavior, so results are identical (covered by an equivalence test against the old loop). Fixes #3655.
This commit is contained in:
parent
9124f991de
commit
d2292d73da
@ -67,11 +67,22 @@ class GrepMatch:
|
|||||||
line: str
|
line: str
|
||||||
|
|
||||||
|
|
||||||
|
# ``should_ignore_name`` runs once per directory entry during glob/grep tree
|
||||||
|
# walks, so we avoid ~50 ``fnmatch`` calls per name. Most ignore patterns are
|
||||||
|
# literal names (O(1) set lookup after normcase); the few glob patterns are
|
||||||
|
# pre-translated into a single combined regex. ``os.path.normcase`` keeps the
|
||||||
|
# same case behavior ``fnmatch`` applies (case-sensitive on POSIX, folded on
|
||||||
|
# Windows).
|
||||||
|
_EXACT_IGNORE_NAMES = frozenset(os.path.normcase(p) for p in IGNORE_PATTERNS if not any(c in p for c in "*?["))
|
||||||
|
_GLOB_IGNORE_PATTERNS = [p for p in IGNORE_PATTERNS if any(c in p for c in "*?[")]
|
||||||
|
_GLOB_IGNORE_RE = re.compile("|".join(fnmatch.translate(os.path.normcase(p)) for p in _GLOB_IGNORE_PATTERNS)) if _GLOB_IGNORE_PATTERNS else None
|
||||||
|
|
||||||
|
|
||||||
def should_ignore_name(name: str) -> bool:
|
def should_ignore_name(name: str) -> bool:
|
||||||
for pattern in IGNORE_PATTERNS:
|
normalized = os.path.normcase(name)
|
||||||
if fnmatch.fnmatch(name, pattern):
|
if normalized in _EXACT_IGNORE_NAMES:
|
||||||
return True
|
return True
|
||||||
return False
|
return _GLOB_IGNORE_RE is not None and _GLOB_IGNORE_RE.match(normalized) is not None
|
||||||
|
|
||||||
|
|
||||||
def should_ignore_path(path: str) -> bool:
|
def should_ignore_path(path: str) -> bool:
|
||||||
|
|||||||
72
backend/tests/test_should_ignore_name.py
Normal file
72
backend/tests/test_should_ignore_name.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"""should_ignore_name must stay behavior-identical to the original per-pattern
|
||||||
|
fnmatch loop while doing O(1) set lookup + one combined glob regex instead of
|
||||||
|
~50 fnmatch calls per directory entry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
|
||||||
|
from deerflow.sandbox.search import IGNORE_PATTERNS, should_ignore_name, should_ignore_path
|
||||||
|
|
||||||
|
|
||||||
|
def _reference(name: str) -> bool:
|
||||||
|
"""Original implementation, kept here as the equivalence oracle."""
|
||||||
|
return any(fnmatch.fnmatch(name, pattern) for pattern in IGNORE_PATTERNS)
|
||||||
|
|
||||||
|
|
||||||
|
_SAMPLES = [
|
||||||
|
# exact-name ignores
|
||||||
|
".git",
|
||||||
|
"node_modules",
|
||||||
|
"__pycache__",
|
||||||
|
".venv",
|
||||||
|
"env",
|
||||||
|
"logs",
|
||||||
|
"coverage",
|
||||||
|
".pytest_cache",
|
||||||
|
".DS_Store",
|
||||||
|
"Thumbs.db",
|
||||||
|
"desktop.ini",
|
||||||
|
# glob ignores
|
||||||
|
"thing.egg-info",
|
||||||
|
"x.swp",
|
||||||
|
"y.swo",
|
||||||
|
"z.log",
|
||||||
|
"a.tmp",
|
||||||
|
"b.temp",
|
||||||
|
"c.bak",
|
||||||
|
"d.cache",
|
||||||
|
"core~",
|
||||||
|
"shortcut.lnk",
|
||||||
|
# kept (must NOT be ignored)
|
||||||
|
"foo.py",
|
||||||
|
"README.md",
|
||||||
|
"src",
|
||||||
|
"myenv",
|
||||||
|
"node_modules_x",
|
||||||
|
"x.git",
|
||||||
|
"log",
|
||||||
|
"main.c",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_matches_reference_for_all_samples():
|
||||||
|
for name in _SAMPLES:
|
||||||
|
assert should_ignore_name(name) == _reference(name), name
|
||||||
|
|
||||||
|
|
||||||
|
def test_known_ignored_names():
|
||||||
|
for name in [".git", "node_modules", "__pycache__", "x.swp", "z.log", "core~", "thing.egg-info"]:
|
||||||
|
assert should_ignore_name(name) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_known_kept_names():
|
||||||
|
for name in ["foo.py", "README.md", "src", "myenv", "node_modules_x", "x.git"]:
|
||||||
|
assert should_ignore_name(name) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_should_ignore_path_segments():
|
||||||
|
assert should_ignore_path("a/node_modules/b") is True
|
||||||
|
assert should_ignore_path("proj/.git/config") is True
|
||||||
|
assert should_ignore_path("a/b/c.py") is False
|
||||||
Loading…
x
Reference in New Issue
Block a user