Skip to content

Commit 5db46fd

Browse files
committed
dap: implement first pass at breakpoints
Implement the first iteration of breakpoints. When a breakpoint is set, it starts unverified. When a thread begins evaluation, it tries to see if a breakpoint corresponds to one of the parsed instructions and will verify it. Breakpoints work when continue is used. At the current moment, setting breakpoints while a thread is currently running doesn't work. Breakpoints are rechecked each time execution is about to restart. Signed-off-by: Jonathan A. Sternberg <[email protected]>
1 parent e3eb64e commit 5db46fd

File tree

2 files changed

+181
-42
lines changed

2 files changed

+181
-42
lines changed

dap/adapter.go

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import (
77
"fmt"
88
"io"
99
"path"
10+
"path/filepath"
11+
"slices"
1012
"sync"
1113
"sync/atomic"
1214

1315
"github.com/docker/buildx/build"
1416
"github.com/docker/buildx/dap/common"
1517
"github.com/google/go-dap"
1618
gateway "github.com/moby/buildkit/frontend/gateway/client"
19+
"github.com/moby/buildkit/solver/pb"
20+
"github.com/opencontainers/go-digest"
1721
"github.com/pkg/errors"
1822
"golang.org/x/sync/errgroup"
1923
)
@@ -33,8 +37,9 @@ type Adapter[C LaunchConfig] struct {
3337
threadsMu sync.RWMutex
3438
nextThreadID int
3539

36-
sourceMap sourceMap
37-
idPool *idPool
40+
breakpointMap *breakpointMap
41+
sourceMap sourceMap
42+
idPool *idPool
3843
}
3944

4045
func New[C LaunchConfig]() *Adapter[C] {
@@ -45,6 +50,7 @@ func New[C LaunchConfig]() *Adapter[C] {
4550
evaluateReqCh: make(chan *evaluateRequest),
4651
threads: make(map[int]*thread),
4752
nextThreadID: 1,
53+
breakpointMap: newBreakpointMap(),
4854
idPool: new(idPool),
4955
}
5056
d.srv = NewServer(d.dapHandler())
@@ -151,14 +157,7 @@ func (d *Adapter[C]) Next(c Context, req *dap.NextRequest, resp *dap.NextRespons
151157
}
152158

153159
func (d *Adapter[C]) SetBreakpoints(c Context, req *dap.SetBreakpointsRequest, resp *dap.SetBreakpointsResponse) error {
154-
// TODO: implement breakpoints
155-
for range req.Arguments.Breakpoints {
156-
// Fail to create all breakpoints that were requested.
157-
resp.Body.Breakpoints = append(resp.Body.Breakpoints, dap.Breakpoint{
158-
Verified: false,
159-
Message: "breakpoints unsupported",
160-
})
161-
}
160+
resp.Body.Breakpoints = d.breakpointMap.Set(req.Arguments.Source.Path, req.Arguments.Breakpoints)
162161
return nil
163162
}
164163

@@ -212,10 +211,11 @@ func (d *Adapter[C]) newThread(ctx Context, name string) (t *thread) {
212211
d.threadsMu.Lock()
213212
id := d.nextThreadID
214213
t = &thread{
215-
id: id,
216-
name: name,
217-
sourceMap: &d.sourceMap,
218-
idPool: d.idPool,
214+
id: id,
215+
name: name,
216+
sourceMap: &d.sourceMap,
217+
breakpointMap: d.breakpointMap,
218+
idPool: d.idPool,
219219
}
220220
d.threads[t.id] = t
221221
d.nextThreadID++
@@ -468,3 +468,100 @@ func (s *sourceMap) Get(fname string) ([]byte, bool) {
468468
}
469469
return v.([]byte), true
470470
}
471+
472+
type breakpointMap struct {
473+
byPath map[string][]dap.Breakpoint
474+
mu sync.RWMutex
475+
476+
nextID atomic.Int64
477+
}
478+
479+
func newBreakpointMap() *breakpointMap {
480+
return &breakpointMap{
481+
byPath: make(map[string][]dap.Breakpoint),
482+
}
483+
}
484+
485+
func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoints []dap.Breakpoint) {
486+
b.mu.Lock()
487+
defer b.mu.Unlock()
488+
489+
prev := b.byPath[fname]
490+
for _, sbp := range sbps {
491+
index := slices.IndexFunc(prev, func(e dap.Breakpoint) bool {
492+
return sbp.Line >= e.Line && sbp.Line <= e.EndLine && sbp.Column >= e.Column && sbp.Column <= e.EndColumn
493+
})
494+
495+
var bp dap.Breakpoint
496+
if index >= 0 {
497+
bp = prev[index]
498+
} else {
499+
bp = dap.Breakpoint{
500+
Id: int(b.nextID.Add(1)),
501+
Line: sbp.Line,
502+
EndLine: sbp.Line,
503+
Column: sbp.Column,
504+
EndColumn: sbp.Column,
505+
}
506+
}
507+
breakpoints = append(breakpoints, bp)
508+
}
509+
b.byPath[fname] = breakpoints
510+
return breakpoints
511+
}
512+
513+
func (b *breakpointMap) Intersect(ctx Context, src *pb.Source, ws string) map[digest.Digest]int {
514+
b.mu.Lock()
515+
defer b.mu.Unlock()
516+
517+
digests := make(map[digest.Digest]int)
518+
519+
NEXT_DIGEST:
520+
for dgst, locs := range src.Locations {
521+
for _, loc := range locs.Locations {
522+
if len(loc.Ranges) == 0 {
523+
continue
524+
}
525+
r := loc.Ranges[0]
526+
527+
info := src.Infos[loc.SourceIndex]
528+
fname := filepath.Join(ws, info.Filename)
529+
530+
bps := b.byPath[fname]
531+
if len(bps) == 0 {
532+
// No breakpoints for this file.
533+
continue
534+
}
535+
536+
for i, bp := range bps {
537+
if !overlaps(r, &bp) {
538+
continue
539+
}
540+
541+
if !bp.Verified {
542+
bp.Line = int(r.Start.Line)
543+
bp.EndLine = int(r.End.Line)
544+
bp.Column = int(r.Start.Character)
545+
bp.EndColumn = int(r.End.Character)
546+
bp.Verified = true
547+
548+
ctx.C() <- &dap.BreakpointEvent{
549+
Event: dap.Event{Event: "breakpoint"},
550+
Body: dap.BreakpointEventBody{
551+
Reason: "changed",
552+
Breakpoint: bp,
553+
},
554+
}
555+
bps[i] = bp
556+
}
557+
digests[digest.Digest(dgst)] = bp.Id
558+
continue NEXT_DIGEST
559+
}
560+
}
561+
}
562+
return digests
563+
}
564+
565+
func overlaps(r *pb.Range, bp *dap.Breakpoint) bool {
566+
return r.Start.Line <= int32(bp.Line) && r.Start.Character <= int32(bp.Column) && r.End.Line >= int32(bp.EndLine) && r.End.Character >= int32(bp.EndColumn)
567+
}

