master
go 294 lines 6.87 KB
Raw
1 package profile
2
3 import (
4 "archive/zip"
5 "bytes"
6 "context"
7 "encoding/json"
8 "fmt"
9 "io"
10 "os"
11 "runtime"
12 "runtime/pprof"
13 "runtime/trace"
14 "sync"
15 "time"
16
17 "github.com/ipfs/go-log/v2"
18 version "github.com/ipfs/kubo"
19 )
20
21 const (
22 CollectorGoroutinesStack = "goroutines-stack"
23 CollectorGoroutinesPprof = "goroutines-pprof"
24 CollectorVersion = "version"
25 CollectorHeap = "heap"
26 CollectorAllocs = "allocs"
27 CollectorBin = "bin"
28 CollectorCPU = "cpu"
29 CollectorMutex = "mutex"
30 CollectorBlock = "block"
31 CollectorTrace = "trace"
32 )
33
34 var (
35 logger = log.Logger("profile")
36 goos = runtime.GOOS
37 )
38
39 type collector struct {
40 outputFile string
41 isExecutable bool
42 collectFunc func(ctx context.Context, opts Options, writer io.Writer) error
43 enabledFunc func(opts Options) bool
44 }
45
46 func (p *collector) outputFileName() string {
47 fName := p.outputFile
48 if p.isExecutable {
49 if goos == "windows" {
50 fName += ".exe"
51 }
52 }
53 return fName
54 }
55
56 var collectors = map[string]collector{
57 CollectorGoroutinesStack: {
58 outputFile: "goroutines.stacks",
59 collectFunc: goroutineStacksText,
60 enabledFunc: func(opts Options) bool { return true },
61 },
62 CollectorGoroutinesPprof: {
63 outputFile: "goroutines.pprof",
64 collectFunc: goroutineStacksProto,
65 enabledFunc: func(opts Options) bool { return true },
66 },
67 CollectorVersion: {
68 outputFile: "version.json",
69 collectFunc: versionInfo,
70 enabledFunc: func(opts Options) bool { return true },
71 },
72 CollectorHeap: {
73 outputFile: "heap.pprof",
74 collectFunc: heapProfile,
75 enabledFunc: func(opts Options) bool { return true },
76 },
77 CollectorAllocs: {
78 outputFile: "allocs.pprof",
79 collectFunc: allocsProfile,
80 enabledFunc: func(opts Options) bool { return true },
81 },
82 CollectorBin: {
83 outputFile: "ipfs",
84 isExecutable: true,
85 collectFunc: binary,
86 enabledFunc: func(opts Options) bool { return true },
87 },
88 CollectorCPU: {
89 outputFile: "cpu.pprof",
90 collectFunc: profileCPU,
91 enabledFunc: func(opts Options) bool { return opts.ProfileDuration > 0 },
92 },
93 CollectorMutex: {
94 outputFile: "mutex.pprof",
95 collectFunc: mutexProfile,
96 enabledFunc: func(opts Options) bool { return opts.ProfileDuration > 0 && opts.MutexProfileFraction > 0 },
97 },
98 CollectorBlock: {
99 outputFile: "block.pprof",
100 collectFunc: blockProfile,
101 enabledFunc: func(opts Options) bool { return opts.ProfileDuration > 0 && opts.BlockProfileRate > 0 },
102 },
103 CollectorTrace: {
104 outputFile: "trace",
105 collectFunc: captureTrace,
106 enabledFunc: func(opts Options) bool { return opts.ProfileDuration > 0 },
107 },
108 }
109
110 type Options struct {
111 Collectors []string
112 ProfileDuration time.Duration
113 MutexProfileFraction int
114 BlockProfileRate time.Duration
115 }
116
117 func WriteProfiles(ctx context.Context, archive *zip.Writer, opts Options) error {
118 p := profiler{
119 archive: archive,
120 opts: opts,
121 }
122 return p.runProfile(ctx)
123 }
124
125 // profiler runs the collectors concurrently and writes the results to the zip archive.
126 type profiler struct {
127 archive *zip.Writer
128 opts Options
129 }
130
131 func (p *profiler) runProfile(ctx context.Context) error {
132 type profileResult struct {
133 fName string
134 buf *bytes.Buffer
135 err error
136 }
137
138 ctx, cancelFn := context.WithCancel(ctx)
139 defer cancelFn()
140
141 collectorsToRun := make([]collector, len(p.opts.Collectors))
142 for i, name := range p.opts.Collectors {
143 c, ok := collectors[name]
144 if !ok {
145 return fmt.Errorf("unknown collector '%s'", name)
146 }
147 collectorsToRun[i] = c
148 }
149
150 results := make(chan profileResult, len(p.opts.Collectors))
151 wg := sync.WaitGroup{}
152 for _, c := range collectorsToRun {
153 if !c.enabledFunc(p.opts) {
154 continue
155 }
156
157 fName := c.outputFileName()
158
159 wg.Add(1)
160 go func(c collector) {
161 defer wg.Done()
162 logger.Infow("collecting profile", "File", fName)
163 defer logger.Infow("profile done", "File", fName)
164 b := bytes.Buffer{}
165 err := c.collectFunc(ctx, p.opts, &b)
166 if err != nil {
167 select {
168 case results <- profileResult{err: fmt.Errorf("generating profile data for %q: %w", fName, err)}:
169 case <-ctx.Done():
170 return
171 }
172 }
173 select {
174 case results <- profileResult{buf: &b, fName: fName}:
175 case <-ctx.Done():
176 }
177 }(c)
178 }
179 go func() {
180 wg.Wait()
181 close(results)
182 }()
183
184 for res := range results {
185 if res.err != nil {
186 return res.err
187 }
188 out, err := p.archive.Create(res.fName)
189 if err != nil {
190 return fmt.Errorf("creating output file %q: %w", res.fName, err)
191 }
192 _, err = io.Copy(out, res.buf)
193 if err != nil {
194 return fmt.Errorf("compressing result %q: %w", res.fName, err)
195 }
196 }
197
198 return nil
199 }
200
201 func goroutineStacksText(ctx context.Context, _ Options, w io.Writer) error {
202 return WriteAllGoroutineStacks(w)
203 }
204
205 func goroutineStacksProto(ctx context.Context, _ Options, w io.Writer) error {
206 return pprof.Lookup("goroutine").WriteTo(w, 0)
207 }
208
209 func heapProfile(ctx context.Context, _ Options, w io.Writer) error {
210 return pprof.Lookup("heap").WriteTo(w, 0)
211 }
212
213 func allocsProfile(ctx context.Context, _ Options, w io.Writer) error {
214 return pprof.Lookup("allocs").WriteTo(w, 0)
215 }
216
217 func versionInfo(ctx context.Context, _ Options, w io.Writer) error {
218 return json.NewEncoder(w).Encode(version.GetVersionInfo())
219 }
220
221 func binary(ctx context.Context, _ Options, w io.Writer) error {
222 var (
223 path string
224 err error
225 )
226 if goos == "linux" {
227 pid := os.Getpid()
228 path = fmt.Sprintf("/proc/%d/exe", pid)
229 } else {
230 path, err = os.Executable()
231 if err != nil {
232 return fmt.Errorf("finding binary path: %w", err)
233 }
234 }
235 fi, err := os.Open(path)
236 if err != nil {
237 return fmt.Errorf("opening binary %q: %w", path, err)
238 }
239 _, err = io.Copy(w, fi)
240 _ = fi.Close()
241 if err != nil {
242 return fmt.Errorf("copying binary %q: %w", path, err)
243 }
244 return nil
245 }
246
247 func mutexProfile(ctx context.Context, opts Options, w io.Writer) error {
248 prev := runtime.SetMutexProfileFraction(opts.MutexProfileFraction)
249 defer runtime.SetMutexProfileFraction(prev)
250 err := waitOrCancel(ctx, opts.ProfileDuration)
251 if err != nil {
252 return err
253 }
254 return pprof.Lookup("mutex").WriteTo(w, 2)
255 }
256
257 func blockProfile(ctx context.Context, opts Options, w io.Writer) error {
258 runtime.SetBlockProfileRate(int(opts.BlockProfileRate.Nanoseconds()))
259 defer runtime.SetBlockProfileRate(0)
260 err := waitOrCancel(ctx, opts.ProfileDuration)
261 if err != nil {
262 return err
263 }
264 return pprof.Lookup("block").WriteTo(w, 2)
265 }
266
267 func profileCPU(ctx context.Context, opts Options, w io.Writer) error {
268 err := pprof.StartCPUProfile(w)
269 if err != nil {
270 return err
271 }
272 defer pprof.StopCPUProfile()
273 return waitOrCancel(ctx, opts.ProfileDuration)
274 }
275
276 func captureTrace(ctx context.Context, opts Options, w io.Writer) error {
277 err := trace.Start(w)
278 if err != nil {
279 return err
280 }
281 defer trace.Stop()
282 return waitOrCancel(ctx, opts.ProfileDuration)
283 }
284
285 func waitOrCancel(ctx context.Context, d time.Duration) error {
286 timer := time.NewTimer(d)
287 defer timer.Stop()
288 select {
289 case <-timer.C:
290 return nil
291 case <-ctx.Done():
292 return ctx.Err()
293 }
294 }