Repository: go-delve/delve (go)

Issue:
dap: show pprof labels in thread names
It would be useful to see pprof labels in VS Code's debugger. One possible way to show them is to append them after the go routine ID in the thread names, e.g. instead of showing just `[Go 42]`, show `[Go 42 key1: value1, key2: value2]`.

<img width="470" alt="image" src="https://github.com/go-delve/delve/assets/1225667/75ac2e73-b4c9-45fd-b63c-5fbd8eea06eb">

1. What version of Delve are you using (`dlv version`)? **1.20.2**
2. What version of Go are you using? (`go version`)? **go1.20.7 darwin/arm64**
3. What operating system and processor architecture are you using? **MacOS 13.5.1, M1**


Pull request:
Show pprof labels in thread names
If a goroutine has labels, we add them after the ID.

Fixes #3493.

Patch:
diff --git a/Documentation/api/dap/README.md b/Documentation/api/dap/README.md
index 74565d07..c75f2d99 100644
--- a/Documentation/api/dap/README.md
+++ b/Documentation/api/dap/README.md
@@ -33,6 +33,7 @@ In addition to the general [DAP spec](https://microsoft.github.io/debug-adapter-
     stackTraceDepth<br>
     showGlobalVariables<br>
     showRegisters<br>
+    showPprofLabels<br>
     hideSystemGoroutines<br>
     goroutineFilters
     </tr>
diff --git a/service/dap/command.go b/service/dap/command.go
index fb81a069..0cff856c 100644
--- a/service/dap/command.go
+++ b/service/dap/command.go
@@ -62,7 +62,14 @@ Type "help" followed by the name of a command for more information about it.`
 	dlv config substitutePath -clear
 	
 		Adds or removes a path substitution rule. If -clear is used all substitutePath rules are removed.
-		See also Documentation/cli/substitutepath.md.`
+		See also Documentation/cli/substitutepath.md.
+
+	dlv config showPprofLabels <label>
+	dlv config showPprofLabels -clear <label>
+	dlv config showPprofLabels -clear
+
+		Adds or removes a label key to show in the callstack view. If -clear is used without an argument,
+		all labels are removed.`
 	msgSources = `Print list of source files.
 
 	dlv sources [<regex>]
@@ -138,7 +145,7 @@ func (s *Session) evaluateConfig(_, _ int, expr string) (string, error) {
 					Areas: []dap.InvalidatedAreas{"variables"},
 				},
 			})
