perf: avoid eager fingerprint variable evaluation (#2883)

This commit is contained in:
Maxime Boucher
2026-07-13 13:58:27 -07:00
committed by GitHub
parent 9200d42282
commit ed24adbc99
3 changed files with 247 additions and 8 deletions

134
taskfile/ast/fingerprint.go Normal file
View File

@@ -0,0 +1,134 @@
package ast
import (
"reflect"
"strings"
)
// ReferencesFingerprintVar reports whether the task references the variable
// produced by the given fingerprint method in fields compiled after it.
func (t *Task) ReferencesFingerprintVar(kind string) bool {
name := strings.ToUpper(kind)
if t == nil || name == "" {
return false
}
for _, status := range t.Status {
if stringReferencesFingerprintVar(status, name) {
return true
}
}
for _, cmd := range t.Cmds {
if cmdReferencesFingerprintVar(cmd, name) {
return true
}
}
for _, dep := range t.Deps {
if depReferencesFingerprintVar(dep, name) {
return true
}
}
for _, precondition := range t.Preconditions {
if preconditionReferencesFingerprintVar(precondition, name) {
return true
}
}
return false
}
func cmdReferencesFingerprintVar(cmd *Cmd, name string) bool {
if cmd == nil {
return false
}
return stringReferencesFingerprintVar(cmd.Cmd, name) ||
stringReferencesFingerprintVar(cmd.Task, name) ||
stringReferencesFingerprintVar(cmd.If, name) ||
forReferencesFingerprintVar(cmd.For, name) ||
varsReferenceFingerprintVar(cmd.Vars, name)
}
func depReferencesFingerprintVar(dep *Dep, name string) bool {
if dep == nil {
return false
}
return stringReferencesFingerprintVar(dep.Task, name) ||
forReferencesFingerprintVar(dep.For, name) ||
varsReferenceFingerprintVar(dep.Vars, name)
}
func preconditionReferencesFingerprintVar(precondition *Precondition, name string) bool {
if precondition == nil {
return false
}
return stringReferencesFingerprintVar(precondition.Sh, name) ||
stringReferencesFingerprintVar(precondition.Msg, name)
}
func forReferencesFingerprintVar(f *For, name string) bool {
if f == nil {
return false
}
if valueReferencesFingerprintVar(f.List, name) {
return true
}
for _, row := range f.Matrix.All() {
if row != nil && (stringReferencesFingerprintVar(row.Ref, name) ||
valueReferencesFingerprintVar(row.Value, name)) {
return true
}
}
return false
}
func varsReferenceFingerprintVar(vars *Vars, name string) bool {
for _, v := range vars.All() {
if valueReferencesFingerprintVar(v.Value, name) ||
valueReferencesFingerprintVar(v.Live, name) ||
stringPointerReferencesFingerprintVar(v.Sh, name) ||
stringReferencesFingerprintVar(v.Ref, name) ||
stringReferencesFingerprintVar(v.Dir, name) {
return true
}
}
return false
}
func valueReferencesFingerprintVar(value any, name string) bool {
if value == nil {
return false
}
if s, ok := value.(string); ok {
return stringReferencesFingerprintVar(s, name)
}
rv := reflect.ValueOf(value)
switch rv.Kind() {
case reflect.Pointer, reflect.Interface:
if rv.IsNil() {
return false
}
return valueReferencesFingerprintVar(rv.Elem().Interface(), name)
case reflect.Array, reflect.Slice:
for i := 0; i < rv.Len(); i++ {
if valueReferencesFingerprintVar(rv.Index(i).Interface(), name) {
return true
}
}
case reflect.Map:
for _, key := range rv.MapKeys() {
if valueReferencesFingerprintVar(key.Interface(), name) ||
valueReferencesFingerprintVar(rv.MapIndex(key).Interface(), name) {
return true
}
}
}
return false
}
func stringPointerReferencesFingerprintVar(s *string, name string) bool {
return s != nil && stringReferencesFingerprintVar(*s, name)
}
func stringReferencesFingerprintVar(s string, name string) bool {
return strings.Contains(s, name)
}

View File

@@ -0,0 +1,103 @@
package ast
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTaskReferencesFingerprintVar(t *testing.T) {
t.Parallel()
fingerprintRef := "{{.CHECKSUM}}"
tests := []struct {
name string
task *Task
want bool
}{
{
name: "nil task",
want: false,
},
{
name: "status",
task: &Task{Status: []string{"test -n " + fingerprintRef}},
want: true,
},
{
name: "command",
task: &Task{Cmds: []*Cmd{{Cmd: "echo " + fingerprintRef}}},
want: true,
},
{
name: "task call",
task: &Task{Cmds: []*Cmd{{Task: "build-" + fingerprintRef}}},
want: true,
},
{
name: "command condition",
task: &Task{Cmds: []*Cmd{{If: "test -n " + fingerprintRef}}},
want: true,
},
{
name: "loop list nested value",
task: &Task{Cmds: []*Cmd{{For: &For{List: []any{map[string]any{"value": fingerprintRef}}}}}},
want: true,
},
{
name: "loop matrix reference",
task: &Task{Cmds: []*Cmd{{For: &For{Matrix: NewMatrix(
&MatrixElement{Key: "item", Value: &MatrixRow{Ref: fingerprintRef}},
)}}}},
want: true,
},
{
name: "command variable",
task: &Task{Cmds: []*Cmd{{Vars: NewVars(
&VarElement{Key: "VALUE", Value: Var{Value: []any{fingerprintRef}}},
)}}},
want: true,
},
{
name: "dynamic command variable",
task: &Task{Cmds: []*Cmd{{Vars: NewVars(
&VarElement{Key: "VALUE", Value: Var{Sh: &fingerprintRef}},
)}}},
want: true,
},
{
name: "dependency",
task: &Task{Deps: []*Dep{{Task: "build-" + fingerprintRef}}},
want: true,
},
{
name: "dependency variable",
task: &Task{Deps: []*Dep{{Vars: NewVars(
&VarElement{Key: "VALUE", Value: Var{Ref: fingerprintRef}},
)}}},
want: true,
},
{
name: "precondition",
task: &Task{Preconditions: []*Precondition{{Msg: fingerprintRef}}},
want: true,
},
{
name: "unrelated fields and nil entries",
task: &Task{
Cmds: []*Cmd{nil, {Cmd: "echo ok"}},
Deps: []*Dep{nil, {Task: "build"}},
Preconditions: []*Precondition{nil, {Sh: "test -f output"}},
Status: []string{"test -f output"},
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.want, tt.task.ReferencesFingerprintVar("checksum"))
})
}
}

View File

@@ -217,15 +217,17 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
checker = fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)
}
value, err := checker.Value(&new)
if err != nil {
return nil, err
}
vars.Set(strings.ToUpper(checker.Kind()), ast.Var{Live: value})
if origTask.ReferencesFingerprintVar(checker.Kind()) {
value, err := checker.Value(&new)
if err != nil {
return nil, err
}
vars.Set(strings.ToUpper(checker.Kind()), ast.Var{Live: value})
// Adding new variables, requires us to refresh the templaters
// cache of the the values manually
cache.ResetCache()
// Adding new variables, requires us to refresh the templaters
// cache of the the values manually
cache.ResetCache()
}
}
if len(origTask.Cmds) > 0 {