diff --git a/src/libkperf/perf_evt.go b/src/libkperf/perf_evt.go new file mode 100644 index 0000000000000000000000000000000000000000..f3d3b1f179af8932f269a7ab03c4b8866cc4bc5f --- /dev/null +++ b/src/libkperf/perf_evt.go @@ -0,0 +1,470 @@ +// 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 goexperiment.libkperf + +package libkperf + +import ( + "unsafe" + "fmt" + "os" + "strconv" + "syscall" + "encoding/binary" +) + +type pmuEvent struct { + fd int + name string + buf []byte + meta *PerfEventMmapPage + data []byte +} + +type pmuList struct { + pd int + events map[int]*pmuEvent // key = fd + enableBrbe bool +} + +var pdCounter int +var pdMap = make(map[int]*pmuList) + +func initMmap(fd int, enableBrbe bool) ([]byte, error){ + //init mmap ring buffer + samplePages := DEFAULT_SAMPLE_PAGES + if enableBrbe { + samplePages = BRBE_SAMPLE_PAGES + } + // 1 meta data page and N data pages + mmapSize := (1 + samplePages) * os.Getpagesize() + buf, err := syscall.Mmap(fd, 0, mmapSize, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + return nil, err + } + return buf, nil +} + +func perfEventOpen(attr *PerfEventAttr, pid, cpu, groupFd, flags int) (fd int, err error) { + r0, _, e1 := syscall.Syscall6( + syscall.SYS_PERF_EVENT_OPEN, + uintptr(unsafe.Pointer(attr)), + uintptr(pid), + uintptr(cpu), + uintptr(groupFd), + uintptr(flags), + 0, + ) + fd = int(r0) + if e1 != 0 { + return -1, errnoErr(e1) + } + return fd, nil +} + +func beginSampling(evtName string, period uint64, enableBrbe bool, pid int) (int, error) { + var attr PerfEventAttr + evtConfig, evtType := getCoreEvent(evtName) + if evtConfig == -1 || evtType == -1 { + return -1, fmt.Errorf("Can not get the config and type of event: %s", evtName) + } + attr.Config = uint64(evtConfig) + attr.Type = uint32(evtType) + attr.Size = uint32(unsafe.Sizeof(attr)) + attr.Sample = period + attr.Bits = PERF_ATTR_FREQ | PERF_ATTR_EXCLUDE_KERNEL + attr.Sample_type = getSampleType(enableBrbe) + if enableBrbe { + attr.Branch_sample_type = PERF_SAMPLE_BRANCH_ANY | PERF_SAMPLE_BRANCH_USER + } + + fd, err := perfEventOpen(&attr, pid, -1, -1, 0) + if fd < 0 { + return -1, err + } + + return fd, nil +} + +type PmuList struct { + pmuMap map[uint]struct{} + maxPd int +} + +func NewPd() int { + for i := 0; i < pdCounter; i++ { + if _, exists := pdMap[i]; !exists { + return i + } + } + + if pdCounter == int(^uint(0)>>1) { + return -1 + } + + pd := pdCounter + pdCounter++ + return pd +} + +func FreePd(pd int) { + if pl, ok := pdMap[pd]; ok { + for _, e := range pl.events { + syscall.Close(e.fd) + } + delete(pdMap, pd) + } +} + +func CheckAttr(pmuAttr *PmuAttr) error { + var cpuType = GetCpuType() + if cpuType == UndefinedType { + return fmt.Errorf("Unsupported architecture for PMU collection") + } + + if len(pmuAttr.EvtList) == 1 && pmuAttr.EvtList[0] == "spe" { + return fmt.Errorf("SPE collection not support now") + } + + if pmuAttr.Period == 0 { + return fmt.Errorf("The period param must be greater than 0") + } + + if pmuAttr.EnableBrbe == true { + if len(pmuAttr.EvtList) != 1 || pmuAttr.EvtList[0] != "cycles" { + return fmt.Errorf("In BRBE collection, only cycles can be set for event") + } + if cpuType != HipF && cpuType != HipG { + return fmt.Errorf("The current cpu type does not support BRBE collection") + } + } + + coreEvents := queryCoreEvent() + for _, evtName := range pmuAttr.EvtList { + if !containsCoreEvent(coreEvents, evtName) { + return fmt.Errorf("Unknown perf core event: %s", evtName) + } + } + return nil +} + +func getTids(pid int) ([]int, error) { + taskDir := fmt.Sprintf("/proc/%d/task", pid) + entries, err := os.ReadDir(taskDir) + if err != nil { + return nil, err + } + tids := []int{} + for _, e := range entries { + if tid, err := strconv.Atoi(e.Name()); err == nil { + tids = append(tids, tid) + } + } + return tids, nil +} + +// PmuOpen initializes a new PMU context with the given attributes. +// It opens and mmaps all requested events, returning a pd handle or an error if setup fails. +func PmuOpen(pmuAttr *PmuAttr) (int, error) { + if err := CheckAttr(pmuAttr); err != nil { + return -1, err + } + + pd := NewPd() + if pd == -1 { + return -1, fmt.Errorf("no available pd") + } + + pl := &pmuList{ + pd: pd, + events: make(map[int]*pmuEvent), + enableBrbe: pmuAttr.EnableBrbe, + } + + tids, err := getTids(os.Getpid()) + if err != nil { + FreePd(pd) + return -1, fmt.Errorf("get tids failed: %v", err) + } + + for _, tid := range tids { + for _, evtName := range pmuAttr.EvtList { + fd, err := beginSampling(evtName, pmuAttr.Period, pmuAttr.EnableBrbe, tid) + if fd == -1 { + FreePd(pd) + return -1, fmt.Errorf("Sampling failed for tid %d: %v", tid, err) + } + + buf, err := initMmap(fd, pmuAttr.EnableBrbe) + if err != nil { + syscall.Close(fd) + FreePd(pd) + return -1, fmt.Errorf("mmap event %s failed: %v", evtName, err) + } + meta := (*PerfEventMmapPage)(unsafe.Pointer(&buf[0])) + pl.events[fd] = &pmuEvent{ + fd: fd, + buf: buf, + meta: meta, + data: buf[meta.Data_offset : meta.Data_offset+meta.Data_size], + name: fmt.Sprintf("%s-tid-%d", evtName, tid), + } + } + } + + pdMap[pd] = pl + return pd, nil +} + +func getSampleType(enableBrbe bool) uint64 { + base := PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_TIME | + PERF_SAMPLE_ID | PERF_SAMPLE_PERIOD | PERF_SAMPLE_CALLCHAIN + + if enableBrbe { + base |= PERF_SAMPLE_BRANCH_STACK + } + + return uint64(base) +} + +func isValidIp(ip uint64) bool { + v := int64(ip) + return v != PERF_CONTEXT_HV && + v != PERF_CONTEXT_KERNEL && + v != PERF_CONTEXT_USER && + v != PERF_CONTEXT_GUEST && + v != PERF_CONTEXT_GUEST_KERNEL && + v != PERF_CONTEXT_GUEST_USER && + v != PERF_CONTEXT_MAX +} + +func parseSample(b []byte, sampleType uint64, name string) Sample { + var s Sample + s.EvtName = name + off := 0 + + // PERF_SAMPLE_IP + if sampleType&PERF_SAMPLE_IP != 0 { + if len(b)-off >= 8 { + s.IP = binary.LittleEndian.Uint64(b[off:]) + off += 8 + } + } + + // PERF_SAMPLE_TID + if sampleType&PERF_SAMPLE_TID != 0 { + if len(b)-off >= 8 { + s.PID = binary.LittleEndian.Uint32(b[off:]) + s.TID = binary.LittleEndian.Uint32(b[off+4:]) + off += 8 + } + } + + // PERF_SAMPLE_TIME + if sampleType&PERF_SAMPLE_TIME != 0 { + if len(b)-off >= 8 { + s.Time = binary.LittleEndian.Uint64(b[off:]) + off += 8 + } + } + + // PERF_SAMPLE_ID + if sampleType&PERF_SAMPLE_ID != 0 { + if len(b)-off >= 8 { + s.ID = binary.LittleEndian.Uint64(b[off:]) + off += 8 + } + } + + // PERF_SAMPLE_PERIOD + if sampleType&PERF_SAMPLE_PERIOD != 0 { + if len(b)-off >= 8 { + s.Period = binary.LittleEndian.Uint64(b[off:]) + off += 8 + } + } + + // PERF_SAMPLE_CALLCHAIN + if sampleType&PERF_SAMPLE_CALLCHAIN != 0 { + if len(b)-off >= 8 { + nr := binary.LittleEndian.Uint64(b[off:]) + off += 8 + var lastIP uint64 + for i := uint64(0); i < nr; i++ { + if len(b)-off < 8 { + break + } + ip := binary.LittleEndian.Uint64(b[off:]) + if isValidIp(ip) { + lastIP = ip + } + off += 8 + } + if lastIP != 0 { + s.Callchain = []uint64{lastIP} + } + } + } + + // PERF_SAMPLE_BRANCH_STACK + if sampleType&PERF_SAMPLE_BRANCH_STACK != 0 { + if len(b)-off >= 8 { + nr := binary.LittleEndian.Uint64(b[off:]) + off += 8 + for i := 0; i < int(nr); i++ { + if len(b)-off < 24 { + break + } + var e PerfBranchEntry + e.FromAddr = binary.LittleEndian.Uint64(b[off:]) + e.ToAddr = binary.LittleEndian.Uint64(b[off+8:]) + e.Flags = binary.LittleEndian.Uint64(b[off+16:]) + off += 24 + s.Branches = append(s.Branches, e) + } + } + } + + return s +} + +func readSamples(evt *pmuEvent, sampleType uint64, handler func(Sample)) error { + head := evt.meta.Data_head + tail := evt.meta.Data_tail + + for tail < head { + offset := tail % uint64(len(evt.data)) + if offset+uint64(unsafe.Sizeof(PerfEventHeader{})) > uint64(len(evt.data)) { + break // incomplete header + } + + header := (*PerfEventHeader)(unsafe.Pointer(&evt.data[offset])) + headerSize := uint64(unsafe.Sizeof(*header)) + + if header.Size < uint16(headerSize) || offset+uint64(header.Size) > uint64(len(evt.data)) { + break // invalid header size + } + + switch header.Type { + case PERF_RECORD_SAMPLE: + payload := evt.data[offset+headerSize : offset+uint64(header.Size)] + s := parseSample(payload, sampleType, evt.name) + handler(s) + default: + // do nothing + } + + tail += uint64(header.Size) + } + + evt.meta.Data_tail = head + return nil +} + +// PmuRead collects samples from all events under a pd. +// Returns all gathered samples or an error if reading fails. +func PmuRead(pd int, enableBrbe bool) ([]Sample, error) { + pl, ok := pdMap[pd] + if !ok { + return nil, fmt.Errorf("PmuRead failed. Invalid pd %d", pd) + } + + sampleType := getSampleType(pl.enableBrbe) + + var samples []Sample + for _, evt := range pl.events { + err := readSamples(evt, sampleType, func(s Sample) { + samples = append(samples, s) + }) + if err != nil { + return samples, err + } + } + + return samples, nil +} + +// PmuReset resets all events under a given pd. +// Returns an error if the ioctl reset call fails. +func PmuReset(pd int) error { + pl, ok := pdMap[pd] + if !ok { + return fmt.Errorf("PmuReset failed. Invalid pd: %d", pd) + } + + for _, evt := range pl.events { + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, + uintptr(evt.fd), + PERF_EVENT_IOC_RESET, + 0) + if errno != 0 { + return errno + } + } + return nil +} + +// PmuEnable enables all events under a given pd. +// Returns an error if the ioctl enable call fails. +func PmuEnable(pd int) error { + pl, ok := pdMap[pd] + if !ok { + return fmt.Errorf("PmuEnable failed. Invalid pd: %d", pd) + } + + for _, evt := range pl.events { + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, + uintptr(evt.fd), + PERF_EVENT_IOC_ENABLE, + 0) + if errno != 0 { + return errno + } + } + return nil +} + +// PmuDisable disables all events under a given pd. +// Returns an error if the ioctl disable call fails. +func PmuDisable(pd int) error { + pl, ok := pdMap[pd] + if !ok { + return fmt.Errorf("PmuDisable failed. Invalid pd: %d", pd) + } + + for _, evt := range pl.events { + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, + uintptr(evt.fd), + PERF_EVENT_IOC_DISABLE, + 0) + if errno != 0 { + return errno + } + } + return nil +} + +// PmuClose close all perf event file descriptors and unmap their buffers for a given pd +// Returns an error if closing the descriptor fails. +func PmuClose(pd int) error { + pl, ok := pdMap[pd] + if !ok { + return fmt.Errorf("PmuClose failed. Invalid pd: %d", pd) + } + + for fd, evt := range pl.events { + if evt.buf != nil { + if err := syscall.Munmap(evt.buf); err != nil { + return fmt.Errorf("munmap failed for %s(fd=%d): %v", evt.name, fd, err) + } + } + + if err := syscall.Close(fd); err != nil { + return fmt.Errorf("close fd failed for %s(fd=%d): %v", evt.name, fd, err) + } + } + + delete(pdMap, pd) + return nil +} diff --git a/src/libkperf/perf_prof.go b/src/libkperf/perf_prof.go new file mode 100644 index 0000000000000000000000000000000000000000..f1a959e3dc115437d0f102f54ddacada98dd502b --- /dev/null +++ b/src/libkperf/perf_prof.go @@ -0,0 +1,149 @@ +// 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 goexperiment.libkperf + +package libkperf + +import ( + "io" + "fmt" + "time" + "internal/profile" +) + +func getLocation(ip uint64, p *profile.Profile, + locMap map[uint64]*profile.Location, + fnMap map[uint64]*profile.Function, + nextLocID, nextFnID *uint64) *profile.Location { + + if loc, ok := locMap[ip]; ok { + return loc + } + + fn, ok := fnMap[ip] + if !ok { + fn = &profile.Function{ + ID: *nextFnID, + Name: fmt.Sprintf("0x%x", ip), + SystemName: fmt.Sprintf("0x%x", ip), + } + fnMap[ip] = fn + p.Function = append(p.Function, fn) + *nextFnID++ + } + + loc := &profile.Location{ + ID: *nextLocID, + Address: ip, + Line: []profile.Line{ + {Function: fn, Line: 1}, + }, + } + locMap[ip] = loc + p.Location = append(p.Location, loc) + *nextLocID++ + return loc +} + +func buildLocations(callchain []uint64, p *profile.Profile, + locMap map[uint64]*profile.Location, + fnMap map[uint64]*profile.Function, + nextLocID, nextFnID *uint64) []*profile.Location { + + var locs []*profile.Location + for _, ip := range callchain { + locs = append(locs, getLocation(ip, p, locMap, fnMap, nextLocID, nextFnID)) + } + return locs +} + +func btoi(b bool) int { + if b { + return 1 + } + return 0 +} + +func (e *PerfBranchEntry) decode() (cycles uint16, misPred, predicted bool) { + misPred = (e.Flags & 0x1) != 0 + predicted = (e.Flags & 0x2) != 0 + cycles = uint16((e.Flags >> 4) & 0xFFFF) + return +} + +func buildSample(s Sample, locs []*profile.Location) *profile.Sample { + sample := &profile.Sample{ + Location: locs, + Value: []int64{0}, + Label: make(map[string][]string), + } + + sample.Value[0] = int64(s.Period) + sample.Label["event"] = []string{s.EvtName} + for i, br := range s.Branches { + cycles, misPred, predicted := br.decode() + branchInfo := fmt.Sprintf("%x %x %d %d %d", + br.FromAddr, br.ToAddr, cycles, btoi(misPred), btoi(predicted)) + key := fmt.Sprintf("%d", i) + sample.Label[key] = []string{branchInfo} + } + + return sample +} + +func convertSamples(samples []Sample, duration int64) *profile.Profile { + if len(samples) == 0 { + return nil + } + + p := &profile.Profile{ + SampleType: []*profile.ValueType{ + {Type: "pmu", Unit: "count"}, + }, + PeriodType: &profile.ValueType{Type: "samples", Unit: "count"}, + Period: 1, + DefaultSampleType: "pmu", + } + + locMap := map[uint64]*profile.Location{} + fnMap := map[uint64]*profile.Function{} + nextLocID := uint64(1) + nextFnID := uint64(1) + + for _, s := range samples { + locs := buildLocations(s.Callchain, p, locMap, fnMap, &nextLocID, &nextFnID) + sample := buildSample(s, locs) + p.Sample = append(p.Sample, sample) + } + + return p +} + +// WriteProf converts PMU profiling samples and writes them to the given writer. +// Returns an error if conversion or writing fails. +func WriteProf(w io.Writer, allSamples []Sample, duration int64) error { + prof := convertSamples(allSamples, duration) + if prof == nil { + // create a empty PMU profile file + empty := &profile.Profile{ + SampleType: []*profile.ValueType{ + {Type: "pmu", Unit: "count"}, + }, + TimeNanos: time.Now().UnixNano(), + DurationNanos: duration, + } + if err := empty.Write(w); err != nil { + return fmt.Errorf("write empty profile failed: %w", err) + } + fmt.Println("PMU prof file written (empty profile)") + return nil + } + + if err := prof.Write(w); err != nil { + return fmt.Errorf("write profile failed: %w", err) + } + + fmt.Println("PMU prof file written successfully") + return nil +} diff --git a/src/runtime/pprof/pmu.go b/src/runtime/pprof/pmu.go new file mode 100644 index 0000000000000000000000000000000000000000..05aa2fe33e23e2a924e4a8cd09b366c97331d0ad --- /dev/null +++ b/src/runtime/pprof/pmu.go @@ -0,0 +1,160 @@ +// 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 goexperiment.libkperf + +// To use the PMU collection, add equivalent profiling support to a standalone +// program, add code like the following to your main function: +// +// var pmuprofile = flag.String("pmuprofile", "", "write pmu profile to `file`") +// +// func main() { +// flag.Parse() +// if *pmuprofile != "" { +// f, err := os.Create(*pmuprofile) +// if err != nil { +// log.Fatal("could not create PMU profile: ", err) +// } +// defer f.Close() // error handling omitted for example +// attr := new(PmuEvtAttr) +// attr.EvtList = []string{"cycles"} +// attr.EnableBrbe = true +// attr.Duration = 10 // unit: s +// attr.Period = 1000 +// _, err := pprof.StartPMUProfile(f, attr) +// if err != nil { +// log.Fatal("PMU collection failed: ", err) +// } +// } +// +// // ... rest of the program ... +// } +// +// The standard HTTP interface to collect PMU data like: +// +// import _ "net/http/pprof" +// ... +// go func() { +// log.Println(http.ListenAndServe("localhost:6060", nil)) +// }() +// +// To collect a 10-second execution to get the BRBE prof file +// curl -o brbe.prof "http://localhost:6060/debug/pprof/profile?event=cycles&period=1000&brbe=true&seconds=10" + +package pprof + +import ( + "fmt" + "libkperf" + "time" + "io" +) + +type CpuType int + +const ( + UndefinedCpu CpuType = iota // 0 + CpuHipA // 1 + CpuHipB // 2 + CpuHipC // 3 + CpuHipF // 4 + CpuHipE // 5 + CpuHipG // 6 +) + +// GetCpuType +func GetCpuType() CpuType { + switch libkperf.GetCpuType() { + case libkperf.HipA: + return CpuHipA + case libkperf.HipB: + return CpuHipB + case libkperf.HipC: + return CpuHipC + case libkperf.HipF: + return CpuHipF + case libkperf.HipE: + return CpuHipE + case libkperf.HipG: + return CpuHipG + default: + return UndefinedCpu + } +} + +// PMU collection params for go pprof +type PmuEvtAttr struct { + Period uint64 + Duration int64 + EvtList []string + EnableBrbe bool + EvtFilter string +} + +// StartPMUProfile enables PMU collection for profiling of the current +// process. After collection, the profile will be written to w. +// StartPMUProfile returns an error when input parameter is invalid +// or PMU syscall execution failed. +// +// Please check whether your environment supports the collections of BRBE events. +func StartPMUProfile(w io.Writer, attr *PmuEvtAttr) (<-chan error, error) { + if attr == nil { + return nil, fmt.Errorf("Error: PmuEvtAttr is empty") + } + pmuAttr := &libkperf.PmuAttr{ + Period: attr.Period, + Duration: attr.Duration, + EvtList: attr.EvtList, + EnableBrbe: attr.EnableBrbe, + EvtFilter: attr.EvtFilter, + } + + pd, err := libkperf.PmuOpen(pmuAttr) + if err != nil { + return nil, err + } + + errCh := make(chan error, 1) + + go func() { + defer libkperf.PmuClose(pd) + + if err := libkperf.PmuReset(pd); err != nil { + errCh <- fmt.Errorf("Reset PMU failed: %w", err) + return + } + + if err := libkperf.PmuEnable(pd); err != nil { + errCh <- fmt.Errorf("Enable PMU failed: %w", err) + return + } + + allSamples := []libkperf.Sample{} + for i := int64(0); i < pmuAttr.Duration; i++ { + time.Sleep(time.Second) + + samples, err := libkperf.PmuRead(pd, pmuAttr.EnableBrbe) + if err != nil { + errCh <- fmt.Errorf("Read PMU failed: %w", err) + return + } + if len(samples) > 0 { + allSamples = append(allSamples, samples...) + } + } + + if err := libkperf.PmuDisable(pd); err != nil { + errCh <- fmt.Errorf("Disable PMU failed: %w", err) + return + } + + if err := libkperf.WriteProf(w, allSamples, pmuAttr.Duration); err != nil { + errCh <- fmt.Errorf("Write prof file failed: %w", err) + return + } + + errCh <- nil + }() + + return errCh, nil +} diff --git a/src/runtime/pprof/pmu_test.go b/src/runtime/pprof/pmu_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f106d1ff9326134e326ef429fee3c6940a0d2e63 --- /dev/null +++ b/src/runtime/pprof/pmu_test.go @@ -0,0 +1,93 @@ +// 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 goexperiment.libkperf + +package pprof + +import ( + "testing" + "os" + "math/rand" + "time" +) + +func generateMatrix(rows, cols int) [][]int { + matrix := make([][]int, rows) + for i := range matrix { + matrix[i] = make([]int, cols) + for j := range matrix[i] { + matrix[i][j] = rand.Intn(10) + } + } + return matrix +} + +func multiply(a, b [][]int) [][]int { + rowsA := len(a) + colsA := len(a[0]) + colsB := len(b[0]) + + result := make([][]int, rowsA) + for i := range result { + result[i] = make([]int, colsB) + for j := 0; j < colsB; j++ { + sum := 0 + for k := 0; k < colsA; k++ { + sum += a[i][k] * b[k][j] + } + result[i][j] = sum + } + } + return result +} + +func run() error { + rand.Seed(time.Now().UnixNano()) + + rows, cols := 300, 300 + a := generateMatrix(rows, cols) + b := generateMatrix(cols, rows) + _ = multiply(a, b) + + return nil +} + +// TestStartPMUProfile runs the complete PMU collcetion process +func TestStartPMUProfile(t *testing.T) { + f, err := os.CreateTemp("", "pmu-*.pprof") + if err != nil { + t.Fatalf("TempFile create failed: %v", err) + } + defer os.Remove(f.Name()) + defer f.Close() + + var brbeCollection = false + var cpuType = GetCpuType() + if cpuType == CpuHipF || cpuType == CpuHipG { + brbeCollection = true + } + pmuAttr := new(PmuEvtAttr) + pmuAttr.EvtList = []string{"cycles"} + pmuAttr.EnableBrbe = brbeCollection + pmuAttr.Duration = 10 + pmuAttr.Period = 4000 + _, err = StartPMUProfile(f, pmuAttr) + if err != nil { + t.Fatalf("StartPMUProfile failed: %v", err) + } + + for i := 0; i < 100; i++ { + if err := run(); err != nil { + t.Fatalf("run failed at iteration %d: %v", i, err) + } + } + + info, err := os.Stat(f.Name()) + if err != nil { + t.Fatalf("stat failed: %v", err) + } + if info.Size() == 0 { + t.Fatalf("profile file is empty") + } +}