Skip to content

Commit cd38ff9

Browse files
authored
ci: limit each timed dense compile to 4 GiB instead of the pytest tree (#1324)
The dense guard job ran make, uv and pytest inside one 4 GiB systemd unit, so roughly 0.5 GiB of harness memory counted against a limit meant for the compiler. Each timed sqb compile now runs in its own transient scope with MemoryMax=4 GiB and no swap, and pytest runs outside it. Budgets are unchanged. Under CI the guards refuse to run without the per-compile limit, and a missing systemd-run or sudo fails naming the scope.
1 parent 8f0728e commit cd38ff9

9 files changed

Lines changed: 87 additions & 26 deletions

File tree

‎.github/workflows/verify.yml‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -760,26 +760,14 @@ jobs:
760760
run: uv python install 3.12
761761
- name: Install dependencies
762762
run: uv sync --locked --all-groups --all-extras
763-
- name: Run dense all-rules and delta edit-loop guards under 4 GiB
763+
# Each timed compile runs in its own 4 GiB transient scope; pytest stays outside it.
764+
- name: Run dense all-rules and delta edit-loop guards with each compile under 4 GiB
764765
env:
765766
BENCHMARK_MODELS: ${{ matrix.models }}
766-
MEMORY_LIMIT_BYTES: "4294967296"
767+
SQLBUILD_COMPILE_MEMORY_LIMIT_BYTES: "4294967296"
767768
run: |
768-
sudo systemd-run \
769-
--unit="sqlbuild-dense-${BENCHMARK_MODELS}-${GITHUB_RUN_ID}" \
770-
--uid="$(id -u)" \
771-
--gid="$(id -g)" \
772-
--same-dir \
773-
--setenv="HOME=${HOME}" \
774-
--setenv="PATH=${PATH}" \
775-
--setenv="SQLBUILD_REQUIRE_MEMORY_LIMIT_BYTES=${MEMORY_LIMIT_BYTES}" \
776-
--property="MemoryMax=${MEMORY_LIMIT_BYTES}" \
777-
--property="MemorySwapMax=0" \
778-
--wait \
779-
--collect \
780-
--pipe \
781-
make test-e2e-dense-compile-performance \
782-
SQLBUILD_BENCHMARK_MODELS="${BENCHMARK_MODELS}"
769+
make test-e2e-dense-compile-performance \
770+
SQLBUILD_BENCHMARK_MODELS="${BENCHMARK_MODELS}"
783771
784772
- name: Run diagnostic-heavy compile guards
785773
if: matrix.models == 1000

‎Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ test-e2e-dense-compile-performance:
334334
-n auto --dist loadfile \
335335
-m cold_compile_performance -k "models_$(SQLBUILD_BENCHMARK_MODELS)_" \
336336
-vv -rP --log-level=INFO --log-cli-level=INFO --color=yes
337-
# DuckShop 10K runs after the dense guards, alone, so it never shares the memory limit with them.
337+
# DuckShop 10K runs after the dense guards, alone, so its compile never overlaps theirs.
338338
if [ "$(SQLBUILD_BENCHMARK_MODELS)" = "10000" ]; then \
339339
env PYTHONUNBUFFERED=1 uv run pytest benchmarks/duckshop-10k/tests -q --color=yes && \
340340
env PYTHONUNBUFFERED=1 SQLBUILD_CONCURRENCY=4 uv run pytest \

‎fensu.toml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,11 +992,21 @@ modules = ["scripts.cold_compile_performance.main.assert_required_cgroup_memory_
992992
symbols = ["assert_required_cgroup_memory_limit"]
993993
reason = "Tests and benchmarks run this tooling; Fensu does not count tests as roots."
994994

995+
[[targets.python.dead_code.roots]]
996+
modules = ["scripts.cold_compile_performance.main.compile_memory_scope_command"]
997+
symbols = ["compile_memory_scope_command"]
998+
reason = "Tests and benchmarks run this tooling; Fensu does not count tests as roots."
999+
9951000
[[targets.python.dead_code.roots]]
9961001
modules = ["scripts.cold_compile_performance.main.read_compile_measurement"]
9971002
symbols = ["read_compile_measurement"]
9981003
reason = "Tests and benchmarks run this tooling; Fensu does not count tests as roots."
9991004

1005+
[[targets.python.dead_code.roots]]
1006+
modules = ["scripts.cold_compile_performance.main.require_compile_memory_scope"]
1007+
symbols = ["require_compile_memory_scope"]
1008+
reason = "Tests and benchmarks run this tooling; Fensu does not count tests as roots."
1009+
10001010
[[targets.python.dead_code.roots]]
10011011
modules = ["scripts.cold_compile_performance.main.semantic_compile_fingerprint"]
10021012
symbols = ["semantic_compile_fingerprint"]

‎scripts/cold_compile_performance/constants.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Constants for fresh-process compile performance guards."""
22

33
UNLIMITED_CGROUP_MEMORY_VALUE: str = "max"
4+
COMPILE_MEMORY_LIMIT_ENV_VAR: str = "SQLBUILD_COMPILE_MEMORY_LIMIT_BYTES"
45
COMPILE_MEASUREMENT_FIELD_COUNT: int = 6
56

67
DENSE_JOIN_THRESHOLDS: tuple[int, ...] = (55, 75, 90, 99)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Wrap one timed compile in a memory-limited transient systemd scope."""
2+
3+
import os
4+
import shutil
5+
import uuid
6+
7+
from scripts.cold_compile_performance.constants import COMPILE_MEMORY_LIMIT_ENV_VAR
8+
from scripts.cold_compile_performance.exceptions import CgroupMemoryLimitError
9+
10+
11+
def compile_memory_scope_command(*, label: str) -> tuple[str, ...]:
12+
"""Prefix for a scope with the configured per-compile limit; refuse to run without it."""
13+
14+
raw_limit: str | None = os.environ.get(COMPILE_MEMORY_LIMIT_ENV_VAR)
15+
if raw_limit is None:
16+
return ()
17+
unit: str = f"sqlbuild-compile-{label}-{os.getpid()}-{uuid.uuid4().hex[:8]}"
18+
systemd_run: str | None = shutil.which("systemd-run")
19+
sudo: str | None = shutil.which("sudo")
20+
caller_path_over_sudo_secure_path: str = f"PATH={os.environ.get('PATH', '')}"
21+
if systemd_run is None or sudo is None:
22+
raise CgroupMemoryLimitError(
23+
f"{COMPILE_MEMORY_LIMIT_ENV_VAR}={raw_limit} requires systemd-run and sudo to start "
24+
f"the transient scope {unit}.scope; refusing to compile without the memory limit"
25+
)
26+
return (
27+
sudo,
28+
"--non-interactive",
29+
"--preserve-env",
30+
systemd_run,
31+
"--scope",
32+
"--quiet",
33+
"--collect",
34+
f"--unit={unit}",
35+
f"--uid={os.getuid()}",
36+
f"--gid={os.getgid()}",
37+
f"--property=MemoryMax={int(raw_limit)}",
38+
"--property=MemorySwapMax=0",
39+
"--",
40+
"/usr/bin/env",
41+
caller_path_over_sudo_secure_path,
42+
)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Require a per-compile memory limit when guards run in CI."""
2+
3+
import os
4+
5+
from scripts.cold_compile_performance.constants import COMPILE_MEMORY_LIMIT_ENV_VAR
6+
from scripts.cold_compile_performance.exceptions import CgroupMemoryLimitError
7+
8+
9+
def require_compile_memory_scope() -> None:
10+
"""Refuse to run in CI unless each timed compile gets its own memory-limited scope."""
11+
12+
if os.environ.get("CI") and COMPILE_MEMORY_LIMIT_ENV_VAR not in os.environ:
13+
raise CgroupMemoryLimitError(
14+
f"CI must set {COMPILE_MEMORY_LIMIT_ENV_VAR} so every timed compile runs in a "
15+
"memory-limited transient systemd scope"
16+
)

‎tests/e2e/src/sqlbuild/cli/commands/main/compile/helpers.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
import sqlbuild.cli.compile_reuse._helpers.provider_settings as provider_settings
3535
import sqlbuild.compiler.compile._helpers.macro_bridge.call_store as call_store_module
3636
import sqlbuild.compiler.macro_bridge.classes.macro_bridge as macro_bridge_class
37+
from scripts.cold_compile_performance.main.compile_memory_scope_command import (
38+
compile_memory_scope_command,
39+
)
3740
from scripts.cold_compile_performance.main.read_compile_measurement import read_compile_measurement
3841
from scripts.cold_compile_performance.main.semantic_compile_fingerprint import (
3942
semantic_compile_fingerprint,
@@ -726,6 +729,7 @@ def _run_timed_sqb(
726729
output_path: Path = project_dir.parent / f"{label}.out"
727730
stderr_path: Path = project_dir.parent / f"{label}.stderr"
728731
command: list[str] = [
732+
*compile_memory_scope_command(label=label),
729733
"/usr/bin/time",
730734
"--quiet",
731735
"--output",

‎tests/e2e/src/sqlbuild/cli/commands/main/compile/test_dense_compile_performance.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
dense_test_path,
1313
write_dense_compile_project,
1414
)
15-
from scripts.cold_compile_performance.main.assert_required_cgroup_memory_limit import (
16-
assert_required_cgroup_memory_limit,
15+
from scripts.cold_compile_performance.main.require_compile_memory_scope import (
16+
require_compile_memory_scope,
1717
)
1818
from sqlbuild._native import (
1919
Rule,
@@ -85,7 +85,7 @@ def test_given_dense_project_when_compiling_cold_then_preserves_rules_semantics_
8585
test_case: DenseCompileGuardTestCase,
8686
tmp_path: Path,
8787
) -> None:
88-
assert_required_cgroup_memory_limit()
88+
require_compile_memory_scope()
8989
project_dir: Path = tmp_path / "dense_orders"
9090
write_dense_compile_project(project_dir=project_dir, model_count=test_case.model_count)
9191
config: RulesConfig = load_rules_config(project_dir=project_dir)
@@ -179,7 +179,7 @@ def test_given_dense_project_when_compiling_warm_and_after_one_edit_then_matches
179179
test_case: DenseWarmEditCompileGuardTestCase,
180180
tmp_path: Path,
181181
) -> None:
182-
assert_required_cgroup_memory_limit()
182+
require_compile_memory_scope()
183183
project_dir: Path = tmp_path / "dense_orders"
184184
write_dense_compile_project(project_dir=project_dir, model_count=test_case.model_count)
185185
edited_model_path: Path = dense_model_path(
@@ -264,7 +264,7 @@ def test_given_dense_project_when_editing_one_file_then_delta_compiles_stay_chea
264264
test_case: DenseDeltaEditLoopGuardTestCase,
265265
tmp_path: Path,
266266
) -> None:
267-
assert_required_cgroup_memory_limit()
267+
require_compile_memory_scope()
268268
project_dir: Path = tmp_path / "dense_orders"
269269
write_dense_compile_project(project_dir=project_dir, model_count=test_case.model_count)
270270
index: int = test_case.edited_model_index

‎tests/e2e/src/sqlbuild/cli/commands/main/compile/test_duckshop_10k_compile.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
import pytest
1212

13-
from scripts.cold_compile_performance.main.assert_required_cgroup_memory_limit import (
14-
assert_required_cgroup_memory_limit,
13+
from scripts.cold_compile_performance.main.require_compile_memory_scope import (
14+
require_compile_memory_scope,
1515
)
1616
from tests.e2e.src.sqlbuild.cli.commands.main.compile._test_types import (
1717
GeneratedBenchmarkCompileTestCase,
@@ -44,7 +44,7 @@
4444
def test_given_generated_duckshop_10k_when_compiling_cold_then_matches_checksum_and_is_clean(
4545
test_case: GeneratedBenchmarkCompileTestCase, tmp_path: Path
4646
) -> None:
47-
assert_required_cgroup_memory_limit()
47+
require_compile_memory_scope()
4848
project_dir: Path = tmp_path / "duckshop_10k"
4949

5050
generated: subprocess.CompletedProcess[str] = run_benchmark_generator(

0 commit comments

Comments
 (0)