Skip to content

Commit c790615

Browse files
committed
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>
1 parent ac1a8ee commit c790615

2 files changed

Lines changed: 95 additions & 6 deletions

File tree

dap/thread.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dap
22

33
import (
44
"context"
5+
"maps"
56
"path/filepath"
67
"slices"
78
"strings"
@@ -138,6 +139,11 @@ type step struct {
138139
// breakpoint resolution.
139140
dgst digest.Digest
140141

142+
// deferred holds the inputs that should have its evaluation deferred.
143+
// These inputs are still included in the references but will only be
144+
// evaluated when needed.
145+
deferred map[int]bool
146+
141147
// in holds the next target when step in is used.
142148
in *step
143149

@@ -241,10 +247,21 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
241247
// If this branch is empty (signified by a nil return value) then
242248
// skip it.
243249
if head.in == nil {
250+
// Always mark this input as deferred since it doesn't have
251+
// an associated branch.
252+
if entrypoint.deferred == nil {
253+
entrypoint.deferred = make(map[int]bool)
254+
}
255+
entrypoint.deferred[i] = true
244256
continue
245257
}
246-
247258
entrypoint.dgst = ""
259+
260+
// Filter this input from the target so it doesn't get solved
261+
// when moving to this step.
262+
head.deferred = make(map[int]bool)
263+
maps.Copy(head.deferred, entrypoint.deferred)
264+
head.deferred[i] = true
248265
entrypoint = &head
249266
}
250267

@@ -256,11 +273,12 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
256273

257274
// Create a new step that refers to the direct parent.
258275
head := &step{
259-
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
260-
in: entrypoint,
261-
next: entrypoint,
262-
out: entrypoint.out,
263-
parent: -1,
276+
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
277+
deferred: entrypoint.deferred,
278+
in: entrypoint,
279+
next: entrypoint,
280+
out: entrypoint.out,
281+
parent: -1,
264282
}
265283
head.frame = t.getStackFrame(head.dgst, entrypoint)
266284
entrypoint = head
@@ -628,6 +646,12 @@ func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[str
628646
if err != nil {
629647
return "", nil, err
630648
}
649+
650+
// If we have marked this input to be deferred, wrap it in a reference
651+
// that suppresses the evaluate call.
652+
if target.deferred[i] {
653+
ref = &deferredReference{Reference: ref}
654+
}
631655
refs[k] = ref
632656
}
633657
return root, refs, nil
@@ -823,3 +847,11 @@ func (r *mountReference) ReadDir(ctx context.Context, req gateway.ReadDirRequest
823847
MountIndex: r.index,
824848
})
825849
}
850+
851+
type deferredReference struct {
852+
gateway.Reference
853+
}
854+
855+
func (r *deferredReference) Evaluate(ctx context.Context) error {
856+
return nil
857+
}

tests/dap_build.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path"
88
"runtime"
99
"slices"
10+
"strings"
1011
"syscall"
1112
"testing"
1213
"time"
@@ -85,6 +86,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
8586
testDapBuildStepNext,
8687
testDapBuildStepOut,
8788
testDapBuildVariables,
89+
testDapBuildDeferredEval,
8890
}
8991

9092
func testDapBuild(t *testing.T, sb integration.Sandbox) {
@@ -857,6 +859,61 @@ func testDapBuildVariables(t *testing.T, sb integration.Sandbox) {
857859
}
858860
}
859861

862+
func testDapBuildDeferredEval(t *testing.T, sb integration.Sandbox) {
863+
dir := createTestProject(t)
864+
client, done, err := dapBuildCmd(t, sb)
865+
require.NoError(t, err)
866+
867+
// Track when we see this message.
868+
seen := make(chan struct{}, 1)
869+
client.RegisterEvent("output", func(em dap.EventMessage) {
870+
e := em.(*dap.OutputEvent)
871+
if strings.Contains(e.Body.Output, "RUN cp /etc/foo /etc/bar") {
872+
select {
873+
case seen <- struct{}{}:
874+
default:
875+
}
876+
}
877+
})
878+
879+
interruptCh := pollInterruptEvents(client)
880+
doLaunch(t, client, commands.LaunchConfig{
881+
Dockerfile: path.Join(dir, "Dockerfile"),
882+
ContextPath: dir,
883+
},
884+
dap.SourceBreakpoint{Line: 7},
885+
)
886+
887+
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
888+
require.NotNil(t, stopped)
889+
890+
// The output event is usually immediate but it can sometimes be delayed due to
891+
// the multithreading in the printer. Just wait for a little bit.
892+
select {
893+
case <-seen:
894+
// We should not have seen this message since the branch this
895+
// message comes from should be deferred because we have
896+
// not passed the breakpoint.
897+
t.Fatal("step has been invoked before intended")
898+
case <-time.After(100 * time.Millisecond):
899+
}
900+
901+
doNext(t, client, stopped.Body.ThreadId)
902+
903+
stopped = waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
904+
require.NotNil(t, stopped)
905+
906+
select {
907+
case <-seen:
908+
// Wait up to a second for the input to be seen.
909+
case <-time.After(time.Second):
910+
t.Fatal("step should have been seen")
911+
}
912+
913+
var exitErr *exec.ExitError
914+
require.ErrorAs(t, done(true), &exitErr)
915+
}
916+
860917
func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
861918
t.Helper()
862919

0 commit comments

Comments
 (0)