package trace
import (
"cmp"
"slices"
"strings"
"time"
)
type Summary struct {
Goroutines map [GoID ]*GoroutineSummary
Tasks map [TaskID ]*UserTaskSummary
}
type GoroutineSummary struct {
ID GoID
Name string
PC uint64
CreationTime Time
StartTime Time
EndTime Time
Regions []*UserRegionSummary
GoroutineExecStats
*goroutineSummary
}
type UserTaskSummary struct {
ID TaskID
Name string
Parent *UserTaskSummary
Children []*UserTaskSummary
Start *Event
End *Event
Logs []*Event
Regions []*UserRegionSummary
Goroutines map [GoID ]*GoroutineSummary
}
func (s *UserTaskSummary ) Complete () bool {
return s .Start != nil && s .End != nil
}
func (s *UserTaskSummary ) Descendents () []*UserTaskSummary {
descendents := []*UserTaskSummary {s }
for _ , child := range s .Children {
descendents = append (descendents , child .Descendents ()...)
}
return descendents
}
type UserRegionSummary struct {
TaskID TaskID
Name string
Start *Event
End *Event
GoroutineExecStats
}
type GoroutineExecStats struct {
ExecTime time .Duration
SchedWaitTime time .Duration
BlockTimeByReason map [string ]time .Duration
SyscallTime time .Duration
SyscallBlockTime time .Duration
TotalTime time .Duration
RangeTime map [string ]time .Duration
}
func (s GoroutineExecStats ) NonOverlappingStats () map [string ]time .Duration {
stats := map [string ]time .Duration {
"Execution time" : s .ExecTime ,
"Sched wait time" : s .SchedWaitTime ,
"Syscall execution time" : s .SyscallTime ,
"Block time (syscall)" : s .SyscallBlockTime ,
"Unknown time" : s .UnknownTime (),
}
for reason , dt := range s .BlockTimeByReason {
stats ["Block time (" +reason +")" ] += dt
}
return stats
}
func (s GoroutineExecStats ) UnknownTime () time .Duration {
sum := s .ExecTime + s .SchedWaitTime + s .SyscallTime +
s .SyscallBlockTime
for _ , dt := range s .BlockTimeByReason {
sum += dt
}
if sum < s .TotalTime {
return s .TotalTime - sum
}
return 0
}
func (s GoroutineExecStats ) sub (v GoroutineExecStats ) (r GoroutineExecStats ) {
r = s .clone ()
r .ExecTime -= v .ExecTime
r .SchedWaitTime -= v .SchedWaitTime
for reason := range s .BlockTimeByReason {
r .BlockTimeByReason [reason ] -= v .BlockTimeByReason [reason ]
}
r .SyscallTime -= v .SyscallTime
r .SyscallBlockTime -= v .SyscallBlockTime
r .TotalTime -= v .TotalTime
for name := range s .RangeTime {
r .RangeTime [name ] -= v .RangeTime [name ]
}
return r
}
func (s GoroutineExecStats ) clone () (r GoroutineExecStats ) {
r = s
r .BlockTimeByReason = make (map [string ]time .Duration )
for reason , dt := range s .BlockTimeByReason {
r .BlockTimeByReason [reason ] = dt
}
r .RangeTime = make (map [string ]time .Duration )
for name , dt := range s .RangeTime {
r .RangeTime [name ] = dt
}
return r
}
func (g *GoroutineSummary ) snapshotStat (lastTs Time ) (ret GoroutineExecStats ) {
ret = g .GoroutineExecStats .clone ()
if g .goroutineSummary == nil {
return ret
}
if g .TotalTime == 0 {
ret .TotalTime = lastTs .Sub (g .CreationTime )
}
if g .lastStartTime != 0 {
ret .ExecTime += lastTs .Sub (g .lastStartTime )
}
if g .lastRunnableTime != 0 {
ret .SchedWaitTime += lastTs .Sub (g .lastRunnableTime )
}
if g .lastBlockTime != 0 {
ret .BlockTimeByReason [g .lastBlockReason ] += lastTs .Sub (g .lastBlockTime )
}
if g .lastSyscallTime != 0 {
ret .SyscallTime += lastTs .Sub (g .lastSyscallTime )
}
if g .lastSyscallBlockTime != 0 {
ret .SchedWaitTime += lastTs .Sub (g .lastSyscallBlockTime )
}
for name , ts := range g .lastRangeTime {
ret .RangeTime [name ] += lastTs .Sub (ts )
}
return ret
}
func (g *GoroutineSummary ) finalize (lastTs Time , trigger *Event ) {
if trigger != nil {
g .EndTime = trigger .Time ()
}
finalStat := g .snapshotStat (lastTs )
g .GoroutineExecStats = finalStat
if !IsSystemGoroutine (g .Name ) {
for _ , s := range g .activeRegions {
s .End = trigger
s .GoroutineExecStats = finalStat .sub (s .GoroutineExecStats )
g .Regions = append (g .Regions , s )
}
}
*(g .goroutineSummary ) = goroutineSummary {}
}
type goroutineSummary struct {
lastStartTime Time
lastRunnableTime Time
lastBlockTime Time
lastBlockReason string
lastSyscallTime Time
lastSyscallBlockTime Time
lastRangeTime map [string ]Time
activeRegions []*UserRegionSummary
}
type Summarizer struct {
gs map [GoID ]*GoroutineSummary
tasks map [TaskID ]*UserTaskSummary
syscallingP map [ProcID ]GoID
syscallingG map [GoID ]ProcID
rangesP map [rangeP ]GoID
lastTs Time
syncTs Time
}
func NewSummarizer () *Summarizer {
return &Summarizer {
gs : make (map [GoID ]*GoroutineSummary ),
tasks : make (map [TaskID ]*UserTaskSummary ),
syscallingP : make (map [ProcID ]GoID ),
syscallingG : make (map [GoID ]ProcID ),
rangesP : make (map [rangeP ]GoID ),
}
}
type rangeP struct {
id ProcID
name string
}
func (s *Summarizer ) Event (ev *Event ) {
if s .syncTs == 0 {
s .syncTs = ev .Time ()
}
s .lastTs = ev .Time ()
switch ev .Kind () {
case EventSync :
s .syncTs = ev .Time ()
case EventStateTransition :
st := ev .StateTransition ()
switch st .Resource .Kind {
case ResourceGoroutine :
id := st .Resource .Goroutine ()
old , new := st .Goroutine ()
if old == new {
break
}
g := s .gs [id ]
switch old {
case GoUndetermined , GoNotExist :
g = &GoroutineSummary {ID : id , goroutineSummary : &goroutineSummary {}}
if old == GoUndetermined {
g .CreationTime = s .syncTs
} else {
g .CreationTime = ev .Time ()
}
g .lastRangeTime = make (map [string ]Time )
g .BlockTimeByReason = make (map [string ]time .Duration )
g .RangeTime = make (map [string ]time .Duration )
if creatorG := s .gs [ev .Goroutine ()]; creatorG != nil && len (creatorG .activeRegions ) > 0 {
regions := creatorG .activeRegions
s := regions [len (regions )-1 ]
g .activeRegions = []*UserRegionSummary {{TaskID : s .TaskID , Start : ev }}
}
s .gs [g .ID ] = g
case GoRunning :
g .ExecTime += ev .Time ().Sub (g .lastStartTime )
g .lastStartTime = 0
case GoWaiting :
if g .lastBlockTime != 0 {
g .BlockTimeByReason [g .lastBlockReason ] += ev .Time ().Sub (g .lastBlockTime )
g .lastBlockTime = 0
}
case GoRunnable :
if g .lastRunnableTime != 0 {
g .SchedWaitTime += ev .Time ().Sub (g .lastRunnableTime )
g .lastRunnableTime = 0
}
case GoSyscall :
if g .lastSyscallTime != 0 {
if g .lastSyscallBlockTime != 0 {
g .SyscallBlockTime += ev .Time ().Sub (g .lastSyscallBlockTime )
g .SyscallTime += g .lastSyscallBlockTime .Sub (g .lastSyscallTime )
} else {
g .SyscallTime += ev .Time ().Sub (g .lastSyscallTime )
}
g .lastSyscallTime = 0
g .lastSyscallBlockTime = 0
delete (s .syscallingP , s .syscallingG [id ])
delete (s .syscallingG , id )
}
}
if g .Name == "" {
stk := st .Stack
if stk != NoStack {
var frame StackFrame
var ok bool
stk .Frames (func (f StackFrame ) bool {
frame = f
ok = true
return true
})
if ok {
g .PC = frame .PC
g .Name = frame .Func
}
}
}
switch new {
case GoRunning :
g .lastStartTime = ev .Time ()
if g .StartTime == 0 {
g .StartTime = ev .Time ()
}
case GoRunnable :
g .lastRunnableTime = ev .Time ()
case GoWaiting :
if st .Reason != "forever" {
g .lastBlockTime = ev .Time ()
g .lastBlockReason = st .Reason
break
}
fallthrough
case GoNotExist :
g .finalize (ev .Time (), ev )
case GoSyscall :
s .syscallingP [ev .Proc ()] = id
s .syscallingG [id ] = ev .Proc ()
g .lastSyscallTime = ev .Time ()
}
case ResourceProc :
id := st .Resource .Proc ()
old , new := st .Proc ()
if old != new && new == ProcIdle {
if goid , ok := s .syscallingP [id ]; ok {
g := s .gs [goid ]
g .lastSyscallBlockTime = ev .Time ()
delete (s .syscallingP , id )
}
}
}
case EventRangeBegin , EventRangeActive :
r := ev .Range ()
var g *GoroutineSummary
switch r .Scope .Kind {
case ResourceGoroutine :
g = s .gs [r .Scope .Goroutine ()]
case ResourceProc :
g = s .gs [ev .Goroutine ()]
if g != nil {
s .rangesP [rangeP {id : r .Scope .Proc (), name : r .Name }] = ev .Goroutine ()
}
}
if g == nil {
break
}
if ev .Kind () == EventRangeActive {
if ts := g .lastRangeTime [r .Name ]; ts != 0 {
g .RangeTime [r .Name ] += s .syncTs .Sub (ts )
}
g .lastRangeTime [r .Name ] = s .syncTs
} else {
g .lastRangeTime [r .Name ] = ev .Time ()
}
case EventRangeEnd :
r := ev .Range ()
var g *GoroutineSummary
switch r .Scope .Kind {
case ResourceGoroutine :
g = s .gs [r .Scope .Goroutine ()]
case ResourceProc :
rp := rangeP {id : r .Scope .Proc (), name : r .Name }
if goid , ok := s .rangesP [rp ]; ok {
if goid == ev .Goroutine () {
g = s .gs [goid ]
}
delete (s .rangesP , rp )
}
}
if g == nil {
break
}
ts := g .lastRangeTime [r .Name ]
if ts == 0 {
break
}
g .RangeTime [r .Name ] += ev .Time ().Sub (ts )
delete (g .lastRangeTime , r .Name )
case EventRegionBegin :
g := s .gs [ev .Goroutine ()]
r := ev .Region ()
region := &UserRegionSummary {
Name : r .Type ,
TaskID : r .Task ,
Start : ev ,
GoroutineExecStats : g .snapshotStat (ev .Time ()),
}
g .activeRegions = append (g .activeRegions , region )
task := s .getOrAddTask (r .Task )
task .Regions = append (task .Regions , region )
task .Goroutines [g .ID ] = g
case EventRegionEnd :
g := s .gs [ev .Goroutine ()]
r := ev .Region ()
var sd *UserRegionSummary
if regionStk := g .activeRegions ; len (regionStk ) > 0 {
n := len (regionStk )
sd = regionStk [n -1 ]
regionStk = regionStk [:n -1 ]
g .activeRegions = regionStk
} else {
sd = &UserRegionSummary {Name : r .Type , TaskID : r .Task }
task := s .getOrAddTask (r .Task )
task .Goroutines [g .ID ] = g
task .Regions = append (task .Regions , sd )
}
sd .GoroutineExecStats = g .snapshotStat (ev .Time ()).sub (sd .GoroutineExecStats )
sd .End = ev
g .Regions = append (g .Regions , sd )
case EventTaskBegin , EventTaskEnd :
t := ev .Task ()
task := s .getOrAddTask (t .ID )
task .Name = t .Type
task .Goroutines [ev .Goroutine ()] = s .gs [ev .Goroutine ()]
if ev .Kind () == EventTaskBegin {
task .Start = ev
} else {
task .End = ev
}
if t .Parent != NoTask && task .Parent == nil {
parent := s .getOrAddTask (t .Parent )
task .Parent = parent
parent .Children = append (parent .Children , task )
}
case EventLog :
log := ev .Log ()
task := s .getOrAddTask (log .Task )
task .Goroutines [ev .Goroutine ()] = s .gs [ev .Goroutine ()]
task .Logs = append (task .Logs , ev )
}
}
func (s *Summarizer ) getOrAddTask (id TaskID ) *UserTaskSummary {
task := s .tasks [id ]
if task == nil {
task = &UserTaskSummary {ID : id , Goroutines : make (map [GoID ]*GoroutineSummary )}
s .tasks [id ] = task
}
return task
}
func (s *Summarizer ) Finalize () *Summary {
for _ , g := range s .gs {
g .finalize (s .lastTs , nil )
slices .SortFunc (g .Regions , func (a , b *UserRegionSummary ) int {
x := a .Start
y := b .Start
if x == nil {
if y == nil {
return 0
}
return -1
}
if y == nil {
return +1
}
return cmp .Compare (x .Time (), y .Time ())
})
g .goroutineSummary = nil
}
return &Summary {
Goroutines : s .gs ,
Tasks : s .tasks ,
}
}
func RelatedGoroutinesV2 (events []Event , goid GoID ) map [GoID ]struct {} {
type unblockEdge struct {
operator GoID
operand GoID
}
var unblockEdges []unblockEdge
for _ , ev := range events {
if ev .Goroutine () == NoGoroutine {
continue
}
if ev .Kind () != EventStateTransition {
continue
}
st := ev .StateTransition ()
if st .Resource .Kind != ResourceGoroutine {
continue
}
id := st .Resource .Goroutine ()
old , new := st .Goroutine ()
if old == new || old != GoWaiting {
continue
}
unblockEdges = append (unblockEdges , unblockEdge {
operator : ev .Goroutine (),
operand : id ,
})
}
gmap := make (map [GoID ]struct {})
gmap [goid ] = struct {}{}
for i := 0 ; i < 2 ; i ++ {
gmap1 := make (map [GoID ]struct {})
for g := range gmap {
gmap1 [g ] = struct {}{}
}
for _ , edge := range unblockEdges {
if _ , ok := gmap [edge .operand ]; ok {
gmap1 [edge .operator ] = struct {}{}
}
}
gmap = gmap1
}
return gmap
}
func IsSystemGoroutine (entryFn string ) bool {
return entryFn == "" || entryFn != "runtime.main" && strings .HasPrefix (entryFn , "runtime." )
}
The pages are generated with Golds v0.7.0-preview . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .