ЗАМЕР
bench/goprof/cores.go
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/interview/golang/profiling
- Как запустить
go run bench/goprof/internals.go go run bench/goprof/cores.go go run bench/goprof/interfere.go go run bench/goprof/exposure.go go run bench/goprof/practice.go
Запись прогона
Замеры для урока «Профилирование»
| Файл | Что делает |
|---|---|
internals.go |
одна нагрузка и шесть профилей: CPU и его частота, flat против cum, куча (alloc_space против inuse_space), выключенные по умолчанию профили блокировок и мьютексов, профиль горутин на утечке, цена профиля CPU |
cores.go |
профиль CPU на одной и на двух занятых горутинах: почему «Total samples» бывает больше 100 % |
interfere.go |
цена режимов «записывать всё» у профилей блокировок, мьютексов и памяти и то, как профиль памяти искажает профиль CPU |
exposure.go |
что отдаёт /debug/pprof на DefaultServeMux без проверки доступа |
practice.go |
источник ответов двух практических задач урока |
go run bench/goprof/internals.go
go run bench/goprof/cores.go
go run bench/goprof/interfere.go
go run bench/goprof/exposure.go
go run bench/goprof/practice.go
Программы сами вызывают go tool pprof, поэтому нужен установленный Go.
Записи прогонов — в runs/, go1.24.7 linux/amd64, GOMAXPROCS=2.
Что здесь важно прочитать правильно
Таблицы — вывод go tool pprof -top. Строки File:, Build ID:,
Time: и Type: из них убраны: они меняются от запуска к запуску.
Наносекунды и миллисекунды не переносятся. Переносится форма: у какой
функции flat почти ноль, а cum — почти всё; какая функция есть в alloc_space
и отсутствует в inuse_space.
Надбавка профиля CPU — в пределах шума. Три серии: −4,0 %, +0,1 %, −0,5 %. Знак не устойчив, устойчиво, что она мала.
Что получилось
Частота профиля CPU — 100 сэмплов в секунду. Она зашита в исходнике
runtime/pprof (const hz = 100). Замер: 203 сэмпла за 2,11 с работы одной
горутины, 96 в секунду.
flat и cum. У main.handle flat 0, cum 1800 мс — 86,96 %: сам он ничего не
делает, под ним hashAll.
Куча. main.churn выделил 596,83 МБ и в inuse_space отсутствует
вовсе; main.retain выделил 16,19 МБ и держит их все.
Блокировки и мьютексы. Без настройки — 0 записей в обоих профилях при
настоящей конкуренции; после SetBlockProfileRate(1) и
SetMutexProfileFraction(1) — 3 и 2.
Утечка горутин. Сто горутин, ждущих канала, в который никто не пишет:
горутин до: 1, после: 101, все сто — под одним стеком.
Процент в «Total samples» — доля процессорного времени от стенного. Одна
занятая горутина: 93,39 %; две на двух ядрах: 181,35 % (cores.go). Одна
горутина занимает не больше одного ядра; несколько — могут набрать больше
процессорного времени, чем прошло по часам.
«Записывать всё» не бесплатно. На крайних нагрузках (interfere.go, три
серии): SetBlockProfileRate(1) — ×1,64–1,90, SetMutexProfileFraction(1) —
×1,35–1,61, MemProfileRate = 1 — ×50–62. При MemProfileRate = 1
runtime.mProf_Malloc занимает 76,64 % профиля CPU той же нагрузки: один
профиль искажает другой. Нагрузки намеренно крайние; переносится направление,
а не кратность.
/debug/pprof на рабочем порту отдаёт всё без проверки доступа.
exposure.go: сервис, зарегистрировавший только /hello на DefaultServeMux,
отдал аргументы запуска (с выдуманным паролем), стеки горутин с путями к
исходникам и профиль кучи — все со статусом 200.
Скрипт
85 строк//go:build ignore
// Профиль CPU на одной и на двух занятых горутинах: почему «Total samples»
// бывает больше 100 %.
//
// ЗАЧЕМ ОТДЕЛЬНАЯ ПРОГРАММА. В internals.go профиль CPU снят с одной
// горутины, и процент в строке «Total samples» там меньше 100. Урок говорит,
// что он бывает и больше, — это утверждение должно опираться на прогон, а
// перезапуск internals.go поменял бы числа, которые урок уже цитирует.
//
// ЧТО ДЕЛАЕТ. Дважды по 2 секунды стенных часов крутит вычисления — в одной
// горутине и в двух — под профилем CPU и печатает строку заголовка из
// `go tool pprof -top`.
//
// ЗАПУСК:
//
// go run bench/goprof/cores.go
package main
import (
"crypto/sha256"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/pprof"
"strings"
"sync"
"time"
)
var sink [32]byte
func burn(d time.Duration) {
b := make([]byte, 64)
for end := time.Now().Add(d); time.Now().Before(end); {
for i := 0; i < 1000; i++ {
sink = sha256.Sum256(b)
b = sink[:]
}
}
}
func profile(goroutines int) string {
path := filepath.Join(os.TempDir(), fmt.Sprintf("goprof-cores-%d.out", goroutines))
f, err := os.Create(path)
if err != nil {
panic(err)
}
if err := pprof.StartCPUProfile(f); err != nil {
panic(err)
}
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() { defer wg.Done(); burn(2 * time.Second) }()
}
wg.Wait()
pprof.StopCPUProfile()
f.Close()
out, err := exec.Command("go", "tool", "pprof", "-top", "-nodecount=1", path).CombinedOutput()
if err != nil {
panic(fmt.Sprintf("%v\n%s", err, out))
}
for _, line := range strings.Split(string(out), "\n") {
if strings.HasPrefix(line, "Duration:") {
return line
}
}
return "нет строки Duration"
}
func main() {
fmt.Printf("%s %s/%s, GOMAXPROCS=%d | профиль CPU на одной и двух горутинах\n\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
fmt.Println(" одна занятая горутина: ", profile(1))
fmt.Println(" две занятые горутины: ", profile(2))
fmt.Println()
fmt.Println(" Одна горутина занимает не больше одного ядра, и процессорного")
fmt.Println(" времени набирается не больше, чем прошло по часам. Две горутины")
fmt.Println(" на двух ядрах набирают почти вдвое больше: процент выше 100.")
}