-		case "goroutineFilters", "hideSystemGoroutines":
+		case "goroutineFilters", "hideSystemGoroutines", "showPprofLabels":
 			// Thread related data has become invalidated.
 			s.send(&dap.InvalidatedEvent{
 				Event: *newEvent("invalidated"),
diff --git a/service/dap/config.go b/service/dap/config.go
index 7844589d..a162501e 100644
--- a/service/dap/config.go
+++ b/service/dap/config.go
@@ -37,6 +37,15 @@ func configureSet(sargs *launchAttachArgs, args string) (bool, string, error) {
 		return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
 	}
 
+	if cfgname == "showPprofLabels" {
+		err := configureSetShowPprofLabels(sargs, rest)
+		if err != nil {
+			return false, "", err
+		}
+		// Print the updated labels
+		return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
+	}
+
 	err := config.ConfigureSetSimple(rest, cfgname, field)
 	if err != nil {
 		return false, "", err
@@ -85,3 +94,37 @@ func configureSetSubstitutePath(args *launchAttachArgs, rest string) error {
 	}
 	return nil
 }
+
+func configureSetShowPprofLabels(args *launchAttachArgs, rest string) error {
+	if strings.TrimSpace(rest) == "-clear" {
+		args.ShowPprofLabels = args.ShowPprofLabels[:0]
+		return nil
+	}
+	delete := false
+	argv := config.SplitQuotedFields(rest, '"')
+	if len(argv) == 2 && argv[0] == "-clear" {
+		argv = argv[1:]
+		delete = true
+	}
+	switch len(argv) {
+	case 0:
+		// do nothing, let caller show the current list of labels
+		return nil
+	case 1:
+		if delete {
+			for i := range args.ShowPprofLabels {
+				if args.ShowPprofLabels[i] == argv[0] {
+					copy(args.ShowPprofLabels[i:], args.ShowPprofLabels[i+1:])
+					args.ShowPprofLabels = args.ShowPprofLabels[:len(args.ShowPprofLabels)-1]
+					return nil
+				}
+			}
+			return fmt.Errorf("could not find label %q", argv[0])
+		} else {
+			args.ShowPprofLabels = append(args.ShowPprofLabels, argv[0])
+		}
+	default:
+		return fmt.Errorf("too many arguments to \"config showPprofLabels\"")
+	}
+	return nil
+}
diff --git a/service/dap/server.go b/service/dap/server.go
index 96e45c8d..e3f2dbbb 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -222,6 +222,11 @@ type launchAttachArgs struct {
 	ShowRegisters bool `cfgName:"showRegisters"`
 	// GoroutineFilters are the filters used when loading goroutines.
 	GoroutineFilters string `cfgName:"goroutineFilters"`
+	// ShowPprofLabels is an array of keys of pprof labels to show as a
+	// goroutine name in the threads view. If the array has one element, only
+	// that label's value will be shown; otherwise, each of the labels will be
+	// shown as "key:value". To show all labels, specify the single element "*".
+	ShowPprofLabels []string `cfgName:"showPprofLabels"`
 	// HideSystemGoroutines indicates if system goroutines should be removed from threads
 	// responses.
 	HideSystemGoroutines bool `cfgName:"hideSystemGoroutines"`
@@ -241,6 +246,7 @@ var defaultArgs = launchAttachArgs{
 	HideSystemGoroutines:         false,
 	ShowRegisters:                false,
 	GoroutineFilters:             "",
+	ShowPprofLabels:              []string{},
 	substitutePathClientToServer: [][2]string{},
 	substitutePathServerToClient: [][2]string{},
 }
@@ -280,11 +286,9 @@ const (
 	maxStringLenInCallRetVars = 1 << 10 // 1024
 )
 
-var (
-	// Max number of goroutines that we will return.
-	// This is a var for testing
-	maxGoroutines = 1 << 10
-)
+// Max number of goroutines that we will return.
+// This is a var for testing
+var maxGoroutines = 1 << 10
 
 // NewServer creates a new DAP Server. It takes an opened Listener
 // via config and assumes its ownership. config.DisconnectChan has to be set;
@@ -355,6 +359,7 @@ func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) {
 	s.args.ShowRegisters = args.ShowRegisters
 	s.args.HideSystemGoroutines = args.HideSystemGoroutines
 	s.args.GoroutineFilters = args.GoroutineFilters
+	s.args.ShowPprofLabels = args.ShowPprofLabels
 	if paths := args.SubstitutePath; len(paths) > 0 {
 		clientToServer := make([][2]string, 0, len(paths))
 		serverToClient := make([][2]string, 0, len(paths))
@@ -686,7 +691,7 @@ func (s *Session) handleRequest(request dap.Message) {
 
 	// Non-blocking request handlers will signal when they are ready
 	// setting up for async execution, so more requests can be processed.
-	resumeRequestLoop := make(chan struct{})
+	resumeRequestLoop := newSyncflag()
 
 	switch request := request.(type) {
 	//--- Asynchronous requests ---
@@ -695,43 +700,43 @@ func (s *Session) handleRequest(request dap.Message) {
 			defer s.recoverPanic(request)
 			s.onConfigurationDoneRequest(request, resumeRequestLoop)
 		}()
-		<-resumeRequestLoop
+		resumeRequestLoop.wait()
 	case *dap.ContinueRequest: // Required
 		go func() {
 			defer s.recoverPanic(request)
 			s.onContinueRequest(request, resumeRequestLoop)
 		}()
-		<-resumeRequestLoop
+		resumeRequestLoop.wait()
 	case *dap.NextRequest: // Required
 		go func() {
 			defer s.recoverPanic(request)
 			s.onNextRequest(request, resumeRequestLoop)
 		}()
-		<-resumeRequestLoop
+		resumeRequestLoop.wait()
 	case *dap.StepInRequest: // Required
 		go func() {
 			defer s.recoverPanic(request)
 			s.onStepInRequest(request, resumeRequestLoop)
 		}()
-		<-resumeRequestLoop
+		resumeRequestLoop.wait()
 	case *dap.StepOutRequest: // Required
 		go func() {
 			defer s.recoverPanic(request)
 			s.onStepOutRequest(request, resumeRequestLoop)
 		}()
-		<-resumeRequestLoop
+		resumeRequestLoop.wait()
 	case *dap.StepBackRequest: // Optional (capability 'supportsStepBack')
 		go func() {
 			defer s.recoverPanic(request)
 			s.onStepBackRequest(request, resumeRequestLoop)
 		}()
-		<-resumeRequestLoop
+		resumeRequestLoop.wait()
 	case *dap.ReverseContinueRequest: // Optional (capability 'supportsStepBack')
 		go func() {
 			defer s.recoverPanic(request)
 			s.onReverseContinueRequest(request, resumeRequestLoop)
 		}()
-		<-resumeRequestLoop
+		resumeRequestLoop.wait()
 	//--- Synchronous requests ---
 	case *dap.SetBreakpointsRequest: // Required
 		s.onSetBreakpointsRequest(request)
@@ -817,7 +822,8 @@ func (s *Session) logToConsole(msg string) {
 		Body: dap.OutputEventBody{
 			Output:   msg + "\n",
 			Category: "console",
-		}})
+		},
+	})
 }
 
 func (s *Session) onInitializeRequest(request *dap.InitializeRequest) {
@@ -889,7 +895,7 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 		return
 	}
 
-	var args = defaultLaunchConfig // narrow copy for initializing non-zero default values
+	args := defaultLaunchConfig // narrow copy for initializing non-zero default values
 	if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil {
 		s.sendShowUserErrorResponse(request.Request,
 			FailedToLaunch, "Failed to launch", fmt.Sprintf("invalid debug configuration - %v", err))
@@ -1002,7 +1008,8 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 				Body: dap.OutputEventBody{
 					Output:   fmt.Sprintf("Build Error: %s\n%s (%s)\n", cmd, strings.TrimSpace(string(out)), err.Error()),
 					Category: "stderr",
-				}})
+				},
+			})
 			// Users are used to checking the Debug Console for build errors.
 			// No need to bother them with a visible pop-up.
 			s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
@@ -1035,7 +1042,7 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 	argsToLog.Cwd, _ = filepath.Abs(args.Cwd)
 	s.config.log.Debugf("launching binary '%s' with config: %s", debugbinary, prettyPrint(argsToLog))
 
