pprof是否能采集cgo的cpu使用情况?

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--

相关推荐
007php00710 分钟前
go语言zero框架下的日志记录的sdk实战案例
linux·开发语言·后端·python·nginx·golang·php
janbar2 小时前
内网穿透之http代理服务器
golang
ZJL-阿友3 小时前
go语言 爬虫 钉钉群机器人
爬虫·golang·钉钉·钉钉群机器人
只会写bug的靓仔5 小时前
无管理员权限 LCU auth-token、port 获取(全网首发 go)
开发语言·后端·golang
LuckyLay5 小时前
Golang学习笔记_12——结构体
笔记·学习·golang
天地人和20166 小时前
[go-zero] 子查询,使用到Squirrel包
开发语言·数据库·golang
007php0079 小时前
go语言zero框架下的操作记录sdk案例实战
java·linux·开发语言·后端·python·nginx·golang
界面开发小八哥19 小时前
智能高效的IDE GoLand v2024.3全新发布——支持最新Go语言
开发语言·后端·golang·go·开发工具
LuckyLay20 小时前
Golang中的go.mod和go.sum
开发语言·后端·golang