123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- package goja
- import (
- "errors"
- "io"
- "strconv"
- "sync"
- "sync/atomic"
- "time"
- "github.com/google/pprof/profile"
- )
- const profInterval = 10 * time.Millisecond
- const profMaxStackDepth = 64
- const (
- profReqNone int32 = iota
- profReqDoSample
- profReqSampleReady
- profReqStop
- )
- type _globalProfiler struct {
- p profiler
- w io.Writer
- enabled int32
- }
- var globalProfiler _globalProfiler
- type profTracker struct {
- req, finished int32
- start, stop time.Time
- numFrames int
- frames [profMaxStackDepth]StackFrame
- }
- type profiler struct {
- mu sync.Mutex
- trackers []*profTracker
- buf *profBuffer
- running bool
- }
- type profFunc struct {
- f profile.Function
- locs map[int32]*profile.Location
- }
- type profSampleNode struct {
- loc *profile.Location
- sample *profile.Sample
- parent *profSampleNode
- children map[*profile.Location]*profSampleNode
- }
- type profBuffer struct {
- funcs map[*Program]*profFunc
- root profSampleNode
- }
- func (pb *profBuffer) addSample(pt *profTracker) {
- sampleFrames := pt.frames[:pt.numFrames]
- n := &pb.root
- for j := len(sampleFrames) - 1; j >= 0; j-- {
- frame := sampleFrames[j]
- if frame.prg == nil {
- continue
- }
- var f *profFunc
- if f = pb.funcs[frame.prg]; f == nil {
- f = &profFunc{
- locs: make(map[int32]*profile.Location),
- }
- if pb.funcs == nil {
- pb.funcs = make(map[*Program]*profFunc)
- }
- pb.funcs[frame.prg] = f
- }
- var loc *profile.Location
- if loc = f.locs[int32(frame.pc)]; loc == nil {
- loc = &profile.Location{}
- f.locs[int32(frame.pc)] = loc
- }
- if nn := n.children[loc]; nn == nil {
- if n.children == nil {
- n.children = make(map[*profile.Location]*profSampleNode, 1)
- }
- nn = &profSampleNode{
- parent: n,
- loc: loc,
- }
- n.children[loc] = nn
- n = nn
- } else {
- n = nn
- }
- }
- smpl := n.sample
- if smpl == nil {
- locs := make([]*profile.Location, 0, len(sampleFrames))
- for n1 := n; n1.loc != nil; n1 = n1.parent {
- locs = append(locs, n1.loc)
- }
- smpl = &profile.Sample{
- Location: locs,
- Value: make([]int64, 2),
- }
- n.sample = smpl
- }
- smpl.Value[0]++
- smpl.Value[1] += int64(pt.stop.Sub(pt.start))
- }
- func (pb *profBuffer) profile() *profile.Profile {
- pr := profile.Profile{}
- pr.SampleType = []*profile.ValueType{
- {Type: "samples", Unit: "count"},
- {Type: "cpu", Unit: "nanoseconds"},
- }
- pr.PeriodType = pr.SampleType[1]
- pr.Period = int64(profInterval)
- mapping := &profile.Mapping{
- ID: 1,
- File: "[ECMAScript code]",
- }
- pr.Mapping = make([]*profile.Mapping, 1, len(pb.funcs)+1)
- pr.Mapping[0] = mapping
- pr.Function = make([]*profile.Function, 0, len(pb.funcs))
- funcNames := make(map[string]struct{})
- var funcId, locId uint64
- for prg, f := range pb.funcs {
- fileName := prg.src.Name()
- funcId++
- f.f.ID = funcId
- f.f.Filename = fileName
- var funcName string
- if prg.funcName != "" {
- funcName = prg.funcName.String()
- } else {
- funcName = "<anonymous>"
- }
- // Make sure the function name is unique, otherwise the graph display merges them into one node, even
- // if they are in different mappings.
- if _, exists := funcNames[funcName]; exists {
- funcName += "." + strconv.FormatUint(f.f.ID, 10)
- } else {
- funcNames[funcName] = struct{}{}
- }
- f.f.Name = funcName
- pr.Function = append(pr.Function, &f.f)
- for pc, loc := range f.locs {
- locId++
- loc.ID = locId
- pos := prg.src.Position(prg.sourceOffset(int(pc)))
- loc.Line = []profile.Line{
- {
- Function: &f.f,
- Line: int64(pos.Line),
- },
- }
- loc.Mapping = mapping
- pr.Location = append(pr.Location, loc)
- }
- }
- pb.addSamples(&pr, &pb.root)
- return &pr
- }
- func (pb *profBuffer) addSamples(p *profile.Profile, n *profSampleNode) {
- if n.sample != nil {
- p.Sample = append(p.Sample, n.sample)
- }
- for _, child := range n.children {
- pb.addSamples(p, child)
- }
- }
- func (p *profiler) run() {
- ticker := time.NewTicker(profInterval)
- counter := 0
- for ts := range ticker.C {
- p.mu.Lock()
- left := len(p.trackers)
- if left == 0 {
- break
- }
- for {
- // This loop runs until either one of the VMs is signalled or all of the VMs are scanned and found
- // busy or deleted.
- if counter >= len(p.trackers) {
- counter = 0
- }
- tracker := p.trackers[counter]
- req := atomic.LoadInt32(&tracker.req)
- if req == profReqSampleReady {
- p.buf.addSample(tracker)
- }
- if atomic.LoadInt32(&tracker.finished) != 0 {
- p.trackers[counter] = p.trackers[len(p.trackers)-1]
- p.trackers[len(p.trackers)-1] = nil
- p.trackers = p.trackers[:len(p.trackers)-1]
- } else {
- counter++
- if req != profReqDoSample {
- // signal the VM to take a sample
- tracker.start = ts
- atomic.StoreInt32(&tracker.req, profReqDoSample)
- break
- }
- }
- left--
- if left <= 0 {
- // all VMs are busy
- break
- }
- }
- p.mu.Unlock()
- }
- ticker.Stop()
- p.running = false
- p.mu.Unlock()
- }
- func (p *profiler) registerVm() *profTracker {
- pt := new(profTracker)
- p.mu.Lock()
- if p.buf != nil {
- p.trackers = append(p.trackers, pt)
- if !p.running {
- go p.run()
- p.running = true
- }
- } else {
- pt.req = profReqStop
- }
- p.mu.Unlock()
- return pt
- }
- func (p *profiler) start() error {
- p.mu.Lock()
- if p.buf != nil {
- p.mu.Unlock()
- return errors.New("profiler is already active")
- }
- p.buf = new(profBuffer)
- p.mu.Unlock()
- return nil
- }
- func (p *profiler) stop() *profile.Profile {
- p.mu.Lock()
- trackers, buf := p.trackers, p.buf
- p.trackers, p.buf = nil, nil
- p.mu.Unlock()
- if buf != nil {
- k := 0
- for i, tracker := range trackers {
- req := atomic.LoadInt32(&tracker.req)
- if req == profReqSampleReady {
- buf.addSample(tracker)
- } else if req == profReqDoSample {
- // In case the VM is requested to do a sample, there is a small chance of a race
- // where we set profReqStop in between the read and the write, so that the req
- // ends up being set to profReqSampleReady. It's no such a big deal if we do nothing,
- // it just means the VM remains in tracing mode until it finishes the current run,
- // but we do an extra cleanup step later just in case.
- if i != k {
- trackers[k] = trackers[i]
- }
- k++
- }
- atomic.StoreInt32(&tracker.req, profReqStop)
- }
- if k > 0 {
- trackers = trackers[:k]
- go func() {
- // Make sure all VMs are requested to stop tracing.
- for {
- k := 0
- for i, tracker := range trackers {
- req := atomic.LoadInt32(&tracker.req)
- if req != profReqStop {
- atomic.StoreInt32(&tracker.req, profReqStop)
- if i != k {
- trackers[k] = trackers[i]
- }
- k++
- }
- }
- if k == 0 {
- return
- }
- trackers = trackers[:k]
- time.Sleep(100 * time.Millisecond)
- }
- }()
- }
- return buf.profile()
- }
- return nil
- }
- /*
- StartProfile enables execution time profiling for all Runtimes within the current process.
- This works similar to pprof.StartCPUProfile and produces the same format which can be consumed by `go tool pprof`.
- There are, however, a few notable differences. Firstly, it's not a CPU profile, rather "execution time" profile.
- It measures the time the VM spends executing an instruction. If this instruction happens to be a call to a
- blocking Go function, the waiting time will be measured. Secondly, the 'cpu' sample isn't simply `count*period`,
- it's the time interval between when sampling was requested and when the instruction has finished. If a VM is still
- executing the same instruction when the time comes for the next sample, the sampling is skipped (i.e. `count` doesn't
- grow).
- If there are multiple functions with the same name, their names get a '.N' suffix, where N is a unique number,
- because otherwise the graph view merges them together (even if they are in different mappings). This includes
- "<anonymous>" functions.
- The sampling period is set to 10ms.
- It returns an error if profiling is already active.
- */
- func StartProfile(w io.Writer) error {
- err := globalProfiler.p.start()
- if err != nil {
- return err
- }
- globalProfiler.w = w
- atomic.StoreInt32(&globalProfiler.enabled, 1)
- return nil
- }
- /*
- StopProfile stops the current profile initiated by StartProfile, if any.
- */
- func StopProfile() {
- atomic.StoreInt32(&globalProfiler.enabled, 0)
- pr := globalProfiler.p.stop()
- if pr != nil {
- _ = pr.Write(globalProfiler.w)
- }
- globalProfiler.w = nil
- }
|