-	var redirected = false
+	redirected := false
 	switch args.OutputMode {
 	case "remote":
 		redirected = true
@@ -1062,7 +1069,8 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 						Body: dap.OutputEventBody{
 							Output:   outs,
 							Category: category,
-						}})
+						},
+					})
 				}
 				if err != nil {
 					if err == io.EOF {
@@ -1663,8 +1671,8 @@ func closeIfOpen(ch chan struct{}) {
 // This is an optional request enabled by capability 'supportsConfigurationDoneRequest'.
 // It gets triggered after all the debug requests that follow initialized event,
 // so the s.debugger is guaranteed to be set. Expects the target to be halted.
-func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest, allowNextStateChange chan struct{}) {
-	defer closeIfOpen(allowNextStateChange)
+func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneRequest, allowNextStateChange *syncflag) {
+	defer allowNextStateChange.raise()
 	if s.args.stopOnEntry {
 		e := &dap.StoppedEvent{
 			Event: *newEvent("stopped"),
@@ -1684,10 +1692,11 @@ func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneReque
 
 // onContinueRequest handles 'continue' request.
 // This is a mandatory request to support.
-func (s *Session) onContinueRequest(request *dap.ContinueRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onContinueRequest(request *dap.ContinueRequest, allowNextStateChange *syncflag) {
 	s.send(&dap.ContinueResponse{
 		Response: *newResponse(request.Request),
-		Body:     dap.ContinueResponseBody{AllThreadsContinued: true}})
+		Body:     dap.ContinueResponseBody{AllThreadsContinued: true},
+	})
 	s.runUntilStopAndNotify(api.Continue, allowNextStateChange)
 }
 
@@ -1762,7 +1771,8 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
 				Body: dap.OutputEventBody{
 					Output:   fmt.Sprintf("Unable to retrieve goroutines: %s\n", err.Error()),
 					Category: "stderr",
-				}})
+				},
+			})
 		}
 		threads = []dap.Thread{{Id: 1, Name: "Dummy"}}
 	} else if len(gs) == 0 {
@@ -1812,10 +1822,41 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
 			if g.Thread != nil && g.Thread.ThreadID() != 0 {
 				thread = fmt.Sprintf(" (Thread %d)", g.Thread.ThreadID())
 			}
+			var labels strings.Builder
+			writeLabelsForKeys := func(keys []string) {
+				for _, k := range keys {
+					labelValue := g.Labels()[k]
+					if labelValue != "" {
+						labels.WriteByte(' ')
+						labels.WriteString(k)
+						labels.WriteByte(':')
+						labels.WriteString(labelValue)
+					}
+				}
+			}
+			if len(s.args.ShowPprofLabels) == 1 {
+				labelKey := s.args.ShowPprofLabels[0]
+				if labelKey == "*" {
+					keys := make([]string, 0, len(g.Labels()))
+					for k := range g.Labels() {
+						keys = append(keys, k)
+					}
+					sort.Strings(keys)
+					writeLabelsForKeys(keys)
+				} else {
+					labelValue := g.Labels()[labelKey]
+					if labelValue != "" {
+						labels.WriteByte(' ')
+						labels.WriteString(labelValue)
+					}
+				}
+			} else {
+				writeLabelsForKeys(s.args.ShowPprofLabels)
+			}
 			// File name and line number are communicated via `stackTrace`
 			// so no need to include them here.
 			loc := g.UserCurrent()
-			threads[i].Name = fmt.Sprintf("%s[Go %d] %s%s", selected, g.ID, fnName(&loc), thread)
+			threads[i].Name = fmt.Sprintf("%s[Go %d%s] %s%s", selected, g.ID, labels.String(), fnName(&loc), thread)
 			threads[i].Id = int(g.ID)
 		}
 	}
