Skip to content

Commit 63a8fc3

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 e5aff80 commit 63a8fc3

2 files changed

Lines changed: 82 additions & 5 deletions

File tree

dap/thread.go

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

33
import (
44
"context"
5+
"maps"
56
"path"
67
"path/filepath"
78
"slices"
@@ -131,6 +132,11 @@ type step struct {
131132
// breakpoint resolution.
132133
dgst digest.Digest
133134

135+
// deferred holds the inputs that should have its evaluation deferred.
136+
// These inputs are still included in the references but will only be
137+
// evaluated when needed.
138+
deferred map[int]bool
139+
134140
// in holds the next target when step in is used.
135141
in *step
136142

@@ -225,6 +231,13 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
225231
// Create the routine associated with this input.
226232
// Associate it with the entrypoint in step.
227233
head.in = t.createBranch(digest.Digest(inp.Digest), entrypoint)
234+
235+
// Filter this input from the target so it doesn't get solved
236+
// when moving to this step.
237+
head.deferred = make(map[int]bool)
238+
maps.Copy(head.deferred, entrypoint.deferred)
239+
head.deferred[i] = true
240+
228241
entrypoint = &head
229242
}
230243

@@ -236,11 +249,12 @@ func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *
236249

237250
// Create a new step that refers to the direct parent.
238251
head := &step{
239-
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
240-
in: entrypoint,
241-
next: entrypoint,
242-
out: entrypoint.out,
243-
parent: -1,
252+
dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest),
253+
deferred: entrypoint.deferred,
254+
in: entrypoint,
255+
next: entrypoint,
256+
out: entrypoint.out,
257+
parent: -1,
244258
}
245259
head.frame = t.getStackFrame(head.dgst, entrypoint)
246260
entrypoint = head
@@ -606,6 +620,12 @@ func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[str
606620
if err != nil {
607621
return "", nil, err
608622
}
623+
624+
// If we have marked this input to be deferred, wrap it in a reference
625+
// that suppresses the evaluate call.
626+
if target.deferred[i] {
627+
ref = &deferredReference{Reference: ref}
628+
}
609629
refs[k] = ref
610630
}
611631
return root, refs, nil
@@ -801,3 +821,11 @@ func (r *mountReference) ReadDir(ctx context.Context, req gateway.ReadDirRequest
801821
MountIndex: r.index,
802822
})
803823
}
824+
825+
type deferredReference struct {
826+
gateway.Reference
827+
}
828+
829+
func (r *deferredReference) Evaluate(ctx context.Context) error {
830+
return nil
831+
}

tests/dap_build.go

Lines changed: 49 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"
@@ -83,6 +84,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
8384
testDapBuildStepNext,
8485
testDapBuildStepOut,
8586
testDapBuildVariables,
87+
testDapBuildDeferredEval,
8688
}
8789

8890
func testDapBuild(t *testing.T, sb integration.Sandbox) {
@@ -794,6 +796,53 @@ func testDapBuildVariables(t *testing.T, sb integration.Sandbox) {
794796
}
795797
}
796798

799+
func testDapBuildDeferredEval(t *testing.T, sb integration.Sandbox) {
800+
dir := createTestProject(t)
801+
client, done, err := dapBuildCmd(t, sb)
802+
require.NoError(t, err)
803+
804+
// Track when we see this message.
805+
seen := false
806+
client.RegisterEvent("output", func(em dap.EventMessage) {
807+
e := em.(*dap.OutputEvent)
808+
seen = seen || strings.Contains(e.Body.Output, "RUN cp /etc/foo /etc/bar")
809+
})
810+
811+
interruptCh := pollInterruptEvents(client)
812+
doLaunch(t, client, commands.LaunchConfig{
813+
Dockerfile: path.Join(dir, "Dockerfile"),
814+
ContextPath: dir,
815+
},
816+
dap.SourceBreakpoint{Line: 7},
817+
)
818+
819+
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
820+
require.NotNil(t, stopped)
821+
822+
// The output event is usually immediate but it can sometimes be delayed due to
823+
// the multithreading in the printer. Just wait for a little bit.
824+
<-time.After(100 * time.Millisecond)
825+
826+
// We should not have seen this message since the branch this
827+
// message comes from should be deferred because we have
828+
// not passed the breakpoint.
829+
require.False(t, seen, "step has been invoked before intended")
830+
doNext(t, client, stopped.Body.ThreadId)
831+
832+
stopped = waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
833+
require.NotNil(t, stopped)
834+
835+
if !seen {
836+
// If we haven't seen the output then wait for a little bit
837+
// due to the printer being potentially delayed.
838+
<-time.After(100 * time.Millisecond)
839+
}
840+
require.True(t, seen, "step should have been seen")
841+
842+
var exitErr *exec.ExitError
843+
require.ErrorAs(t, done(true), &exitErr)
844+
}
845+
797846
func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
798847
t.Helper()
799848

0 commit comments

Comments
 (0)