dap: defer inputs for a step to prevent overeager evaluation
When the debug thread was updated to always solve inputs from the operation that it was tied to it became a bit overeager to evaluate them. The intention of the steps is to have a single direct parent and then potentially multiple "function calls" that can be evaluated with step into and step out to leave. With the change, that logic stayed in, but the inputs were always being evaluated before they were stepped into or over. Now, when we construct the steps, we also attach a list of inputs that we should defer evaluation on to ensure we don't execute inputs that haven't been executed yet. It will then wrap the reference with a version that causes `Evaluate` to do nothing. This prevents the overeager evaluation but allows the reference to be evaluated if we need to read the filesystem. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
+38
-6
@@ -2,6 +2,7 @@ package dap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"maps"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -138,6 +139,11 @@ type step struct {
|
|||||||
// breakpoint resolution.
|
// breakpoint resolution.
|
||||||
dgst digest.Digest
|
dgst digest.Digest
|
||||||
|
|
||||||
|
// deferred holds the inputs that should have its evaluation deferred.
|
||||||
|
// These inputs are still included in the references but will only be
|
||||||
|
// evaluated when needed.
|
||||||
|
deferred map[int]bool
|
||||||
|
|
||||||
// in holds the next target when step in is used.
|
// in holds the next target when step in is used.
|
||||||
in *step
|
in *step
|
||||||
|
|
||||||
@@ -241,10 +247,21 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
|
|||||||
// If this branch is empty (signified by a nil return value) then
|
// If this branch is empty (signified by a nil return value) then
|
||||||
// skip it.
|
// skip it.
|
||||||
if head.in == nil {
|
if head.in == nil {
|
||||||
|
// Always mark this input as deferred since it doesn't have
|
||||||
|
// an associated branch.
|
||||||
|
if entrypoint.deferred == nil {
|
||||||
|
entrypoint.deferred = make(map[int]bool)
|
||||||
|
}
|
||||||
|
entrypoint.deferred[i] = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
entrypoint.dgst = ""
|
entrypoint.dgst = ""
|
||||||
|
|
||||||
|
// Filter this input from the target so it doesn't get solved
|
||||||
|
// when moving to this step.
|
||||||
|
head.deferred = make(map[int]bool)
|
||||||
|
maps.Copy(head.deferred, entrypoint.deferred)
|
||||||
|
head.deferred[i] = true
|
||||||
entrypoint = &head
|
entrypoint = &head
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,11 +273,12 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
|
|||||||
|
|
||||||
// Create a new step that refers to the direct parent.
|
// Create a new step that refers to the direct parent.
|
||||||
head := &step{
|
head := &step{
|
||||||
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
|
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
|
||||||
in: entrypoint,
|
deferred: entrypoint.deferred,
|
||||||
next: entrypoint,
|
in: entrypoint,
|
||||||
out: entrypoint.out,
|
next: entrypoint,
|
||||||
parent: -1,
|
out: entrypoint.out,
|
||||||
|
parent: -1,
|
||||||
}
|
}
|
||||||
head.frame = t.getStackFrame(head.dgst, entrypoint)
|
head.frame = t.getStackFrame(head.dgst, entrypoint)
|
||||||
entrypoint = head
|
entrypoint = head
|
||||||
@@ -628,6 +646,12 @@ func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have marked this input to be deferred, wrap it in a reference
|
||||||
|
// that suppresses the evaluate call.
|
||||||
|
if target.deferred[i] {
|
||||||
|
ref = &deferredReference{Reference: ref}
|
||||||
|
}
|
||||||
refs[k] = ref
|
refs[k] = ref
|
||||||
}
|
}
|
||||||
return root, refs, nil
|
return root, refs, nil
|
||||||
@@ -823,3 +847,11 @@ func (r *mountReference) ReadDir(ctx context.Context, req gateway.ReadDirRequest
|
|||||||
MountIndex: r.index,
|
MountIndex: r.index,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type deferredReference struct {
|
||||||
|
gateway.Reference
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *deferredReference) Evaluate(ctx context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -85,6 +86,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
|
|||||||
testDapBuildStepNext,
|
testDapBuildStepNext,
|
||||||
testDapBuildStepOut,
|
testDapBuildStepOut,
|
||||||
testDapBuildVariables,
|
testDapBuildVariables,
|
||||||
|
testDapBuildDeferredEval,
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDapBuild(t *testing.T, sb integration.Sandbox) {
|
func testDapBuild(t *testing.T, sb integration.Sandbox) {
|
||||||
@@ -857,6 +859,61 @@ func testDapBuildVariables(t *testing.T, sb integration.Sandbox) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDapBuildDeferredEval(t *testing.T, sb integration.Sandbox) {
|
||||||
|
dir := createTestProject(t)
|
||||||
|
client, done, err := dapBuildCmd(t, sb)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Track when we see this message.
|
||||||
|
seen := make(chan struct{}, 1)
|
||||||
|
client.RegisterEvent("output", func(em dap.EventMessage) {
|
||||||
|
e := em.(*dap.OutputEvent)
|
||||||
|
if strings.Contains(e.Body.Output, "RUN cp /etc/foo /etc/bar") {
|
||||||
|
select {
|
||||||
|
case seen <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
interruptCh := pollInterruptEvents(client)
|
||||||
|
doLaunch(t, client, commands.LaunchConfig{
|
||||||
|
Dockerfile: path.Join(dir, "Dockerfile"),
|
||||||
|
ContextPath: dir,
|
||||||
|
},
|
||||||
|
dap.SourceBreakpoint{Line: 7},
|
||||||
|
)
|
||||||
|
|
||||||
|
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
|
||||||
|
require.NotNil(t, stopped)
|
||||||
|
|
||||||
|
// The output event is usually immediate but it can sometimes be delayed due to
|
||||||
|
// the multithreading in the printer. Just wait for a little bit.
|
||||||
|
select {
|
||||||
|
case <-seen:
|
||||||
|
// We should not have seen this message since the branch this
|
||||||
|
// message comes from should be deferred because we have
|
||||||
|
// not passed the breakpoint.
|
||||||
|
t.Fatal("step has been invoked before intended")
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
doNext(t, client, stopped.Body.ThreadId)
|
||||||
|
|
||||||
|
stopped = waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
|
||||||
|
require.NotNil(t, stopped)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-seen:
|
||||||
|
// Wait up to a second for the input to be seen.
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("step should have been seen")
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
require.ErrorAs(t, done(true), &exitErr)
|
||||||
|
}
|
||||||
|
|
||||||
func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
|
func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user