@@ -1836,7 +1877,7 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
 //   - "remote" -- attaches client to a debugger already attached to a process.
 //     Required args: none (host/port are used externally to connect)
 func (s *Session) onAttachRequest(request *dap.AttachRequest) {
-	var args = defaultAttachConfig // narrow copy for initializing non-zero default values
+	args := defaultAttachConfig // narrow copy for initializing non-zero default values
 	if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil {
 		s.sendShowUserErrorResponse(request.Request, FailedToAttach, "Failed to attach", fmt.Sprintf("invalid debug configuration - %v", err))
 		return
@@ -1922,21 +1963,21 @@ func (s *Session) onAttachRequest(request *dap.AttachRequest) {
 
 // onNextRequest handles 'next' request.
 // This is a mandatory request to support.
-func (s *Session) onNextRequest(request *dap.NextRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onNextRequest(request *dap.NextRequest, allowNextStateChange *syncflag) {
 	s.sendStepResponse(request.Arguments.ThreadId, &dap.NextResponse{Response: *newResponse(request.Request)})
 	s.stepUntilStopAndNotify(api.Next, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange)
 }
 
 // onStepInRequest handles 'stepIn' request
 // This is a mandatory request to support.
-func (s *Session) onStepInRequest(request *dap.StepInRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onStepInRequest(request *dap.StepInRequest, allowNextStateChange *syncflag) {
 	s.sendStepResponse(request.Arguments.ThreadId, &dap.StepInResponse{Response: *newResponse(request.Request)})
 	s.stepUntilStopAndNotify(api.Step, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange)
 }
 
 // onStepOutRequest handles 'stepOut' request
 // This is a mandatory request to support.
-func (s *Session) onStepOutRequest(request *dap.StepOutRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onStepOutRequest(request *dap.StepOutRequest, allowNextStateChange *syncflag) {
 	s.sendStepResponse(request.Arguments.ThreadId, &dap.StepOutResponse{Response: *newResponse(request.Request)})
 	s.stepUntilStopAndNotify(api.StepOut, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange)
 }
@@ -1993,8 +2034,8 @@ func (s *Session) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int6
 // a channel that will be closed to signal that an
 // asynchronous command has completed setup or was interrupted
 // due to an error, so the server is ready to receive new requests.
-func (s *Session) stepUntilStopAndNotify(command string, threadId int, granularity dap.SteppingGranularity, allowNextStateChange chan struct{}) {
-	defer closeIfOpen(allowNextStateChange)
+func (s *Session) stepUntilStopAndNotify(command string, threadId int, granularity dap.SteppingGranularity, allowNextStateChange *syncflag) {
+	defer allowNextStateChange.raise()
 	_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: int64(threadId)}, nil)
 	if err != nil {
 		s.config.log.Errorf("Error switching goroutines while stepping: %v", err)
@@ -2597,7 +2638,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
 
 	// Some of the types might be fully or partially not loaded based on LoadConfig.
 	// Those that are fully missing (e.g. due to hitting MaxVariableRecurse), can be reloaded in place.
-	var reloadVariable = func(v *proc.Variable, qualifiedNameOrExpr string) (value string) {
+	reloadVariable := func(v *proc.Variable, qualifiedNameOrExpr string) (value string) {
 		// We might be loading variables from the frame that's not topmost, so use
 		// frame-independent address-based expression, not fully-qualified name as per
 		// https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#looking-into-variables.
@@ -2949,7 +2990,7 @@ func (s *Session) onRestartRequest(request *dap.RestartRequest) {
 
 // onStepBackRequest handles 'stepBack' request.
 // This is an optional request enabled by capability 'supportsStepBackRequest'.
-func (s *Session) onStepBackRequest(request *dap.StepBackRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onStepBackRequest(request *dap.StepBackRequest, allowNextStateChange *syncflag) {
 	s.sendStepResponse(request.Arguments.ThreadId, &dap.StepBackResponse{Response: *newResponse(request.Request)})
 	s.stepUntilStopAndNotify(api.ReverseNext, request.Arguments.ThreadId, request.Arguments.Granularity, allowNextStateChange)
 }
@@ -2957,7 +2998,7 @@ func (s *Session) onStepBackRequest(request *dap.StepBackRequest, allowNextState
 // onReverseContinueRequest performs a rewind command call up to the previous
 // breakpoint or the start of the process
 // This is an optional request enabled by capability 'supportsStepBackRequest'.
-func (s *Session) onReverseContinueRequest(request *dap.ReverseContinueRequest, allowNextStateChange chan struct{}) {
+func (s *Session) onReverseContinueRequest(request *dap.ReverseContinueRequest, allowNextStateChange *syncflag) {
 	s.send(&dap.ReverseContinueResponse{
 		Response: *newResponse(request.Request),
 	})
@@ -3531,6 +3572,7 @@ func newEvent(event string) *dap.Event {
 
 const BetterBadAccessError = `invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation]
 Unable to propagate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)`
+
 const BetterNextWhileNextingError = `Unable to step while the previous step is interrupted by a breakpoint.
 Use 'Continue' to resume the original step command.`
 
@@ -3572,7 +3614,7 @@ func (s *Session) checkHaltRequested() bool {
 
 // resumeOnce is a helper function to resume the execution
 // of the target when the program is halted.
-func (s *Session) resumeOnce(command string, allowNextStateChange chan struct{}) (bool, *api.DebuggerState, error) {
+func (s *Session) resumeOnce(command string, allowNextStateChange *syncflag) (bool, *api.DebuggerState, error) {
 	// No other goroutines should be able to try to resume
 	// or halt execution while this goroutine is resuming
 	// execution, so we do not miss those events.
@@ -3581,7 +3623,7 @@ func (s *Session) resumeOnce(command string, allowNextStateChange chan struct{})
 	s.changeStateMu.Lock()
 	go func() {
 		defer s.changeStateMu.Unlock()
-		defer closeIfOpen(allowNextStateChange)
+		defer allowNextStateChange.raise()
 		<-asyncSetupDone
 	}()
 
@@ -3602,7 +3644,7 @@ func (s *Session) resumeOnce(command string, allowNextStateChange chan struct{})
 // a channel that will be closed to signal that an
 // asynchronous command has completed setup or was interrupted
 // due to an error, so the server is ready to receive new requests.
-func (s *Session) runUntilStopAndNotify(command string, allowNextStateChange chan struct{}) {
+func (s *Session) runUntilStopAndNotify(command string, allowNextStateChange *syncflag) {
 	state, err := s.runUntilStop(command, allowNextStateChange)
 
 	if s.conn.isClosed() {
@@ -3714,7 +3756,7 @@ func (s *Session) runUntilStopAndNotify(command string, allowNextStateChange cha
 	}
 }
 
-func (s *Session) runUntilStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
+func (s *Session) runUntilStop(command string, allowNextStateChange *syncflag) (*api.DebuggerState, error) {
 	// Clear any manual stop requests that came in before we started running.
 	s.setHaltRequested(false)
 
@@ -3731,11 +3773,11 @@ func (s *Session) runUntilStop(command string, allowNextStateChange chan struct{
 }
 
 // Make this a var, so it can be stubbed in testing.
-var resumeOnceAndCheckStop = func(s *Session, command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
+var resumeOnceAndCheckStop = func(s *Session, command string, allowNextStateChange *syncflag) (*api.DebuggerState, error) {
 	return s.resumeOnceAndCheckStop(command, allowNextStateChange)
 }
 
-func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
+func (s *Session) resumeOnceAndCheckStop(command string, allowNextStateChange *syncflag) (*api.DebuggerState, error) {
 	resumed, state, err := s.resumeOnce(command, allowNextStateChange)
 	// We should not try to process the log points if the program was not
 	// resumed or there was an error.
@@ -3920,3 +3962,32 @@ func parseLogPoint(msg string) (bool, *logMessage, error) {
 		args:   args,
 	}, nil
 }
+
+type syncflag struct {
+	mu   sync.Mutex
+	cond *sync.Cond
+	flag bool
+}
+
+func newSyncflag() *syncflag {
+	r := &syncflag{
+		flag: false,
+	}
+	r.cond = sync.NewCond(&r.mu)
+	return r
+}
+
+func (s *syncflag) wait() {
+	s.mu.Lock()
+	for !s.flag {
+		s.cond.Wait()
+	}
+	s.mu.Unlock()
+}
+
+func (s *syncflag) raise() {
+	s.mu.Lock()
+	s.flag = true
+	s.mu.Unlock()
+	s.cond.Broadcast()
+}
diff --git a/service/dap/types.go b/service/dap/types.go
index 812dd1a4..1ab685f9 100644
--- a/service/dap/types.go
+++ b/service/dap/types.go
@@ -190,6 +190,12 @@ type LaunchAttachCommonConfig struct {
 	// https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md#goroutines
 	GoroutineFilters string `json:"goroutineFilters,omitempty"`
 
+	// Array of string values indicating the keys of pprof labels to show as a
+	// goroutine name in the threads view. If the array has one element, only
+	// that label's value will be shown; otherwise, each of the labels will be
+	// shown as "key:value". To show all labels, specify the single element "*".
+	ShowPprofLabels []string `json:"showPprofLabels,omitempty"`
+
 	// An array of mappings from a local path (client) to the remote path (debugger).
 	// This setting is useful when working in a file system with symbolic links,
 	// running remote debugging, or debugging an executable compiled externally.


Test patch:
diff --git a/service/dap/config_test.go b/service/dap/config_test.go
index a65b6ca4..6701804a 100644
--- a/service/dap/config_test.go
+++ b/service/dap/config_test.go
@@ -18,14 +18,14 @@ func TestListConfig(t *testing.T) {
 			args: args{
 				args: &launchAttachArgs{},
 			},
-			want: formatConfig(0, false, false, "", false, [][2]string{}),
+			want: formatConfig(0, false, false, "", []string{}, false, [][2]string{}),
 		},
 		{
 			name: "default values",
 			args: args{
 				args: &defaultArgs,
 			},
-			want: formatConfig(50, false, false, "", false, [][2]string{}),
+			want: formatConfig(50, false, false, "", []string{}, false, [][2]string{}),
 		},
 		{
 			name: "custom values",
@@ -33,11 +33,13 @@ func TestListConfig(t *testing.T) {
 				args: &launchAttachArgs{
 					StackTraceDepth:              35,
 					ShowGlobalVariables:          true,
+					GoroutineFilters:             "SomeFilter",
+					ShowPprofLabels:              []string{"SomeLabel"},
 					substitutePathClientToServer: [][2]string{{"hello", "world"}},
 					substitutePathServerToClient: [][2]string{{"world", "hello"}},
 				},
 			},
-			want: formatConfig(35, true, false, "", false, [][2]string{{"hello", "world"}}),
+			want: formatConfig(35, true, false, "SomeFilter", []string{"SomeLabel"}, false, [][2]string{{"hello", "world"}}),
 		},
 	}
 	for _, tt := range tests {
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index b727bbfd..3a5200dd 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -31,12 +31,16 @@ import (
 	"github.com/google/go-dap"
 )
 
-const stopOnEntry bool = true
-const hasChildren bool = true
-const noChildren bool = false
+const (
+	stopOnEntry bool = true
+	hasChildren bool = true
+	noChildren  bool = false
+)
 
-const localsScope = 1000
-const globalsScope = 1001
+const (
+	localsScope  = 1000
+	globalsScope = 1001
+)
 
 var testBackend string
 
@@ -288,7 +292,8 @@ func TestSessionStop(t *testing.T) {
 			}
 			session := NewSession(conn, &Config{
 				Config:        &service.Config{DisconnectChan: make(chan struct{})},
-				StopTriggered: make(chan struct{})}, nil)
+				StopTriggered: make(chan struct{}),
+			}, nil)
 			serveDAPCodecDone := make(chan struct{})
 			go func() {
 				session.ServeDAPCodec()
@@ -812,7 +817,8 @@ func TestPreSetBreakpoint(t *testing.T) {
 //	wantFrames - number of frames returned (length of StackTraceResponse.Body.StackFrames array).
 //	wantTotalFrames - total number of stack frames available (StackTraceResponse.Body.TotalFrames).
 func checkStackFramesExact(t *testing.T, got *dap.StackTraceResponse,
-	wantStartName string, wantStartLine interface{}, wantStartID, wantFrames, wantTotalFrames int) {
+	wantStartName string, wantStartLine interface{}, wantStartID, wantFrames, wantTotalFrames int,
+) {
 	t.Helper()
 	checkStackFramesNamed("", t, got, wantStartName, wantStartLine, wantStartID, wantFrames, wantTotalFrames, true)
 }
@@ -883,7 +889,8 @@ func TestFilterGoroutines(t *testing.T) {
 				client.LaunchRequestWithArgs(map[string]interface{}{
 					"mode":        "exec",
 					"program":     fixture.Path,
-					"stopOnEntry": !stopOnEntry})
+					"stopOnEntry": !stopOnEntry,
+				})
 			},
 			// Set breakpoints
 			fixture.Source, []int{30},
@@ -920,17 +927,19 @@ func TestFilterGoroutines(t *testing.T) {
 				},
 				disconnect: false,
 			}})
-
 	})
 }
 
 func checkStackFramesHasMore(t *testing.T, got *dap.StackTraceResponse,
-	wantStartName string, wantStartLine, wantStartID, wantFrames, wantTotalFrames int) {
+	wantStartName string, wantStartLine, wantStartID, wantFrames, wantTotalFrames int,
+) {
 	t.Helper()
 	checkStackFramesNamed("", t, got, wantStartName, wantStartLine, wantStartID, wantFrames, wantTotalFrames, false)
 }
+
 func checkStackFramesNamed(testName string, t *testing.T, got *dap.StackTraceResponse,
-	wantStartName string, wantStartLine interface{}, wantStartID, wantFrames, wantTotalFrames int, totalExact bool) {
+	wantStartName string, wantStartLine interface{}, wantStartID, wantFrames, wantTotalFrames int, totalExact bool,
+) {
 	t.Helper()
 	if totalExact && got.Body.TotalFrames != wantTotalFrames {
 		t.Errorf("%s\ngot  %#v\nwant TotalFrames=%d", testName, got.Body.TotalFrames, wantTotalFrames)
@@ -1202,7 +1211,6 @@ func TestStackTraceRequest(t *testing.T) {
 					client.StackTraceRequest(1, 0, 0)
 					stResp = client.ExpectStackTraceResponse(t)
 					checkStackFramesExact(t, stResp, "main.main", 18, startHandle, 3, 3)
-
 				},
 				disconnect: false,
 			}})
@@ -1382,7 +1390,6 @@ func TestSelectedThreadsRequest(t *testing.T) {
 					oe := client.ExpectOutputEvent(t)
 					if !strings.HasPrefix(oe.Body.Output, "Too many goroutines") {
 						t.Errorf("got %#v, expected Output=\"Too many goroutines...\"\n", oe)
-
 					}
 					tr := client.ExpectThreadsResponse(t)
 
@@ -1403,10 +1410,71 @@ func TestSelectedThreadsRequest(t *testing.T) {
 				},
 				disconnect: true,
 			}})
-
 	})
 }
 
+func TestGoroutineLabels(t *testing.T) {
+	tests := []struct {
+		showPprofLabelsConfig   []string
+		expectedPrefixWithLabel string
+	}{
+		{[]string{}, "* [Go 1]"},
+		{[]string{"k1"}, "* [Go 1 v1]"},
+		{[]string{"k2"}, "* [Go 1 v2]"},
+		{[]string{"k2", "k1"}, "* [Go 1 k2:v2 k1:v1]"}, // When passing keys explicitly, we show them in the given order
+		{[]string{"unknown"}, "* [Go 1]"},
+		{[]string{"unknown", "k1"}, "* [Go 1 k1:v1]"},
+		{[]string{"*"}, "* [Go 1 k1:v1 k2:v2]"}, // Special case for showing all labels; labels are shown sorted by key
+	}
+	for _, tc := range tests {
+		runTest(t, "goroutineLabels", func(client *daptest.Client, fixture protest.Fixture) {
+			runDebugSessionWithBPs(t, client, "launch",
+				// Launch
+				func() {
+					client.LaunchRequestWithArgs(map[string]interface{}{
+						"mode":                 "exec",
+						"program":              fixture.Path,
+						"hideSystemGoroutines": true,
+						"showPprofLabels":      tc.showPprofLabelsConfig,
+						"stopOnEntry":          !stopOnEntry,
+					})
+				},
+				// Breakpoints are set within the program
+				"", []int{},
+				[]onBreakpoint{{
+					execute: func() {
+						client.ThreadsRequest()
+						tr := client.ExpectThreadsResponse(t)
+						if len(tr.Body.Threads) != 1 {
+							t.Errorf("got %d threads, expected 1\n", len(tr.Body.Threads))
+						}
+						// The first breakpoint is before the call to pprof.Do; no labels yet:
+						expectedPrefix := "* [Go 1]"
+						if !strings.HasPrefix(tr.Body.Threads[0].Name, expectedPrefix) {
+							t.Errorf("got %s, expected %s\n", tr.Body.Threads[0].Name, expectedPrefix)
+						}
+
+						client.ContinueRequest(1)
+						client.ExpectContinueResponse(t)
+						client.ExpectStoppedEvent(t)
+						checkStop(t, client, 1, "main.f", 21)
+						client.ThreadsRequest()
+						tr = client.ExpectThreadsResponse(t)
+						if len(tr.Body.Threads) != 1 {
+							t.Errorf("got %d threads, expected 1\n", len(tr.Body.Threads))
+						}
+						// The second breakpoint is inside pprof.Do, so there are labels:
+						if !strings.HasPrefix(tr.Body.Threads[0].Name, tc.expectedPrefixWithLabel) {
+							t.Errorf("got %s, expected %s\n", tr.Body.Threads[0].Name, tc.expectedPrefixWithLabel)
+						}
+					},
+					disconnect: true,
+				}},
+			)
+		})
+	}
+}
+
 func TestHideSystemGoroutinesRequest(t *testing.T) {
 	tests := []struct{ hideSystemGoroutines bool }{
 		{hideSystemGoroutines: true},
@@ -2288,7 +2356,7 @@ func TestVariablesLoading(t *testing.T) {
 					client.StackTraceRequest(1, 0, 0)
 					client.ExpectStackTraceResponse(t)
 
-					var loadvars = func(frame int) {
+					loadvars := func(frame int) {
 						client.ScopesRequest(frame)
 						scopes := client.ExpectScopesResponse(t)
 						localsRef := 0
@@ -3311,7 +3379,6 @@ func TestSetFunctionBreakpoints(t *testing.T) {
 						t.Errorf("got %#v, want Reason=\"function breakpoint\", ThreadId=1", se)
 					}
 					checkStop(t, client, 1, "main.anotherFunction", 26)
-
 				},
 				disconnect: true,
 			}})
@@ -3464,7 +3531,7 @@ func TestHaltPreventsAutoResume(t *testing.T) {
 						// Send a halt request when trying to resume the program after being
 						// interrupted. This should allow the log message to be processed,
 						// but keep the process from continuing beyond the line.
-						resumeOnceAndCheckStop = func(s *Session, command string, allowNextStateChange chan struct{}) (*api.DebuggerState, error) {
+						resumeOnceAndCheckStop = func(s *Session, command string, allowNextStateChange *syncflag) (*api.DebuggerState, error) {
 							// This should trigger after the log message is sent, but before
 							// execution is resumed.
 							if command == api.DirectionCongruentContinue {
@@ -3613,7 +3680,6 @@ func TestSetBreakpointWhileRunning(t *testing.T) {
 						t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", se)
 					}
 					checkStop(t, client, 1, "main.sayhi", 9)
-
 				},
 				disconnect: true,
 			}})
@@ -3668,7 +3734,6 @@ func TestSetFunctionBreakpointWhileRunning(t *testing.T) {
 						t.Errorf("\ngot  %#v\nwant Reason='breakpoint' AllThreadsStopped=true ThreadId=1", se)
 					}
 					checkStop(t, client, 1, "main.main", 16)
-
 				},
 				disconnect: true,
 			}})
@@ -3805,7 +3870,6 @@ func substitutePathTestHelper(t *testing.T, fixture protest.Fixture, client *dap
 		// Set breakpoints
 		filepath.Join(nonexistentDir, "loopprog.go"), []int{8},
 		[]onBreakpoint{{
-
 			execute: func() {
 				checkStop(t, client, 1, "main.loop", 8)
 			},
@@ -4063,15 +4127,16 @@ func TestEvaluateRequest(t *testing.T) {
 	})
 }
 
-func formatConfig(depth int, showGlobals, showRegisters bool, goroutineFilters string, hideSystemGoroutines bool, substitutePath [][2]string) string {
+func formatConfig(depth int, showGlobals, showRegisters bool, goroutineFilters string, showPprofLabels []string, hideSystemGoroutines bool, substitutePath [][2]string) string {
 	formatStr := `stackTraceDepth	%d
 showGlobalVariables	%v
 showRegisters	%v
 goroutineFilters	%q
+showPprofLabels	%v
 hideSystemGoroutines	%v
 substitutePath	%v
 `
-	return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, goroutineFilters, hideSystemGoroutines, substitutePath)
+	return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, goroutineFilters, showPprofLabels, hideSystemGoroutines, substitutePath)
 }
 
 func TestEvaluateCommandRequest(t *testing.T) {
@@ -4108,7 +4173,7 @@ Type 'dlv help' followed by a command for full documentation.
 
 					client.EvaluateRequest("dlv config -list", 1000, "repl")
 					got = client.ExpectEvaluateResponse(t)
-					checkEval(t, got, formatConfig(50, false, false, "", false, [][2]string{}), noChildren)
+					checkEval(t, got, formatConfig(50, false, false, "", []string{}, false, [][2]string{}), noChildren)
 
 					// Read and modify showGlobalVariables.
 					client.EvaluateRequest("dlv config -list showGlobalVariables", 1000, "repl")
@@ -4129,7 +4194,7 @@ Type 'dlv help' followed by a command for full documentation.
 
 					client.EvaluateRequest("dlv config -list", 1000, "repl")
 					got = client.ExpectEvaluateResponse(t)
-					checkEval(t, got, formatConfig(50, true, false, "", false, [][2]string{}), noChildren)
+					checkEval(t, got, formatConfig(50, true, false, "", []string{}, false, [][2]string{}), noChildren)
 
 					client.ScopesRequest(1000)
 					scopes = client.ExpectScopesResponse(t)
@@ -4719,7 +4784,7 @@ func testNextParkedHelper(t *testing.T, client *daptest.Client, fixture protest.
 	client.SetBreakpointsRequest(fixture.Source, []int{8})
 	client.ExpectSetBreakpointsResponse(t)
 
-	var parkedGoid = -1
+	parkedGoid := -1
 	for parkedGoid < 0 {
 		client.ContinueRequest(1)
 		client.ExpectContinueResponse(t)
@@ -4849,6 +4914,7 @@ func TestStepOutPreservesGoroutine(t *testing.T) {
 			}})
 	})
 }
+
 func checkStopOnNextWhileNextingError(t *testing.T, client *daptest.Client, threadID int) {
 	t.Helper()
 	oe := client.ExpectOutputEvent(t)
@@ -5046,7 +5112,6 @@ func TestPanicBreakpointOnContinue(t *testing.T) {
 						} else if frame.Source != nil && frame.Source.PresentationHint != "" {
 							t.Errorf("\ngot Body.StackFrames[%d]=%#v\nwant Source.PresentationHint=\"\"", i, frame)
 						}
-
 					}
 				},
 				disconnect: true,
@@ -5131,7 +5196,6 @@ func TestFatalThrowBreakpoint(t *testing.T) {
 					if eInfo.Body.ExceptionId != "fatal error" || !strings.HasPrefix(eInfo.Body.Description, errorPrefix) {
 						t.Errorf("\ngot  %#v\nwant ExceptionId=\"runtime error\" Text=%s", eInfo, errorPrefix)
 					}
-
 				},
 				disconnect: true,
 			}})
@@ -5307,7 +5371,8 @@ func TestLaunchDebugRequest(t *testing.T) {
 		// only relying on the source to be built in response to LaunchRequest.
 		runDebugSession(t, client, "launch", func() {
 			client.LaunchRequestWithArgs(map[string]interface{}{
-				"mode": "debug", "program": fixture.Source, "output": tmpBin})
+				"mode": "debug", "program": fixture.Source, "output": tmpBin,
+			})
 		})
 	})
 	// Wait for the test to finish to capture all stderr
