Skip to content

Commit de509bf

Browse files
committed
feat: cache results across invocations
Add a cache layer for target-determinator results. The cache key is based on the digest of the git tree object among other fields. The configured target metadata is not stored in the cache because they take a lot space and are not needed except for `-verbose` mode. The cache is not read from (but results are saved) if `-verbose` is set. One can disable the loading and saving to cache with `--nocache_results`. Other changes: - Add a --cache-dir option to customise the path of worktree caching and results caching. - The path to the worktree cache is now in the "worktrees" sub-directory. Results are under "results" subdirectory.
1 parent c5362ca commit de509bf

14 files changed

Lines changed: 804 additions & 39 deletions

File tree

README.md

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,48 @@ Target determinator is a binary (and Go API) used to determine which Bazel targe
77
For simple listing, the `target-determinator` binary is supplied:
88

99
```
10-
Usage of target-determinator:
11-
target-determinator <before-revision>
12-
Where <before-revision> may be any commit revision - full commit hashes, short commit hashes, tags, branches, etc.
10+
Usage of bazel-bin/target-determinator/target-determinator_/target-determinator:
11+
-analysis-cache-clear-strategy string
12+
Strategy for clearing the analysis cache. Accepted values: skip,shutdown,discard. (default "skip")
1313
-bazel string
14-
Bazel binary (basename on $PATH, or absolute or relative path) to run (default "bazel")
14+
Bazel binary (basename on $PATH, or absolute or relative path) to run. (default "bazel")
15+
-bazel-opts value
16+
Options to pass to Bazel. Assumed to apply to build and cquery. Options should use relative paths for repository
17+
files (see --bazel-startup-opts).
18+
-bazel-startup-opts value
19+
Startup options to pass to Bazel. Options such as '--bazelrc' should use relative paths for files under the
20+
repository to avoid issues (TD may check out the repository in a temporary directory).
21+
-before-query-error-behavior string
22+
How to behave if the 'before' revision query fails. Accepted values: fatal,ignore-and-build-all (default
23+
"ignore-and-build-all")
24+
-cache-dir string
25+
Cache directory to avoid existing re-computations. Note: home- and system- bazelrc files, environment variables,
26+
and host hardware/OS are not included in the results cache key. Use --nocache_results if necessary. (default
27+
"/Users/rchossart/.cache/target-determinator")
28+
-compare-queries-around-analysis-cache-clear
29+
Whether to check for query result differences before and after analysis cache clears. This is a temporary flag
30+
for performing real-world analysis.
31+
-delete-cached-worktree
32+
Delete created worktrees after use when created. Keeping them can make subsequent invocations faster.
33+
-enforce-clean value
34+
Pass --enforce-clean=enforce-clean to fail if the repository is unclean, or --enforce-clean=allow-ignored to
35+
allow ignored untracked files (the default). (default allow-ignored)
36+
-filter-incompatible-targets
37+
Whether to filter out incompatible targets from the candidate set of affected targets. (default true)
1538
-ignore-file value
16-
Files to ignore for git operations, relative to the working-directory. These files shan't affect the Bazel graph.
39+
Files to ignore for git operations, relative to the working-directory. These files shan't affect the Bazel
40+
graph.
41+
-nocache_results
42+
Disable loading and saving of results to the cache.
1743
-targets bazel query
18-
Targets to consider. Accepts any valid bazel query expression (see https://bazel.build/reference/query). (default "//...")
44+
Targets to consider. Accepts any valid bazel query expression (see https://bazel.build/reference/query).
45+
(default "//...")
1946
-verbose
20-
Whether to explain (messily) why each target is getting run
47+
Whether to explain (messily) why each target is getting run
48+
-version
49+
Print the version of the tool and exit.
2150
-working-directory string
22-
Working directory to query (default ".")
51+
Working directory to query. (default ".")
2352
```
2453

2554
This binary lists targets to stdout, one-per-line, which were affected between <before-revision> and the currently checked-out revision.
@@ -61,6 +90,32 @@ type WalkCallback func(label.Label, []Difference, *analysis.ConfiguredTarget)
6190

6291
This can be used to flexibly build your own logic handling the affected targets to drive whatever analysis you want.
6392

