Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,31 @@ and when a provider's settings use sources SQLBuild cannot list exactly, such as
SQLBuild keeps one stored result per project and target under
`target/cache/compiler/project-reuse-v1/`. It is replaced atomically; a damaged or unreadable entry is
ignored and compile runs in full. Storing a result normally takes a fraction of a second after the
report is ready. When it has to hash a lot of data or the project has very many files, compile
prints `Recording compile for reuse...` and a completion line to stderr before the report.
report is ready. When it has to hash a lot of data, the project has very many files, or it records
thousands of new model renders, compile prints `Recording compile for reuse...` and a completion
line to stderr before the report.

### Compiling after model-only edits

When the only changes since the stored compile are edits to existing model files, compile runs in
full but reuses the stored render of every unchanged model, together with the diagnostics and
environment reads that rendering reported. Only the edited models are rendered again. Analysis,
contracts, rules, and artifact writes still run for the whole project, so downstream effects of an
edit, such as a changed column type, are reported exactly as in an uncached compile. The output is
byte-identical to `sqb compile --no-cache`.

Every other change renders the whole project: adding, removing, or renaming a model, and editing
macros or the Python modules they import, configuration, `path_defaults`, enums, constants, scope
folders, seeds, sources, tests, or audits. A model whose render read `run.id` or provider settings
is always rendered again.

Renders are stored only when a compile result for the same project and target is already stored,
so a compile in a fresh checkout or after deleting `target/` stores none and costs nothing extra.
The next full compile stores them, and edits after that reuse them. Stored renders are compressed
and kept for the two newest compiles in the same folder. When the stored renders are large, compile prints
`Loading stored renders...` and a completion line to stderr before it starts. While renders are
reused, the Python modules recorded by the previous compile stay tracked even if this compile did
not import them, so editing such a module runs the next compile in full.

Known limits:

Expand All @@ -138,7 +161,8 @@ Known limits:
A replayed JSON report is byte-identical to the previous one except for `compile_timings`, which
holds `project_reuse_hits`, `project_reuse_misses`, `project_reuse_bypasses`,
`project_reuse_check_ms`, and `total_ms`. Full compiles report the same reuse counters alongside
the phase timings. In text mode, a replayed compile does not print the per-phase progress lines.
the phase timings, plus `render_reuse_hits` and `render_reuse_misses` for model renders reused or
rendered again after model-only edits. In text mode, a replayed compile does not print the per-phase progress lines.
Lifecycle sinks receive only the invocation events for a replayed compile; per-phase `operation`
events are not emitted because no compile phase runs.

Expand Down
57 changes: 31 additions & 26 deletions src/sqlbuild/cli/commands/main/project/_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
CompileCommandResult,
CompileWriteResult,
)
from sqlbuild.cli.compile_render_reuse.main._activated_render_reuse import activated_render_reuse
from sqlbuild.cli.compile_render_reuse.main._render_reuse_session import render_reuse_session
from sqlbuild.cli.compile_render_reuse.models import CompileRenderReuse
from sqlbuild.cli.compile_reuse.classes.recorded_compile_output import RecordedCompileOutput
from sqlbuild.cli.compile_reuse.constants import COMPILE_ARTIFACT_WRITES
from sqlbuild.cli.compile_reuse.main._compile_reuse_timings import compile_reuse_timings
Expand Down Expand Up @@ -64,34 +67,35 @@ def run_compile(
else reuse_attempt
)
output: RecordedCompileOutput = RecordedCompileOutput()
render_reuse: CompileRenderReuse | None = render_reuse_session(attempt=attempt)
status: TransientStatusReporter | None = start_compile_status(
json_output=request.json_output,
no_color=request.no_color,
)
try:
with (
paused_cyclic_collection(),
collect_compile_timings() as detailed_timings,
COMPILE_INPUT_READS.recording() as input_reads,
COMPILE_ARTIFACT_WRITES.recording() as artifact_writes,
PreparedCompileArtifacts(
enabled=not request.profile_flags.skip_write
) as prepared_artifacts,
):
result: CompileCommandResult = _run_compile_with_status(
request=effective_request,
total_start=total_start,
status=status,
detailed_timings=detailed_timings,
prepared_artifacts=prepared_artifacts,
output=output,
reuse_timings_ms=compile_reuse_timings(attempt=attempt),
)
finally:
if status is not None:
status.close()
try:
with paused_cyclic_collection():
with paused_cyclic_collection():
try:
with (
collect_compile_timings() as detailed_timings,
COMPILE_INPUT_READS.recording() as input_reads,
COMPILE_ARTIFACT_WRITES.recording() as artifact_writes,
activated_render_reuse(render_reuse=render_reuse),
PreparedCompileArtifacts(
enabled=not request.profile_flags.skip_write
) as prepared_artifacts,
):
result: CompileCommandResult = _run_compile_with_status(
request=effective_request,
total_start=total_start,
status=status,
detailed_timings=detailed_timings,
prepared_artifacts=prepared_artifacts,
output=output,
reuse_timings_ms=compile_reuse_timings(attempt=attempt),
)
finally:
if status is not None:
status.close()
try:
write_reusable_compile(
attempt=attempt,
output=output,
Expand All @@ -102,9 +106,10 @@ def run_compile(
artifacts_written=result.artifacts_written,
dag_artifact_path=result.dag_artifact_path,
json_output=request.json_output,
render_reuse=render_reuse,
)
finally:
output.print_stdout()
finally:
output.print_stdout()
return result.exit_code


Expand Down
1 change: 1 addition & 0 deletions src/sqlbuild/cli/compile_render_reuse/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Reuse unaffected renders of the stored compile in the next full compile."""
Empty file.
49 changes: 49 additions & 0 deletions src/sqlbuild/cli/compile_render_reuse/_helpers/load_notice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Lifecycle lines for loading a large render store before the compile status starts."""

from __future__ import annotations

import sys
import time
from pathlib import Path

from sqlbuild.cli.compile_render_reuse.constants import (
RENDER_LOAD_DONE_MESSAGE,
RENDER_LOAD_FAILED_MESSAGE,
RENDER_LOAD_NOTICE_BYTES,
RENDER_LOAD_START_MESSAGE,
)
from sqlbuild.cli.compile_reuse.constants import BYTES_PER_MEBIBYTE, REUSE_RENDER_STATE_SUFFIX


def start_load_notice(*, entry_path: Path) -> float | None:
"""Announce loading on stderr when the slot's stored render files are large."""

stored_bytes: int = sum(
_file_size(path=path)
for path in entry_path.parent.glob(f"{entry_path.stem}-*{REUSE_RENDER_STATE_SUFFIX}")
)
if stored_bytes < RENDER_LOAD_NOTICE_BYTES:
return None
print(
RENDER_LOAD_START_MESSAGE.format(mebibytes=stored_bytes / BYTES_PER_MEBIBYTE),
file=sys.stderr,
)
return time.monotonic()


def _file_size(*, path: Path) -> int:
"""Size a stored render file, treating one removed by a concurrent compile as empty."""

try:
return path.stat().st_size
except OSError:
return 0


def finish_load_notice(*, started: float | None, loaded: bool) -> None:
"""Report whether an announced load produced usable renders."""

if started is None:
return
message: str = RENDER_LOAD_DONE_MESSAGE if loaded else RENDER_LOAD_FAILED_MESSAGE
print(message.format(seconds=time.monotonic() - started), file=sys.stderr)
Loading
Loading