@@ -5343,20 +5408,23 @@ func TestLaunchRequestDefaults(t *testing.T) {
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
 		runDebugSession(t, client, "launch", func() {
 			client.LaunchRequestWithArgs(map[string]interface{}{
-				"mode": "" /*"debug" by default*/, "program": fixture.Source, "output": "__mybin"})
+				"mode": "" /*"debug" by default*/, "program": fixture.Source, "output": "__mybin",
+			})
 		})
 	})
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
 		runDebugSession(t, client, "launch", func() {
 			client.LaunchRequestWithArgs(map[string]interface{}{
-				/*"mode":"debug" by default*/ "program": fixture.Source, "output": "__mybin"})
+				/*"mode":"debug" by default*/ "program": fixture.Source, "output": "__mybin",
+			})
 		})
 	})
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
 		runDebugSession(t, client, "launch", func() {
 			// Use the temporary output binary.
 			client.LaunchRequestWithArgs(map[string]interface{}{
-				"mode": "debug", "program": fixture.Source})
+				"mode": "debug", "program": fixture.Source,
+			})
 		})
 	})
 }
@@ -5373,7 +5441,8 @@ func TestLaunchRequestOutputPath(t *testing.T) {
 			func() {
 				client.LaunchRequestWithArgs(map[string]interface{}{
 					"mode": "debug", "program": fixture.Source, "output": inrel,
-					"cwd": filepath.Dir(wd)})
+					"cwd": filepath.Dir(wd),
+				})
 			},
 			// Set breakpoints
 			fixture.Source, []int{12},