dap/thread.go

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ type thread struct {
2323
name string
2424

2525
// Persistent state from the adapter.
26-
idPool *idPool
27-
sourceMap *sourceMap
26+
idPool *idPool
27+
sourceMap *sourceMap
28+
breakpointMap *breakpointMap
2829

2930
// Inputs to the evaluate call.
3031
c gateway.Client
@@ -36,6 +37,7 @@ type thread struct {
3637
def *llb.Definition
3738
ops map[digest.Digest]*pb.Op
3839
head digest.Digest
40+
bps map[digest.Digest]int
3941

4042
// Runtime state for the evaluate call.
4143
regions []*region
@@ -80,15 +82,18 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, ref gateway.Reference,
8082
}
8183

8284
for {
85+
if step == stepContinue {
86+
t.setBreakpoints(ctx)
87+
}
8388
pos, err := t.seekNext(ctx, step)
8489

85-
reason, desc := t.needsDebug(pos, step, err)
86-
if reason == "" {
90+
event := t.needsDebug(pos, step, err)
91+
if event.Reason == "" {
8792
return err
8893
}
8994

9095
select {
91-
case step = <-t.pause(ctx, err, reason, desc):
96+
case step = <-t.pause(ctx, err, event):
9297
case <-ctx.Done():
9398
return context.Cause(ctx)
9499
}
@@ -100,7 +105,11 @@ func (t *thread) init(ctx Context, c gateway.Client, ref gateway.Reference, meta
100105
t.ref = ref
101106
t.meta = meta
102107
t.sourcePath = inputs.ContextPath
103-
return t.createRegions(ctx)
108+
109+
if err := t.getLLBState(ctx); err != nil {
110+
return err
111+
}
112+
return t.createRegions()
104113
}
105114

106115
func (t *thread) reset() {
@@ -111,17 +120,23 @@ func (t *thread) reset() {
111120
t.ops = nil
112121
}
113122

114-
func (t *thread) needsDebug(target digest.Digest, step stepType, err error) (reason, desc string) {
123+
func (t *thread) needsDebug(target digest.Digest, step stepType, err error) (e dap.StoppedEventBody) {
115124
if err != nil {
116-
reason = "exception"
117-
desc = "Encountered an error during result evaluation"
118-
} else if target != "" && step == stepNext {
119-
reason = "step"
125+
e.Reason = "exception"
126+
e.Description = "Encountered an error during result evaluation"
127+
} else if step == stepNext && target != "" {
128+
e.Reason = "step"
129+
} else if step == stepContinue {
130+
if id, ok := t.bps[target]; ok {
131+
e.Reason = "breakpoint"
132+
e.Description = "Paused on breakpoint"
133+
e.HitBreakpointIds = []int{id}
134+
}
120135
}
121136
return
122137
}
123138

124-
func (t *thread) pause(c Context, err error, reason, desc string) <-chan stepType {
139+
func (t *thread) pause(c Context, err error, event dap.StoppedEventBody) <-chan stepType {
125140
t.mu.Lock()
126141
defer t.mu.Unlock()
127142

@@ -140,13 +155,10 @@ func (t *thread) pause(c Context, err error, reason, desc string) <-chan stepTyp
140155
}
141156
}
142157

158+
event.ThreadId = t.id
143159
c.C() <- &dap.StoppedEvent{
144160
Event: dap.Event{Event: "stopped"},
145-
Body: dap.StoppedEventBody{
146-
Reason: reason,
147-
Description: desc,
148-
ThreadId: t.id,
149-
},
161+
Body: event,
150162
}
151163
return t.paused
152164
}
@@ -232,6 +244,10 @@ func (t *thread) getLLBState(ctx Context) error {
232244
return err
233245
}
234246

247+
func (t *thread) setBreakpoints(ctx Context) {
248+
t.bps = t.breakpointMap.Intersect(ctx, t.def.Source, t.sourcePath)
249+
}
250+
235251
func (t *thread) findBacklinks() map[digest.Digest]map[digest.Digest]struct{} {
236252
backlinks := make(map[digest.Digest]map[digest.Digest]struct{})
237253
for dgst := range t.ops {
@@ -249,11 +265,7 @@ func (t *thread) findBacklinks() map[digest.Digest]map[digest.Digest]struct{} {
249265
return backlinks
250266
}
251267

252-
func (t *thread) createRegions(ctx Context) error {
253-
if err := t.getLLBState(ctx); err != nil {
254-
return err
255-
}
256-
268+
func (t *thread) createRegions() error {
257269
// Find the links going from inputs to their outputs.
258270
// This isn't represented in the LLB graph but we need it to ensure
259271
// an op only has one child and whether we are allowed to visit a node.
@@ -363,8 +375,11 @@ func (t *thread) seekNext(ctx Context, step stepType) (digest.Digest, error) {
363375
}
364376

365377
target := t.head
366-
if step == stepNext {
367-
target = t.nextDigest()
378+
switch step {
379+
case stepNext:
380+
target = t.nextDigest(nil)
381+
case stepContinue:
382+
target = t.continueDigest()
368383
}
369384

370385
if target == "" {
@@ -394,11 +409,27 @@ func (t *thread) seek(ctx Context, target digest.Digest) (digest.Digest, error)
394409
return t.curPos, err
395410
}
396411

397-
func (t *thread) nextDigest() digest.Digest {
412+
func (t *thread) nextDigest(fn func(digest.Digest) bool) digest.Digest {
413+
isValid := func(dgst digest.Digest) bool {
414+
// Skip this digest because it has no locations in the source file.
415+
if loc, ok := t.def.Source.Locations[string(dgst)]; !ok || len(loc.Locations) == 0 {
416+
return false
417+
}
418+
419+
// If a custom function has been set for validation, use it.
420+
return fn == nil || fn(dgst)
421+
}
422+
398423
// If we have no position, automatically select the first step.
399424
if t.curPos == "" {
400425
r := t.regions[len(t.regions)-1]
401-
return r.digests[0]
426+
if isValid(r.digests[0]) {
427+
return r.digests[0]
428+
}
429+
430+
// We cannot use the first position. Treat the first position as our
431+
// current position so we can iterate.
432+
t.curPos = r.digests[0]
402433
}
403434

404435
// Look up the region associated with our current position.
@@ -426,15 +457,26 @@ func (t *thread) nextDigest() digest.Digest {
426457
}
427458

428459
next := r.digests[i]
429-
if loc, ok := t.def.Source.Locations[string(next)]; !ok || len(loc.Locations) == 0 {
430-
// Skip this digest because it has no locations in the source file.
460+
if !isValid(next) {
431461
i++
432462
continue
433463
}
434464
return next
435465
}
436466
}
437467

468+
func (t *thread) continueDigest() digest.Digest {
469+
if len(t.bps) == 0 {
470+
return t.head
471+
}
472+
473+
fn := func(dgst digest.Digest) bool {
474+
_, ok := t.bps[dgst]
475+
return ok
476+
}
477+
return t.nextDigest(fn)
478+
}
479+
438480
func (t *thread) solve(ctx context.Context, target digest.Digest) (gateway.Reference, error) {
439481
if target == t.head {
440482
return t.ref, nil

0 commit comments

Comments
 (0)