93+
## Caching
94+
95+
Target Determinator caches the results of Bazel cquery invocations across runs. On a cache hit, the expensive cquery and hashing work for a given commit is skipped entirely.
96+
97+
The cache key is derived from:
98+
99+
- The target-determinator binary itself (SHA-256 hash)
100+
- The Bazel version (`bazel info release`)
101+
- The git tree SHA of the queried commit
102+
- The target pattern (e.g. `//...`)
103+
- CLI options that may affect cquery results, such as `--filter-incompatible-targets` and the Bazel startup/build options passed via `--bazel-startup-opts` / `--bazel-opts`
104+
105+
*Not* included in the cache key:
106+
107+
- User and system bazelrc files (`~/.bazelrc`, `/etc/bazel.bazelrc`, and files they import)
108+
- The host machine (hardware, OS). Cache entries produced on one machine are not guaranteed to be valid on another (e.g. a different CPU architecture can change which platform-constrained targets are selected). Do not share the cache directory across machines.
109+
- Environment variables, whether they are used by Bazel or not.
110+
111+
### Environment variables and caching
112+
113+
Without caching, the "before" and "after" cquery calls are both made with the same environment variables. Taken in the context of a CI pipeline run, for example, this means that even the "before" computation uses the *current* (or "after") environment variables, not the environment variables that existed when the "before" commit was built. That answers the question "what targets differ between these two commits, assuming the environment was the same?".
114+
115+
With caching, however, the "before" result may have been computed in an earlier pipeline run, under the environment variables that were in effect *at that time*. If an environment variable affected Bazel's query output (e.g. because it is referenced by `--workspace_status`, `--action_env`, `--test_env`, or a repo rule), the cached result reflects the old environment, while the "after" result reflects the new one. The two results are then compared under different conditions, which may produce spurious differences.
116+
117+
In practice this matters most in release pipelines where stamping or versioning variables (e.g. `MY_PKG_VERSION`) change between runs. If you want to answer "which targets would have changed, assuming the environment is the same before and after?", run `target-determinator` with `--nocache_results` to force both computations to happen in the same environment.
118+
64119
## How to get Target Determinator
65120

