Skip to content

Commit 4f1d90e

Browse files
committed
Performance: optimisation massive du démarrage du build (hashing métadonnées, élagage de scan et préparation asynchrone)
1 parent 9c3ba46 commit 4f1d90e

4 files changed

Lines changed: 111 additions & 37 deletions

File tree

Core/Locking/__init__.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,39 @@ def installed_distributions_snapshot() -> dict[str, str]:
7777
def included_workspace_files(
7878
workspace: Path, exclude_patterns: list[str]
7979
) -> list[Path]:
80+
"""
81+
Return a list of files to be included in the workspace snapshot.
82+
Optimized to skip excluded directories early.
83+
"""
8084
included: list[Path] = []
81-
for path in sorted(workspace.rglob("*")):
82-
if not path.is_file():
83-
continue
84-
rel = path.relative_to(workspace).as_posix()
85-
if rel.startswith(".ark/"):
86-
continue
87-
if any(_matches_exclude_pattern(rel, pattern) for pattern in exclude_patterns):
88-
continue
89-
included.append(path)
90-
return included
85+
ws_str = str(workspace.resolve())
86+
87+
# Prune list for os.walk
88+
prune_dirs = {".git", ".ark", "__pycache__", "venv", ".venv", "build", "dist"}
89+
90+
import os
91+
for root, dirs, files in os.walk(ws_str):
92+
# 1. Early pruning of common heavy/system directories
93+
dirs[:] = [d for d in dirs if d not in prune_dirs]
94+
95+
# 2. Apply custom exclude patterns to directories
96+
rel_root = os.path.relpath(root, ws_str)
97+
if rel_root == ".":
98+
rel_root = ""
99+
100+
if rel_root:
101+
if any(_matches_exclude_pattern(rel_root + "/", p) for p in exclude_patterns):
102+
dirs[:] = [] # Stop recursion here
103+
continue
104+
105+
# 3. Process files
106+
for f in files:
107+
rel_path = os.path.join(rel_root, f) if rel_root else f
108+
if any(_matches_exclude_pattern(rel_path, p) for p in exclude_patterns):
109+
continue
110+
included.append(Path(root) / f)
111+
112+
return sorted(included)
91113

92114

93115
def _matches_exclude_pattern(relative_path: str, pattern: str) -> bool:
@@ -106,14 +128,21 @@ def _matches_exclude_pattern(relative_path: str, pattern: str) -> bool:
106128

107129

108130
def compute_workspace_hash(workspace: Path, exclude_patterns: list[str]) -> str:
131+
"""
132+
Compute a fast hash of the workspace using file metadata (path, size, mtime).
133+
This is much faster than reading full content for large projects.
134+
"""
109135
digest = sha256()
110136
for path in included_workspace_files(workspace, exclude_patterns):
111-
rel = path.relative_to(workspace).as_posix().encode("utf-8")
112-
digest.update(rel)
113-
digest.update(b"\0")
114-
digest.update(path.read_bytes())
115-
digest.update(b"\0")
116-
return "sha256:" + digest.hexdigest()
137+
try:
138+
stat = path.stat()
139+
# We hash the relative path, size, and mtime
140+
rel = path.relative_to(workspace).as_posix()
141+
entry = f"{rel}|{stat.st_size}|{stat.st_mtime}"
142+
digest.update(entry.encode("utf-8"))
143+
except Exception:
144+
continue
145+
return "metadata-sha256:" + digest.hexdigest()
117146

118147

119148
def get_git_commit_hash(workspace: Path) -> str | None:

