1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package driver
16
17 import (
18 "bytes"
19 "fmt"
20 "io"
21 "net/http"
22 "net/url"
23 "os"
24 "os/exec"
25 "path/filepath"
26 "runtime"
27 "strconv"
28 "strings"
29 "sync"
30 "time"
31
32 "github.com/google/pprof/internal/measurement"
33 "github.com/google/pprof/internal/plugin"
34 "github.com/google/pprof/profile"
35 )
36
37
38
39
40
41 func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
42 sources := make([]profileSource, 0, len(s.Sources))
43 for _, src := range s.Sources {
44 sources = append(sources, profileSource{
45 addr: src,
46 source: s,
47 })
48 }
49
50 bases := make([]profileSource, 0, len(s.Base))
51 for _, src := range s.Base {
52 bases = append(bases, profileSource{
53 addr: src,
54 source: s,
55 })
56 }
57
58 p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI, o.HTTPTransport)
59 if err != nil {
60 return nil, err
61 }
62
63 if pbase != nil {
64 if s.DiffBase {
65 pbase.SetLabel("pprof::base", []string{"true"})
66 }
67 if s.Normalize {
68 err := p.Normalize(pbase)
69 if err != nil {
70 return nil, err
71 }
72 }
73 pbase.Scale(-1)
74 p, m, err = combineProfiles([]*profile.Profile{p, pbase}, []plugin.MappingSources{m, mbase})
75 if err != nil {
76 return nil, err
77 }
78 }
79
80 if s.AllFrames {
81 p.DropFrames = ""
82 p.KeepFrames = ""
83 }
84
85
86 if err := o.Sym.Symbolize(s.Symbolize, m, p); err != nil {
87 return nil, err
88 }
89 p.RemoveUninteresting()
90 unsourceMappings(p)
91
92 if s.Comment != "" {
93 p.Comments = append(p.Comments, s.Comment)
94 }
95
96
97 if save {
98 dir, err := setTmpDir(o.UI)
99 if err != nil {
100 return nil, err
101 }
102
103 prefix := "pprof."
104 if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
105 prefix += filepath.Base(p.Mapping[0].File) + "."
106 }
107 for _, s := range p.SampleType {
108 prefix += s.Type + "."
109 }
110
111 tempFile, err := newTempFile(dir, prefix, ".pb.gz")
112 if err == nil {
113 if err = p.Write(tempFile); err == nil {
114 o.UI.PrintErr("Saved profile in ", tempFile.Name())
115 }
116 }
117 if err != nil {
118 o.UI.PrintErr("Could not save profile: ", err)
119 }
120 }
121
122 if err := p.CheckValid(); err != nil {
123 return nil, err
124 }
125
126 return p, nil
127 }
128
129 func grabSourcesAndBases(sources, bases []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, *profile.Profile, plugin.MappingSources, plugin.MappingSources, bool, error) {
130 wg := sync.WaitGroup{}
131 wg.Add(2)
132 var psrc, pbase *profile.Profile
133 var msrc, mbase plugin.MappingSources
134 var savesrc, savebase bool
135 var errsrc, errbase error
136 var countsrc, countbase int
137 go func() {
138 defer wg.Done()
139 psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui, tr)
140 }()
141 go func() {
142 defer wg.Done()
143 pbase, mbase, savebase, countbase, errbase = chunkedGrab(bases, fetch, obj, ui, tr)
144 }()
145 wg.Wait()
146 save := savesrc || savebase
147
148 if errsrc != nil {
149 return nil, nil, nil, nil, false, fmt.Errorf("problem fetching source profiles: %v", errsrc)
150 }
151 if errbase != nil {
152 return nil, nil, nil, nil, false, fmt.Errorf("problem fetching base profiles: %v,", errbase)
153 }
154 if countsrc == 0 {
155 return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any source profiles")
156 }
157 if countbase == 0 && len(bases) > 0 {
158 return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any base profiles")
159 }
160 if want, got := len(sources), countsrc; want != got {
161 ui.PrintErr(fmt.Sprintf("Fetched %d source profiles out of %d", got, want))
162 }
163 if want, got := len(bases), countbase; want != got {
164 ui.PrintErr(fmt.Sprintf("Fetched %d base profiles out of %d", got, want))
165 }
166
167 return psrc, pbase, msrc, mbase, save, nil
168 }
169
170
171
172
173 func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
174 const chunkSize = 128
175
176 var p *profile.Profile
177 var msrc plugin.MappingSources
178 var save bool
179 var count int
180
181 for start := 0; start < len(sources); start += chunkSize {
182 end := min(start+chunkSize, len(sources))
183 chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui, tr)
184 switch {
185 case chunkErr != nil:
186 return nil, nil, false, 0, chunkErr
187 case chunkP == nil:
188 continue
189 case p == nil:
190 p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount
191 default:
192 p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc})
193 if chunkErr != nil {
194 return nil, nil, false, 0, chunkErr
195 }
196 if chunkSave {
197 save = true
198 }
199 count += chunkCount
200 }
201 }
202
203 return p, msrc, save, count, nil
204 }
205
206
207 func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
208 wg := sync.WaitGroup{}
209 wg.Add(len(sources))
210 for i := range sources {
211 go func(s *profileSource) {
212 defer wg.Done()
213 s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui, tr)
214 }(&sources[i])
215 }
216 wg.Wait()
217
218 var save bool
219 profiles := make([]*profile.Profile, 0, len(sources))
220 msrcs := make([]plugin.MappingSources, 0, len(sources))
221 for i := range sources {
222 s := &sources[i]
223 if err := s.err; err != nil {
224 ui.PrintErr(s.addr + ": " + err.Error())
225 continue
226 }
227 save = save || s.remote
228 profiles = append(profiles, s.p)
229 msrcs = append(msrcs, s.msrc)
230 *s = profileSource{}
231 }
232
233 if len(profiles) == 0 {
234 return nil, nil, false, 0, nil
235 }
236
237 p, msrc, err := combineProfiles(profiles, msrcs)
238 if err != nil {
239 return nil, nil, false, 0, err
240 }
241 return p, msrc, save, len(profiles), nil
242 }
243
244 func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) {
245
246
247
248
249
250 if err := profile.CompatibilizeSampleTypes(profiles); err != nil {
251 return nil, nil, err
252 }
253 if err := measurement.ScaleProfiles(profiles); err != nil {
254 return nil, nil, err
255 }
256
257
258 if len(profiles) == 1 && len(msrcs) == 1 {
259 return profiles[0], msrcs[0], nil
260 }
261
262 p, err := profile.Merge(profiles)
263 if err != nil {
264 return nil, nil, err
265 }
266
267
268 msrc := make(plugin.MappingSources)
269 for _, ms := range msrcs {
270 for m, s := range ms {
271 msrc[m] = append(msrc[m], s...)
272 }
273 }
274 return p, msrc, nil
275 }
276
277 type profileSource struct {
278 addr string
279 source *source
280
281 p *profile.Profile
282 msrc plugin.MappingSources
283 remote bool
284 err error
285 }
286
287 func homeEnv() string {
288 switch runtime.GOOS {
289 case "windows":
290 return "USERPROFILE"
291 case "plan9":
292 return "home"
293 default:
294 return "HOME"
295 }
296 }
297
298
299
300
301 func setTmpDir(ui plugin.UI) (string, error) {
302 var dirs []string
303 if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" {
304 dirs = append(dirs, profileDir)
305 }
306 if homeDir := os.Getenv(homeEnv()); homeDir != "" {
307 dirs = append(dirs, filepath.Join(homeDir, "pprof"))
308 }
309 dirs = append(dirs, os.TempDir())
310 for _, tmpDir := range dirs {
311 if err := os.MkdirAll(tmpDir, 0755); err != nil {
312 ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error())
313 continue
314 }
315 return tmpDir, nil
316 }
317 return "", fmt.Errorf("failed to identify temp dir")
318 }
319
320 const testSourceAddress = "pproftest.local"
321
322
323
324
325 func grabProfile(s *source, source string, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {
326 var src string
327 duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second
328 if fetcher != nil {
329 p, src, err = fetcher.Fetch(source, duration, timeout)
330 if err != nil {
331 return
332 }
333 }
334 if err != nil || p == nil {
335
336 p, src, err = fetch(source, duration, timeout, ui, tr)
337 if err != nil {
338 return
339 }
340 }
341
342 if err = p.CheckValid(); err != nil {
343 return
344 }
345
346
347 locateBinaries(p, s, obj, ui)
348
349
350 if src != "" {
351 msrc = collectMappingSources(p, src)
352 remote = true
353 if strings.HasPrefix(src, "http://"+testSourceAddress) {
354
355
356 remote = false
357 }
358 }
359 return
360 }
361
362
363 func collectMappingSources(p *profile.Profile, source string) plugin.MappingSources {
364 ms := plugin.MappingSources{}
365 for _, m := range p.Mapping {
366 src := struct {
367 Source string
368 Start uint64
369 }{
370 source, m.Start,
371 }
372 key := m.BuildID
373 if key == "" {
374 key = m.File
375 }
376 if key == "" {
377
378
379
380
381
382 m.File = source
383 key = source
384 }
385 ms[key] = append(ms[key], src)
386 }
387 return ms
388 }
389
390
391
392 func unsourceMappings(p *profile.Profile) {
393 for _, m := range p.Mapping {
394 if m.BuildID == "" && filepath.VolumeName(m.File) == "" {
395 if u, err := url.Parse(m.File); err == nil && u.IsAbs() {
396 m.File = ""
397 }
398 }
399 }
400 }
401
402
403
404 func locateBinaries(p *profile.Profile, s *source, obj plugin.ObjTool, ui plugin.UI) {
405
406 searchPath := os.Getenv("PPROF_BINARY_PATH")
407 if searchPath == "" {
408
409 searchPath = filepath.Join(os.Getenv(homeEnv()), "pprof", "binaries")
410 }
411 mapping:
412 for _, m := range p.Mapping {
413 var noVolumeFile string
414 var baseName string
415 var dirName string
416 if m.File != "" {
417 noVolumeFile = strings.TrimPrefix(m.File, filepath.VolumeName(m.File))
418 baseName = filepath.Base(m.File)
419 dirName = filepath.Dir(noVolumeFile)
420 }
421
422 for _, path := range filepath.SplitList(searchPath) {
423 var fileNames []string
424 if m.BuildID != "" {
425 fileNames = []string{filepath.Join(path, m.BuildID, baseName)}
426 if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
427 fileNames = append(fileNames, matches...)
428 }
429 fileNames = append(fileNames, filepath.Join(path, noVolumeFile, m.BuildID))
430
431
432
433 fileNames = append(fileNames, filepath.Join(path, m.BuildID[:2], m.BuildID[2:]+".debug"))
434 }
435 if m.File != "" {
436
437
438 fileNames = append(fileNames, filepath.Join(path, baseName))
439 fileNames = append(fileNames, filepath.Join(path, noVolumeFile))
440
441
442 fileNames = append(fileNames, filepath.Join(path, noVolumeFile+".debug"))
443 fileNames = append(fileNames, filepath.Join(path, dirName, ".debug", baseName+".debug"))
444 fileNames = append(fileNames, filepath.Join(path, "usr", "lib", "debug", dirName, baseName+".debug"))
445 }
446 for _, name := range fileNames {
447 if f, err := obj.Open(name, m.Start, m.Limit, m.Offset, m.KernelRelocationSymbol); err == nil {
448 defer f.Close()
449 fileBuildID := f.BuildID()
450 if m.BuildID != "" && m.BuildID != fileBuildID {
451 ui.PrintErr("Ignoring local file " + name + ": build-id mismatch (" + m.BuildID + " != " + fileBuildID + ")")
452 } else {
453
454
455 m.File = name
456 continue mapping
457 }
458 }
459 }
460 }
461 }
462 if len(p.Mapping) == 0 {
463
464
465
466
467 m := &profile.Mapping{ID: 1}
468 p.Mapping = []*profile.Mapping{m}
469 for _, l := range p.Location {
470 l.Mapping = m
471 }
472 }
473
474
475 if execName, buildID := s.ExecName, s.BuildID; execName != "" || buildID != "" {
476 m := p.Mapping[0]
477 if execName != "" {
478
479
480 m.File = execName
481 }
482
483
484
485 if buildID != "" && m.BuildID == "" {
486 m.BuildID = buildID
487 }
488 }
489 }
490
491
492
493
494 func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, src string, err error) {
495 var f io.ReadCloser
496
497
498 if _, err = os.Stat(source); err == nil {
499 if isPerfFile(source) {
500 f, err = convertPerfData(source, ui)
501 } else {
502 f, err = os.Open(source)
503 }
504 } else {
505 sourceURL, timeout := adjustURL(source, duration, timeout)
506 if sourceURL != "" {
507 ui.Print("Fetching profile over HTTP from " + sourceURL)
508 if duration > 0 {
509 ui.Print(fmt.Sprintf("Please wait... (%v)", duration))
510 }
511 f, err = fetchURL(sourceURL, timeout, tr)
512 src = sourceURL
513 }
514 }
515 if err == nil {
516 defer f.Close()
517 p, err = profile.Parse(f)
518 }
519 return
520 }
521
522
523 func fetchURL(source string, timeout time.Duration, tr http.RoundTripper) (io.ReadCloser, error) {
524 client := &http.Client{
525 Transport: tr,
526 Timeout: timeout + 5*time.Second,
527 }
528 resp, err := client.Get(source)
529 if err != nil {
530 return nil, fmt.Errorf("http fetch: %v", err)
531 }
532 if resp.StatusCode != http.StatusOK {
533 defer resp.Body.Close()
534 return nil, statusCodeError(resp)
535 }
536
537 return resp.Body, nil
538 }
539
540 func statusCodeError(resp *http.Response) error {
541 if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
542
543 if body, err := io.ReadAll(resp.Body); err == nil {
544 return fmt.Errorf("server response: %s - %s", resp.Status, body)
545 }
546 }
547 return fmt.Errorf("server response: %s", resp.Status)
548 }
549
550
551
552 func isPerfFile(path string) bool {
553 sourceFile, openErr := os.Open(path)
554 if openErr != nil {
555 return false
556 }
557 defer sourceFile.Close()
558
559
560
561 perfHeader := []byte("PERFILE2")
562 actualHeader := make([]byte, len(perfHeader))
563 if _, readErr := sourceFile.Read(actualHeader); readErr != nil {
564 return false
565 }
566 return bytes.Equal(actualHeader, perfHeader)
567 }
568
569
570
571
572 func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) {
573 ui.Print(fmt.Sprintf(
574 "Converting %s to a profile.proto... (May take a few minutes)",
575 perfPath))
576 profile, err := newTempFile(os.TempDir(), "pprof_", ".pb.gz")
577 if err != nil {
578 return nil, err
579 }
580 deferDeleteTempFile(profile.Name())
581 cmd := exec.Command("perf_to_profile", "-i", perfPath, "-o", profile.Name(), "-f")
582 cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
583 if err := cmd.Run(); err != nil {
584 profile.Close()
585 return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
586 }
587 return profile, nil
588 }
589
590
591
592
593 func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
594 u, err := url.Parse(source)
595 if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
596
597
598 u, err = url.Parse("http://" + source)
599 }
600 if err != nil || u.Host == "" {
601 return "", 0
602 }
603
604
605 values := u.Query()
606 if duration > 0 {
607 values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
608 } else {
609 if urlSeconds := values.Get("seconds"); urlSeconds != "" {
610 if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
611 duration = time.Duration(us) * time.Second
612 }
613 }
614 }
615 if timeout <= 0 {
616 if duration > 0 {
617 timeout = duration + duration/2
618 } else {
619 timeout = 60 * time.Second
620 }
621 }
622 u.RawQuery = values.Encode()
623 return u.String(), timeout
624 }
625
View as plain text