Sin descripción

fetch.go 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  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. "crypto/tls"
  18. "fmt"
  19. "io"
  20. "io/ioutil"
  21. "net/http"
  22. "net/url"
  23. "os"
  24. "os/exec"
  25. "path/filepath"
  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)+len(s.Base))
  40. for _, src := range s.Sources {
  41. sources = append(sources, profileSource{
  42. addr: src,
  43. source: s,
  44. scale: 1,
  45. })
  46. }
  47. for _, src := range s.Base {
  48. sources = append(sources, profileSource{
  49. addr: src,
  50. source: s,
  51. scale: -1,
  52. })
  53. }
  54. p, msrcs, save, cnt, err := chunkedGrab(sources, o.Fetch, o.Obj, o.UI)
  55. if err != nil {
  56. return nil, err
  57. }
  58. if cnt == 0 {
  59. return nil, fmt.Errorf("failed to fetch any profiles")
  60. }
  61. if want, got := len(sources), cnt; want != got {
  62. o.UI.PrintErr(fmt.Sprintf("fetched %d profiles out of %d", got, want))
  63. }
  64. // Symbolize the merged profile.
  65. if err := o.Sym.Symbolize(s.Symbolize, msrcs, p); err != nil {
  66. return nil, err
  67. }
  68. p.RemoveUninteresting()
  69. unsourceMappings(p)
  70. // Save a copy of the merged profile if there is at least one remote source.
  71. if save {
  72. dir, err := setTmpDir(o.UI)
  73. if err != nil {
  74. return nil, err
  75. }
  76. prefix := "pprof."
  77. if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
  78. prefix += filepath.Base(p.Mapping[0].File) + "."
  79. }
  80. for _, s := range p.SampleType {
  81. prefix += s.Type + "."
  82. }
  83. tempFile, err := newTempFile(dir, prefix, ".pb.gz")
  84. if err == nil {
  85. if err = p.Write(tempFile); err == nil {
  86. o.UI.PrintErr("Saved profile in ", tempFile.Name())
  87. }
  88. }
  89. if err != nil {
  90. o.UI.PrintErr("Could not save profile: ", err)
  91. }
  92. }
  93. if err := p.CheckValid(); err != nil {
  94. return nil, err
  95. }
  96. return p, nil
  97. }
  98. // chunkedGrab fetches the profiles described in source and merges them into
  99. // a single profile. It fetches a chunk of profiles concurrently, with a maximum
  100. // chunk size to limit its memory usage.
  101. func chunkedGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (*profile.Profile, plugin.MappingSources, bool, int, error) {
  102. const chunkSize = 64
  103. var p *profile.Profile
  104. var msrc plugin.MappingSources
  105. var save bool
  106. var count int
  107. for start := 0; start < len(sources); start += chunkSize {
  108. end := start + chunkSize
  109. if end > len(sources) {
  110. end = len(sources)
  111. }
  112. chunkP, chunkMsrc, chunkSave, chunkCount, chunkErr := concurrentGrab(sources[start:end], fetch, obj, ui)
  113. switch {
  114. case chunkErr != nil:
  115. return nil, nil, false, 0, chunkErr
  116. case chunkP == nil:
  117. continue
  118. case p == nil:
  119. p, msrc, save, count = chunkP, chunkMsrc, chunkSave, chunkCount
  120. default:
  121. p, msrc, chunkErr = combineProfiles([]*profile.Profile{p, chunkP}, []plugin.MappingSources{msrc, chunkMsrc})
  122. if chunkErr != nil {
  123. return nil, nil, false, 0, chunkErr
  124. }
  125. if chunkSave {
  126. save = true
  127. }
  128. count += chunkCount
  129. }
  130. }
  131. return p, msrc, save, count, nil
  132. }
  133. // concurrentGrab fetches multiple profiles concurrently
  134. func concurrentGrab(sources []profileSource, fetch plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (*profile.Profile, plugin.MappingSources, bool, int, error) {
  135. wg := sync.WaitGroup{}
  136. wg.Add(len(sources))
  137. for i := range sources {
  138. go func(s *profileSource) {
  139. defer wg.Done()
  140. s.p, s.msrc, s.remote, s.err = grabProfile(s.source, s.addr, s.scale, fetch, obj, ui)
  141. }(&sources[i])
  142. }
  143. wg.Wait()
  144. var save bool
  145. profiles := make([]*profile.Profile, 0, len(sources))
  146. msrcs := make([]plugin.MappingSources, 0, len(sources))
  147. for i := range sources {
  148. s := &sources[i]
  149. if err := s.err; err != nil {
  150. ui.PrintErr(s.addr + ": " + err.Error())
  151. continue
  152. }
  153. save = save || s.remote
  154. profiles = append(profiles, s.p)
  155. msrcs = append(msrcs, s.msrc)
  156. *s = profileSource{}
  157. }
  158. if len(profiles) == 0 {
  159. return nil, nil, false, 0, nil
  160. }
  161. p, msrc, err := combineProfiles(profiles, msrcs)
  162. if err != nil {
  163. return nil, nil, false, 0, err
  164. }
  165. return p, msrc, save, len(profiles), nil
  166. }
  167. func combineProfiles(profiles []*profile.Profile, msrcs []plugin.MappingSources) (*profile.Profile, plugin.MappingSources, error) {
  168. // Merge profiles.
  169. if err := measurement.ScaleProfiles(profiles); err != nil {
  170. return nil, nil, err
  171. }
  172. p, err := profile.Merge(profiles)
  173. if err != nil {
  174. return nil, nil, err
  175. }
  176. // Combine mapping sources.
  177. msrc := make(plugin.MappingSources)
  178. for _, ms := range msrcs {
  179. for m, s := range ms {
  180. msrc[m] = append(msrc[m], s...)
  181. }
  182. }
  183. return p, msrc, nil
  184. }
  185. type profileSource struct {
  186. addr string
  187. source *source
  188. scale float64
  189. p *profile.Profile
  190. msrc plugin.MappingSources
  191. remote bool
  192. err error
  193. }
  194. // setTmpDir prepares the directory to use to save profiles retrieved
  195. // remotely. It is selected from PPROF_TMPDIR, defaults to $HOME/pprof.
  196. func setTmpDir(ui plugin.UI) (string, error) {
  197. if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir != "" {
  198. return profileDir, nil
  199. }
  200. for _, tmpDir := range []string{os.Getenv("HOME") + "/pprof", os.TempDir()} {
  201. if err := os.MkdirAll(tmpDir, 0755); err != nil {
  202. ui.PrintErr("Could not use temp dir ", tmpDir, ": ", err.Error())
  203. continue
  204. }
  205. return tmpDir, nil
  206. }
  207. return "", fmt.Errorf("failed to identify temp dir")
  208. }
  209. // grabProfile fetches a profile. Returns the profile, sources for the
  210. // profile mappings, a bool indicating if the profile was fetched
  211. // remotely, and an error.
  212. func grabProfile(s *source, source string, scale float64, fetcher plugin.Fetcher, obj plugin.ObjTool, ui plugin.UI) (p *profile.Profile, msrc plugin.MappingSources, remote bool, err error) {
  213. var src string
  214. duration, timeout := time.Duration(s.Seconds)*time.Second, time.Duration(s.Timeout)*time.Second
  215. if fetcher != nil {
  216. p, src, err = fetcher.Fetch(source, duration, timeout)
  217. if err != nil {
  218. return
  219. }
  220. }
  221. if err != nil || p == nil {
  222. // Fetch the profile over HTTP or from a file.
  223. p, src, err = fetch(source, duration, timeout, ui)
  224. if err != nil {
  225. return
  226. }
  227. }
  228. if err = p.CheckValid(); err != nil {
  229. return
  230. }
  231. // Apply local changes to the profile.
  232. p.Scale(scale)
  233. // Update the binary locations from command line and paths.
  234. locateBinaries(p, s, obj, ui)
  235. // Collect the source URL for all mappings.
  236. if src != "" {
  237. msrc = collectMappingSources(p, src)
  238. remote = true
  239. }
  240. return
  241. }
  242. // collectMappingSources saves the mapping sources of a profile.
  243. func collectMappingSources(p *profile.Profile, source string) plugin.MappingSources {
  244. ms := plugin.MappingSources{}
  245. for _, m := range p.Mapping {
  246. src := struct {
  247. Source string
  248. Start uint64
  249. }{
  250. source, m.Start,
  251. }
  252. key := m.BuildID
  253. if key == "" {
  254. key = m.File
  255. }
  256. if key == "" {
  257. // If there is no build id or source file, use the source as the
  258. // mapping file. This will enable remote symbolization for this
  259. // mapping, in particular for Go profiles on the legacy format.
  260. // The source is reset back to empty string by unsourceMapping
  261. // which is called after symbolization is finished.
  262. m.File = source
  263. key = source
  264. }
  265. ms[key] = append(ms[key], src)
  266. }
  267. return ms
  268. }
  269. // unsourceMappings iterates over the mappings in a profile and replaces file
  270. // set to the remote source URL by collectMappingSources back to empty string.
  271. func unsourceMappings(p *profile.Profile) {
  272. for _, m := range p.Mapping {
  273. if m.BuildID == "" {
  274. if u, err := url.Parse(m.File); err == nil && u.IsAbs() {
  275. m.File = ""
  276. }
  277. }
  278. }
  279. }
  280. // locateBinaries searches for binary files listed in the profile and, if found,
  281. // updates the profile accordingly.
  282. func locateBinaries(p *profile.Profile, s *source, obj plugin.ObjTool, ui plugin.UI) {
  283. // Construct search path to examine
  284. searchPath := os.Getenv("PPROF_BINARY_PATH")
  285. if searchPath == "" {
  286. // Use $HOME/pprof/binaries as default directory for local symbolization binaries
  287. searchPath = filepath.Join(os.Getenv("HOME"), "pprof", "binaries")
  288. }
  289. mapping:
  290. for i, m := range p.Mapping {
  291. var baseName string
  292. // Replace executable filename/buildID with the overrides from source.
  293. // Assumes the executable is the first Mapping entry.
  294. if i == 0 {
  295. if s.ExecName != "" {
  296. m.File = s.ExecName
  297. }
  298. if s.BuildID != "" {
  299. m.BuildID = s.BuildID
  300. }
  301. }
  302. if m.File != "" {
  303. baseName = filepath.Base(m.File)
  304. }
  305. for _, path := range filepath.SplitList(searchPath) {
  306. var fileNames []string
  307. if m.BuildID != "" {
  308. fileNames = []string{filepath.Join(path, m.BuildID, baseName)}
  309. if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
  310. fileNames = append(fileNames, matches...)
  311. }
  312. }
  313. if baseName != "" {
  314. fileNames = append(fileNames, filepath.Join(path, baseName))
  315. }
  316. for _, name := range fileNames {
  317. if f, err := obj.Open(name, m.Start, m.Limit, m.Offset); err == nil {
  318. defer f.Close()
  319. fileBuildID := f.BuildID()
  320. if m.BuildID != "" && m.BuildID != fileBuildID {
  321. ui.PrintErr("Ignoring local file " + name + ": build-id mismatch (" + m.BuildID + " != " + fileBuildID + ")")
  322. } else {
  323. m.File = name
  324. continue mapping
  325. }
  326. }
  327. }
  328. }
  329. }
  330. }
  331. // fetch fetches a profile from source, within the timeout specified,
  332. // producing messages through the ui. It returns the profile and the
  333. // url of the actual source of the profile for remote profiles.
  334. func fetch(source string, duration, timeout time.Duration, ui plugin.UI) (p *profile.Profile, src string, err error) {
  335. var f io.ReadCloser
  336. if sourceURL, timeout := adjustURL(source, duration, timeout); sourceURL != "" {
  337. ui.Print("Fetching profile over HTTP from " + sourceURL)
  338. if duration > 0 {
  339. ui.Print(fmt.Sprintf("Please wait... (%v)", duration))
  340. }
  341. f, err = fetchURL(sourceURL, timeout)
  342. src = sourceURL
  343. } else if isPerfFile(source) {
  344. f, err = convertPerfData(source, ui)
  345. } else {
  346. f, err = os.Open(source)
  347. }
  348. if err == nil {
  349. defer f.Close()
  350. p, err = profile.Parse(f)
  351. }
  352. return
  353. }
  354. // fetchURL fetches a profile from a URL using HTTP.
  355. func fetchURL(source string, timeout time.Duration) (io.ReadCloser, error) {
  356. resp, err := httpGet(source, timeout)
  357. if err != nil {
  358. return nil, fmt.Errorf("http fetch: %v", err)
  359. }
  360. if resp.StatusCode != http.StatusOK {
  361. defer resp.Body.Close()
  362. return nil, statusCodeError(resp)
  363. }
  364. return resp.Body, nil
  365. }
  366. func statusCodeError(resp *http.Response) error {
  367. if resp.Header.Get("X-Go-Pprof") != "" && strings.Contains(resp.Header.Get("Content-Type"), "text/plain") {
  368. // error is from pprof endpoint
  369. if body, err := ioutil.ReadAll(resp.Body); err == nil {
  370. return fmt.Errorf("server response: %s - %s", resp.Status, body)
  371. }
  372. }
  373. return fmt.Errorf("server response: %s", resp.Status)
  374. }
  375. // isPerfFile checks if a file is in perf.data format. It also returns false
  376. // if it encounters an error during the check.
  377. func isPerfFile(path string) bool {
  378. sourceFile, openErr := os.Open(path)
  379. if openErr != nil {
  380. return false
  381. }
  382. defer sourceFile.Close()
  383. // If the file is the output of a perf record command, it should begin
  384. // with the string PERFILE2.
  385. perfHeader := []byte("PERFILE2")
  386. actualHeader := make([]byte, len(perfHeader))
  387. if _, readErr := sourceFile.Read(actualHeader); readErr != nil {
  388. return false
  389. }
  390. return bytes.Equal(actualHeader, perfHeader)
  391. }
  392. // convertPerfData converts the file at path which should be in perf.data format
  393. // using the perf_to_profile tool and returns the file containing the
  394. // profile.proto formatted data.
  395. func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) {
  396. ui.Print(fmt.Sprintf(
  397. "Converting %s to a profile.proto... (May take a few minutes)",
  398. perfPath))
  399. profile, err := newTempFile(os.TempDir(), "pprof_", ".pb.gz")
  400. if err != nil {
  401. return nil, err
  402. }
  403. deferDeleteTempFile(profile.Name())
  404. cmd := exec.Command("perf_to_profile", perfPath, profile.Name())
  405. if err := cmd.Run(); err != nil {
  406. profile.Close()
  407. return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
  408. }
  409. return profile, nil
  410. }
  411. // adjustURL validates if a profile source is a URL and returns an
  412. // cleaned up URL and the timeout to use for retrieval over HTTP.
  413. // If the source cannot be recognized as a URL it returns an empty string.
  414. func adjustURL(source string, duration, timeout time.Duration) (string, time.Duration) {
  415. u, err := url.Parse(source)
  416. if err != nil || (u.Host == "" && u.Scheme != "" && u.Scheme != "file") {
  417. // Try adding http:// to catch sources of the form hostname:port/path.
  418. // url.Parse treats "hostname" as the scheme.
  419. u, err = url.Parse("http://" + source)
  420. }
  421. if err != nil || u.Host == "" {
  422. return "", 0
  423. }
  424. // Apply duration/timeout overrides to URL.
  425. values := u.Query()
  426. if duration > 0 {
  427. values.Set("seconds", fmt.Sprint(int(duration.Seconds())))
  428. } else {
  429. if urlSeconds := values.Get("seconds"); urlSeconds != "" {
  430. if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil {
  431. duration = time.Duration(us) * time.Second
  432. }
  433. }
  434. }
  435. if timeout <= 0 {
  436. if duration > 0 {
  437. timeout = duration + duration/2
  438. } else {
  439. timeout = 60 * time.Second
  440. }
  441. }
  442. u.RawQuery = values.Encode()
  443. return u.String(), timeout
  444. }
  445. // httpGet is a wrapper around http.Get; it is defined as a variable
  446. // so it can be redefined during for testing.
  447. var httpGet = func(source string, timeout time.Duration) (*http.Response, error) {
  448. url, err := url.Parse(source)
  449. if err != nil {
  450. return nil, err
  451. }
  452. var tlsConfig *tls.Config
  453. if url.Scheme == "https+insecure" {
  454. tlsConfig = &tls.Config{
  455. InsecureSkipVerify: true,
  456. }
  457. url.Scheme = "https"
  458. source = url.String()
  459. }
  460. client := &http.Client{
  461. Transport: &http.Transport{
  462. ResponseHeaderTimeout: timeout + 5*time.Second,
  463. Proxy: http.ProxyFromEnvironment,
  464. TLSClientConfig: tlsConfig,
  465. },
  466. }
  467. return client.Get(source)
  468. }