@@ -5421,7 +5490,8 @@ func TestNoDebug_GoodExitStatus(t *testing.T) {
 	runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
 		runNoDebugSession(t, client, func() {
 			client.LaunchRequestWithArgs(map[string]interface{}{
-				"noDebug": true, "mode": "debug", "program": fixture.Source, "output": "__mybin"})
+				"noDebug": true, "mode": "debug", "program": fixture.Source, "output": "__mybin",
+			})
 		}, 0)
 	})
 }
@@ -5430,7 +5500,8 @@ func TestNoDebug_BadExitStatus(t *testing.T) {
 	runTest(t, "issue1101", func(client *daptest.Client, fixture protest.Fixture) {
 		runNoDebugSession(t, client, func() {
 			client.LaunchRequestWithArgs(map[string]interface{}{
-				"noDebug": true, "mode": "exec", "program": fixture.Path})
+				"noDebug": true, "mode": "exec", "program": fixture.Path,
+			})
 		}, 2)
 	})
 }
@@ -5458,11 +5529,12 @@ func TestNoDebug_AcceptNoRequestsButDisconnect(t *testing.T) {
 		client.InitializeRequest()
 		client.ExpectInitializeResponseAndCapabilities(t)
 		client.LaunchRequestWithArgs(map[string]interface{}{
-			"noDebug": true, "mode": "exec", "program": fixture.Path})
+			"noDebug": true, "mode": "exec", "program": fixture.Path,
+		})
 		client.ExpectLaunchResponse(t)
 
 		// Anything other than disconnect should get rejected
