From c1a76f4070873971c7733299901d84afacdbfce6 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Sun, 16 Aug 2026 11:41:33 +0800 Subject: [PATCH 1/5] runtime: support native CPU profiling --- runtime/internal/lib/runtime/_wrap/profile.c | 249 ++++++++++++++++++ .../lib/runtime/cpuprof_read_stub_llgo.go | 15 ++ .../lib/runtime/cpuprof_sigprof_llgo.go | 120 +++++++++ .../internal/lib/runtime/cpuprof_stub_llgo.go | 5 + .../lib/runtime/pprof_linkname_llgo.go | 17 +- .../lib/runtime/pprof_runtime_stub_llgo.go | 2 - .../internal/lib/runtime/runtime_default.go | 2 +- test/std/runtime/pprof/pprof_test.go | 66 ++++- 8 files changed, 451 insertions(+), 25 deletions(-) create mode 100644 runtime/internal/lib/runtime/_wrap/profile.c create mode 100644 runtime/internal/lib/runtime/cpuprof_read_stub_llgo.go create mode 100644 runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go create mode 100644 runtime/internal/lib/runtime/cpuprof_stub_llgo.go diff --git a/runtime/internal/lib/runtime/_wrap/profile.c b/runtime/internal/lib/runtime/_wrap/profile.c new file mode 100644 index 0000000000..30667161f7 --- /dev/null +++ b/runtime/internal/lib/runtime/_wrap/profile.c @@ -0,0 +1,249 @@ +/* CPU profiling for native LLGo executables. + * + * SIGPROF interrupts arbitrary code, so the handler only snapshots register + * state and frame-pointer slots into a fixed ring. It does not allocate, + * acquire a blocking lock, or call into Go. Ordinary Go code drains the ring + * later and converts it to runtime/pprof's raw record stream. */ +#define _XOPEN_SOURCE 700 +#define _DARWIN_C_SOURCE 1 +#if defined(__linux__) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include + +#if defined(__APPLE__) || defined(__linux__) +#include +#endif + +#define LLGO_PROF_STACK 64 +#define LLGO_PROF_SAMPLES 2048 +#define LLGO_PROF_MAX_FP_STRIDE (1u << 20) + +struct llgo_prof_sample { + uint32_t n; + uintptr_t pc[LLGO_PROF_STACK]; +}; + +static struct llgo_prof_sample llgo_prof_ring[LLGO_PROF_SAMPLES]; +static unsigned int llgo_prof_read_index; +static unsigned int llgo_prof_write_index; +static volatile int llgo_prof_lock; +static volatile int llgo_prof_active; +static volatile uint64_t llgo_prof_lost; + +extern int llgo_mem_readable(void *p); + +static int llgo_prof_try_lock(void) +{ + return __atomic_exchange_n(&llgo_prof_lock, 1, __ATOMIC_ACQUIRE) == 0; +} + +static void llgo_prof_lock_wait(void) +{ + while (!llgo_prof_try_lock()) { + } +} + +static void llgo_prof_unlock(void) +{ + __atomic_store_n(&llgo_prof_lock, 0, __ATOMIC_RELEASE); +} + +static void llgo_prof_drop(void) +{ + __atomic_fetch_add(&llgo_prof_lost, 1, __ATOMIC_RELAXED); +} + +#if defined(__APPLE__) || defined(__linux__) +static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) +{ + uintptr_t pc = 0, fp = 0; + uintptr_t word = sizeof(uintptr_t); + unsigned int next; + struct llgo_prof_sample *sample; + ucontext_t *uc = (ucontext_t *)uctx; + int saved_errno = errno; + (void)sig; + (void)info; + (void)uc; + + if (!__atomic_load_n(&llgo_prof_active, __ATOMIC_ACQUIRE)) + return; +#if defined(__APPLE__) && defined(__aarch64__) + pc = (uintptr_t)uc->uc_mcontext->__ss.__pc; + fp = (uintptr_t)uc->uc_mcontext->__ss.__fp; +#elif defined(__APPLE__) && defined(__x86_64__) + pc = (uintptr_t)uc->uc_mcontext->__ss.__rip; + fp = (uintptr_t)uc->uc_mcontext->__ss.__rbp; +#elif defined(__linux__) && defined(__aarch64__) + pc = (uintptr_t)uc->uc_mcontext.pc; + fp = (uintptr_t)uc->uc_mcontext.regs[29]; +#elif defined(__linux__) && defined(__x86_64__) + pc = (uintptr_t)uc->uc_mcontext.gregs[16 /* REG_RIP */]; + fp = (uintptr_t)uc->uc_mcontext.gregs[10 /* REG_RBP */]; +#endif + if (pc == 0 || !llgo_prof_try_lock()) { + llgo_prof_drop(); + errno = saved_errno; + return; + } + if (!__atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED)) { + llgo_prof_unlock(); + errno = saved_errno; + return; + } + + next = llgo_prof_write_index + 1; + if (next == LLGO_PROF_SAMPLES) + next = 0; + if (next == llgo_prof_read_index) { + llgo_prof_drop(); + llgo_prof_unlock(); + errno = saved_errno; + return; + } + + sample = &llgo_prof_ring[llgo_prof_write_index]; + sample->n = 1; + /* runtime.CallersFrames subtracts one from every sampled PC. */ + sample->pc[0] = pc + 1; + while (fp != 0 && sample->n < LLGO_PROF_STACK) { + uintptr_t prev, ret; + if ((fp & (word - 1)) != 0 || + !llgo_mem_readable((void *)fp) || + !llgo_mem_readable((void *)(fp + word))) + break; + prev = *(uintptr_t *)fp; + ret = *(uintptr_t *)(fp + word); + if (ret < 4096) + break; + sample->pc[sample->n++] = ret; + if (prev <= fp || prev - fp > LLGO_PROF_MAX_FP_STRIDE || + (prev & (word - 1)) != 0) + break; + fp = prev; + } + llgo_prof_write_index = next; + llgo_prof_unlock(); + errno = saved_errno; +} +#endif + +/* Returns 1 on success, 0 while an old profile is still draining, and -1 + * when the OS rejects SIGPROF or ITIMER_PROF setup. */ +int llgo_cpu_profile_start(int hz) +{ +#if defined(__APPLE__) || defined(__linux__) + struct sigaction sa; + struct itimerval timer; + uint64_t usec; + int saved_errno = errno; + + if (hz <= 0) { + errno = saved_errno; + return -1; + } + llgo_prof_lock_wait(); + if (__atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED) || + llgo_prof_read_index != llgo_prof_write_index) { + llgo_prof_unlock(); + errno = saved_errno; + return 0; + } + /* Reinstall for every profile: user signal code may have changed the + * process disposition since the preceding profile stopped. */ + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = llgo_prof_signal; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO | SA_RESTART; + if (sigaction(SIGPROF, &sa, 0) != 0) { + llgo_prof_unlock(); + errno = saved_errno; + return -1; + } + llgo_prof_read_index = 0; + llgo_prof_write_index = 0; + __atomic_store_n(&llgo_prof_lost, 0, __ATOMIC_RELAXED); + __atomic_store_n(&llgo_prof_active, 1, __ATOMIC_RELEASE); + llgo_prof_unlock(); + + usec = 1000000u / (unsigned int)hz; + if (usec == 0) + usec = 1; + memset(&timer, 0, sizeof(timer)); + timer.it_interval.tv_sec = (time_t)(usec / 1000000u); + timer.it_interval.tv_usec = (suseconds_t)(usec % 1000000u); + timer.it_value = timer.it_interval; + if (setitimer(ITIMER_PROF, &timer, 0) != 0) { + __atomic_store_n(&llgo_prof_active, 0, __ATOMIC_RELEASE); + errno = saved_errno; + return -1; + } + errno = saved_errno; + return 1; +#else + (void)hz; + return -1; +#endif +} + +void llgo_cpu_profile_stop(void) +{ +#if defined(__APPLE__) || defined(__linux__) + struct itimerval timer; + int saved_errno = errno; + memset(&timer, 0, sizeof(timer)); + setitimer(ITIMER_PROF, &timer, 0); + __atomic_store_n(&llgo_prof_active, 0, __ATOMIC_RELEASE); + /* Wait for a handler that already owns the ring to finish. A handler + * interrupting this critical section only records a dropped sample. */ + llgo_prof_lock_wait(); + llgo_prof_unlock(); + errno = saved_errno; +#endif +} + +int llgo_cpu_profile_read(uintptr_t *pc, int cap) +{ + struct llgo_prof_sample *sample; + unsigned int i; + int n; + + if (pc == 0 || cap <= 0) + return 0; + llgo_prof_lock_wait(); + if (llgo_prof_read_index == llgo_prof_write_index) { + llgo_prof_unlock(); + return 0; + } + sample = &llgo_prof_ring[llgo_prof_read_index]; + n = (int)sample->n; + if (n > cap) + n = cap; + for (i = 0; i < (unsigned int)n; i++) + pc[i] = sample->pc[i]; + llgo_prof_read_index++; + if (llgo_prof_read_index == LLGO_PROF_SAMPLES) + llgo_prof_read_index = 0; + llgo_prof_unlock(); + return n; +} + +uint64_t llgo_cpu_profile_take_lost(void) +{ + return __atomic_exchange_n(&llgo_prof_lost, 0, __ATOMIC_RELAXED); +} + +int llgo_cpu_profile_empty(void) +{ + int empty; + llgo_prof_lock_wait(); + empty = llgo_prof_read_index == llgo_prof_write_index; + llgo_prof_unlock(); + return empty; +} diff --git a/runtime/internal/lib/runtime/cpuprof_read_stub_llgo.go b/runtime/internal/lib/runtime/cpuprof_read_stub_llgo.go new file mode 100644 index 0000000000..73b2d41d92 --- /dev/null +++ b/runtime/internal/lib/runtime/cpuprof_read_stub_llgo.go @@ -0,0 +1,15 @@ +//go:build (darwin || linux) && (baremetal || (!amd64 && !arm64)) + +package runtime + +import "unsafe" + +var ( + cpuProfilePeriodRecord = [3]uint64{3, 0, 100} + cpuProfilePeriodTags [1]unsafe.Pointer +) + +//go:linkname runtime_pprof_readProfile runtime/pprof.readProfile +func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool) { + return cpuProfilePeriodRecord[:], cpuProfilePeriodTags[:], true +} diff --git a/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go b/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go new file mode 100644 index 0000000000..4faf2f5343 --- /dev/null +++ b/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go @@ -0,0 +1,120 @@ +//go:build !baremetal && !wasm && (darwin || linux) && (amd64 || arm64) + +package runtime + +import ( + "unsafe" + + c "github.com/xgo-dev/llgo/runtime/internal/clite" + latomic "sync/atomic" +) + +const maxCPUProfileStack = 64 + +//go:linkname c_cpuProfileStart C.llgo_cpu_profile_start +func c_cpuProfileStart(hz int32) int32 + +//go:linkname c_cpuProfileStop C.llgo_cpu_profile_stop +func c_cpuProfileStop() + +//go:linkname c_cpuProfileRead C.llgo_cpu_profile_read +func c_cpuProfileRead(p unsafe.Pointer, n int32) int32 + +//go:linkname c_cpuProfileTakeLost C.llgo_cpu_profile_take_lost +func c_cpuProfileTakeLost() uint64 + +//go:linkname c_cpuProfileEmpty C.llgo_cpu_profile_empty +func c_cpuProfileEmpty() int32 + +var ( + cpuProfileRate int32 + cpuProfileOpen uint32 + cpuProfilePeriodPending uint32 + cpuProfilePeriodRecord = [3]uint64{3, 0, 100} + cpuProfilePeriodTags [1]unsafe.Pointer +) + +// SetCPUProfileRate starts or stops process CPU-time sampling. The signal +// handler and its ring buffer live in profile.c so the sampling path neither +// allocates Go memory nor enters the Go runtime. +func SetCPUProfileRate(hz int) { + if hz < 0 { + hz = 0 + } + if hz > 1000000 { + hz = 1000000 + } + if hz == 0 { + if latomic.LoadInt32(&cpuProfileRate) != 0 { + c_cpuProfileStop() + latomic.StoreInt32(&cpuProfileRate, 0) + } + return + } + if !latomic.CompareAndSwapUint32(&cpuProfileOpen, 0, 1) { + print("runtime: cannot set cpu profile rate until previous profile has finished.\n") + return + } + + switch c_cpuProfileStart(int32(hz)) { + case 1: + cpuProfilePeriodRecord[2] = uint64(hz) + latomic.StoreUint32(&cpuProfilePeriodPending, 1) + latomic.StoreInt32(&cpuProfileRate, int32(hz)) + case 0: + // Samples from a stopped profile have not been drained yet. + latomic.StoreUint32(&cpuProfileOpen, 0) + print("runtime: cannot set cpu profile rate until previous profile has finished.\n") + default: + // Preserve runtime/pprof's stream contract even if the OS rejected + // SIGPROF setup: the caller gets a valid empty profile, not a + // malformed stream or a blocked writer. + cpuProfilePeriodRecord[2] = uint64(hz) + latomic.StoreUint32(&cpuProfilePeriodPending, 1) + } +} + +//go:linkname runtime_pprof_readProfile runtime/pprof.readProfile +func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool) { + if latomic.CompareAndSwapUint32(&cpuProfilePeriodPending, 1, 0) { + return cpuProfilePeriodRecord[:], cpuProfilePeriodTags[:], false + } + + var pcs [maxCPUProfileStack]uintptr + for { + lost := c_cpuProfileTakeLost() + n := int(c_cpuProfileRead(unsafe.Pointer(&pcs[0]), maxCPUProfileStack)) + if lost != 0 || n != 0 { + // runtime/pprof waits 100 ms between reads on Darwin. Drain a + // chunk, rather than one sample, so a 100 Hz producer cannot + // outrun the writer. + data = make([]uint64, 0, 1024) + tags = make([]unsafe.Pointer, 0, 16) + if lost != 0 { + data = append(data, 4, 0, 0, lost) + tags = append(tags, nil) + } + for records := 0; n != 0 && records < 256; records++ { + data = append(data, uint64(3+n), 0, 1) + for i := 0; i < n; i++ { + data = append(data, uint64(pcs[i])) + } + tags = append(tags, nil) + if records == 255 { + break + } + n = int(c_cpuProfileRead(unsafe.Pointer(&pcs[0]), maxCPUProfileStack)) + } + return data, tags, false + } + if latomic.LoadInt32(&cpuProfileRate) == 0 && c_cpuProfileEmpty() != 0 { + latomic.StoreUint32(&cpuProfileOpen, 0) + return nil, nil, true + } + + // Linux's profile writer expects readProfile to block. Keep the wait + // out of the signal path and poll at the 100 Hz profiler's period; + // Darwin already sleeps in pprof. + c.Usleep(10000) + } +} diff --git a/runtime/internal/lib/runtime/cpuprof_stub_llgo.go b/runtime/internal/lib/runtime/cpuprof_stub_llgo.go new file mode 100644 index 0000000000..413272dfc2 --- /dev/null +++ b/runtime/internal/lib/runtime/cpuprof_stub_llgo.go @@ -0,0 +1,5 @@ +//go:build baremetal || wasm || (!darwin && !linux) || (!amd64 && !arm64) + +package runtime + +func SetCPUProfileRate(hz int) {} diff --git a/runtime/internal/lib/runtime/pprof_linkname_llgo.go b/runtime/internal/lib/runtime/pprof_linkname_llgo.go index 3dff79d9a4..022d508825 100644 --- a/runtime/internal/lib/runtime/pprof_linkname_llgo.go +++ b/runtime/internal/lib/runtime/pprof_linkname_llgo.go @@ -39,21 +39,8 @@ func runtime_expandFinalInlineFrame(stk []uintptr) []uintptr { //go:linkname pprof_cyclesPerSecond runtime/pprof.runtime_cyclesPerSecond func pprof_cyclesPerSecond() int64 { - return 1 -} - -var ( - // C library entry points may reach this hook before ordinary package - // initialization, so keep the minimal profile data in static arrays. - cpuProfilePeriodRecord = [3]uint64{3, 0, 100} // [len, timestamp, hz] - cpuProfilePeriodTags [1]unsafe.Pointer // one tag slot for the period record -) - -//go:linkname runtime_pprof_readProfile runtime/pprof.readProfile -func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool) { - // Provide a minimal, valid profile stream for runtime/pprof. - // The stdlib expects at least the initial "period" record (3 uint64s). - return cpuProfilePeriodRecord[:], cpuProfilePeriodTags[:], true + // LLGo's runtime clock uses nanoseconds. + return 1e9 } //go:linkname pprof_goroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels diff --git a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go index 3a7f615a2b..a5ac9a4b2c 100644 --- a/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go +++ b/runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go @@ -97,8 +97,6 @@ func NumGoroutine() int { return 1 } -func SetCPUProfileRate(hz int) {} - const funcForPCCacheSets = 1024 const funcForPCCacheWays = 4 diff --git a/runtime/internal/lib/runtime/runtime_default.go b/runtime/internal/lib/runtime/runtime_default.go index 4aa1ef0e7c..7bfa1851c3 100644 --- a/runtime/internal/lib/runtime/runtime_default.go +++ b/runtime/internal/lib/runtime/runtime_default.go @@ -8,7 +8,7 @@ import ( const ( LLGoPackage = "link" - LLGoFiles = "_wrap/runtime.c; _wrap/debugtrap.c; _wrap/fault.c; _wrap/dynunwind.c" + LLGoFiles = "_wrap/runtime.c; _wrap/debugtrap.c; _wrap/fault.c; _wrap/dynunwind.c; _wrap/profile.c" ) //go:linkname c_maxprocs C.llgo_maxprocs diff --git a/test/std/runtime/pprof/pprof_test.go b/test/std/runtime/pprof/pprof_test.go index eb62e106fa..7757536f18 100644 --- a/test/std/runtime/pprof/pprof_test.go +++ b/test/std/runtime/pprof/pprof_test.go @@ -2,11 +2,44 @@ package pprof_test import ( "bytes" + "compress/gzip" "context" + "io" "runtime/pprof" "testing" + "time" ) +//go:noinline +func cpuProfileHotLoop(d time.Duration) uint64 { + deadline := time.Now().Add(d) + x := uint64(1) + for time.Now().Before(deadline) { + for i := 0; i < 10000; i++ { + x = x*1664525 + 1013904223 + } + } + return x +} + +func requireCPUProfileContains(t *testing.T, data []byte, function string) { + t.Helper() + zr, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + t.Fatalf("CPU profile is not valid gzip: %v", err) + } + raw, err := io.ReadAll(zr) + if err != nil { + t.Fatalf("read CPU profile: %v", err) + } + if err := zr.Close(); err != nil { + t.Fatalf("close CPU profile reader: %v", err) + } + if !bytes.Contains(raw, []byte(function)) { + t.Fatalf("CPU profile does not contain sampled function %q (compressed=%d bytes)", function, len(data)) + } +} + func TestStartStopCPUProfile(t *testing.T) { var buf bytes.Buffer err := pprof.StartCPUProfile(&buf) @@ -15,15 +48,10 @@ func TestStartStopCPUProfile(t *testing.T) { } defer pprof.StopCPUProfile() - for i := 0; i < 1000; i++ { - _ = i * i - } + _ = cpuProfileHotLoop(500 * time.Millisecond) pprof.StopCPUProfile() - - if buf.Len() == 0 { - t.Error("CPU profile is empty") - } + requireCPUProfileContains(t, buf.Bytes(), "cpuProfileHotLoop") } func TestStartCPUProfileTwice(t *testing.T) { @@ -40,6 +68,30 @@ func TestStartCPUProfileTwice(t *testing.T) { } pprof.StopCPUProfile() + + var restarted bytes.Buffer + if err := pprof.StartCPUProfile(&restarted); err != nil { + t.Fatalf("StartCPUProfile after stop failed: %v", err) + } + _ = cpuProfileHotLoop(300 * time.Millisecond) + pprof.StopCPUProfile() + requireCPUProfileContains(t, restarted.Bytes(), "cpuProfileHotLoop") +} + +func TestCPUProfileGoroutine(t *testing.T) { + var buf bytes.Buffer + if err := pprof.StartCPUProfile(&buf); err != nil { + t.Fatalf("StartCPUProfile failed: %v", err) + } + defer pprof.StopCPUProfile() + + done := make(chan uint64, 1) + go func() { + done <- cpuProfileHotLoop(500 * time.Millisecond) + }() + <-done + pprof.StopCPUProfile() + requireCPUProfileContains(t, buf.Bytes(), "cpuProfileHotLoop") } func TestWriteHeapProfile(t *testing.T) { From 596837058056707f901396060b4c1c01a44f852c Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Sun, 16 Aug 2026 22:00:42 +0800 Subject: [PATCH 2/5] runtime: coordinate CPU profiling signals --- runtime/internal/lib/runtime/_wrap/profile.c | 125 +++++++++-- .../lib/runtime/cpuprof_sigprof_llgo.go | 26 +++ .../internal/lib/runtime/cpuprof_stub_llgo.go | 4 + runtime/internal/lib/runtime/signal_llgo.go | 15 +- test/go/cpuprof_signal_llgo_test.go | 210 ++++++++++++++++++ 5 files changed, 353 insertions(+), 27 deletions(-) create mode 100644 test/go/cpuprof_signal_llgo_test.go diff --git a/runtime/internal/lib/runtime/_wrap/profile.c b/runtime/internal/lib/runtime/_wrap/profile.c index 30667161f7..55079ef6a8 100644 --- a/runtime/internal/lib/runtime/_wrap/profile.c +++ b/runtime/internal/lib/runtime/_wrap/profile.c @@ -35,6 +35,10 @@ static unsigned int llgo_prof_write_index; static volatile int llgo_prof_lock; static volatile int llgo_prof_active; static volatile uint64_t llgo_prof_lost; +#if defined(__APPLE__) || defined(__linux__) +static struct sigaction llgo_prof_previous_action; +static int llgo_prof_previous_valid; +#endif extern int llgo_mem_readable(void *p); @@ -60,20 +64,83 @@ static void llgo_prof_drop(void) } #if defined(__APPLE__) || defined(__linux__) +static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx); + +static int llgo_prof_action_is_ours(const struct sigaction *sa) +{ + return (sa->sa_flags & SA_SIGINFO) != 0 && + sa->sa_sigaction == llgo_prof_signal; +} + +static int llgo_prof_install_signal_locked(void) +{ + struct sigaction current; + struct sigaction sa; + + if (sigaction(SIGPROF, 0, ¤t) != 0) + return -1; + if (llgo_prof_action_is_ours(¤t)) + return 0; + + /* Keep the disposition that was current immediately before this install. + * A default disposition is normalized to ignore, as the Go runtime does: + * a final pending timer signal must not terminate the process after Stop. */ + llgo_prof_previous_action = current; + if ((current.sa_flags & SA_SIGINFO) == 0 && + current.sa_handler == SIG_DFL) + llgo_prof_previous_action.sa_handler = SIG_IGN; + llgo_prof_previous_valid = 1; + + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = llgo_prof_signal; + sa.sa_mask = current.sa_mask; + sa.sa_flags = SA_SIGINFO | SA_RESTART; +#ifdef SA_ONSTACK + sa.sa_flags |= current.sa_flags & SA_ONSTACK; +#endif + return sigaction(SIGPROF, &sa, 0); +} + +static void llgo_prof_restore_signal_locked(void) +{ + struct sigaction current; + + if (!llgo_prof_previous_valid || sigaction(SIGPROF, 0, ¤t) != 0) + return; + /* Do not overwrite a handler installed by foreign code that did not use + * the coordinated os/signal path below. */ + if (llgo_prof_action_is_ours(¤t)) + sigaction(SIGPROF, &llgo_prof_previous_action, 0); +} + static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) { uintptr_t pc = 0, fp = 0; uintptr_t word = sizeof(uintptr_t); unsigned int next; struct llgo_prof_sample *sample; + int active; ucontext_t *uc = (ucontext_t *)uctx; int saved_errno = errno; (void)sig; (void)info; (void)uc; - if (!__atomic_load_n(&llgo_prof_active, __ATOMIC_ACQUIRE)) + active = __atomic_load_n(&llgo_prof_active, __ATOMIC_ACQUIRE); + /* Like the Go runtime, the profiler owns SIGPROF while active. In + * particular, timer samples are not forwarded to os/signal watchers. */ + if (!active) { + errno = saved_errno; return; + } + if (!llgo_prof_try_lock()) { + llgo_prof_drop(); + errno = saved_errno; + return; + } + active = __atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED); + if (!active) + goto done; #if defined(__APPLE__) && defined(__aarch64__) pc = (uintptr_t)uc->uc_mcontext->__ss.__pc; fp = (uintptr_t)uc->uc_mcontext->__ss.__fp; @@ -87,15 +154,9 @@ static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) pc = (uintptr_t)uc->uc_mcontext.gregs[16 /* REG_RIP */]; fp = (uintptr_t)uc->uc_mcontext.gregs[10 /* REG_RBP */]; #endif - if (pc == 0 || !llgo_prof_try_lock()) { + if (pc == 0) { llgo_prof_drop(); - errno = saved_errno; - return; - } - if (!__atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED)) { - llgo_prof_unlock(); - errno = saved_errno; - return; + goto done; } next = llgo_prof_write_index + 1; @@ -103,9 +164,7 @@ static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) next = 0; if (next == llgo_prof_read_index) { llgo_prof_drop(); - llgo_prof_unlock(); - errno = saved_errno; - return; + goto done; } sample = &llgo_prof_ring[llgo_prof_write_index]; @@ -129,6 +188,7 @@ static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) fp = prev; } llgo_prof_write_index = next; +done: llgo_prof_unlock(); errno = saved_errno; } @@ -139,7 +199,6 @@ static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) int llgo_cpu_profile_start(int hz) { #if defined(__APPLE__) || defined(__linux__) - struct sigaction sa; struct itimerval timer; uint64_t usec; int saved_errno = errno; @@ -155,13 +214,7 @@ int llgo_cpu_profile_start(int hz) errno = saved_errno; return 0; } - /* Reinstall for every profile: user signal code may have changed the - * process disposition since the preceding profile stopped. */ - memset(&sa, 0, sizeof(sa)); - sa.sa_sigaction = llgo_prof_signal; - sigemptyset(&sa.sa_mask); - sa.sa_flags = SA_SIGINFO | SA_RESTART; - if (sigaction(SIGPROF, &sa, 0) != 0) { + if (llgo_prof_install_signal_locked() != 0) { llgo_prof_unlock(); errno = saved_errno; return -1; @@ -170,7 +223,6 @@ int llgo_cpu_profile_start(int hz) llgo_prof_write_index = 0; __atomic_store_n(&llgo_prof_lost, 0, __ATOMIC_RELAXED); __atomic_store_n(&llgo_prof_active, 1, __ATOMIC_RELEASE); - llgo_prof_unlock(); usec = 1000000u / (unsigned int)hz; if (usec == 0) @@ -181,9 +233,12 @@ int llgo_cpu_profile_start(int hz) timer.it_value = timer.it_interval; if (setitimer(ITIMER_PROF, &timer, 0) != 0) { __atomic_store_n(&llgo_prof_active, 0, __ATOMIC_RELEASE); + llgo_prof_restore_signal_locked(); + llgo_prof_unlock(); errno = saved_errno; return -1; } + llgo_prof_unlock(); errno = saved_errno; return 1; #else @@ -197,17 +252,39 @@ void llgo_cpu_profile_stop(void) #if defined(__APPLE__) || defined(__linux__) struct itimerval timer; int saved_errno = errno; + llgo_prof_lock_wait(); memset(&timer, 0, sizeof(timer)); setitimer(ITIMER_PROF, &timer, 0); __atomic_store_n(&llgo_prof_active, 0, __ATOMIC_RELEASE); - /* Wait for a handler that already owns the ring to finish. A handler - * interrupting this critical section only records a dropped sample. */ - llgo_prof_lock_wait(); + llgo_prof_restore_signal_locked(); llgo_prof_unlock(); errno = saved_errno; #endif } +/* Serialize a libuv SIGPROF watcher update with profiler start/stop. While the + * lock is held, an interrupting profiling signal is dropped instead of + * blocking in signal context. */ +void llgo_cpu_profile_signal_update_begin(void) +{ +#if defined(__APPLE__) || defined(__linux__) + llgo_prof_lock_wait(); +#endif +} + +int llgo_cpu_profile_signal_update_end(void) +{ +#if defined(__APPLE__) || defined(__linux__) + int ret = 0; + if (__atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED)) + ret = llgo_prof_install_signal_locked(); + llgo_prof_unlock(); + return ret; +#else + return 0; +#endif +} + int llgo_cpu_profile_read(uintptr_t *pc, int cap) { struct llgo_prof_sample *sample; diff --git a/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go b/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go index 4faf2f5343..13ec2cadac 100644 --- a/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go +++ b/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go @@ -11,6 +11,9 @@ import ( const maxCPUProfileStack = 64 +// SIGPROF is 27 on the native Darwin/Linux architectures supported here. +const cpuProfileSignal = 27 + //go:linkname c_cpuProfileStart C.llgo_cpu_profile_start func c_cpuProfileStart(hz int32) int32 @@ -26,6 +29,12 @@ func c_cpuProfileTakeLost() uint64 //go:linkname c_cpuProfileEmpty C.llgo_cpu_profile_empty func c_cpuProfileEmpty() int32 +//go:linkname c_cpuProfileSignalUpdateBegin C.llgo_cpu_profile_signal_update_begin +func c_cpuProfileSignalUpdateBegin() + +//go:linkname c_cpuProfileSignalUpdateEnd C.llgo_cpu_profile_signal_update_end +func c_cpuProfileSignalUpdateEnd() int32 + var ( cpuProfileRate int32 cpuProfileOpen uint32 @@ -34,6 +43,23 @@ var ( cpuProfilePeriodTags [1]unsafe.Pointer ) +func cpuProfileSignalUpdateBegin(sig uint32) bool { + if sig != cpuProfileSignal { + return false + } + c_cpuProfileSignalUpdateBegin() + return true +} + +func cpuProfileSignalUpdateEnd(locked bool) { + if !locked { + return + } + if c_cpuProfileSignalUpdateEnd() != 0 { + print("runtime: failed to restore CPU profiling SIGPROF handler.\n") + } +} + // SetCPUProfileRate starts or stops process CPU-time sampling. The signal // handler and its ring buffer live in profile.c so the sampling path neither // allocates Go memory nor enters the Go runtime. diff --git a/runtime/internal/lib/runtime/cpuprof_stub_llgo.go b/runtime/internal/lib/runtime/cpuprof_stub_llgo.go index 413272dfc2..d410278b19 100644 --- a/runtime/internal/lib/runtime/cpuprof_stub_llgo.go +++ b/runtime/internal/lib/runtime/cpuprof_stub_llgo.go @@ -3,3 +3,7 @@ package runtime func SetCPUProfileRate(hz int) {} + +func cpuProfileSignalUpdateBegin(sig uint32) bool { return false } + +func cpuProfileSignalUpdateEnd(locked bool) {} diff --git a/runtime/internal/lib/runtime/signal_llgo.go b/runtime/internal/lib/runtime/signal_llgo.go index b2172a6207..2ae515fd93 100644 --- a/runtime/internal/lib/runtime/signal_llgo.go +++ b/runtime/internal/lib/runtime/signal_llgo.go @@ -88,10 +88,14 @@ func startSignalWatcher(sig uint32, st *sigState) { }) st.inited = true } + var code int + locked := cpuProfileSignalUpdateBegin(sig) submitTimerWork(func() bool { - checkUV("uv_signal_start", int(libuv.SignalStartRuntime(&st.handle, c.Int(sig)))) + code = int(libuv.SignalStartRuntime(&st.handle, c.Int(sig))) return true }) + cpuProfileSignalUpdateEnd(locked) + checkUV("uv_signal_start", code) } // signal_enable enables Go signal delivery for sig. @@ -123,16 +127,21 @@ func signal_disable(sig uint32) { sigMu.Unlock() return } - if st.active && !st.ignored { + if st.active { st.active = false + st.ignored = false doStop = true } sigMu.Unlock() if doStop { + var code int + locked := cpuProfileSignalUpdateBegin(sig) submitTimerWork(func() bool { - checkUV("uv_signal_stop", int(st.handle.Stop())) + code = int(st.handle.Stop()) return true }) + cpuProfileSignalUpdateEnd(locked) + checkUV("uv_signal_stop", code) } } diff --git a/test/go/cpuprof_signal_llgo_test.go b/test/go/cpuprof_signal_llgo_test.go new file mode 100644 index 0000000000..a34bbc238b --- /dev/null +++ b/test/go/cpuprof_signal_llgo_test.go @@ -0,0 +1,210 @@ +//go:build llgo && !baremetal && !wasm && (darwin || linux) && (amd64 || arm64) + +package gotest + +import ( + "bytes" + "compress/gzip" + "io" + "os" + "os/signal" + "runtime" + "runtime/pprof" + "syscall" + "testing" + "time" + "unsafe" +) + +//go:linkname readCPUProfileRaw runtime/pprof.readProfile +func readCPUProfileRaw() (data []uint64, tags []unsafe.Pointer, eof bool) + +//go:noinline +func cpuProfileSignalHotLoop(d time.Duration) uint64 { + deadline := time.Now().Add(d) + x := uint64(1) + for time.Now().Before(deadline) { + for i := 0; i < 10000; i++ { + x = x*1664525 + 1013904223 + } + } + return x +} + +func requireCPUProfileSignalFunction(t *testing.T, data []byte) { + t.Helper() + zr, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + t.Fatalf("CPU profile is not valid gzip: %v", err) + } + raw, err := io.ReadAll(zr) + if err != nil { + t.Fatalf("read CPU profile: %v", err) + } + if err := zr.Close(); err != nil { + t.Fatalf("close CPU profile reader: %v", err) + } + if !bytes.Contains(raw, []byte("cpuProfileSignalHotLoop")) { + t.Fatalf("CPU profile does not contain the hot function (compressed=%d bytes)", len(data)) + } +} + +func sendSIGPROF(t *testing.T, phase string) { + t.Helper() + proc, err := os.FindProcess(os.Getpid()) + if err != nil { + t.Fatalf("%s: FindProcess: %v", phase, err) + } + if err := proc.Signal(syscall.SIGPROF); err != nil { + t.Fatalf("%s: send SIGPROF: %v", phase, err) + } +} + +func waitForSIGPROF(t *testing.T, c <-chan os.Signal, phase string) { + t.Helper() + select { + case got := <-c: + if got != syscall.SIGPROF { + t.Fatalf("%s: got signal %v, want SIGPROF", phase, got) + } + case <-time.After(time.Second): + t.Fatalf("%s: timeout waiting for SIGPROF", phase) + } +} + +func requireNoSIGPROF(t *testing.T, c <-chan os.Signal, phase string) { + t.Helper() + select { + case got := <-c: + t.Fatalf("%s: SIGPROF unexpectedly reached os/signal as %v", phase, got) + case <-time.After(20 * time.Millisecond): + } +} + +func TestCPUProfileSIGPROFNotifyBeforeStart(t *testing.T) { + c := make(chan os.Signal, 8) + signal.Notify(c, syscall.SIGPROF) + t.Cleanup(func() { signal.Stop(c) }) + + var profile bytes.Buffer + if err := pprof.StartCPUProfile(&profile); err != nil { + t.Fatalf("StartCPUProfile: %v", err) + } + t.Cleanup(pprof.StopCPUProfile) + _ = cpuProfileSignalHotLoop(300 * time.Millisecond) + requireNoSIGPROF(t, c, "watcher started before profiling") + sendSIGPROF(t, "while profiling") + requireNoSIGPROF(t, c, "user SIGPROF while profiling") + pprof.StopCPUProfile() + requireCPUProfileSignalFunction(t, profile.Bytes()) + + // Stopping profiling must restore the libuv watcher that was active when + // profiling began. + sendSIGPROF(t, "after profiling") + waitForSIGPROF(t, c, "after profiling") +} + +func TestCPUProfileSIGPROFNotifyDuringProfile(t *testing.T) { + var profile bytes.Buffer + if err := pprof.StartCPUProfile(&profile); err != nil { + t.Fatalf("StartCPUProfile: %v", err) + } + t.Cleanup(pprof.StopCPUProfile) + + c := make(chan os.Signal, 8) + signal.Notify(c, syscall.SIGPROF) + t.Cleanup(func() { signal.Stop(c) }) + _ = cpuProfileSignalHotLoop(300 * time.Millisecond) + requireNoSIGPROF(t, c, "watcher started during profiling") + + // Removing the watcher must not remove the profiler's handler. + signal.Stop(c) + _ = cpuProfileSignalHotLoop(300 * time.Millisecond) + pprof.StopCPUProfile() + requireCPUProfileSignalFunction(t, profile.Bytes()) +} + +func TestCPUProfileSIGPROFIgnoreReset(t *testing.T) { + var profile bytes.Buffer + if err := pprof.StartCPUProfile(&profile); err != nil { + t.Fatalf("StartCPUProfile: %v", err) + } + t.Cleanup(pprof.StopCPUProfile) + + signal.Ignore(syscall.SIGPROF) + t.Cleanup(func() { signal.Reset(syscall.SIGPROF) }) + if !signal.Ignored(syscall.SIGPROF) { + t.Fatal("SIGPROF is not ignored after signal.Ignore") + } + _ = cpuProfileSignalHotLoop(300 * time.Millisecond) + + // Reset changes the libuv watcher while profiling is still active. It + // must restore the logical signal state without removing the profiler. + signal.Reset(syscall.SIGPROF) + if signal.Ignored(syscall.SIGPROF) { + t.Fatal("SIGPROF is still ignored after signal.Reset") + } + _ = cpuProfileSignalHotLoop(300 * time.Millisecond) + + pprof.StopCPUProfile() + requireCPUProfileSignalFunction(t, profile.Bytes()) +} + +func TestCPUProfileSIGPROFRepeatedLifecycle(t *testing.T) { + c := make(chan os.Signal, 8) + signal.Notify(c, syscall.SIGPROF) + t.Cleanup(func() { signal.Stop(c) }) + + for round := 0; round < 3; round++ { + var profile bytes.Buffer + if err := pprof.StartCPUProfile(&profile); err != nil { + t.Fatalf("round %d: StartCPUProfile: %v", round, err) + } + _ = cpuProfileSignalHotLoop(200 * time.Millisecond) + pprof.StopCPUProfile() + requireCPUProfileSignalFunction(t, profile.Bytes()) + + // Every Stop must hand SIGPROF ownership back to the same watcher, + // and the next Start must be able to take ownership again. + phase := "after repeated profile" + sendSIGPROF(t, phase) + waitForSIGPROF(t, c, phase) + } +} + +func TestCPUProfileLostSamples(t *testing.T) { + // runtime/pprof intentionally fixes normal profiling at 100 Hz. Use the + // runtime entry point directly so the producer can fill the 2048-entry ring + // while no profile writer is draining it. + duration := 500 * time.Millisecond + if runtime.GOOS == "linux" { + // Linux limits effective ITIMER_PROF delivery to roughly its timer + // resolution, so leave enough time for more than 2048 deliveries. + duration = 3 * time.Second + } + runtime.SetCPUProfileRate(100000) + defer runtime.SetCPUProfileRate(0) + _ = cpuProfileSignalHotLoop(duration) + runtime.SetCPUProfileRate(0) + + var lost uint64 + for { + data, _, eof := readCPUProfileRaw() + for len(data) != 0 { + n := int(data[0]) + if n <= 0 || n > len(data) { + t.Fatalf("malformed raw CPU profile record length %d in %d words", n, len(data)) + } + if n == 4 && data[1] == 0 && data[2] == 0 { + lost += data[3] + } + data = data[n:] + } + if eof { + break + } + } + if lost == 0 { + t.Fatal("high-rate CPU profile did not report lost samples") + } +} From 63676faa166453aefb80f350be5a022b5d3d02fb Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Mon, 17 Aug 2026 07:59:22 +0800 Subject: [PATCH 3/5] test: make CPU profile overflow deterministic --- test/go/cpuprof_signal_llgo_test.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/test/go/cpuprof_signal_llgo_test.go b/test/go/cpuprof_signal_llgo_test.go index a34bbc238b..3ca31476c7 100644 --- a/test/go/cpuprof_signal_llgo_test.go +++ b/test/go/cpuprof_signal_llgo_test.go @@ -19,6 +19,9 @@ import ( //go:linkname readCPUProfileRaw runtime/pprof.readProfile func readCPUProfileRaw() (data []uint64, tags []unsafe.Pointer, eof bool) +//go:linkname raiseCPUProfileSignal C.raise +func raiseCPUProfileSignal(sig int32) int32 + //go:noinline func cpuProfileSignalHotLoop(d time.Duration) uint64 { deadline := time.Now().Add(d) @@ -173,18 +176,17 @@ func TestCPUProfileSIGPROFRepeatedLifecycle(t *testing.T) { } func TestCPUProfileLostSamples(t *testing.T) { - // runtime/pprof intentionally fixes normal profiling at 100 Hz. Use the - // runtime entry point directly so the producer can fill the 2048-entry ring - // while no profile writer is draining it. - duration := 500 * time.Millisecond - if runtime.GOOS == "linux" { - // Linux limits effective ITIMER_PROF delivery to roughly its timer - // resolution, so leave enough time for more than 2048 deliveries. - duration = 3 * time.Second - } - runtime.SetCPUProfileRate(100000) + // Install the real profiling handler, then synchronously deliver more + // signals than the 2048-entry ring can hold while no writer drains it. + // Using raise instead of a high-rate timer keeps this independent of the + // process CPU time available on a busy CI runner. + runtime.SetCPUProfileRate(1) defer runtime.SetCPUProfileRate(0) - _ = cpuProfileSignalHotLoop(duration) + for i := 0; i < 4096; i++ { + if ret := raiseCPUProfileSignal(int32(syscall.SIGPROF)); ret != 0 { + t.Fatalf("raise SIGPROF %d: return value %d", i, ret) + } + } runtime.SetCPUProfileRate(0) var lost uint64 From 1f8877a4cf6ee5fc7db94449008d017fffd22b10 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 18 Aug 2026 18:49:57 +0800 Subject: [PATCH 4/5] runtime: harden native CPU profiling --- runtime/internal/lib/runtime/_wrap/fault.c | 7 + runtime/internal/lib/runtime/_wrap/profile.c | 169 +++++++++++++----- .../lib/runtime/cpuprof_read_stub_llgo.go | 2 +- .../lib/runtime/cpuprof_sigprof_llgo.go | 34 ++-- test/go/cpuprof_signal_llgo_test.go | 9 + 5 files changed, 163 insertions(+), 58 deletions(-) diff --git a/runtime/internal/lib/runtime/_wrap/fault.c b/runtime/internal/lib/runtime/_wrap/fault.c index 641f1a1fe1..5e2b15fd04 100644 --- a/runtime/internal/lib/runtime/_wrap/fault.c +++ b/runtime/internal/lib/runtime/_wrap/fault.c @@ -26,6 +26,11 @@ static long llgo_pagesz; /* primed at handler install, out of signal context */ static void (*llgo_fault_go)(uintptr_t pc, uintptr_t fp, int sig); +/* profile.c arms this recovery point only while dereferencing a sampled + * frame-pointer chain. A bad frame truncates that sample instead of entering + * the Go fault path while the profiler ring lock is held. */ +extern int llgo_cpu_profile_fault_recover(void); + /* Dynamic-libunwind fault unwinding (dynunwind.c); no-ops when disabled * (LLGO_DYNUNWIND=0) or no libunwind flavor resolved. */ extern void llgo_dynunwind_init(void); @@ -38,6 +43,8 @@ static void llgo_fault_trampoline(int sig, siginfo_t *info, void *uctx) uintptr_t pc = 0, fp = 0; ucontext_t *uc = (ucontext_t *)uctx; (void)info; + if (llgo_cpu_profile_fault_recover()) + return; if (llgo_in_fault) { signal(sig, SIG_DFL); raise(sig); diff --git a/runtime/internal/lib/runtime/_wrap/profile.c b/runtime/internal/lib/runtime/_wrap/profile.c index 55079ef6a8..7c5c9b94f4 100644 --- a/runtime/internal/lib/runtime/_wrap/profile.c +++ b/runtime/internal/lib/runtime/_wrap/profile.c @@ -11,10 +11,15 @@ #endif #include +#include +#include +#include #include #include #include +#include #include +#include #if defined(__APPLE__) || defined(__linux__) #include @@ -32,30 +37,37 @@ struct llgo_prof_sample { static struct llgo_prof_sample llgo_prof_ring[LLGO_PROF_SAMPLES]; static unsigned int llgo_prof_read_index; static unsigned int llgo_prof_write_index; -static volatile int llgo_prof_lock; +static volatile int llgo_prof_ring_lock; +static pthread_mutex_t llgo_prof_state_lock = PTHREAD_MUTEX_INITIALIZER; static volatile int llgo_prof_active; static volatile uint64_t llgo_prof_lost; #if defined(__APPLE__) || defined(__linux__) static struct sigaction llgo_prof_previous_action; static int llgo_prof_previous_valid; +static sigjmp_buf llgo_prof_fault_jmp; +static volatile uintptr_t llgo_prof_fault_owner; +static volatile int llgo_prof_fault_active; #endif -extern int llgo_mem_readable(void *p); - -static int llgo_prof_try_lock(void) +static int llgo_prof_ring_try_lock(void) { - return __atomic_exchange_n(&llgo_prof_lock, 1, __ATOMIC_ACQUIRE) == 0; + return __atomic_exchange_n(&llgo_prof_ring_lock, 1, __ATOMIC_ACQUIRE) == 0; } -static void llgo_prof_lock_wait(void) +static void llgo_prof_ring_lock_wait(void) { - while (!llgo_prof_try_lock()) { + unsigned int spins = 0; + while (!llgo_prof_ring_try_lock()) { + if (++spins == 64) { + sched_yield(); + spins = 0; + } } } -static void llgo_prof_unlock(void) +static void llgo_prof_ring_unlock(void) { - __atomic_store_n(&llgo_prof_lock, 0, __ATOMIC_RELEASE); + __atomic_store_n(&llgo_prof_ring_lock, 0, __ATOMIC_RELEASE); } static void llgo_prof_drop(void) @@ -66,6 +78,52 @@ static void llgo_prof_drop(void) #if defined(__APPLE__) || defined(__linux__) static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx); +/* Called first by the runtime fault handler. The global jump buffer is safe + * because the ring lock permits only one active profile walk; the owner check + * prevents a simultaneous fault on another pthread from jumping across + * threads. */ +int llgo_cpu_profile_fault_recover(void) +{ + int expected = 1; + + if (!__atomic_load_n(&llgo_prof_fault_active, __ATOMIC_ACQUIRE) || + __atomic_load_n(&llgo_prof_fault_owner, __ATOMIC_RELAXED) != + (uintptr_t)pthread_self()) + return 0; + if (!__atomic_compare_exchange_n(&llgo_prof_fault_active, &expected, 0, + 0, __ATOMIC_ACQ_REL, + __ATOMIC_ACQUIRE)) + return 0; + siglongjmp(llgo_prof_fault_jmp, 1); + return 1; +} + +static void llgo_prof_walk_frames(struct llgo_prof_sample *sample, uintptr_t fp) +{ + uintptr_t word = sizeof(uintptr_t); + + __atomic_store_n(&llgo_prof_fault_owner, (uintptr_t)pthread_self(), + __ATOMIC_RELAXED); + if (sigsetjmp(llgo_prof_fault_jmp, 1) != 0) + return; + __atomic_store_n(&llgo_prof_fault_active, 1, __ATOMIC_RELEASE); + while (fp != 0 && sample->n < LLGO_PROF_STACK) { + uintptr_t prev, ret; + if ((fp & (word - 1)) != 0) + break; + prev = *(uintptr_t *)fp; + ret = *(uintptr_t *)(fp + word); + if (ret < 4096) + break; + sample->pc[sample->n++] = ret; + if (prev <= fp || prev - fp > LLGO_PROF_MAX_FP_STRIDE || + (prev & (word - 1)) != 0) + break; + fp = prev; + } + __atomic_store_n(&llgo_prof_fault_active, 0, __ATOMIC_RELEASE); +} + static int llgo_prof_action_is_ours(const struct sigaction *sa) { return (sa->sa_flags & SA_SIGINFO) != 0 && @@ -116,7 +174,6 @@ static void llgo_prof_restore_signal_locked(void) static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) { uintptr_t pc = 0, fp = 0; - uintptr_t word = sizeof(uintptr_t); unsigned int next; struct llgo_prof_sample *sample; int active; @@ -133,7 +190,7 @@ static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) errno = saved_errno; return; } - if (!llgo_prof_try_lock()) { + if (!llgo_prof_ring_try_lock()) { llgo_prof_drop(); errno = saved_errno; return; @@ -171,25 +228,10 @@ static void llgo_prof_signal(int sig, siginfo_t *info, void *uctx) sample->n = 1; /* runtime.CallersFrames subtracts one from every sampled PC. */ sample->pc[0] = pc + 1; - while (fp != 0 && sample->n < LLGO_PROF_STACK) { - uintptr_t prev, ret; - if ((fp & (word - 1)) != 0 || - !llgo_mem_readable((void *)fp) || - !llgo_mem_readable((void *)(fp + word))) - break; - prev = *(uintptr_t *)fp; - ret = *(uintptr_t *)(fp + word); - if (ret < 4096) - break; - sample->pc[sample->n++] = ret; - if (prev <= fp || prev - fp > LLGO_PROF_MAX_FP_STRIDE || - (prev & (word - 1)) != 0) - break; - fp = prev; - } + llgo_prof_walk_frames(sample, fp); llgo_prof_write_index = next; done: - llgo_prof_unlock(); + llgo_prof_ring_unlock(); errno = saved_errno; } #endif @@ -207,15 +249,18 @@ int llgo_cpu_profile_start(int hz) errno = saved_errno; return -1; } - llgo_prof_lock_wait(); + pthread_mutex_lock(&llgo_prof_state_lock); + llgo_prof_ring_lock_wait(); if (__atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED) || llgo_prof_read_index != llgo_prof_write_index) { - llgo_prof_unlock(); + llgo_prof_ring_unlock(); + pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; return 0; } if (llgo_prof_install_signal_locked() != 0) { - llgo_prof_unlock(); + llgo_prof_ring_unlock(); + pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; return -1; } @@ -234,11 +279,13 @@ int llgo_cpu_profile_start(int hz) if (setitimer(ITIMER_PROF, &timer, 0) != 0) { __atomic_store_n(&llgo_prof_active, 0, __ATOMIC_RELEASE); llgo_prof_restore_signal_locked(); - llgo_prof_unlock(); + llgo_prof_ring_unlock(); + pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; return -1; } - llgo_prof_unlock(); + llgo_prof_ring_unlock(); + pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; return 1; #else @@ -252,23 +299,25 @@ void llgo_cpu_profile_stop(void) #if defined(__APPLE__) || defined(__linux__) struct itimerval timer; int saved_errno = errno; - llgo_prof_lock_wait(); + pthread_mutex_lock(&llgo_prof_state_lock); + llgo_prof_ring_lock_wait(); memset(&timer, 0, sizeof(timer)); setitimer(ITIMER_PROF, &timer, 0); __atomic_store_n(&llgo_prof_active, 0, __ATOMIC_RELEASE); llgo_prof_restore_signal_locked(); - llgo_prof_unlock(); + llgo_prof_ring_unlock(); + pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; #endif } -/* Serialize a libuv SIGPROF watcher update with profiler start/stop. While the - * lock is held, an interrupting profiling signal is dropped instead of - * blocking in signal context. */ +/* Serialize a libuv SIGPROF watcher update with profiler start/stop. This + * state mutex is deliberately separate from the ring lock, so reads and + * sampling continue while the Go timer thread performs the libuv round-trip. */ void llgo_cpu_profile_signal_update_begin(void) { #if defined(__APPLE__) || defined(__linux__) - llgo_prof_lock_wait(); + pthread_mutex_lock(&llgo_prof_state_lock); #endif } @@ -278,7 +327,7 @@ int llgo_cpu_profile_signal_update_end(void) int ret = 0; if (__atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED)) ret = llgo_prof_install_signal_locked(); - llgo_prof_unlock(); + pthread_mutex_unlock(&llgo_prof_state_lock); return ret; #else return 0; @@ -293,9 +342,9 @@ int llgo_cpu_profile_read(uintptr_t *pc, int cap) if (pc == 0 || cap <= 0) return 0; - llgo_prof_lock_wait(); + llgo_prof_ring_lock_wait(); if (llgo_prof_read_index == llgo_prof_write_index) { - llgo_prof_unlock(); + llgo_prof_ring_unlock(); return 0; } sample = &llgo_prof_ring[llgo_prof_read_index]; @@ -307,7 +356,7 @@ int llgo_cpu_profile_read(uintptr_t *pc, int cap) llgo_prof_read_index++; if (llgo_prof_read_index == LLGO_PROF_SAMPLES) llgo_prof_read_index = 0; - llgo_prof_unlock(); + llgo_prof_ring_unlock(); return n; } @@ -319,8 +368,38 @@ uint64_t llgo_cpu_profile_take_lost(void) int llgo_cpu_profile_empty(void) { int empty; - llgo_prof_lock_wait(); + llgo_prof_ring_lock_wait(); empty = llgo_prof_read_index == llgo_prof_write_index; - llgo_prof_unlock(); + llgo_prof_ring_unlock(); return empty; } + +/* Exercise the guarded frame walk against a mapped but unreadable page. This + * is kept as a narrow runtime test hook so a regression fails deterministically + * instead of depending on a corrupt frame pointer appearing in a sample. */ +int llgo_cpu_profile_test_fault_recovery(void) +{ +#if defined(__APPLE__) || defined(__linux__) + struct llgo_prof_sample sample; + long page_size = sysconf(_SC_PAGESIZE); + void *page; + int n; + + if (page_size <= 0) + page_size = 4096; + page = mmap(0, (size_t)page_size, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (page == MAP_FAILED) + return -1; + sample.n = 1; + sample.pc[0] = 1; + llgo_prof_ring_lock_wait(); + llgo_prof_walk_frames(&sample, (uintptr_t)page); + llgo_prof_ring_unlock(); + n = (int)sample.n; + munmap(page, (size_t)page_size); + return n; +#else + return -1; +#endif +} diff --git a/runtime/internal/lib/runtime/cpuprof_read_stub_llgo.go b/runtime/internal/lib/runtime/cpuprof_read_stub_llgo.go index 73b2d41d92..94f738ffef 100644 --- a/runtime/internal/lib/runtime/cpuprof_read_stub_llgo.go +++ b/runtime/internal/lib/runtime/cpuprof_read_stub_llgo.go @@ -1,4 +1,4 @@ -//go:build (darwin || linux) && (baremetal || (!amd64 && !arm64)) +//go:build baremetal || wasm || (!darwin && !linux) || (!amd64 && !arm64) package runtime diff --git a/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go b/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go index 13ec2cadac..850f5ce583 100644 --- a/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go +++ b/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go @@ -3,16 +3,21 @@ package runtime import ( + latomic "sync/atomic" "unsafe" c "github.com/xgo-dev/llgo/runtime/internal/clite" - latomic "sync/atomic" + csyscall "github.com/xgo-dev/llgo/runtime/internal/clite/syscall" ) -const maxCPUProfileStack = 64 - -// SIGPROF is 27 on the native Darwin/Linux architectures supported here. -const cpuProfileSignal = 27 +const ( + maxCPUProfileStack = 64 + maxCPUProfileDrainRecords = 256 + maxCPUProfileDrainData = 4 + maxCPUProfileDrainRecords*(3+maxCPUProfileStack) + maxCPUProfileDrainTags = 1 + maxCPUProfileDrainRecords + cpuProfilePollUsec = 10000 + cpuProfileSignal = uint32(csyscall.SIGPROF) +) //go:linkname c_cpuProfileStart C.llgo_cpu_profile_start func c_cpuProfileStart(hz int32) int32 @@ -41,6 +46,8 @@ var ( cpuProfilePeriodPending uint32 cpuProfilePeriodRecord = [3]uint64{3, 0, 100} cpuProfilePeriodTags [1]unsafe.Pointer + cpuProfileDrainData [maxCPUProfileDrainData]uint64 + cpuProfileDrainTags [maxCPUProfileDrainTags]unsafe.Pointer ) func cpuProfileSignalUpdateBegin(sig uint32) bool { @@ -114,19 +121,22 @@ func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool // runtime/pprof waits 100 ms between reads on Darwin. Drain a // chunk, rather than one sample, so a 100 Hz producer cannot // outrun the writer. - data = make([]uint64, 0, 1024) - tags = make([]unsafe.Pointer, 0, 16) + // The writer consumes these package-level scratch slices before + // its next call, as required by readProfile's contract. + data = cpuProfileDrainData[:0] + tags = cpuProfileDrainTags[:0] if lost != 0 { data = append(data, 4, 0, 0, lost) tags = append(tags, nil) } - for records := 0; n != 0 && records < 256; records++ { + for records := 0; n != 0; records++ { data = append(data, uint64(3+n), 0, 1) for i := 0; i < n; i++ { data = append(data, uint64(pcs[i])) } tags = append(tags, nil) - if records == 255 { + // Do not consume a sample that cannot be emitted in this chunk. + if records+1 == maxCPUProfileDrainRecords { break } n = int(c_cpuProfileRead(unsafe.Pointer(&pcs[0]), maxCPUProfileStack)) @@ -139,8 +149,8 @@ func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool } // Linux's profile writer expects readProfile to block. Keep the wait - // out of the signal path and poll at the 100 Hz profiler's period; - // Darwin already sleeps in pprof. - c.Usleep(10000) + // out of the signal path and poll every 10 ms (the default 100 Hz + // period); Darwin already sleeps between non-blocking reads. + c.Usleep(cpuProfilePollUsec) } } diff --git a/test/go/cpuprof_signal_llgo_test.go b/test/go/cpuprof_signal_llgo_test.go index 3ca31476c7..0dcaf181e2 100644 --- a/test/go/cpuprof_signal_llgo_test.go +++ b/test/go/cpuprof_signal_llgo_test.go @@ -22,6 +22,9 @@ func readCPUProfileRaw() (data []uint64, tags []unsafe.Pointer, eof bool) //go:linkname raiseCPUProfileSignal C.raise func raiseCPUProfileSignal(sig int32) int32 +//go:linkname testCPUProfileFaultRecovery C.llgo_cpu_profile_test_fault_recovery +func testCPUProfileFaultRecovery() int32 + //go:noinline func cpuProfileSignalHotLoop(d time.Duration) uint64 { deadline := time.Now().Add(d) @@ -84,6 +87,12 @@ func requireNoSIGPROF(t *testing.T, c <-chan os.Signal, phase string) { } } +func TestCPUProfileFaultRecovery(t *testing.T) { + if got := testCPUProfileFaultRecovery(); got != 1 { + t.Fatalf("guarded frame walk returned %d frames, want interrupted PC only", got) + } +} + func TestCPUProfileSIGPROFNotifyBeforeStart(t *testing.T) { c := make(chan os.Signal, 8) signal.Notify(c, syscall.SIGPROF) From 74c36475a50c79d80be7b3a42b036e98e7985c9c Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 18 Aug 2026 21:28:23 +0800 Subject: [PATCH 5/5] runtime: move CPU profile coordination to Go --- runtime/internal/lib/runtime/_wrap/profile.c | 93 +++++++------------ .../lib/runtime/cpuprof_sigprof_llgo.go | 82 +++++++++------- .../internal/lib/runtime/cpuprof_stub_llgo.go | 4 +- runtime/internal/lib/runtime/signal_llgo.go | 8 +- 4 files changed, 91 insertions(+), 96 deletions(-) diff --git a/runtime/internal/lib/runtime/_wrap/profile.c b/runtime/internal/lib/runtime/_wrap/profile.c index 7c5c9b94f4..0759ae797a 100644 --- a/runtime/internal/lib/runtime/_wrap/profile.c +++ b/runtime/internal/lib/runtime/_wrap/profile.c @@ -38,7 +38,6 @@ static struct llgo_prof_sample llgo_prof_ring[LLGO_PROF_SAMPLES]; static unsigned int llgo_prof_read_index; static unsigned int llgo_prof_write_index; static volatile int llgo_prof_ring_lock; -static pthread_mutex_t llgo_prof_state_lock = PTHREAD_MUTEX_INITIALIZER; static volatile int llgo_prof_active; static volatile uint64_t llgo_prof_lost; #if defined(__APPLE__) || defined(__linux__) @@ -130,7 +129,7 @@ static int llgo_prof_action_is_ours(const struct sigaction *sa) sa->sa_sigaction == llgo_prof_signal; } -static int llgo_prof_install_signal_locked(void) +static int llgo_prof_install_signal(void) { struct sigaction current; struct sigaction sa; @@ -159,7 +158,7 @@ static int llgo_prof_install_signal_locked(void) return sigaction(SIGPROF, &sa, 0); } -static void llgo_prof_restore_signal_locked(void) +static void llgo_prof_restore_signal(void) { struct sigaction current; @@ -249,18 +248,15 @@ int llgo_cpu_profile_start(int hz) errno = saved_errno; return -1; } - pthread_mutex_lock(&llgo_prof_state_lock); llgo_prof_ring_lock_wait(); if (__atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED) || llgo_prof_read_index != llgo_prof_write_index) { llgo_prof_ring_unlock(); - pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; return 0; } - if (llgo_prof_install_signal_locked() != 0) { + if (llgo_prof_install_signal() != 0) { llgo_prof_ring_unlock(); - pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; return -1; } @@ -278,14 +274,12 @@ int llgo_cpu_profile_start(int hz) timer.it_value = timer.it_interval; if (setitimer(ITIMER_PROF, &timer, 0) != 0) { __atomic_store_n(&llgo_prof_active, 0, __ATOMIC_RELEASE); - llgo_prof_restore_signal_locked(); + llgo_prof_restore_signal(); llgo_prof_ring_unlock(); - pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; return -1; } llgo_prof_ring_unlock(); - pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; return 1; #else @@ -299,79 +293,60 @@ void llgo_cpu_profile_stop(void) #if defined(__APPLE__) || defined(__linux__) struct itimerval timer; int saved_errno = errno; - pthread_mutex_lock(&llgo_prof_state_lock); llgo_prof_ring_lock_wait(); memset(&timer, 0, sizeof(timer)); setitimer(ITIMER_PROF, &timer, 0); __atomic_store_n(&llgo_prof_active, 0, __ATOMIC_RELEASE); - llgo_prof_restore_signal_locked(); + llgo_prof_restore_signal(); llgo_prof_ring_unlock(); - pthread_mutex_unlock(&llgo_prof_state_lock); errno = saved_errno; #endif } -/* Serialize a libuv SIGPROF watcher update with profiler start/stop. This - * state mutex is deliberately separate from the ring lock, so reads and - * sampling continue while the Go timer thread performs the libuv round-trip. */ -void llgo_cpu_profile_signal_update_begin(void) +/* A libuv SIGPROF watcher update may replace the process disposition. The Go + * control plane serializes that update with profile start/stop, then asks the + * native sampler to take ownership back while profiling is active. */ +int llgo_cpu_profile_refresh_signal(void) { #if defined(__APPLE__) || defined(__linux__) - pthread_mutex_lock(&llgo_prof_state_lock); -#endif -} - -int llgo_cpu_profile_signal_update_end(void) -{ -#if defined(__APPLE__) || defined(__linux__) - int ret = 0; if (__atomic_load_n(&llgo_prof_active, __ATOMIC_RELAXED)) - ret = llgo_prof_install_signal_locked(); - pthread_mutex_unlock(&llgo_prof_state_lock); - return ret; + return llgo_prof_install_signal(); + return 0; #else return 0; #endif } -int llgo_cpu_profile_read(uintptr_t *pc, int cap) +int llgo_cpu_profile_drain(uintptr_t *pc, uint32_t *lengths, + int max_records, int max_stack, + uint64_t *lost, int *empty) { struct llgo_prof_sample *sample; - unsigned int i; - int n; + unsigned int i, n; + int records = 0; - if (pc == 0 || cap <= 0) + if (pc == 0 || lengths == 0 || max_records <= 0 || max_stack <= 0 || + lost == 0 || empty == 0) return 0; llgo_prof_ring_lock_wait(); - if (llgo_prof_read_index == llgo_prof_write_index) { - llgo_prof_ring_unlock(); - return 0; + *lost = __atomic_exchange_n(&llgo_prof_lost, 0, __ATOMIC_RELAXED); + while (llgo_prof_read_index != llgo_prof_write_index && + records < max_records) { + sample = &llgo_prof_ring[llgo_prof_read_index]; + n = sample->n; + if (n > (unsigned int)max_stack) + n = (unsigned int)max_stack; + lengths[records] = n; + for (i = 0; i < n; i++) + pc[(size_t)records * (size_t)max_stack + i] = sample->pc[i]; + records++; + llgo_prof_read_index++; + if (llgo_prof_read_index == LLGO_PROF_SAMPLES) + llgo_prof_read_index = 0; } - sample = &llgo_prof_ring[llgo_prof_read_index]; - n = (int)sample->n; - if (n > cap) - n = cap; - for (i = 0; i < (unsigned int)n; i++) - pc[i] = sample->pc[i]; - llgo_prof_read_index++; - if (llgo_prof_read_index == LLGO_PROF_SAMPLES) - llgo_prof_read_index = 0; - llgo_prof_ring_unlock(); - return n; -} - -uint64_t llgo_cpu_profile_take_lost(void) -{ - return __atomic_exchange_n(&llgo_prof_lost, 0, __ATOMIC_RELAXED); -} - -int llgo_cpu_profile_empty(void) -{ - int empty; - llgo_prof_ring_lock_wait(); - empty = llgo_prof_read_index == llgo_prof_write_index; + *empty = llgo_prof_read_index == llgo_prof_write_index; llgo_prof_ring_unlock(); - return empty; + return records; } /* Exercise the guarded frame walk against a mapped but unreadable page. This diff --git a/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go b/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go index 850f5ce583..0bcdd1935f 100644 --- a/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go +++ b/runtime/internal/lib/runtime/cpuprof_sigprof_llgo.go @@ -7,6 +7,7 @@ import ( "unsafe" c "github.com/xgo-dev/llgo/runtime/internal/clite" + psync "github.com/xgo-dev/llgo/runtime/internal/clite/pthread/sync" csyscall "github.com/xgo-dev/llgo/runtime/internal/clite/syscall" ) @@ -25,46 +26,57 @@ func c_cpuProfileStart(hz int32) int32 //go:linkname c_cpuProfileStop C.llgo_cpu_profile_stop func c_cpuProfileStop() -//go:linkname c_cpuProfileRead C.llgo_cpu_profile_read -func c_cpuProfileRead(p unsafe.Pointer, n int32) int32 +//go:linkname c_cpuProfileDrain C.llgo_cpu_profile_drain +func c_cpuProfileDrain(pcs, lengths unsafe.Pointer, maxRecords, maxStack int32, lost *uint64, empty *int32) int32 -//go:linkname c_cpuProfileTakeLost C.llgo_cpu_profile_take_lost -func c_cpuProfileTakeLost() uint64 - -//go:linkname c_cpuProfileEmpty C.llgo_cpu_profile_empty -func c_cpuProfileEmpty() int32 - -//go:linkname c_cpuProfileSignalUpdateBegin C.llgo_cpu_profile_signal_update_begin -func c_cpuProfileSignalUpdateBegin() - -//go:linkname c_cpuProfileSignalUpdateEnd C.llgo_cpu_profile_signal_update_end -func c_cpuProfileSignalUpdateEnd() int32 +//go:linkname c_cpuProfileRefreshSignal C.llgo_cpu_profile_refresh_signal +func c_cpuProfileRefreshSignal() int32 var ( + // cpuProfileStateMu is the Go control-plane lock. It serializes native + // sampler start/stop with libuv SIGPROF watcher changes, but is never + // acquired by the asynchronous signal handler. + cpuProfileStateOnce psync.Once + cpuProfileStateMu psync.Mutex + cpuProfileRate int32 cpuProfileOpen uint32 cpuProfilePeriodPending uint32 cpuProfilePeriodRecord = [3]uint64{3, 0, 100} cpuProfilePeriodTags [1]unsafe.Pointer - cpuProfileDrainData [maxCPUProfileDrainData]uint64 - cpuProfileDrainTags [maxCPUProfileDrainTags]unsafe.Pointer + + // The native sampler fills these fixed scratch buffers in one call. The + // runtime/pprof contract permits only one reader and consumes the returned + // slices before calling readProfile again. + cpuProfileDrainData [maxCPUProfileDrainData]uint64 + cpuProfileDrainTags [maxCPUProfileDrainTags]unsafe.Pointer + cpuProfileDrainPCs [maxCPUProfileDrainRecords * maxCPUProfileStack]uintptr + cpuProfileDrainLengths [maxCPUProfileDrainRecords]uint32 ) -func cpuProfileSignalUpdateBegin(sig uint32) bool { +func ensureCPUProfileState() { + cpuProfileStateOnce.Do(func() { + cpuProfileStateMu.Init(nil) + }) +} + +func cpuProfileSignalLock(sig uint32) bool { if sig != cpuProfileSignal { return false } - c_cpuProfileSignalUpdateBegin() + ensureCPUProfileState() + cpuProfileStateMu.Lock() return true } -func cpuProfileSignalUpdateEnd(locked bool) { +func cpuProfileSignalUnlock(locked bool) { if !locked { return } - if c_cpuProfileSignalUpdateEnd() != 0 { + if c_cpuProfileRefreshSignal() != 0 { print("runtime: failed to restore CPU profiling SIGPROF handler.\n") } + cpuProfileStateMu.Unlock() } // SetCPUProfileRate starts or stops process CPU-time sampling. The signal @@ -77,6 +89,10 @@ func SetCPUProfileRate(hz int) { if hz > 1000000 { hz = 1000000 } + ensureCPUProfileState() + cpuProfileStateMu.Lock() + defer cpuProfileStateMu.Unlock() + if hz == 0 { if latomic.LoadInt32(&cpuProfileRate) != 0 { c_cpuProfileStop() @@ -113,11 +129,18 @@ func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool return cpuProfilePeriodRecord[:], cpuProfilePeriodTags[:], false } - var pcs [maxCPUProfileStack]uintptr for { - lost := c_cpuProfileTakeLost() - n := int(c_cpuProfileRead(unsafe.Pointer(&pcs[0]), maxCPUProfileStack)) - if lost != 0 || n != 0 { + var lost uint64 + var empty int32 + records := int(c_cpuProfileDrain( + unsafe.Pointer(&cpuProfileDrainPCs[0]), + unsafe.Pointer(&cpuProfileDrainLengths[0]), + maxCPUProfileDrainRecords, + maxCPUProfileStack, + &lost, + &empty, + )) + if lost != 0 || records != 0 { // runtime/pprof waits 100 ms between reads on Darwin. Drain a // chunk, rather than one sample, so a 100 Hz producer cannot // outrun the writer. @@ -129,21 +152,18 @@ func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool data = append(data, 4, 0, 0, lost) tags = append(tags, nil) } - for records := 0; n != 0; records++ { + for record := 0; record < records; record++ { + n := int(cpuProfileDrainLengths[record]) data = append(data, uint64(3+n), 0, 1) + base := record * maxCPUProfileStack for i := 0; i < n; i++ { - data = append(data, uint64(pcs[i])) + data = append(data, uint64(cpuProfileDrainPCs[base+i])) } tags = append(tags, nil) - // Do not consume a sample that cannot be emitted in this chunk. - if records+1 == maxCPUProfileDrainRecords { - break - } - n = int(c_cpuProfileRead(unsafe.Pointer(&pcs[0]), maxCPUProfileStack)) } return data, tags, false } - if latomic.LoadInt32(&cpuProfileRate) == 0 && c_cpuProfileEmpty() != 0 { + if latomic.LoadInt32(&cpuProfileRate) == 0 && empty != 0 { latomic.StoreUint32(&cpuProfileOpen, 0) return nil, nil, true } diff --git a/runtime/internal/lib/runtime/cpuprof_stub_llgo.go b/runtime/internal/lib/runtime/cpuprof_stub_llgo.go index d410278b19..907e7c46b3 100644 --- a/runtime/internal/lib/runtime/cpuprof_stub_llgo.go +++ b/runtime/internal/lib/runtime/cpuprof_stub_llgo.go @@ -4,6 +4,6 @@ package runtime func SetCPUProfileRate(hz int) {} -func cpuProfileSignalUpdateBegin(sig uint32) bool { return false } +func cpuProfileSignalLock(sig uint32) bool { return false } -func cpuProfileSignalUpdateEnd(locked bool) {} +func cpuProfileSignalUnlock(locked bool) {} diff --git a/runtime/internal/lib/runtime/signal_llgo.go b/runtime/internal/lib/runtime/signal_llgo.go index 2ae515fd93..865f67bdb9 100644 --- a/runtime/internal/lib/runtime/signal_llgo.go +++ b/runtime/internal/lib/runtime/signal_llgo.go @@ -89,12 +89,12 @@ func startSignalWatcher(sig uint32, st *sigState) { st.inited = true } var code int - locked := cpuProfileSignalUpdateBegin(sig) + locked := cpuProfileSignalLock(sig) submitTimerWork(func() bool { code = int(libuv.SignalStartRuntime(&st.handle, c.Int(sig))) return true }) - cpuProfileSignalUpdateEnd(locked) + cpuProfileSignalUnlock(locked) checkUV("uv_signal_start", code) } @@ -135,12 +135,12 @@ func signal_disable(sig uint32) { sigMu.Unlock() if doStop { var code int - locked := cpuProfileSignalUpdateBegin(sig) + locked := cpuProfileSignalLock(sig) submitTimerWork(func() bool { code = int(st.handle.Stop()) return true }) - cpuProfileSignalUpdateEnd(locked) + cpuProfileSignalUnlock(locked) checkUV("uv_signal_stop", code) } }