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 }