-		var ExpectNoDebugError = func(cmd string) {
+		ExpectNoDebugError := func(cmd string) {
 			er := client.ExpectErrorResponse(t)
 			if !checkErrorMessageFormat(er.Body.Error, fmt.Sprintf("noDebug mode: unable to process '%s' request", cmd)) {
 				t.Errorf("\ngot %#v\nwant 'noDebug mode: unable to process '%s' request'", er, cmd)
@@ -5531,7 +5603,8 @@ func TestLaunchRequestWithRelativeBuildPath(t *testing.T) {
 	dlvwd, _ := os.Getwd()
 	runDebugSession(t, client, "launch", func() {
 		client.LaunchRequestWithArgs(map[string]interface{}{
-			"mode": "debug", "program": program, "cwd": filepath.Dir(dlvwd)})
+			"mode": "debug", "program": program, "cwd": filepath.Dir(dlvwd),
+		})
 	})
 	<-serverStopped
 }
@@ -5550,7 +5623,8 @@ func TestLaunchRequestWithRelativeExecPath(t *testing.T) {
 		defer os.Remove(symlink)
 		runDebugSession(t, client, "launch", func() {
 			client.LaunchRequestWithArgs(map[string]interface{}{
-				"mode": "exec", "program": symlink})
+				"mode": "exec", "program": symlink,
+			})
 		})
 	})
 }
