Skip to content

Update Plz query to make it quicker - #3654

Open
jamespitt wants to merge 4 commits into
thought-machine:masterfrom
jamespitt:plz-query
Open

jamespitt wants to merge 4 commits into
thought-machine:masterfrom
jamespitt:plz-query

Conversation

@jamespitt

@jamespitt jamespitt commented Nov 6, 2026 •

Copy link
Copy Markdown

This is based from some of the ideas in https://github.com/Tinder/bazel-diff - the aim here is to allow us to speed up plz query. There's a couple of things going on:

  1. plz query changes used to check out the tip of --since to build the "before" graph. It now uses scm.MergeBase(since, HEAD), which matches how the changed files were already calculated. Targets that changed on the base branch after the branch point are no longer reported as changed.
  2. Add in some new commands:

plz query hashes parses the whole graph and writes a snapshot. Its flags are --output_file, --json and --print_fingerprint. The last one prints the fingerprint without parsing.

plz query changes gains four flags:

  • --before_hashes reads a snapshot of the merge base instead of checking out and parsing it.
  • --write_after_hashes also writes a snapshot of HEAD as a by-product, so CI can reuse it later.
  • --force_before_hashes uses a snapshot even if its revision isn't the merge base.
  • --strict_hashes fails instead of falling back when the snapshot is unusable.
  1. Parallelise diffing a graph against a snapshot - this is based on this - master...peterebden:please:peter/faster-diffgraphs

  2. Add benchmarks for creating and diffing graph snapshots - again from @peterebden's work

`plz query changes` now checks out the merge base of `--since` and HEAD to
build the "before" graph, rather than the tip of `--since`. This is
consistent with how the changed files are calculated (`--since...HEAD`), so
targets that changed on the base branch after the branch point are no longer
reported as changed.

If the merge base can't be determined (e.g. an unsupported SCM), it warns and
falls back to the tip of `--since`.

Adds `MergeBase` to the `scm.SCM` interface. Bumps the version to 17.36.0.
`plz query changes` has to parse the whole graph at both the base revision and
HEAD, which means checking out the base and parsing it every time. In CI the
base is usually the same revision for many runs, so this adds a way to compute
it once and reuse it.

* `plz query hashes` writes a snapshot of every target's rule hash and tool
  path hash, plus a header recording the revision, whether the working tree was
  dirty, the config hash, the hashes of any `pass_env` variables and a
  fingerprint of the things that make snapshots comparable (format and plz
  versions, host and target arch, profiles and `-o` overrides).
* `plz query changes --before_hashes` diffs against a snapshot instead of
  checking out and parsing the merge base. The snapshot is rejected if its
  format or fingerprint differs, it came from a dirty working tree, it wasn't
  generated at the merge base, or the values of `pass_env` variables differ.
  We then fall back to parsing the merge base, or fail with `--strict_hashes`.
  `--force_before_hashes` skips the revision check.
* `plz query changes --write_after_hashes` writes a snapshot of HEAD as a
  by-product, so it can be used as the base for a later run.

Snapshots use `encoding/gob`, with maps flattened to sorted slices so identical
graphs produce identical files, and are gzipped when the filename ends in `.gz`.
`--json` writes a human-readable form for debugging, which can't be read back.

`DiffGraphs` is now implemented in terms of `DiffSnapshot`; the tests check the
two give identical results.
diffSnapshot computed RuleHash and the tool hash for every target in the
after graph sequentially. Do it in chunks across all CPUs instead, sharing the
chunking helper with NewSnapshot, and short-circuit to "everything changed"
when the config hash differs rather than hashing each target to find that out.

On a synthetic graph of ~500k targets this takes diffing from ~850ms to
~340ms. Based on the approach in peter/faster-diffgraphs.
Benchmarks NewSnapshot and diffSnapshot over synthetic graphs of 5k, 50k and
500k targets, with and without ~1% of targets changed. The synthetic graph
generator and benchmark layout are taken from peter/faster-diffgraphs.

Adds a `changes_benchmark` target using the existing `benchmark` build def, and
excludes `*_benchmark_test.go` from `query_test`.

@peterebden peterebden left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice thanks!

Comment thread src/please.go
// snapshotFingerprintComponents returns the things that must match for a graph snapshot to be
// comparable with the current one. The config hash isn't included because a config change is a
// legitimate change (it marks everything as changed).
func snapshotFingerprintComponents() map[string]string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd ideally like this kind of logic (I think pretty much from here down) to live in a sub-package (this is probably in query somewhere since it's specific to query changes)

Comment thread src/query/changes.go
// Note that this is not symmetric; targets that have been removed from 'before' do not appear
// (because this is designed to be fed into 'plz test' and we can't test targets that no longer exist).
func DiffGraphs(before, after *core.BuildState, files []string, level int, includeSubrepos bool) core.BuildLabels {
return DiffSnapshot(NewSnapshot(before), after, files, level, includeSubrepos)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI in #3636 I moved this out in order to reduce peak memory usage; this will increase it because you'll need to hold the full version of both graphs and the additional snapshot all at once. Ideally the flow is something like

  • parse before graph
  • derive before snapshot
  • drop 'before' state (setting it to nil is enough)
  • kick off a GC run
  • parse after graph
  • run diff

I think that's fairly important, we have hit memory limits here before

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.

2 participants