Skip to content

perf(sg): skip sagefile rebuild when nothing changed#802

Draft
CervEdin wants to merge 1 commit intomasterfrom
cache-sagefile
Draft

perf(sg): skip sagefile rebuild when nothing changed#802
CervEdin wants to merge 1 commit intomasterfrom
cache-sagefile

Conversation

@CervEdin
Copy link
Copy Markdown

Problem

Every make invocation rebuilds the sagefile binary (~1.9s) even when nothing in .sage/ changed. The actual task (e.g. mdformat) takes ~180ms. The generated Makefile marks $(sagefile) as .PHONY, forcing unconditional rebuilds.

Solution

Fold dependency resolution into the sagefile binary as a --deps flag. Uses go/build.ImportDir to resolve which .go files participate in the build (respecting build tags, excluding _test.go), and prints them so Make can track timestamps.

How it works

  • Sagefile exists: $(shell $(sagefile) --deps .sage) returns the dep list, Make checks timestamps
  • First build: ifneq/wildcard guard leaves deps empty, Make builds unconditionally (target missing)
  • --deps fails/empty: $(error) tells user to run make sage
  • Local replace directives: Parses go.mod for local replace targets, walks each for .go files — so developing sage itself correctly triggers rebuilds

Results

Scenario Before After
No changes ~2s (full rebuild) ~0.5s (skip)
Source touched ~2s ~2s (correct rebuild)
Test file touched ~2s ~0.5s (correctly skipped)
sg/path.go touched (local replace) ~2s ~2s (correct rebuild)

The generated Makefile marks $(sagefile) as .PHONY, which forces Make to
rebuild the sagefile binary on every invocation. In practice this means
every `make` target pays a ~2s tax for `go mod tidy && go run .` even
when nothing in .sage/ has changed, while the actual work (e.g. running
mdformat) often takes under 200ms.

An earlier version of the Makefile generator used file-based deps but
this was replaced with .PHONY in b9faf86, likely because tracking sage's
own library source is hard when developing sage itself (where a local
`replace` directive means changes don't flow through go.mod).

We fold dependency resolution into the sagefile binary itself as a
"--deps" flag. The generated init() intercepts "--deps" before the
normal target dispatch and calls SagefileDeps(), which uses
go/build.ImportDir to resolve exactly which .go files participate in the
build for the current platform — respecting build tags and excluding
_test.go files, without the ~350ms cost of `go list`. The Makefile
passes includePath as an argument so the output is relative to the
Makefile's directory, handling nested namespace Makefiles correctly.

For the bootstrap case where the sagefile does not exist yet, an
ifneq/wildcard guard leaves sage_source_files empty and Make builds
unconditionally since the target file is missing. If --deps returns
empty on an existing binary (suggesting a stale or broken sagefile),
Make errors out with a message pointing the user to `make sage`.

To close the local-replace gap that motivated the original .PHONY, we
also parse go.mod for local replace directives and walk each target
directory for non-test .go files. This means that when developing sage
itself, touching sg/path.go or tools/sgmdformat/tools.go correctly
triggers a rebuild. Consumer repos have no local replaces, so their
hot path is just ImportDir plus a go.mod read.

Signed-off-by: Erik Cervin-Edin <erik.cervin-edin@einride.tech>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant