| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| //go:build !plan9 && !windows |
| |
| package main |
| |
| // Regression test for https://go.dev/issue/72870. Go code called from C should |
| // never be reported as external code. |
| |
| /* |
| #include <pthread.h> |
| |
| void go_callback1(); |
| void go_callback2(); |
| |
| static void *callback_pprof_thread(void *arg) { |
| go_callback1(); |
| return 0; |
| } |
| |
| static void c_callback(void) { |
| go_callback2(); |
| } |
| |
| static void start_callback_pprof_thread() { |
| pthread_t th; |
| pthread_attr_t attr; |
| pthread_attr_init(&attr); |
| pthread_create(&th, &attr, callback_pprof_thread, 0); |
| // Don't join, caller will watch pprof. |
| } |
| */ |
| import "C" |
| |
| import ( |
| "bytes" |
| "fmt" |
| "internal/profile" |
| "os" |
| "runtime/pprof" |
| "time" |
| ) |
| |
| func init() { |
| register("CgoCallbackPprof", CgoCallbackPprof) |
| } |
| |
| func CgoCallbackPprof() { |
| C.start_callback_pprof_thread() |
| |
| var buf bytes.Buffer |
| if err := pprof.StartCPUProfile(&buf); err != nil { |
| fmt.Printf("Error starting CPU profile: %v\n", err) |
| os.Exit(1) |
| } |
| time.Sleep(1 * time.Second) |
| pprof.StopCPUProfile() |
| |
| p, err := profile.Parse(&buf) |
| if err != nil { |
| fmt.Printf("Error parsing profile: %v\n", err) |
| os.Exit(1) |
| } |
| |
| foundCallee := false |
| for _, s := range p.Sample { |
| funcs := flattenFrames(s) |
| if len(funcs) == 0 { |
| continue |
| } |
| |
| leaf := funcs[0] |
| if leaf.Name != "main.go_callback1_callee" { |
| continue |
| } |
| foundCallee = true |
| |
| if len(funcs) < 2 { |
| fmt.Printf("Profile: %s\n", p) |
| frames := make([]string, len(funcs)) |
| for i := range funcs { |
| frames[i] = funcs[i].Name |
| } |
| fmt.Printf("FAIL: main.go_callback1_callee sample missing caller in frames %v\n", frames) |
| os.Exit(1) |
| } |
| |
| if funcs[1].Name != "main.go_callback1" { |
| // In https://go.dev/issue/72870, this will be runtime._ExternalCode. |
| fmt.Printf("Profile: %s\n", p) |
| frames := make([]string, len(funcs)) |
| for i := range funcs { |
| frames[i] = funcs[i].Name |
| } |
| fmt.Printf("FAIL: main.go_callback1_callee sample caller got %s want main.go_callback1 in frames %v\n", funcs[1].Name, frames) |
| os.Exit(1) |
| } |
| } |
| |
| if !foundCallee { |
| fmt.Printf("Missing main.go_callback1_callee sample in profile %s\n", p) |
| os.Exit(1) |
| } |
| |
| fmt.Printf("OK\n") |
| } |
| |
| // Return the frame functions in s, regardless of inlining. |
| func flattenFrames(s *profile.Sample) []*profile.Function { |
| ret := make([]*profile.Function, 0, len(s.Location)) |
| for _, loc := range s.Location { |
| for _, line := range loc.Line { |
| ret = append(ret, line.Function) |
| } |
| } |
| return ret |
| } |
| |
| //export go_callback1 |
| func go_callback1() { |
| // This is a separate function just to ensure we have another Go |
| // function as the caller in the profile. |
| go_callback1_callee() |
| } |
| |
| func go_callback1_callee() { |
| C.c_callback() |
| |
| // Spin for CPU samples. |
| for { |
| } |
| } |
| |
| //export go_callback2 |
| func go_callback2() { |
| } |