package trace
import (
"bufio"
"bytes"
"cmp"
"encoding/binary"
"fmt"
"io"
"slices"
"strings"
"time"
"internal/trace/tracev2"
"internal/trace/version"
)
type generation struct {
gen uint64
batches map [ThreadID ][]batch
batchMs []ThreadID
cpuSamples []cpuSample
minTs timestamp
*evTable
}
type spilledBatch struct {
gen uint64
*batch
}
func readGeneration(r *bufio .Reader , spill *spilledBatch , ver version .Version ) (*generation , *spilledBatch , error ) {
g := &generation {
evTable : &evTable {
pcs : make (map [uint64 ]frame ),
},
batches : make (map [ThreadID ][]batch ),
}
if spill != nil {
g .gen = spill .gen
g .minTs = spill .batch .time
if err := processBatch (g , *spill .batch , ver ); err != nil {
return nil , nil , err
}
spill = nil
}
var spillErr error
for {
b , gen , err := readBatch (r )
if err == io .EOF {
break
}
if err != nil {
if g .gen != 0 {
spillErr = err
break
}
return nil , nil , err
}
if gen == 0 {
return nil , nil , fmt .Errorf ("invalid generation number %d" , gen )
}
if g .gen == 0 {
g .gen = gen
}
if gen == g .gen +1 {
spill = &spilledBatch {gen : gen , batch : &b }
break
}
if gen != g .gen {
return nil , nil , fmt .Errorf ("generations out of order" )
}
if g .minTs == 0 || b .time < g .minTs {
g .minTs = b .time
}
if err := processBatch (g , b , ver ); err != nil {
return nil , nil , err
}
}
if g .freq == 0 {
return nil , nil , fmt .Errorf ("no frequency event found" )
}
if ver >= version .Go125 && !g .hasClockSnapshot {
return nil , nil , fmt .Errorf ("no clock snapshot event found" )
}
g .stacks .compactify ()
g .strings .compactify ()
if err := validateStackStrings (&g .stacks , &g .strings , g .pcs ); err != nil {
return nil , nil , err
}
for i := range g .cpuSamples {
s := &g .cpuSamples [i ]
s .time = g .freq .mul (timestamp (s .time ))
}
slices .SortFunc (g .cpuSamples , func (a , b cpuSample ) int {
return cmp .Compare (a .time , b .time )
})
return g , spill , spillErr
}
func processBatch(g *generation , b batch , ver version .Version ) error {
switch {
case b .isStringsBatch ():
if err := addStrings (&g .strings , b ); err != nil {
return err
}
case b .isStacksBatch ():
if err := addStacks (&g .stacks , g .pcs , b ); err != nil {
return err
}
case b .isCPUSamplesBatch ():
samples , err := addCPUSamples (g .cpuSamples , b )
if err != nil {
return err
}
g .cpuSamples = samples
case b .isSyncBatch (ver ):
if err := setSyncBatch (&g .sync , b , ver ); err != nil {
return err
}
case b .exp != tracev2 .NoExperiment :
if g .expBatches == nil {
g .expBatches = make (map [tracev2 .Experiment ][]ExperimentalBatch )
}
if err := addExperimentalBatch (g .expBatches , b ); err != nil {
return err
}
default :
if _ , ok := g .batches [b .m ]; !ok {
g .batchMs = append (g .batchMs , b .m )
}
g .batches [b .m ] = append (g .batches [b .m ], b )
}
return nil
}
func validateStackStrings(
stacks *dataTable [stackID , stack ],
strings *dataTable [stringID , string ],
frames map [uint64 ]frame ,
) error {
var err error
stacks .forEach (func (id stackID , stk stack ) bool {
for _ , pc := range stk .pcs {
frame , ok := frames [pc ]
if !ok {
err = fmt .Errorf ("found unknown pc %x for stack %d" , pc , id )
return false
}
_, ok = strings .get (frame .funcID )
if !ok {
err = fmt .Errorf ("found invalid func string ID %d for stack %d" , frame .funcID , id )
return false
}
_, ok = strings .get (frame .fileID )
if !ok {
err = fmt .Errorf ("found invalid file string ID %d for stack %d" , frame .fileID , id )
return false
}
}
return true
})
return err
}
func addStrings(stringTable *dataTable [stringID , string ], b batch ) error {
if !b .isStringsBatch () {
return fmt .Errorf ("internal error: addStrings called on non-string batch" )
}
r := bytes .NewReader (b .data )
hdr , err := r .ReadByte ()
if err != nil || tracev2 .EventType (hdr ) != tracev2 .EvStrings {
return fmt .Errorf ("missing strings batch header" )
}
var sb strings .Builder
for r .Len () != 0 {
ev , err := r .ReadByte ()
if err != nil {
return err
}
if tracev2 .EventType (ev ) != tracev2 .EvString {
return fmt .Errorf ("expected string event, got %d" , ev )
}
id , err := binary .ReadUvarint (r )
if err != nil {
return err
}
len , err := binary .ReadUvarint (r )
if err != nil {
return err
}
if len > tracev2 .MaxEventTrailerDataSize {
return fmt .Errorf ("invalid string size %d, maximum is %d" , len , tracev2 .MaxEventTrailerDataSize )
}
n , err := io .CopyN (&sb , r , int64 (len ))
if n != int64 (len ) {
return fmt .Errorf ("failed to read full string: read %d but wanted %d" , n , len )
}
if err != nil {
return fmt .Errorf ("copying string data: %w" , err )
}
s := sb .String ()
sb .Reset ()
if err := stringTable .insert (stringID (id ), s ); err != nil {
return err
}
}
return nil
}
func addStacks(stackTable *dataTable [stackID , stack ], pcs map [uint64 ]frame , b batch ) error {
if !b .isStacksBatch () {
return fmt .Errorf ("internal error: addStacks called on non-stacks batch" )
}
r := bytes .NewReader (b .data )
hdr , err := r .ReadByte ()
if err != nil || tracev2 .EventType (hdr ) != tracev2 .EvStacks {
return fmt .Errorf ("missing stacks batch header" )
}
for r .Len () != 0 {
ev , err := r .ReadByte ()
if err != nil {
return err
}
if tracev2 .EventType (ev ) != tracev2 .EvStack {
return fmt .Errorf ("expected stack event, got %d" , ev )
}
id , err := binary .ReadUvarint (r )
if err != nil {
return err
}
nFrames , err := binary .ReadUvarint (r )
if err != nil {
return err
}
if nFrames > tracev2 .MaxFramesPerStack {
return fmt .Errorf ("invalid stack size %d, maximum is %d" , nFrames , tracev2 .MaxFramesPerStack )
}
frames := make ([]uint64 , 0 , nFrames )
for i := uint64 (0 ); i < nFrames ; i ++ {
pc , err := binary .ReadUvarint (r )
if err != nil {
return fmt .Errorf ("reading frame %d's PC for stack %d: %w" , i +1 , id , err )
}
funcID , err := binary .ReadUvarint (r )
if err != nil {
return fmt .Errorf ("reading frame %d's funcID for stack %d: %w" , i +1 , id , err )
}
fileID , err := binary .ReadUvarint (r )
if err != nil {
return fmt .Errorf ("reading frame %d's fileID for stack %d: %w" , i +1 , id , err )
}
line , err := binary .ReadUvarint (r )
if err != nil {
return fmt .Errorf ("reading frame %d's line for stack %d: %w" , i +1 , id , err )
}
frames = append (frames , pc )
if _ , ok := pcs [pc ]; !ok {
pcs [pc ] = frame {
pc : pc ,
funcID : stringID (funcID ),
fileID : stringID (fileID ),
line : line ,
}
}
}
if err := stackTable .insert (stackID (id ), stack {pcs : frames }); err != nil {
return err
}
}
return nil
}
func addCPUSamples(samples []cpuSample , b batch ) ([]cpuSample , error ) {
if !b .isCPUSamplesBatch () {
return nil , fmt .Errorf ("internal error: addCPUSamples called on non-CPU-sample batch" )
}
r := bytes .NewReader (b .data )
hdr , err := r .ReadByte ()
if err != nil || tracev2 .EventType (hdr ) != tracev2 .EvCPUSamples {
return nil , fmt .Errorf ("missing CPU samples batch header" )
}
for r .Len () != 0 {
ev , err := r .ReadByte ()
if err != nil {
return nil , err
}
if tracev2 .EventType (ev ) != tracev2 .EvCPUSample {
return nil , fmt .Errorf ("expected CPU sample event, got %d" , ev )
}
ts , err := binary .ReadUvarint (r )
if err != nil {
return nil , err
}
m , err := binary .ReadUvarint (r )
if err != nil {
return nil , err
}
mid := ThreadID (m )
p , err := binary .ReadUvarint (r )
if err != nil {
return nil , err
}
pid := ProcID (p )
g , err := binary .ReadUvarint (r )
if err != nil {
return nil , err
}
goid := GoID (g )
if g == 0 {
goid = NoGoroutine
}
s , err := binary .ReadUvarint (r )
if err != nil {
return nil , err
}
samples = append (samples , cpuSample {
schedCtx : schedCtx {
M : mid ,
P : pid ,
G : goid ,
},
time : Time (ts ),
stack : stackID (s ),
})
}
return samples , nil
}
type sync struct {
freq frequency
hasClockSnapshot bool
snapTime timestamp
snapMono uint64
snapWall time .Time
}
func setSyncBatch(s *sync , b batch , ver version .Version ) error {
if !b .isSyncBatch (ver ) {
return fmt .Errorf ("internal error: setSyncBatch called on non-sync batch" )
}
r := bytes .NewReader (b .data )
if ver >= version .Go125 {
hdr , err := r .ReadByte ()
if err != nil || tracev2 .EventType (hdr ) != tracev2 .EvSync {
return fmt .Errorf ("missing sync batch header" )
}
}
lastTs := b .time
for r .Len () != 0 {
ev , err := r .ReadByte ()
if err != nil {
return err
}
et := tracev2 .EventType (ev )
switch {
case et == tracev2 .EvFrequency :
if s .freq != 0 {
return fmt .Errorf ("found multiple frequency events" )
}
f , err := binary .ReadUvarint (r )
if err != nil {
return err
}
s .freq = frequency (1.0 / (float64 (f ) / 1e9 ))
case et == tracev2 .EvClockSnapshot && ver >= version .Go125 :
if s .hasClockSnapshot {
return fmt .Errorf ("found multiple clock snapshot events" )
}
s .hasClockSnapshot = true
tdiff , err := binary .ReadUvarint (r )
if err != nil {
return err
}
lastTs += timestamp (tdiff )
s .snapTime = lastTs
mono , err := binary .ReadUvarint (r )
if err != nil {
return err
}
s .snapMono = mono
sec , err := binary .ReadUvarint (r )
if err != nil {
return err
}
nsec , err := binary .ReadUvarint (r )
if err != nil {
return err
}
s .snapWall = time .Unix (int64 (sec ), int64 (nsec ))
default :
return fmt .Errorf ("expected frequency or clock snapshot event, got %d" , ev )
}
}
return nil
}
func addExperimentalBatch(expBatches map [tracev2 .Experiment ][]ExperimentalBatch , b batch ) error {
if b .exp == tracev2 .NoExperiment {
return fmt .Errorf ("internal error: addExperimentalBatch called on non-experimental batch" )
}
expBatches [b .exp ] = append (expBatches [b .exp ], ExperimentalBatch {
Thread : b .m ,
Data : b .data ,
})
return nil
}
The pages are generated with Golds v0.7.7-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 .