Core/deps_analyser/analyser.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,27 @@ def collect_project_dependencies(
708708
all_deps: set[str] = set()
709709

710710
# 1. Scan configuration files (high priority, more accurate than imports)
711+
712+
# requirements.txt / requirements.in (Very high priority, can skip full scan if found)
713+
reqs_found = False
714+
for req_file in ["requirements.txt", "requirements.in"]:
715+
path = os.path.join(workspace_dir, req_file)
716+
if os.path.isfile(path):
717+
try:
718+
with open(path, "r", encoding="utf-8") as f:
719+
for line in f:
720+
line = line.strip()
721+
if line and not line.startswith(("#", "-")):
722+
# Basic parsing of requirement line
723+
name = re.split(r"[<>=!~\s]", line)[0].strip()
724+
if name:
725+
all_deps.add(name)
726+
reqs_found = True
727+
except Exception:
728+
pass
729+
730+
# If we found explicit requirements, we can potentially skip the expensive scan
731+
# unless specific validation is requested. For now, we continue but mark it.
711732

712733
# pyproject.toml
713734
pyproject_path = os.path.join(workspace_dir, "pyproject.toml")
@@ -793,6 +814,10 @@ def collect_project_dependencies(
793814
pass
794815

795816
# 2. Scan imports in all Python files (fallback/validation)
817+
# OPTIMIZATION: If we already found dependencies in config files, we skip the slow full scan
818+
if reqs_found and all_deps:
819+
return all_deps
820+
796821
modules_from_imports = set()
797822
workspace_python_files = []
798823
for root, dirs, files in os.walk(workspace_dir):

Ui/Gui/Compilation/compiler.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def __init__(
6060
engine_id: str,
6161
context: BuildContext,
6262
engine_config: Optional[Dict[str, Any]] = None,
63+
ark_config: Optional[Dict[str, Any]] = None,
64+
python_version: str | None = None,
6365
):
6466
"""
6567
Initialize the compilation thread.
@@ -69,6 +71,8 @@ def __init__(
6971
self.engine_id = engine_id
7072
self.context = context
7173
self.engine_config = engine_config
74+
self.ark_config = ark_config
75+
self.python_version = python_version
7276
self.cancel_requested = False
7377
self.start_time: Optional[datetime] = None
7478

@@ -79,6 +83,30 @@ def run(self) -> None:
7983

8084
self.progress_update.emit(0, "Process started")
8185

86+
# 1. Build Preparation (Background)
87+
if self.ark_config:
88+
try:
89+
from Core.Locking import build_lock_payload, write_lock_files
90+
from engine_sdk import build_context_object_from_ark_config
91+
from Ui.Cli.helpers import engine_config_from_lock
92+
93+
self.output_ready.emit("🔒 Génération du verrou de compilation (lock file)...")
94+
lock_payload = build_lock_payload(
95+
self.workspace,
96+
self.ark_config,
97+
engine_id=self.engine_id,
98+
python_version=self.python_version,
99+
)
100+
write_lock_files(self.workspace, lock_payload)
101+
102+
# Update context and config from the fresh lock
103+
self.context = build_context_object_from_ark_config(self.ark_config)
104+
self.engine_config = engine_config_from_lock(lock_payload)
105+
except Exception as e:
106+
self.error_ready.emit(f"Error during preparation: {e}")
107+
self.finished.emit(1)
108+
return
109+
82110
def _on_stdout(line: str):
83111
self.output_ready.emit(line)
84112
self._update_progress(line)
@@ -223,11 +251,13 @@ def compile_from_context(
223251
engine_id: str,
224252
context: BuildContext,
225253
engine_config: Optional[Dict[str, Any]] = None,
254+
ark_config: Optional[Dict[str, Any]] = None,
255+
python_version: str | None = None,
226256
) -> bool:
227257
"""
228258
Start an async compilation from a BuildContext.
229259
"""
230-
if self.is_running:
260+
if self.is_running():
231261
self.log_message.emit("warning", "Compilation already in progress")
232262
return False
233263

@@ -250,6 +280,8 @@ def compile_from_context(
250280
engine_id=engine_id,
251281
context=context,
252282
engine_config=engine_config,
283+
ark_config=ark_config,
284+
python_version=python_version,
253285
)
254286

255287
# Connecter les signaux

Ui/Gui/Dialogs/CompilerDialog.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -227,16 +227,9 @@ def _t(fr: str, en: str) -> str:
227227
except Exception:
228228
pass
229229

230-
# 4. Generate Lock and Context (Exact CLI Code)
230+
# 4. Preparation (Resolve context and engine config)
231231
try:
232-
log_i18n_level(
233-
self,
234-
"info",
235-
"🔒 Génération du verrou de compilation (lock file)...",
236-
"🔒 Generating build lock file...",
237-
)
238-
239-
# Resolve Python version for locking
232+
# Resolve Python version for locking (will be used in background thread)
240233
python_version = None
241234
try:
242235
from Core.Compiler.utils import get_interpreter_version_str
@@ -251,17 +244,10 @@ def _t(fr: str, en: str) -> str:
251244
except Exception:
252245
pass
253246

254-
lock_payload = build_lock_payload(
255-
Path(self.workspace_dir),
256-
validated.config,
257-
engine_id=engine_id,
258-
python_version=python_version,
259-
)
260-
write_lock_files(Path(self.workspace_dir), lock_payload)
261-
262-
# Context and Config from lock/config (source of truth)
247+
# Use BuildContext helper to get initial context
263248
context = build_context_object_from_ark_config(validated.config)
264-
engine_config = engine_config_from_lock(lock_payload)
249+
# In UI mode, we use current overrides. The thread will update them from the fresh lock.
250+
engine_config = getattr(self, "_config_overrides", {})
265251
except Exception as e:
266252
log_i18n_level(
267253
self,
@@ -280,7 +266,7 @@ def _t(fr: str, en: str) -> str:
280266
engine = None
281267
if engine is None:
282268
try:
283-
engine = create(engine_id)
269+
engine = engines_loader.create(engine_id)
284270
except Exception:
285271
pass
286272
engine_name = getattr(engine, "name", engine_id)
@@ -369,6 +355,8 @@ def _after_bcasl(_report=None) -> None:
369355
engine_id=engine_id,
370356
context=context,
371357
engine_config=engine_config,
358+
ark_config=validated.config, # Passing this triggers background lock generation
359+
python_version=python_version,
372360
)
373361

374362
if not success:

0 commit comments

Comments
 (0)