Bez popisu

fetch.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. // Copyright 2014 Google Inc. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package driver
  15. import (
  16. "bytes"
  17. "fmt"
  18. "io"
  19. "io/ioutil"
  20. "net/http"
  21. "net/url"
  22. "os"
  23. "os/exec"
  24. "path/filepath"
  25. "runtime"
  26. "strconv"
  27. "strings"
  28. "sync"
  29. "time"
  30. "github.com/google/pprof/internal/measurement"
  31. "github.com/google/pprof/internal/plugin"
  32. "github.com/google/pprof/profile"
  33. )
  34. // fetchProfiles fetches and symbolizes the profiles specified by s.
  35. // It will merge all the profiles it is able to retrieve, even if
  36. // there are some failures. It will return an error if it is unable to
  37. // fetch any profiles.
  38. func fetchProfiles(s *source, o *plugin.Options) (*profile.Profile, error) {
  39. sources := make([]profileSource, 0, len(s.Sources))
  40. for _, src := range s.Sources {
  41. sources = append(sources, profileSource{
  42. addr: src,
  43. source: s,
  44. })
  45. }
  46. bases := make([]profileSource, 0, len(s.Base))
  47. for _, src := range s.Base {
  48. bases = append(bases, profileSource{
  49. addr: src,
  50. source: s,
  51. })
  52. }
  53. p, pbase, m, mbase, save, err := grabSourcesAndBases(sources, bases, o.Fetch, o.Obj, o.UI, o.HTTPTransport)
  54. if err != nil {
  55. return nil, err
  56. }
  57. if pbase != nil {
  58. if s.DiffBase {
  59. pbase.SetLabel("pprof::base", []string{"true"})
  60. }
  61. if s.Normalize {
  62. err := p.Normalize(pbase)
  63. if err != nil {
  64. return nil, err
  65. }
  66. }
  67. pbase.Scale(-1)
  68. p, m, err = combineProfiles([]*profile.Profile{p, pbase}, []plugin.MappingSources{m, mbase})
  69. if err != nil {
  70. return nil, err
  71. }
  72. }
  73. // Symbolize the merged profile.
  74. if err := o.Sym.Symbolize(s.Symbolize, m, p); err != nil {
  75. return nil, err
  76. }
  77. p.RemoveUninteresting()
  78. unsourceMappings(p)
  79. if s.Comment != "" {
  80. p.Comments = append(p.Comments, s.Comment)
  81. }
  82. // Save a copy of the merged profile if there is at least one remote source.
  83. if save {
  84. dir, err := setTmpDir(o.UI)
  85. if err != nil {
  86. return nil, err
  87. }
  88. prefix := "pprof."
  89. if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
  90. prefix += filepath.Base(p.Mapping[0].File) + "."
  91. }
  92. for _, s := range p.SampleType {
  93. prefix += s.Type + "."
  94. }
  95. tempFile, err := newTempFile(dir, prefix, ".pb.gz")
  96. if err == nil {
  97. if err = p.Write(tempFile); err == nil {
  98. o.UI.PrintErr("Saved profile in ", tempFile.Name())
  99. }
  100. }
  101. if err != nil {
  102. o.UI.PrintErr("Could not save profile: ", err)
  103. }
  104. }
  105. if err := p.CheckValid(); err != nil {
  106. return nil, err
  107. }
  108. return p, nil
  109. }
  110. 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) {
  111. wg := sync.WaitGroup{}
  112. wg.Add(2)
  113. var psrc, pbase *profile.Profile
  114. var msrc, mbase plugin.MappingSources
  115. var savesrc, savebase bool
  116. var errsrc, errbase error
  117. var countsrc, countbase int
  118. go func() {
  119. defer wg.Done()
  120. psrc, msrc, savesrc, countsrc, errsrc = chunkedGrab(sources, fetch, obj, ui, tr)
  121. }()
  122. go func() {
  123. defer wg.Done()
  124. pbase, mbase, savebase, countbase, errbase = chunkedGrab(bases, fetch, obj, ui, tr)
  125. }()
  126. wg.Wait()
  127. save := savesrc || savebase
  128. if errsrc != nil {
  129. return nil, nil, nil, nil, false, fmt.Errorf("problem fetching source profiles: %v", errsrc)
  130. }
  131. if errbase != nil {
  132. return nil, nil, nil, nil, false, fmt.Errorf("problem fetching base profiles: %v,", errbase)
  133. }
  134. if countsrc == 0 {
  135. return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any source profiles")
  136. }
  137. if countbase == 0 && len(bases) > 0 {
  138. return nil, nil, nil, nil, false, fmt.Errorf("failed to fetch any base profiles")
  139. }
  140. if want, got := len(sources), countsrc; want != got {
  141. ui.PrintErr(fmt.Sprintf("Fetched %d source profiles out of %d", got, want))
  142. }
  143. if want, got := len(bases), countbase; want != got {
  144. ui.PrintErr(fmt.Sprintf("Fetched %d base profiles out of %d", got, want))
  145. }
  146. return psrc, pbase, msrc, mbase, save, nil
  147. }
  148. // chunkedGrab fetches the profiles described in source and merges them into
  149. // a single profile. It fetches a chunk of profiles concurrently, with a maximum
  150. // chunk size to limit its memory usage.
  151. func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
  152. const chunkSize = 64
  153. var p *profile.Profile
  154. var msrc plugin.MappingSources
  155. var save bool
  156. var count int
  157. for start := 0; start < len(sources); start += chunkSize {
  158. end := start + chunkSize
  159. if end > len(sources) {
  160. end = len(sources)
  161. }
  162. chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui, tr)
  163. switch {
  164. case chunkErr != nil:
  165. return nil, nil, false, 0, chunkErr
  166. case chunkP == nil:
  167. continue
  168. case p == nil:
  169. p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount
  170. default:
  171. p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc})
  172. if chunkErr != nil {
  173. return nil, nil, false, 0, chunkErr
  174. }
  175. if chunkSave {
  176. save = true
  177. }
  178. count += chunkCount
  179. }
  180. }
  181. return p, msrc, save, count, nil
  182. }
  183. // concurrentGrab fetches multiple profiles concurrently
  184. func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI, tr http.RoundTripper) (*profile.Profile, plugin.MappingSources, bool, int, error) {
  185. wg := sync.WaitGroup{}
  186. wg.Add(len(sources))
  187. for i := range sources {
  188. go func(s *profileSource) {
  189. defer wg.Done()
  190. s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, fetch, obj, ui, tr)
  191. }(&sources[i])
  192. }
  193. wg.Wait()
  194. var save bool
  195. profiles := make([]*profile.Profile, 0, len(sources))
  196. msrcs := make([]plugin.MappingSources, 0, len(sources))
  197. for i := range sources {
  198. s := &sources[i]
  199. if err := s.err; err != nil {
  200. ui.PrintErr(s.addr + ": " + err.Error())
  201. continue
  202. }
  203. save = save || s.remote
  204. profiles = append(profiles, s.p)
  205. msrcs = append(msrcs, s.msrc)
  206. *s = profileSource{}
  207. }
  208. if len(profiles) == 0 {
  209. return nil, nil, false, 0, nil
  210. }
  211. p, msrc, err := combineProfiles(profiles, msrcs)
  212. if err != nil {
  213. return nil, nil, false, 0, err
  214. }
  215. return p, msrc, save, len(profiles), nil
  216. }
  217. func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) {
  218. // Merge profiles.
  219. if err := measurement.ScaleProfiles(profiles); err != nil {
  220. return nil, nil, err
  221. }
  222. p, err := profile.Merge(profiles)
  223. if err != nil {
  224. return nil, nil, err
  225. }
  226. // Combine mapping sources.
  227. msrc := make(plugin.MappingSources)
  228. for _, ms := range msrcs {
  229. for m, s := range ms {
  230. msrc[m] = append(msrc[m], s...)
  231. }
  232. }
  233. return p, msrc, nil
  234. }
  235. type profileSource struct {
  236. addr string
  237. source *source
  238. p *profile.Profile
  239. msrc plugin.MappingSources
  240. remote bool
  241. err error
  242. }
  243. func homeEnv() string {
  244. switch runtime.GOOS {
  245. case "windows":
  246. return "USERPROFILE"
  247. case "plan9":
  248. return "home"
  249. default:
  250. return "HOME"
  251. }
  252. }
  253. // setTmpDir prepares the directory to use to save profiles retrieved
  254. // remotely. It is selected from PPROF_TMPDIR, defaults to $HOME/pprof, and, if
  255. // $HOME is not set, falls back to os.TempDir().
  256. func setTmpDir(ui plugin.UI) (string, error) {
  257. var dirs []string
  258. if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" {
  259. dirs = append(dirs, profileDir)
  260. }
  261. if homeDir := os.Getenv(homeEnv()); homeDir != "" {
  262. dirs = append(dirs, filepath.Join(homeDir, "pprof"))
  263. }
  264. dirs = append(dirs, os.TempDir())
  265. for _, tmpDir := range dirs {
  266. if err := os.MkdirAll(tmpDir, 0755); err != nil {
  267. ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error())
  268. continue
  269. }
  270. return tmpDir, nil
  271. }
  272. return "", fmt.Errorf("failed to identify temp dir")
  273. }
  274. const testSourceAddress = "pproftest.local"
  275. // grabProfile fetches a profile. Returns the profile, sources for the
  276. // profile mappings, a bool indicating if the profile was fetched
  277. // remotely, and an error.
  278. 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) {
  279. var src string
  280. duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second
  281. if fetcher != nil {
  282. p, src, err = fetcher.Fetch(source, duration, timeout)
  283. if err != nil {
  284. return
  285. }
  286. }
  287. if err != nil || p == nil {
  288. // Fetch the profile over HTTP or from a file.
  289. p, src, err = fetch(source, duration, timeout, ui, tr)
  290. if err != nil {
  291. return
  292. }
  293. }
  294. if err = p.CheckValid(); err != nil {
  295. return
  296. }
  297. // Update the binary locations from command line and paths.
  298. locateBinaries(p, s, obj, ui)
  299. // Collect the source URL for all mappings.
  300. if src != "" {
  301. msrc = collectMappingSources(p, src)
  302. remote = true
  303. if strings.HasPrefix(src, "http://"+testSourceAddress) {
  304. // Treat test inputs as local to avoid saving
  305. // testcase profiles during driver testing.
  306. remote = false
  307. }
  308. }
  309. return
  310. }
  311. // collectMappingSources saves the mapping sources of a profile.
  312. func collectMappingSources(p *profile.Profile, source string) plugin.MappingSources {
  313. ms := plugin.MappingSources{}
  314. for _, m := range p.Mapping {
  315. src := struct {
  316. Source string
  317. Start uint64
  318. }{
  319. source, m.Start,
  320. }
  321. key := m.BuildID
  322. if key == "" {
  323. key = m.File
  324. }
  325. if key == "" {
  326. // If there is no build id or source file, use the source as the
  327. // mapping file. This will enable remote symbolization for this
  328. // mapping, in particular for Go profiles on the legacy format.
  329. // The source is reset back to empty string by unsourceMapping
  330. // which is called after symbolization is finished.
  331. m.File = source
  332. key = source
  333. }
  334. ms[key] = append(ms[key], src)
  335. }
  336. return ms
  337. }
  338. // unsourceMappings iterates over the mappings in a profile and replaces file
  339. // set to the remote source URL by collectMappingSources back to empty string.
  340. func unsourceMappings(p *profile.Profile) {
  341. for _, m := range p.Mapping {
  342. if m.BuildID == "" {
  343. if u, err := url.Parse(m.File); err == nil && u.IsAbs() {
  344. m.File = ""
  345. }
  346. }
  347. }
  348. }
  349. // locateBinaries searches for binary files listed in the profile and, if found,
  350. // updates the profile accordingly.
  351. func locateBinaries(p *profile.Profile, s *source, obj plugin.ObjTool, ui plugin.UI) {
  352. // Construct search path to examine
  353. searchPath := os.Getenv("PPROF_BINARY_PATH")
  354. if searchPath == "" {
  355. // Use $HOME/pprof/binaries as default directory for local symbolization binaries
  356. searchPath = filepath.Join(os.Getenv(homeEnv()), "pprof", "binaries")
  357. }
  358. mapping:
  359. for _, m := range p.Mapping {
  360. var baseName string
  361. if m.File != "" {
  362. baseName = filepath.Base(m.File)
  363. }
  364. for _, path := range filepath.SplitList(searchPath) {
  365. var fileNames []string
  366. if m.BuildID != "" {
  367. fileNames = []string{filepath.Join(path, m.BuildID, baseName)}
  368. if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
  369. fileNames = append(fileNames, matches...)
  370. }
  371. fileNames = append(fileNames, filepath.Join(path, m.File, m.BuildID)) // perf path format
  372. }
  373. if m.File != "" {
  374. // Try both the basename and the full path, to support the same directory
  375. // structure as the perf symfs option.
  376. if baseName != "" {
  377. fileNames = append(fileNames, filepath.Join(path, baseName))
  378. }
  379. fileNames = append(fileNames, filepath.Join(path, m.File))
  380. }
  381. for _, name := range fileNames {
  382. if f, err := obj.Open(name, m.Start, m.Limit, m.Offset); err == nil {
  383. defer f.Close()
  384. fileBuildID := f.BuildID()
  385. if m.BuildID != "" && m.BuildID != fileBuildID {
  386. ui.PrintErr("Ignoring local file " + name + ": build-id mismatch (" + m.BuildID + " != " + fileBuildID + ")")
  387. } else {
  388. m.File = name
  389. continue mapping
  390. }
  391. }
  392. }
  393. }
  394. }
  395. if len(p.Mapping) == 0 {
  396. // If there are no mappings, add a fake mapping to attempt symbolization.
  397. // This is useful for some profiles generated by the golang runtime, which
  398. // do not include any mappings. Symbolization with a fake mapping will only
  399. // be successful against a non-PIE binary.
  400. m := &profile.Mapping{ID: 1}
  401. p.Mapping = []*profile.Mapping{m}
  402. for _, l := range p.Location {
  403. l.Mapping = m
  404. }
  405. }
  406. // Replace executable filename/buildID with the overrides from source.
  407. // Assumes the executable is the first Mapping entry.
  408. if execName, buildID := s.ExecName, s.BuildID; execName != "" || buildID != "" {
  409. m := p.Mapping[0]
  410. if execName != "" {
  411. m.File = execName
  412. }
  413. if buildID != "" {
  414. m.BuildID = buildID
  415. }
  416. }
  417. }
  418. // fetch fetches a profile from source, within the timeout specified,
  419. // producing messages through the ui. It returns the profile and the
  420. // url of the actual source of the profile for remote profiles.
  421. func fetch(source string, duration, timeout time.Duration, ui plugin.UI, tr http.RoundTripper) (p *profile.Profile, src string, err error) {
  422. var f io.ReadCloser
  423. if sourceURL, timeout := adjustURL(source, duration, timeout); sourceURL != "" {
  424. ui.Print("Fetching profile over HTTP from " + sourceURL)
  425. if duration > 0 {
  426. ui.Print(fmt.Sprintf("Please wait... (%v)", duration))
  427. }
  428. f, err = fetchURL(sourceURL, timeout, tr)
  429. src = sourceURL
  430. } else if isPerfFile(source) {
  431. f, err = convertPerfData(source, ui)
  432. } else {
  433. f, err = os.Open(source)
  434. }
  435. if err == nil {
  436. defer f.Close()
  437. p, err = profile.Parse(f)
  438. }
  439. return
  440. }
  441. // fetchURL fetches a profile from a URL using HTTP.
  442. func fetchURL(source string, timeout time.Duration, tr http.RoundTripper) (io.ReadCloser, error) {
  443. client := &http.Client{
  444. Transport: tr,
  445. Timeout: timeout + 5*time.Second,
  446. }
  447. resp, err := client.Get(source)
  448. if err != nil {
  449. return nil, fmt.Errorf("http fetch: %v", err)
  450. }
  451. if resp.StatusCode != http.StatusOK {
  452. defer resp.Body.Close()
  453. return nil, statusCodeError(resp)
  454. }
  455. return resp.Body, nil
  456. }
  457. func statusCodeError(resp *http.Response) error {
  458. if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
  459. // error is from pprof endpoint
  460. if body, err := ioutil.ReadAll(resp.Body); err == nil {
  461. return fmt.Errorf("server response: %s - %s", resp.Status, body)
  462. }
  463. }
  464. return fmt.Errorf("server response: %s", resp.Status)
  465. }
  466. // isPerfFile checks if a file is in perf.data format. It also returns false
  467. // if it encounters an error during the check.
  468. func isPerfFile(path string) bool {
  469. sourceFile, openErr := os.Open(path)
  470. if openErr != nil {
  471. return false
  472. }
  473. defer sourceFile.Close()
  474. // If the file is the output of a perf record command, it should begin
  475. // with the string PERFILE2.
  476. perfHeader := []byte("PERFILE2")
  477. actualHeader := make([]byte, len(perfHeader))
  478. if _, readErr := sourceFile.Read(actualHeader); readErr != nil {
  479. return false
  480. }
  481. return bytes.Equal(actualHeader, perfHeader)
  482. }
  483. // convertPerfData converts the file at path which should be in perf.data format
  484. // using the perf_to_profile tool and returns the file containing the
  485. // profile.proto formatted data.
  486. func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) {
  487. ui.Print(fmt.Sprintf(
  488. "Converting %s to a profile.proto... (May take a few minutes)",
  489. perfPath))
  490. profile, err := newTempFile(os.TempDir(), "pprof_", ".pb.gz")
  491. if err != nil {
  492. return nil, err
  493. }
  494. deferDeleteTempFile(profile.Name())
  495. cmd := exec.Command("perf_to_profile", "-i", perfPath, "-o", profile.Name(), "-f")
  496. cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
  497. if err := cmd.Run(); err != nil {
  498. profile.Close()
  499. return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
  500. }
  501. return profile, nil
  502. }
  503. // adjustURL validates if a profile source is a URL and returns an
  504. // cleaned up URL and the timeout to use for retrieval over HTTP.
  505. // If the source cannot be recognized as a URL it returns an empty string.
  506. func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
  507. u, err := url.Parse(source)
  508. if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
  509. // Try adding http:// to catch sources of the form hostname:port/path.
  510. // url.Parse treats "hostname" as the scheme.
  511. u, err = url.Parse("http://" + source)
  512. }
  513. if err != nil || u.Host == "" {
  514. return "", 0
  515. }
  516. // Apply duration/timeout overrides to URL.
  517. values := u.Query()
  518. if duration > 0 {
  519. values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
  520. } else {
  521. if urlSeconds := values.Get("seconds"); urlSeconds != "" {
  522. if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
  523. duration = time.Duration(us) * time.Second
  524. }
  525. }
  526. }
  527. if timeout <= 0 {
  528. if duration > 0 {
  529. timeout = duration + duration/2
  530. } else {
  531. timeout = 60 * time.Second
  532. }
  533. }
  534. u.RawQuery = values.Encode()
  535. return u.String(), timeout
  536. }