@@ -5640,7 +5714,8 @@ func TestLaunchTestRequest(t *testing.T) {
 								checkVarExact(t, locals, i, "wd", "wd", fmt.Sprintf("%q", tc.wantWD), "string", noChildren)
 							}
 						}
-					}}})
+					},
+				}})
 
 			<-serverStopped
 		})
@@ -5656,7 +5731,8 @@ func TestLaunchRequestWithArgs(t *testing.T) {
 		runDebugSession(t, client, "launch", func() {
 			client.LaunchRequestWithArgs(map[string]interface{}{
 				"mode": "exec", "program": fixture.Path,
-				"args": []string{"test", "pass flag"}})
+				"args": []string{"test", "pass flag"},
+			})
 		})
 	})
 }
@@ -5672,7 +5748,8 @@ func TestLaunchRequestWithBuildFlags(t *testing.T) {
 			// only relying on the source to be built in response to LaunchRequest.
 			client.LaunchRequestWithArgs(map[string]interface{}{
 				"mode": "debug", "program": fixture.Source, "output": "__mybin",
-				"buildFlags": "-ldflags '-X main.Hello=World'"})
+				"buildFlags": "-ldflags '-X main.Hello=World'",
+			})
 		})
 	})
 }
@@ -5684,7 +5761,8 @@ func TestLaunchRequestWithBuildFlags2(t *testing.T) {
 			// only relying on the source to be built in response to LaunchRequest.
 			client.LaunchRequestWithArgs(map[string]interface{}{
 				"mode": "debug", "program": fixture.Source, "output": "__mybin",
-				"buildFlags": []string{"-ldflags", "-X main.Hello=World"}})
+				"buildFlags": []string{"-ldflags", "-X main.Hello=World"},
+			})
 		})
 	})
 }
@@ -5747,7 +5825,6 @@ func TestLaunchRequestWithEnv(t *testing.T) {
 			wantY:     true,
 		},
 	} {
-
 		t.Run(tc.name, func(t *testing.T) {
 			for k, v := range tc.initEnv {
 				if v != nil {
@@ -5794,7 +5871,8 @@ func TestAttachRequest(t *testing.T) {
 			// Attach
 			func() {
 				client.AttachRequest(map[string]interface{}{
-					/*"mode": "local" by default*/ "processId": cmd.Process.Pid, "stopOnEntry": false})
+					/*"mode": "local" by default*/ "processId": cmd.Process.Pid, "stopOnEntry": false,
+				})
 				client.ExpectCapabilitiesEventSupportTerminateDebuggee(t)
 			},
 			// Set breakpoints
@@ -7126,7 +7204,7 @@ func TestDisassemble(t *testing.T) {
 					}
 
 					// Request invalid instructions.
-					var checkInvalidInstruction = func(instructions []dap.DisassembledInstruction, count int, address uint64) {
+					checkInvalidInstruction := func(instructions []dap.DisassembledInstruction, count int, address uint64) {
 						if len(instructions) != count {
 							t.Errorf("\ngot %#v\nwant len(instructions) = %d", dr, count)
 						}