github.com/consensys/gnark@v0.11.0/profile/profile.go (about)

     1  // Package profile provides a simple way to generate pprof compatible gnark circuit profile.
     2  //
     3  // Since the gnark frontend compiler is not thread safe and operates in a single go-routine,
     4  // this package is also NOT thread safe and is meant to be called in the same go-routine.
     5  package profile
     6  
     7  import (
     8  	"bytes"
     9  	"os"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  	"sync"
    14  	"sync/atomic"
    15  
    16  	"github.com/consensys/gnark/logger"
    17  	"github.com/consensys/gnark/profile/internal/report"
    18  	"github.com/google/pprof/profile"
    19  )
    20  
    21  var (
    22  	sessions       []*Profile // active sessions
    23  	activeSessions uint32
    24  )
    25  
    26  // Profile represents an active constraint system profiling session.
    27  type Profile struct {
    28  	// defaults to ./gnark.pprof
    29  	// if blank, profiile is not written to disk
    30  	filePath string
    31  
    32  	// actual pprof profile struct
    33  	// details on pprof format: https://github.com/google/pprof/blob/main/proto/README.md
    34  	pprof profile.Profile
    35  
    36  	functions map[string]*profile.Function
    37  	locations map[uint64]*profile.Location
    38  
    39  	onceSetName sync.Once
    40  
    41  	chDone chan struct{}
    42  }
    43  
    44  // Option defines configuration Options for Profile.
    45  type Option func(*Profile)
    46  
    47  // WithPath controls the profile destination file. If blank, profile is not written.
    48  //
    49  // Defaults to ./gnark.pprof.
    50  func WithPath(path string) Option {
    51  	return func(p *Profile) {
    52  		p.filePath = path
    53  	}
    54  }
    55  
    56  // WithNoOutput indicates that the profile is not going to be written to disk.
    57  //
    58  // This is equivalent to WithPath("")
    59  func WithNoOutput() Option {
    60  	return func(p *Profile) {
    61  		p.filePath = ""
    62  	}
    63  }
    64  
    65  // Start creates a new active profiling session. When Stop() is called, this session is removed from
    66  // active profiling sessions and may be serialized to disk as a pprof compatible file (see ProfilePath option).
    67  //
    68  // All calls to profile.Start() and Stop() are meant to be executed in the same go routine (frontend.Compile).
    69  //
    70  // It is allowed to create multiple overlapping profiling sessions in one circuit.
    71  func Start(options ...Option) *Profile {
    72  
    73  	// start the worker first time a profiling session starts.
    74  	onceInit.Do(func() {
    75  		go worker()
    76  	})
    77  
    78  	p := Profile{
    79  		functions: make(map[string]*profile.Function),
    80  		locations: make(map[uint64]*profile.Location),
    81  		filePath:  filepath.Join(".", "gnark.pprof"),
    82  		chDone:    make(chan struct{}),
    83  	}
    84  	p.pprof.SampleType = []*profile.ValueType{{
    85  		Type: "constraints",
    86  		Unit: "count",
    87  	}}
    88  
    89  	for _, option := range options {
    90  		option(&p)
    91  	}
    92  
    93  	log := logger.Logger()
    94  	if p.filePath == "" {
    95  		log.Warn().Msg("gnark profiling enabled [not writing to disk]")
    96  	} else {
    97  		log.Info().Str("path", p.filePath).Msg("gnark profiling enabled")
    98  	}
    99  
   100  	// add the session to active sessions
   101  	chCommands <- command{p: &p}
   102  	atomic.AddUint32(&activeSessions, 1)
   103  
   104  	return &p
   105  }
   106  
   107  // Stop removes the profile from active session and may write the pprof file to disk. See ProfilePath option.
   108  func (p *Profile) Stop() {
   109  	log := logger.Logger()
   110  
   111  	if p.chDone == nil {
   112  		log.Fatal().Msg("gnark profile stopped multiple times")
   113  	}
   114  
   115  	// ask worker routine to remove ourselves from the active sessions
   116  	chCommands <- command{p: p, remove: true}
   117  
   118  	// wait for worker routine to remove us.
   119  	<-p.chDone
   120  	p.chDone = nil
   121  
   122  	// if filePath is set, serialize profile to disk in pprof format
   123  	if p.filePath != "" {
   124  		f, err := os.Create(p.filePath)
   125  		if err != nil {
   126  			log.Fatal().Err(err).Msg("could not create gnark profile")
   127  		}
   128  		if err := p.pprof.Write(f); err != nil {
   129  			log.Error().Err(err).Msg("writing profile")
   130  		}
   131  		f.Close()
   132  		log.Info().Str("path", p.filePath).Msg("gnark profiling disabled")
   133  	} else {
   134  		log.Warn().Msg("gnark profiling disabled [not writing to disk]")
   135  	}
   136  
   137  }
   138  
   139  // NbConstraints return number of collected samples (constraints) by the profile session
   140  func (p *Profile) NbConstraints() int {
   141  	return len(p.pprof.Sample)
   142  }
   143  
   144  // Top return a similar output than pprof top command
   145  func (p *Profile) Top() string {
   146  	r := report.NewDefault(&p.pprof, report.Options{
   147  		OutputFormat:  report.Tree,
   148  		CompactLabels: true,
   149  		NodeFraction:  0.005,
   150  		EdgeFraction:  0.001,
   151  		SampleValue:   func(v []int64) int64 { return v[0] },
   152  		SampleUnit:    "count",
   153  	})
   154  	var buf bytes.Buffer
   155  	report.Generate(&buf, r)
   156  	return buf.String()
   157  }
   158  
   159  // RecordConstraint add a sample (with count == 1) to all the active profiling sessions.
   160  func RecordConstraint() {
   161  	if n := atomic.LoadUint32(&activeSessions); n == 0 {
   162  		return // do nothing, no active session.
   163  	}
   164  
   165  	// collect the stack and send it async to the worker
   166  	pc := make([]uintptr, 20)
   167  	n := runtime.Callers(3, pc)
   168  	if n == 0 {
   169  		return
   170  	}
   171  	pc = pc[:n]
   172  	chCommands <- command{pc: pc}
   173  }
   174  
   175  func (p *Profile) getLocation(frame *runtime.Frame) *profile.Location {
   176  	l, ok := p.locations[uint64(frame.PC)]
   177  	if !ok {
   178  		// first let's see if we have the function.
   179  		f, ok := p.functions[frame.File+frame.Function]
   180  		if !ok {
   181  			fe := strings.Split(frame.Function, "/")
   182  			fName := fe[len(fe)-1]
   183  			f = &profile.Function{
   184  				ID:         uint64(len(p.functions) + 1),
   185  				Name:       fName,
   186  				SystemName: frame.Function,
   187  				Filename:   frame.File,
   188  			}
   189  
   190  			p.functions[frame.File+frame.Function] = f
   191  			p.pprof.Function = append(p.pprof.Function, f)
   192  		}
   193  
   194  		l = &profile.Location{
   195  			ID:   uint64(len(p.locations) + 1),
   196  			Line: []profile.Line{{Function: f, Line: int64(frame.Line)}},
   197  		}
   198  		p.locations[uint64(frame.PC)] = l
   199  		p.pprof.Location = append(p.pprof.Location, l)
   200  	}
   201  
   202  	return l
   203  }