This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 01e0ace414e Speed up provider asset change detection by pruning 
dependency directories (#72784)
01e0ace414e is described below

commit 01e0ace414e074cc3fa47e741680d15c7ce5d0b2
Author: Andrew Chang <[email protected]>
AuthorDate: Wed Sep 9 19:12:05 2026 +0800

    Speed up provider asset change detection by pruning dependency directories 
(#72784)
    
    The provider asset hash already excluded node_modules and .pnpm-store,
    but only after listing and resolving every path under them. That is
    about two seconds per provider on each breeze start-airflow for
    nothing. Pruning the directories during the walk removes the cost.
---
 scripts/ci/prek/compile_provider_assets.py         |  24 ++--
 .../tests/ci/prek/test_compile_provider_assets.py  | 133 +++++++++++++++++++++
 2 files changed, 144 insertions(+), 13 deletions(-)

diff --git a/scripts/ci/prek/compile_provider_assets.py 
b/scripts/ci/prek/compile_provider_assets.py
index a0f16fb594f..1edd674fe5a 100755
--- a/scripts/ci/prek/compile_provider_assets.py
+++ b/scripts/ci/prek/compile_provider_assets.py
@@ -20,7 +20,6 @@ from __future__ import annotations
 import argparse
 import hashlib
 import os
-import re
 import shutil
 import subprocess
 import sys
@@ -83,15 +82,16 @@ PROVIDERS_PATHS = {
 }
 
 
-def get_directory_hash(directory: Path, skip_path_regexps: list[str]) -> str:
-    files = sorted(directory.rglob("*"))
-    for skip_path_regexp in skip_path_regexps:
-        matcher = re.compile(skip_path_regexp)
-        files = [file for file in files if not 
matcher.match(os.fspath(file.resolve()))]
+SKIPPED_DIRECTORY_NAMES = frozenset({"node_modules", ".pnpm-store"})
+
+
+def get_directory_hash(directory: Path) -> str:
     sha = hashlib.sha256()
-    for file in files:
-        if file.is_file() and not file.name.startswith("."):
-            sha.update(file.read_bytes())
+    for root, directory_names, file_names in os.walk(directory):
+        directory_names[:] = sorted(name for name in directory_names if name 
not in SKIPPED_DIRECTORY_NAMES)
+        for file_name in sorted(file_names):
+            if not file_name.startswith("."):
+                sha.update((Path(root) / file_name).read_bytes())
     return sha.hexdigest()
 
 
@@ -103,8 +103,6 @@ if __name__ not in ("__main__", "__mp_main__"):
 
 INTERNAL_SERVER_ERROR = "500 Internal Server Error"
 
-SKIP_PATH_REGEXPS = [".*/node_modules.*", ".*/.pnpm-store.*"]
-
 
 def compile_assets(provider_name: str):
     if provider_name not in PROVIDERS_PATHS:
@@ -117,7 +115,7 @@ def compile_assets(provider_name: str):
     provider_paths["hash"].parent.mkdir(exist_ok=True, parents=True)
     if dist_directory.exists():
         old_hash = provider_paths["hash"].read_text().strip() if 
provider_paths["hash"].exists() else ""
-        new_hash = get_directory_hash(www_directory, 
skip_path_regexps=SKIP_PATH_REGEXPS)
+        new_hash = get_directory_hash(www_directory)
         if new_hash == old_hash:
             print(f"The '{www_directory}' directory has not changed! Skip 
regeneration.")
             return
@@ -143,7 +141,7 @@ def compile_assets(provider_name: str):
             print(result.stdout + "\n" + result.stderr)
             sys.exit(result.returncode)
     subprocess.check_call(["pnpm", "build"], cwd=os.fspath(www_directory), 
env=env)
-    new_hash = get_directory_hash(www_directory, 
skip_path_regexps=SKIP_PATH_REGEXPS)
+    new_hash = get_directory_hash(www_directory)
     provider_paths["hash"].write_text(new_hash + "\n")
     print(f"Assets compiled successfully. New hash: {new_hash}")
 
diff --git a/scripts/tests/ci/prek/test_compile_provider_assets.py 
b/scripts/tests/ci/prek/test_compile_provider_assets.py
new file mode 100644
index 00000000000..fdada1f7a09
--- /dev/null
+++ b/scripts/tests/ci/prek/test_compile_provider_assets.py
@@ -0,0 +1,133 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Tests for the change detection in compile_provider_assets.py.
+
+The script refuses to be imported as a module, so these tests run the real
+script as a subprocess against a stubbed ``pnpm`` and a stubbed
+``common_prek_utils`` that points all paths into a temporary directory.
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+import subprocess
+import sys
+import textwrap
+from pathlib import Path
+
+import pytest
+
+SCRIPT_PATH = Path(__file__).resolve().parents[3] / "ci" / "prek" / 
"compile_provider_assets.py"
+
+AI_WWW_RELATIVE_PATH = 
Path("providers/common/ai/src/airflow/providers/common/ai/plugins/www")
+
+PNPM_STUB = textwrap.dedent(
+    """\
+    #!/usr/bin/env python3
+    import os
+    import sys
+    from pathlib import Path
+
+    with open(os.environ["PNPM_CALL_LOG"], "a") as call_log:
+        call_log.write(" ".join(sys.argv[1:]) + "\\n")
+
+    output_directory = Path("node_modules/pkg") if sys.argv[1] == "install" 
else Path("dist")
+    output_directory.mkdir(parents=True, exist_ok=True)
+    (output_directory / "index.js").write_text("")
+    """
+)
+
+
+class ScriptHarness:
+    def __init__(self, tmp_path: Path):
+        airflow_root = tmp_path / "airflow_root"
+        self.www_directory = airflow_root / AI_WWW_RELATIVE_PATH
+        (self.www_directory / "src").mkdir(parents=True)
+        (self.www_directory / "src" / "main.ts").write_text("export const main 
= 1;\n")
+
+        script_dir = tmp_path / "prek"
+        script_dir.mkdir()
+        self.script_path = script_dir / SCRIPT_PATH.name
+        shutil.copy(SCRIPT_PATH, self.script_path)
+        (script_dir / "common_prek_utils.py").write_text(
+            textwrap.dedent(
+                f"""\
+                from pathlib import Path
+
+                AIRFLOW_ROOT_PATH = Path({os.fspath(airflow_root)!r})
+                """
+            )
+        )
+
+        bin_dir = tmp_path / "bin"
+        bin_dir.mkdir()
+        pnpm_stub = bin_dir / "pnpm"
+        pnpm_stub.write_text(PNPM_STUB)
+        pnpm_stub.chmod(0o755)
+        self.call_log = tmp_path / "pnpm_calls.txt"
+        self.env = {
+            **os.environ,
+            "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}",
+            "PNPM_CALL_LOG": os.fspath(self.call_log),
+        }
+
+    def run_script(self) -> subprocess.CompletedProcess:
+        return subprocess.run(
+            [sys.executable, os.fspath(self.script_path), "ai"],
+            env=self.env,
+            capture_output=True,
+            text=True,
+            check=True,
+        )
+
+    def read_pnpm_calls(self) -> list[str]:
+        return self.call_log.read_text().splitlines() if 
self.call_log.exists() else []
+
+
[email protected]
+def harness(tmp_path):
+    return ScriptHarness(tmp_path)
+
+
[email protected](
+    "changed_file",
+    [
+        pytest.param(Path(".pnpm-store/v10/files/00/abc"), id="pnpm-store"),
+        pytest.param(Path("node_modules/pkg/index.js"), id="node-modules"),
+    ],
+)
+def test_ignores_dependency_store_changes(harness, changed_file):
+    harness.run_script()
+    calls_after_first_build = harness.read_pnpm_calls()
+
+    (harness.www_directory / changed_file).parent.mkdir(parents=True, 
exist_ok=True)
+    (harness.www_directory / changed_file).write_text("changed")
+    result = harness.run_script()
+
+    assert harness.read_pnpm_calls() == calls_after_first_build
+    assert "has not changed! Skip regeneration." in result.stdout
+
+
+def test_rebuilds_when_sources_change(harness):
+    harness.run_script()
+    calls_after_first_build = harness.read_pnpm_calls()
+
+    (harness.www_directory / "src" / "main.ts").write_text("export const main 
= 2;\n")
+    harness.run_script()
+
+    assert harness.read_pnpm_calls() == [*calls_after_first_build, "install 
--frozen-lockfile", "build"]

Reply via email to