66121
Pre-built binary releases are published as [GitHub Releases](https://github.com/bazel-contrib/target-determinator/releases) for most changes.

cli/flags.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package cli
33
import (
44
"flag"
55
"fmt"
6+
"log"
67
"os"
8+
"path"
79
"path/filepath"
810
"strings"
911

@@ -79,6 +81,8 @@ type CommonFlags struct {
7981
AnalysisCacheClearStrategy *string
8082
CompareQueriesAroundAnalysisCacheClear bool
8183
FilterIncompatibleTargets bool
84+
CacheDirectory *string
85+
NoCacheResults bool
8286
}
8387

8488
func StrPtr() *string {
@@ -101,6 +105,8 @@ func RegisterCommonFlags() *CommonFlags {
101105
AnalysisCacheClearStrategy: StrPtr(),
102106
CompareQueriesAroundAnalysisCacheClear: false,
103107
FilterIncompatibleTargets: true,
108+
CacheDirectory: StrPtr(),
109+
NoCacheResults: false,
104110
}
105111
flag.BoolVar(&commonFlags.Version, "version", false, "Print the version of the tool and exit.")
106112
flag.StringVar(commonFlags.WorkingDirectory, "working-directory", ".", "Working directory to query.")
@@ -121,9 +127,19 @@ func RegisterCommonFlags() *CommonFlags {
121127
flag.StringVar(commonFlags.AnalysisCacheClearStrategy, "analysis-cache-clear-strategy", "skip", "Strategy for clearing the analysis cache. Accepted values: skip,shutdown,discard.")
122128
flag.BoolVar(&commonFlags.CompareQueriesAroundAnalysisCacheClear, "compare-queries-around-analysis-cache-clear", false, "Whether to check for query result differences before and after analysis cache clears. This is a temporary flag for performing real-world analysis.")
123129
flag.BoolVar(&commonFlags.FilterIncompatibleTargets, "filter-incompatible-targets", true, "Whether to filter out incompatible targets from the candidate set of affected targets.")
130+
flag.StringVar(commonFlags.CacheDirectory, "cache-dir", defaultCacheDir(), "Cache directory to avoid existing re-computations. Note: home- and system- bazelrc files, environment variables, and host hardware/OS are not included in the results cache key. Use --nocache_results if necessary.")
131+
flag.BoolVar(&commonFlags.NoCacheResults, "nocache_results", false, "Disable loading and saving of results to the cache.")
124132
return &commonFlags
125133
}
126134

135+
func defaultCacheDir() string {
136+
homeDir, err := os.UserHomeDir()
137+
if err != nil {
138+
log.Printf("failed to determine home dir: %v. Caching will be disabled.", err)
139+
}
140+
return path.Join(homeDir, ".cache", "target-determinator")
141+
}
142+
127143
type CommonConfig struct {
128144
Context *pkg.Context
129145
RevisionBefore pkg.LabelledGitRev
@@ -187,6 +203,8 @@ func ResolveCommonConfig(commonFlags *CommonFlags, beforeRevStr string) (*Common
187203
CompareQueriesAroundAnalysisCacheClear: commonFlags.CompareQueriesAroundAnalysisCacheClear,
188204
FilterIncompatibleTargets: commonFlags.FilterIncompatibleTargets,
189205
EnforceCleanRepo: commonFlags.EnforceCleanRepo == EnforceClean,
206+
CacheDirectory: *commonFlags.CacheDirectory,
207+
NoCacheResults: commonFlags.NoCacheResults,
190208
}
191209

192210
// Non-context attributes

common/relpath.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
// RelPath represents a relative path.
11-
// It interprets the inner path as if it did not have a trailing slash.
11+
// It interprets the inner path as if it did not have a leading slash.
1212
type RelPath struct {
1313
path key.Path
1414
}

pkg/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go_library(
55
srcs = [
66
"bazel.go",
77
"bazel_info.go",
8+
"cache.go",
89
"configurations.go",
910
"hash_cache.go",
1011
"normalizer.go",
@@ -33,6 +34,7 @@ go_library(
3334
go_test(
3435
name = "pkg_test",
3536
srcs = [
37+
"cache_test.go",
3638
"hash_cache_test.go",
3739
"normalizer_test.go",
3840
"target_determinator_test.go",
@@ -42,6 +44,7 @@ go_test(
4244
rundir = ".",
4345
deps = [
4446
"//common",
47+
"//common/sorted_set",
4548
"//third_party/protobuf/bazel/analysis",
4649
"//third_party/protobuf/bazel/build",
4750
"@bazel_gazelle//label",

pkg/bazel.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package pkg
22

33
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"encoding/json"
47
"fmt"
58
"io"
69
"os"
@@ -22,7 +25,16 @@ type BazelCmdConfig struct {
2225
Stderr io.Writer
2326
}
2427

28+
type HashableKey interface {
29+
// HashKey returns a deterministic string that captures all the struct's
30+
// fields that can affect target-determinator results. It is included in
31+
// the results cache key for each invocation.
32+
HashKey() string
33+
}
34+
2535
type BazelCmd interface {
36+
HashableKey
37+
2638
Execute(config BazelCmdConfig, startupArgs []string, command string, args ...string) (int, error)
2739
Cquery(bazelRelease string, config BazelCmdConfig, startupArgs []string, args ...string) (int, error)
2840
}
@@ -42,6 +54,24 @@ var _buildLikeCommands = map[string]struct{}{
4254
"test": {},
4355
}
4456

57+
// HashKey returns a SHA-256 digest of the cache-affecting fields: BazelStartupOpts and BazelOpts.
58+
// Both slices are included in order, as their ordering affects Bazel behaviour.
59+
//
60+
// The Bazel version from BazelPath is already available in the context so, to avoid running another bazel subprocess,
61+
// it is not taken as input to the resulting hash.
62+
func (c DefaultBazelCmd) HashKey() string {
63+
type fields struct {
64+
BazelStartupOpts []string
65+
BazelOpts []string
66+
}
67+
data, _ := json.Marshal(fields{
68+
BazelStartupOpts: c.BazelStartupOpts,
69+
BazelOpts: c.BazelOpts,
70+
})
71+
h := sha256.Sum256(data)
72+
return hex.EncodeToString(h[:])
73+
}
74+
4575
// Execute calls bazel with the provided arguments.
4676
// It returns the exit status code or -1 if it errored before the process could start.
4777
func (c DefaultBazelCmd) Execute(config BazelCmdConfig, startupArgs []string, command string, args ...string) (int, error) {

0 commit comments

Comments
 (0)