pprof是否能采集cgo的cpu使用情况?
下面以 go1.23.3 linux/amd64为例,通过例子来进行实验:
测试采样期间不返回的c函数:
pprof_cgo.go
Go
package main
/*
#include <stdio.h>
#include <unistd.h>
#include <time.h>
int fib(int n) {
if (n == 0 || n == 1) {
return n;
}
return fib(n-2) + fib(n-1);
}
void cFib() {
time_t now_time;
time(&now_time);
printf("cFib start: %s\n", ctime(&now_time));
int total = 0;
int i;
for (i=0; i<1000; i++) {
total = total + fib(i);
}
time(&now_time);
printf("cFib end: %s, total: %d\n", ctime(&now_time), total);
}
void cSleep() {
sleep(0.1);
}
*/
import "C"
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"time"
)
func main() {
go func() {
_ = http.ListenAndServe("0.0.0.0:9091", nil)
}()
var doFunc func()
if len(os.Args) > 1 {
switch os.Args[1] {
case "cfib":
doFunc = doCFib
case "gofib":
doFunc = doGoFib
case "csleep":
doFunc = doCSleep
case "gosleep":
doFunc = doGoSleep
}
}
if doFunc == nil {
fmt.Printf("usage: cfib/gofib/csleep/gosleep\n")
return
}
fmt.Printf("do func: %+v\n", os.Args[1])
//并发数
var ConChan = make(chan bool, 2)
for {
ConChan <- true
go func() {
defer func() {
<-ConChan
}()
doFunc()
}()
}
}
func doCFib() {
C.cFib()
}
func doCSleep() {
C.cSleep()
}
func doGoSleep() {
time.Sleep(100 * time.Millisecond)
}
func doGoFib() {
fmt.Printf("doGoFib start: %s\n", time.Now().Format(time.DateTime))
total := 0
for i := 0; i < 1000; i++ {
total = total + fib(i)
}
fmt.Printf("doGoFib end: %s, total: %d\n", time.Now().Format(time.DateTime), total)
}
func fib(n int) int {
if n == 0 || n == 1 {
return n
}
return fib(n-2) + fib(n-1)
}
pprof采集代码:
bash
go tool pprof -http=192.168.36.5:9000 http://127.0.0.1:9091/debug/pprof/profile
c函数的cpu密集计算测试:
执行分支:./pprof_cgo cfib
pprof进行cpu采样后如图:
pprof并未采集到c函数fib的耗时。
而通过perf采集到的火焰图如下:
perf可以采集到c函数fib的耗时。
go函数的cpu密集计算测试:
执行分支:./pprof_cgo gofib
pprof进行cpu采样后如图:
pprof可以采集到go函数fib的耗时。
通过perf采集到的火焰图如下:
perf可以采集到go函数fib的耗时。
对采样期间不返回的c函数的采样结论:
对于cgo中c函数长期运行且在采样期间不返回的函数,pprof无法采集cgo中c函数的cpu开销。
通过gdb进一步分析:
bash
gdb -p `ps aux | grep pprof_cgo | grep -v 'grep' | awk '{print $2}'`
添加断点(以 go1.23.3 linux/amd64为例):
bash
b runtime.sigprof
b runtime.sigprofNonGo
b runtime.sigprofNonGoPC
启动采集程序以触发sigprof
bash
go tool pprof -http=192.168.36.5:9000 http://127.0.0.1:9091/debug/pprof/profile
观察gdb输出:
在第5416行退出了sigprof,查看此行代码:
我们添加新的断点,以观察是否有跳过5416行的情况:
bash
delete
b runtime/proc.go:5425
如图:
新断点并没有出现命中的情况,也就是cgo中的c函数执行线程的mp.profilehz == 0都是成立的。
查看profilehz的定义:
这个属性赋值的入口分别是:
1、runtime.cgocallbackg1,cgo调用返回时通过runtime.sched.profilehz变量设置。
2、runtime.setThreadCPUProfiler,开始cpu采样时设置。
3、runtime.execute,在m上调度新协程时通过runtime.sched.profilehz变量设置。
4、runtime.exitsyscall0,退出系统调用时通过runtime.sched.profilehz变量设置。
5、runtime.park_m,协程排队被唤醒重新运行时通过runtime.sched.profilehz变量设置。
6、runtime.schedule,调度新协程时通过runtime.sched.profilehz变量设置。
而runtime.sched.profilehz变量是通过runtime.SetCPUProfileRate设置的,而runtime.SetCPUProfileRate是通过runtime.StartCPUProfile调用的,也就是开始采样时设置的。
gdb分析结论:
所以猜测,cgo中的c函数需要在采样过程中返回go代码,才能被采样到,在c函数返回go前的耗时不能被采样,如果一旦返回go一次之后,同线程的c函数调用即可正常采样。
c函数耗时最长不能超过采样总时长,如30s,否则一直不能返回,c函数也就不能被采样。
c函数最佳耗时按golang默认的采样频率100HZ算,cgo的c函数耗时最好不超过10ms,这样即使每次cgo调用新起线程也不会损失采样。
测试采样期间频繁返回的c函数:
修改我们的测试代码:
1、将fib函数循环次数由1000调为30。
2、去掉print。
3、同时运行cFib和goFib,便于对比耗时。
pprof_cgo.go
Go
package main
/*
#include <stdio.h>
#include <unistd.h>
#include <time.h>
int fib(int n) {
if (n == 0 || n == 1) {
return n;
}
return fib(n-2) + fib(n-1);
}
void cFib() {
time_t now_time;
time(&now_time);
//printf("cFib start: %s\n", ctime(&now_time));
int total = 0;
int i;
for (i=0; i<30; i++) {
total = total + fib(i);
}
time(&now_time);
//printf("cFib end: %s, total: %d\n", ctime(&now_time), total);
}
void cSleep() {
sleep(0.1);
}
*/
import "C"
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"time"
)
func main() {
go func() {
_ = http.ListenAndServe("0.0.0.0:9091", nil)
}()
var doFunc func()
if len(os.Args) > 1 {
switch os.Args[1] {
case "cfib":
doFunc = doCFib
case "gofib":
doFunc = doGoFib
case "csleep":
doFunc = doCSleep
case "gosleep":
doFunc = doGoSleep
}
}
if doFunc == nil {
fmt.Printf("usage: cfib/gofib/csleep/gosleep\n")
return
}
fmt.Printf("do func: %+v\n", os.Args[1])
//并发数
var ConChan = make(chan bool, 2)
for {
ConChan <- true
go func() {
defer func() {
<-ConChan
}()
doFunc()
}()
ConChan <- true
go func() {
defer func() {
<-ConChan
}()
doGoFib()
}()
}
}
func doCFib() {
C.cFib()
}
func doCSleep() {
C.cSleep()
}
func doGoSleep() {
time.Sleep(100 * time.Millisecond)
}
func doGoFib() {
//fmt.Printf("doGoFib start: %s\n", time.Now().Format(time.DateTime))
total := 0
for i := 0; i < 30; i++ {
total = total + fib(i)
}
//fmt.Printf("doGoFib end: %s, total: %d\n", time.Now().Format(time.DateTime), total)
}
func fib(n int) int {
if n == 0 || n == 1 {
return n
}
return fib(n-2) + fib(n-1)
}
执行分支:./pprof_cgo cfib
pprof进行cpu采样后如图:
runtime.cgocall耗时占34.2%,main.doGoFib耗时占比22.9%
通过perf采集到的火焰图如下:
runtime.cgocall耗时占33.93%,main.doGoFib耗时占比21.48%
两种工具采样的结果接近。
再次通过gdb分析:
这次mp.profilehz不再为空,且跳到了最后的else分支。
对采样期间可以频繁返回的c函数的采样结论:
可以对采样期间频繁返回的c函数的耗时进行采样,但耗时都算在了runtime.cgocall中。
--end--