$ go version
go version go1.26-devel_4ab1aec007 Fri Aug 1 11:56:27 2025 -0700 darwin/arm64

The "go tool trace" command provides access to some execution tracing data in pprof format, including the calling context, count, and duration of the program's (non-raw) syscalls.

It's described in the "go tool trace" web UI as: "All four profiles represent causes of delay that prevent a goroutine from running on a logical processor: because it was waiting for the network, for a synchronization operation on a mutex or channel, for a system call, or for a logical processor to become available."

The filtering is incorrect for the syscall profile. (I haven't verified the other three profile types.) It depends on a StateTransition event into the Syscall state which includes a stack (good), but it fails to find the end of the syscall because it expects another StateTransition event out of the Syscall state which also includes a stack. The event marking the end of a syscall doesn't have a call stack, so it's not detected.

This results in most of the Syscall events being absent from the profile. Here's a reproducer demonstrating the problem: a goroutine calls hidden1 to do a single syscall and exit, another goroutine calls hidden2 to do two syscalls and then exit, and a third goroutine calls visible which does a single syscall but also takes care to generate a non-syscall-related StateTransition event which contains a stack before exiting. Only the visible goroutine ends up in the profile.

All four syscalls across all three goroutines should appear in the profile, with durations corresponding to the time between the "Running->Syscall" and "Syscall->Running" StateTransition events.

CC @mknyszek @cagedmantis

Here's the incorrect tool behavior:

$ go version
go version go1.26-devel_4ab1aec007 Fri Aug 1 11:56:27 2025 -0700 darwin/arm64

$ go run /tmp/trace-pprof-syscall.go -trace /tmp/t1

$ go tool trace -pprof=syscall /tmp/t1 > /tmp/p1

$ go tool pprof -traces /tmp/p1
Main binary filename not available.
Type: delay
-----------+-------------------------------------------------------
    6.78us   syscall.Stat
             os.statNolog.func1
             os.ignoringEINTR
             os.statNolog
             os.Stat
             main.stat
             main.visible
-----------+-------------------------------------------------------

And the relevant contents of the execution trace:

$ go run github.com/rhysh/go-tracing-toolbox/cmd/etgrep -input /tmp/t1 | grep Syscall
M=8667515904 P=0 G=8 StateTransition Time=2303192401384704 Resource=Goroutine(8) Reason="" GoID=8 Running->Syscall
M=8667515904 P=0 G=8 StateTransition Time=2303192401390912 Resource=Goroutine(8) Reason="" GoID=8 Syscall->Running
M=8667515904 P=0 G=7 StateTransition Time=2303192401392832 Resource=Goroutine(7) Reason="" GoID=7 Running->Syscall
M=8667515904 P=0 G=7 StateTransition Time=2303192401395584 Resource=Goroutine(7) Reason="" GoID=7 Syscall->Running
M=8667515904 P=0 G=7 StateTransition Time=2303192401396160 Resource=Goroutine(7) Reason="" GoID=7 Running->Syscall
M=6161674240 P=2 G=6 StateTransition Time=2303192401396416 Resource=Goroutine(6) Reason="" GoID=6 Running->Syscall
M=8667515904 P=0 G=7 StateTransition Time=2303192401398656 Resource=Goroutine(7) Reason="" GoID=7 Syscall->Running
M=6161674240 P=2 G=6 StateTransition Time=2303192401403584 Resource=Goroutine(6) Reason="" GoID=6 Syscall->Running

$ go run github.com/rhysh/go-tracing-toolbox/cmd/etgrep -input /tmp/t1 -stacks -goroutine 6
M=6161674240 P=2 G=-1 StateTransition Time=2303192401381120 Resource=Goroutine(6) Reason="" GoID=6 Runnable->Running
M=6161674240 P=2 G=6 Metric Time=2303192401393856 Name="/memory/classes/heap/objects:bytes" Value=Value{Uint64(2752512)}
M=6161674240 P=2 G=6 Metric Time=2303192401395392 Name="/memory/classes/heap/objects:bytes" Value=Value{Uint64(2760704)}
M=6161674240 P=2 G=6 StateTransition Time=2303192401396416 Resource=Goroutine(6) Reason="" GoID=6 Running->Syscall
  100c79577 syscall.Stat /usr/local/go/src/syscall/zsyscall_darwin_arm64.go:1963
  100c8843f os.statNolog.func1 /usr/local/go/src/os/stat_unix.go:32
  100c88430 os.ignoringEINTR /usr/local/go/src/os/file_posix.go:256
  100c88420 os.statNolog /usr/local/go/src/os/stat_unix.go:31
  100c8807f os.Stat /usr/local/go/src/os/stat.go:13
  100c9a707 main.stat /tmp/trace-pprof-syscall.go:38
  100c9a76b main.hidden1 /tmp/trace-pprof-syscall.go:45
M=6161674240 P=2 G=6 StateTransition Time=2303192401403584 Resource=Goroutine(6) Reason="" GoID=6 Syscall->Running
M=6161674240 P=2 G=6 StateTransition Time=2303192401403968 Resource=Goroutine(6) Reason="" GoID=6 Running->NotExist

$ go run github.com/rhysh/go-tracing-toolbox/cmd/etgrep -input /tmp/t1 -stacks -goroutine 7
M=8667515904 P=0 G=-1 StateTransition Time=2303192401392256 Resource=Goroutine(7) Reason="" GoID=7 Runnable->Running
M=8667515904 P=0 G=7 StateTransition Time=2303192401392832 Resource=Goroutine(7) Reason="" GoID=7 Running->Syscall
  100c79577 syscall.Stat /usr/local/go/src/syscall/zsyscall_darwin_arm64.go:1963
  100c8843f os.statNolog.func1 /usr/local/go/src/os/stat_unix.go:32
  100c88430 os.ignoringEINTR /usr/local/go/src/os/file_posix.go:256
  100c88420 os.statNolog /usr/local/go/src/os/stat_unix.go:31
  100c8807f os.Stat /usr/local/go/src/os/stat.go:13
  100c9a707 main.stat /tmp/trace-pprof-syscall.go:38
  100c9a7ab main.hidden2 /tmp/trace-pprof-syscall.go:49
M=8667515904 P=0 G=7 StateTransition Time=2303192401395584 Resource=Goroutine(7) Reason="" GoID=7 Syscall->Running
M=8667515904 P=0 G=7 StateTransition Time=2303192401396160 Resource=Goroutine(7) Reason="" GoID=7 Running->Syscall
  100c79577 syscall.Stat /usr/local/go/src/syscall/zsyscall_darwin_arm64.go:1963
  100c8843f os.statNolog.func1 /usr/local/go/src/os/stat_unix.go:32
  100c88430 os.ignoringEINTR /usr/local/go/src/os/file_posix.go:256
  100c88420 os.statNolog /usr/local/go/src/os/stat_unix.go:31
  100c8807f os.Stat /usr/local/go/src/os/stat.go:13
  100c9a707 main.stat /tmp/trace-pprof-syscall.go:38
  100c9a7af main.hidden2 /tmp/trace-pprof-syscall.go:50
M=8667515904 P=0 G=7 StateTransition Time=2303192401398656 Resource=Goroutine(7) Reason="" GoID=7 Syscall->Running
M=8667515904 P=0 G=7 StateTransition Time=2303192401398912 Resource=Goroutine(7) Reason="" GoID=7 Running->NotExist

$ go run github.com/rhysh/go-tracing-toolbox/cmd/etgrep -input /tmp/t1 -stacks -goroutine 8
M=8667515904 P=0 G=-1 StateTransition Time=2303192401383872 Resource=Goroutine(8) Reason="" GoID=8 Runnable->Running
M=8667515904 P=0 G=8 StateTransition Time=2303192401384704 Resource=Goroutine(8) Reason="" GoID=8 Running->Syscall
  100c79577 syscall.Stat /usr/local/go/src/syscall/zsyscall_darwin_arm64.go:1963
  100c8843f os.statNolog.func1 /usr/local/go/src/os/stat_unix.go:32
  100c88430 os.ignoringEINTR /usr/local/go/src/os/file_posix.go:256
  100c88420 os.statNolog /usr/local/go/src/os/stat_unix.go:31
  100c8807f os.Stat /usr/local/go/src/os/stat.go:13
  100c9a707 main.stat /tmp/trace-pprof-syscall.go:38
  100c9a7eb main.visible /tmp/trace-pprof-syscall.go:54
M=8667515904 P=0 G=8 StateTransition Time=2303192401390912 Resource=Goroutine(8) Reason="" GoID=8 Syscall->Running
M=8667515904 P=0 G=8 StateTransition Time=2303192401391488 Resource=Goroutine(8) Reason="sleep" GoID=8 Running->Waiting
  100c5d5ef time.Sleep /usr/local/go/src/runtime/time.go:363
  100c9a7f7 main.visible /tmp/trace-pprof-syscall.go:55
M=6160527360 P=3 G=-1 StateTransition Time=2303192402542400 Resource=Goroutine(8) Reason="" GoID=8 Waiting->Runnable
M=6160527360 P=3 G=-1 StateTransition Time=2303192402545536 Resource=Goroutine(8) Reason="" GoID=8 Runnable->Running
M=6160527360 P=3 G=8 StateTransition Time=2303192402545856 Resource=Goroutine(8) Reason="" GoID=8 Running->NotExist

And the reproducer:

$ cat /tmp/trace-pprof-syscall.go
package main

import (
        "bytes"
        "flag"
        "log"
        "os"
        "runtime/trace"
        "time"
)

func main() {
        out := flag.String("trace", "", "Path to execution trace file")
        flag.Parse()

        var buf bytes.Buffer
        err := trace.Start(&buf)
        if err != nil {
                log.Fatalf("trace.Start: %v", err)
        }
        defer func() {
                trace.Stop()

                err = os.WriteFile(*out, buf.Bytes(), 0600)
                if err != nil {
                        log.Fatalf("os.WriteFile: %v", err)
                }
        }()

        go hidden1()
        go hidden2()
        go visible()

        time.Sleep(10 * time.Millisecond)
}

func stat() {
        _, err := os.Stat(".")
        if err != nil {
                log.Fatalf("os.Stat: %v", err)
        }
}

func hidden1() {
        stat()
}

func hidden2() {
        stat()
        stat()
}

func visible() {
        stat()
        time.Sleep(1 * time.Millisecond)
}

Comment From: gabyhelp

Related Issues

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)