説明なし

source.go 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  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 report
  15. // This file contains routines related to the generation of annotated
  16. // source listings.
  17. import (
  18. "bufio"
  19. "fmt"
  20. "html/template"
  21. "io"
  22. "os"
  23. "path/filepath"
  24. "strconv"
  25. "strings"
  26. "github.com/google/pprof/internal/graph"
  27. "github.com/google/pprof/internal/measurement"
  28. "github.com/google/pprof/internal/plugin"
  29. )
  30. // printSource prints an annotated source listing, include all
  31. // functions with samples that match the regexp rpt.options.symbol.
  32. // The sources are sorted by function name and then by filename to
  33. // eliminate potential nondeterminism.
  34. func printSource(w io.Writer, rpt *Report) error {
  35. o := rpt.options
  36. g := rpt.newGraph(nil)
  37. // Identify all the functions that match the regexp provided.
  38. // Group nodes for each matching function.
  39. var functions graph.Nodes
  40. functionNodes := make(map[string]graph.Nodes)
  41. for _, n := range g.Nodes {
  42. if !o.Symbol.MatchString(n.Info.Name) {
  43. continue
  44. }
  45. if functionNodes[n.Info.Name] == nil {
  46. functions = append(functions, n)
  47. }
  48. functionNodes[n.Info.Name] = append(functionNodes[n.Info.Name], n)
  49. }
  50. functions.Sort(graph.NameOrder)
  51. sourcePath := o.SourcePath
  52. if sourcePath == "" {
  53. wd, err := os.Getwd()
  54. if err != nil {
  55. return fmt.Errorf("could not stat current dir: %v", err)
  56. }
  57. sourcePath = wd
  58. }
  59. reader := newSourceReader(sourcePath, o.TrimPath)
  60. fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
  61. for _, fn := range functions {
  62. name := fn.Info.Name
  63. // Identify all the source files associated to this function.
  64. // Group nodes for each source file.
  65. var sourceFiles graph.Nodes
  66. fileNodes := make(map[string]graph.Nodes)
  67. for _, n := range functionNodes[name] {
  68. if n.Info.File == "" {
  69. continue
  70. }
  71. if fileNodes[n.Info.File] == nil {
  72. sourceFiles = append(sourceFiles, n)
  73. }
  74. fileNodes[n.Info.File] = append(fileNodes[n.Info.File], n)
  75. }
  76. if len(sourceFiles) == 0 {
  77. fmt.Fprintf(w, "No source information for %s\n", name)
  78. continue
  79. }
  80. sourceFiles.Sort(graph.FileOrder)
  81. // Print each file associated with this function.
  82. for _, fl := range sourceFiles {
  83. filename := fl.Info.File
  84. fns := fileNodes[filename]
  85. flatSum, cumSum := fns.Sum()
  86. fnodes, _, err := getSourceFromFile(filename, reader, fns, 0, 0)
  87. fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename)
  88. fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n",
  89. rpt.formatValue(flatSum), rpt.formatValue(cumSum),
  90. measurement.Percentage(cumSum, rpt.total))
  91. if err != nil {
  92. fmt.Fprintf(w, " Error: %v\n", err)
  93. continue
  94. }
  95. for _, fn := range fnodes {
  96. fmt.Fprintf(w, "%10s %10s %6d:%s\n", valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt), fn.Info.Lineno, fn.Info.Name)
  97. }
  98. }
  99. }
  100. return nil
  101. }
  102. // printWebSource prints an annotated source listing, include all
  103. // functions with samples that match the regexp rpt.options.symbol.
  104. func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error {
  105. printHeader(w, rpt)
  106. if err := PrintWebList(w, rpt, obj, -1); err != nil {
  107. return err
  108. }
  109. printPageClosing(w)
  110. return nil
  111. }
  112. // PrintWebList prints annotated source listing of rpt to w.
  113. func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) error {
  114. o := rpt.options
  115. g := rpt.newGraph(nil)
  116. // If the regexp source can be parsed as an address, also match
  117. // functions that land on that address.
  118. var address *uint64
  119. if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil {
  120. address = &hex
  121. }
  122. sourcePath := o.SourcePath
  123. if sourcePath == "" {
  124. wd, err := os.Getwd()
  125. if err != nil {
  126. return fmt.Errorf("could not stat current dir: %v", err)
  127. }
  128. sourcePath = wd
  129. }
  130. reader := newSourceReader(sourcePath, o.TrimPath)
  131. type fileFunction struct {
  132. fileName, functionName string
  133. }
  134. // Extract interesting symbols from binary files in the profile and
  135. // classify samples per symbol.
  136. symbols := symbolsFromBinaries(rpt.prof, g, o.Symbol, address, obj)
  137. symNodes := nodesPerSymbol(g.Nodes, symbols)
  138. // Identify sources associated to a symbol by examining
  139. // symbol samples. Classify samples per source file.
  140. fileNodes := make(map[fileFunction]graph.Nodes)
  141. if len(symNodes) == 0 {
  142. for _, n := range g.Nodes {
  143. if n.Info.File == "" || !o.Symbol.MatchString(n.Info.Name) {
  144. continue
  145. }
  146. ff := fileFunction{n.Info.File, n.Info.Name}
  147. fileNodes[ff] = append(fileNodes[ff], n)
  148. }
  149. } else {
  150. for _, nodes := range symNodes {
  151. for _, n := range nodes {
  152. if n.Info.File != "" {
  153. ff := fileFunction{n.Info.File, n.Info.Name}
  154. fileNodes[ff] = append(fileNodes[ff], n)
  155. }
  156. }
  157. }
  158. }
  159. if len(fileNodes) == 0 {
  160. return fmt.Errorf("no source information for %s", o.Symbol.String())
  161. }
  162. sourceFiles := make(graph.Nodes, 0, len(fileNodes))
  163. for _, nodes := range fileNodes {
  164. sNode := *nodes[0]
  165. sNode.Flat, sNode.Cum = nodes.Sum()
  166. sourceFiles = append(sourceFiles, &sNode)
  167. }
  168. // Limit number of files printed?
  169. if maxFiles < 0 {
  170. sourceFiles.Sort(graph.FileOrder)
  171. } else {
  172. sourceFiles.Sort(graph.FlatNameOrder)
  173. if maxFiles < len(sourceFiles) {
  174. sourceFiles = sourceFiles[:maxFiles]
  175. }
  176. }
  177. // Print each file associated with this function.
  178. for _, n := range sourceFiles {
  179. ff := fileFunction{n.Info.File, n.Info.Name}
  180. fns := fileNodes[ff]
  181. asm := assemblyPerSourceLine(symbols, fns, ff.fileName, obj, o.IntelSyntax)
  182. start, end := sourceCoordinates(asm)
  183. fnodes, path, err := getSourceFromFile(ff.fileName, reader, fns, start, end)
  184. if err != nil {
  185. fnodes, path = getMissingFunctionSource(ff.fileName, asm, start, end)
  186. }
  187. printFunctionHeader(w, ff.functionName, path, n.Flat, n.Cum, rpt)
  188. for _, fn := range fnodes {
  189. printFunctionSourceLine(w, fn, asm[fn.Info.Lineno], reader, rpt)
  190. }
  191. printFunctionClosing(w)
  192. }
  193. return nil
  194. }
  195. // sourceCoordinates returns the lowest and highest line numbers from
  196. // a set of assembly statements.
  197. func sourceCoordinates(asm map[int][]assemblyInstruction) (start, end int) {
  198. for l := range asm {
  199. if start == 0 || l < start {
  200. start = l
  201. }
  202. if end == 0 || l > end {
  203. end = l
  204. }
  205. }
  206. return start, end
  207. }
  208. // assemblyPerSourceLine disassembles the binary containing a symbol
  209. // and classifies the assembly instructions according to its
  210. // corresponding source line, annotating them with a set of samples.
  211. func assemblyPerSourceLine(objSyms []*objSymbol, rs graph.Nodes, src string, obj plugin.ObjTool, intelSyntax bool) map[int][]assemblyInstruction {
  212. assembly := make(map[int][]assemblyInstruction)
  213. // Identify symbol to use for this collection of samples.
  214. o := findMatchingSymbol(objSyms, rs)
  215. if o == nil {
  216. return assembly
  217. }
  218. // Extract assembly for matched symbol
  219. insts, err := obj.Disasm(o.sym.File, o.sym.Start, o.sym.End, intelSyntax)
  220. if err != nil {
  221. return assembly
  222. }
  223. srcBase := filepath.Base(src)
  224. anodes := annotateAssembly(insts, rs, o.base)
  225. var lineno = 0
  226. var prevline = 0
  227. for _, an := range anodes {
  228. // Do not rely solely on the line number produced by Disasm
  229. // since it is not what we want in the presence of inlining.
  230. //
  231. // E.g., suppose we are printing source code for F and this
  232. // instruction is from H where F called G called H and both
  233. // of those calls were inlined. We want to use the line
  234. // number from F, not from H (which is what Disasm gives us).
  235. //
  236. // So find the outer-most linenumber in the source file.
  237. found := false
  238. if frames, err := o.file.SourceLine(an.address + o.base); err == nil {
  239. for i := len(frames) - 1; i >= 0; i-- {
  240. if filepath.Base(frames[i].File) == srcBase {
  241. for j := i - 1; j >= 0; j-- {
  242. an.inlineCalls = append(an.inlineCalls, callID{frames[j].File, frames[j].Line})
  243. }
  244. lineno = frames[i].Line
  245. found = true
  246. break
  247. }
  248. }
  249. }
  250. if !found && filepath.Base(an.file) == srcBase {
  251. lineno = an.line
  252. }
  253. if lineno != 0 {
  254. if lineno != prevline {
  255. // This instruction starts a new block
  256. // of contiguous instructions on this line.
  257. an.startsBlock = true
  258. }
  259. prevline = lineno
  260. assembly[lineno] = append(assembly[lineno], an)
  261. }
  262. }
  263. return assembly
  264. }
  265. // findMatchingSymbol looks for the symbol that corresponds to a set
  266. // of samples, by comparing their addresses.
  267. func findMatchingSymbol(objSyms []*objSymbol, ns graph.Nodes) *objSymbol {
  268. for _, n := range ns {
  269. for _, o := range objSyms {
  270. if filepath.Base(o.sym.File) == filepath.Base(n.Info.Objfile) &&
  271. o.sym.Start <= n.Info.Address-o.base &&
  272. n.Info.Address-o.base <= o.sym.End {
  273. return o
  274. }
  275. }
  276. }
  277. return nil
  278. }
  279. // printHeader prints the page header for a weblist report.
  280. func printHeader(w io.Writer, rpt *Report) {
  281. fmt.Fprintln(w, `
  282. <!DOCTYPE html>
  283. <html>
  284. <head>
  285. <meta charset="UTF-8">
  286. <title>Pprof listing</title>`)
  287. fmt.Fprintln(w, weblistPageCSS)
  288. fmt.Fprintln(w, weblistPageScript)
  289. fmt.Fprint(w, "</head>\n<body>\n\n")
  290. var labels []string
  291. for _, l := range ProfileLabels(rpt) {
  292. labels = append(labels, template.HTMLEscapeString(l))
  293. }
  294. fmt.Fprintf(w, `<div class="legend">%s<br>Total: %s</div>`,
  295. strings.Join(labels, "<br>\n"),
  296. rpt.formatValue(rpt.total),
  297. )
  298. }
  299. // printFunctionHeader prints a function header for a weblist report.
  300. func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64, rpt *Report) {
  301. fmt.Fprintf(w, `<h2>%s</h2><p class="filename">%s</p>
  302. <pre onClick="pprof_toggle_asm(event)">
  303. Total: %10s %10s (flat, cum) %s
  304. `,
  305. template.HTMLEscapeString(name), template.HTMLEscapeString(path),
  306. rpt.formatValue(flatSum), rpt.formatValue(cumSum),
  307. measurement.Percentage(cumSum, rpt.total))
  308. }
  309. // printFunctionSourceLine prints a source line and the corresponding assembly.
  310. func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyInstruction, reader *sourceReader, rpt *Report) {
  311. if len(assembly) == 0 {
  312. fmt.Fprintf(w,
  313. "<span class=line> %6d</span> <span class=nop> %10s %10s %8s %s </span>\n",
  314. fn.Info.Lineno,
  315. valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt),
  316. "", template.HTMLEscapeString(fn.Info.Name))
  317. return
  318. }
  319. fmt.Fprintf(w,
  320. "<span class=line> %6d</span> <span class=deadsrc> %10s %10s %8s %s </span>",
  321. fn.Info.Lineno,
  322. valueOrDot(fn.Flat, rpt), valueOrDot(fn.Cum, rpt),
  323. "", template.HTMLEscapeString(fn.Info.Name))
  324. srcIndent := indentation(fn.Info.Name)
  325. fmt.Fprint(w, "<span class=asm>")
  326. var curCalls []callID
  327. for i, an := range assembly {
  328. if an.startsBlock && i != 0 {
  329. // Insert a separator between discontiguous blocks.
  330. fmt.Fprintf(w, " %8s %28s\n", "", "⋮")
  331. }
  332. var fileline string
  333. if an.file != "" {
  334. fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.file), an.line)
  335. }
  336. flat, cum := an.flat, an.cum
  337. if an.flatDiv != 0 {
  338. flat = flat / an.flatDiv
  339. }
  340. if an.cumDiv != 0 {
  341. cum = cum / an.cumDiv
  342. }
  343. // Print inlined call context.
  344. for j, c := range an.inlineCalls {
  345. if j < len(curCalls) && curCalls[j] == c {
  346. // Skip if same as previous instruction.
  347. continue
  348. }
  349. curCalls = nil
  350. fline, ok := reader.line(c.file, c.line)
  351. if !ok {
  352. fline = ""
  353. }
  354. text := strings.Repeat(" ", srcIndent+4+4*j) + strings.TrimSpace(fline)
  355. fmt.Fprintf(w, " %8s %10s %10s %8s <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n",
  356. "", "", "", "",
  357. template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
  358. template.HTMLEscapeString(filepath.Base(c.file)), c.line)
  359. }
  360. curCalls = an.inlineCalls
  361. text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
  362. fmt.Fprintf(w, " %8s %10s %10s %8x: %s <span class=unimportant>%s</span>\n",
  363. "", valueOrDot(flat, rpt), valueOrDot(cum, rpt), an.address,
  364. template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
  365. template.HTMLEscapeString(fileline))
  366. }
  367. fmt.Fprintln(w, "</span>")
  368. }
  369. // printFunctionClosing prints the end of a function in a weblist report.
  370. func printFunctionClosing(w io.Writer) {
  371. fmt.Fprintln(w, "</pre>")
  372. }
  373. // printPageClosing prints the end of the page in a weblist report.
  374. func printPageClosing(w io.Writer) {
  375. fmt.Fprintln(w, weblistPageClosing)
  376. }
  377. // getSourceFromFile collects the sources of a function from a source
  378. // file and annotates it with the samples in fns. Returns the sources
  379. // as nodes, using the info.name field to hold the source code.
  380. func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
  381. lineNodes := make(map[int]graph.Nodes)
  382. // Collect source coordinates from profile.
  383. const margin = 5 // Lines before first/after last sample.
  384. if start == 0 {
  385. if fns[0].Info.StartLine != 0 {
  386. start = fns[0].Info.StartLine
  387. } else {
  388. start = fns[0].Info.Lineno - margin
  389. }
  390. } else {
  391. start -= margin
  392. }
  393. if end == 0 {
  394. end = fns[0].Info.Lineno
  395. }
  396. end += margin
  397. for _, n := range fns {
  398. lineno := n.Info.Lineno
  399. nodeStart := n.Info.StartLine
  400. if nodeStart == 0 {
  401. nodeStart = lineno - margin
  402. }
  403. nodeEnd := lineno + margin
  404. if nodeStart < start {
  405. start = nodeStart
  406. } else if nodeEnd > end {
  407. end = nodeEnd
  408. }
  409. lineNodes[lineno] = append(lineNodes[lineno], n)
  410. }
  411. if start < 1 {
  412. start = 1
  413. }
  414. var src graph.Nodes
  415. for lineno := start; lineno <= end; lineno++ {
  416. line, ok := reader.line(file, lineno)
  417. if !ok {
  418. break
  419. }
  420. flat, cum := lineNodes[lineno].Sum()
  421. src = append(src, &graph.Node{
  422. Info: graph.NodeInfo{
  423. Name: strings.TrimRight(line, "\n"),
  424. Lineno: lineno,
  425. },
  426. Flat: flat,
  427. Cum: cum,
  428. })
  429. }
  430. if err := reader.fileError(file); err != nil {
  431. return nil, file, err
  432. }
  433. return src, file, nil
  434. }
  435. // getMissingFunctionSource creates a dummy function body to point to
  436. // the source file and annotates it with the samples in asm.
  437. func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction, start, end int) (graph.Nodes, string) {
  438. var fnodes graph.Nodes
  439. for i := start; i <= end; i++ {
  440. insts := asm[i]
  441. if len(insts) == 0 {
  442. continue
  443. }
  444. var group assemblyInstruction
  445. for _, insn := range insts {
  446. group.flat += insn.flat
  447. group.cum += insn.cum
  448. group.flatDiv += insn.flatDiv
  449. group.cumDiv += insn.cumDiv
  450. }
  451. flat := group.flatValue()
  452. cum := group.cumValue()
  453. fnodes = append(fnodes, &graph.Node{
  454. Info: graph.NodeInfo{
  455. Name: "???",
  456. Lineno: i,
  457. },
  458. Flat: flat,
  459. Cum: cum,
  460. })
  461. }
  462. return fnodes, filename
  463. }
  464. // sourceReader provides access to source code with caching of file contents.
  465. type sourceReader struct {
  466. // searchPath is a filepath.ListSeparator-separated list of directories where
  467. // source files should be searched.
  468. searchPath string
  469. // trimPath is a filepath.ListSeparator-separated list of paths to trim.
  470. trimPath string
  471. // files maps from path name to a list of lines.
  472. // files[*][0] is unused since line numbering starts at 1.
  473. files map[string][]string
  474. // errors collects errors encountered per file. These errors are
  475. // consulted before returning out of these module.
  476. errors map[string]error
  477. }
  478. func newSourceReader(searchPath, trimPath string) *sourceReader {
  479. return &sourceReader{
  480. searchPath,
  481. trimPath,
  482. make(map[string][]string),
  483. make(map[string]error),
  484. }
  485. }
  486. func (reader *sourceReader) fileError(path string) error {
  487. return reader.errors[path]
  488. }
  489. func (reader *sourceReader) line(path string, lineno int) (string, bool) {
  490. lines, ok := reader.files[path]
  491. if !ok {
  492. // Read and cache file contents.
  493. lines = []string{""} // Skip 0th line
  494. f, err := openSourceFile(path, reader.searchPath, reader.trimPath)
  495. if err != nil {
  496. reader.errors[path] = err
  497. } else {
  498. s := bufio.NewScanner(f)
  499. for s.Scan() {
  500. lines = append(lines, s.Text())
  501. }
  502. f.Close()
  503. if s.Err() != nil {
  504. reader.errors[path] = err
  505. }
  506. }
  507. reader.files[path] = lines
  508. }
  509. if lineno <= 0 || lineno >= len(lines) {
  510. return "", false
  511. }
  512. return lines[lineno], true
  513. }
  514. // openSourceFile opens a source file from a name encoded in a profile. File
  515. // names in a profile after can be relative paths, so search them in each of
  516. // the paths in searchPath and their parents. In case the profile contains
  517. // absolute paths, additional paths may be configured to trim from the source
  518. // paths in the profile. This effectively turns the path into a relative path
  519. // searching it using searchPath as usual).
  520. func openSourceFile(path, searchPath, trim string) (*os.File, error) {
  521. path = trimPath(path, trim, searchPath)
  522. // If file is still absolute, require file to exist.
  523. if filepath.IsAbs(path) {
  524. f, err := os.Open(path)
  525. return f, err
  526. }
  527. // Scan each component of the path.
  528. for _, dir := range filepath.SplitList(searchPath) {
  529. // Search up for every parent of each possible path.
  530. for {
  531. filename := filepath.Join(dir, path)
  532. if f, err := os.Open(filename); err == nil {
  533. return f, nil
  534. }
  535. parent := filepath.Dir(dir)
  536. if parent == dir {
  537. break
  538. }
  539. dir = parent
  540. }
  541. }
  542. return nil, fmt.Errorf("could not find file %s on path %s", path, searchPath)
  543. }
  544. // trimPath cleans up a path by removing prefixes that are commonly
  545. // found on profiles plus configured prefixes.
  546. // TODO(aalexand): Consider optimizing out the redundant work done in this
  547. // function if it proves to matter.
  548. func trimPath(path, trimPath, searchPath string) string {
  549. // Keep path variable intact as it's used below to form the return value.
  550. sPath, searchPath := filepath.ToSlash(path), filepath.ToSlash(searchPath)
  551. if trimPath == "" {
  552. // If the trim path is not configured, try to guess it heuristically:
  553. // search for basename of each search path in the original path and, if
  554. // found, strip everything up to and including the basename. So, for
  555. // example, given original path "/some/remote/path/my-project/foo/bar.c"
  556. // and search path "/my/local/path/my-project" the heuristic will return
  557. // "/my/local/path/my-project/foo/bar.c".
  558. for _, dir := range filepath.SplitList(searchPath) {
  559. want := "/" + filepath.Base(dir) + "/"
  560. if found := strings.Index(sPath, want); found != -1 {
  561. return path[found+len(want):]
  562. }
  563. }
  564. }
  565. // Trim configured trim prefixes.
  566. trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/")
  567. for _, trimPath := range trimPaths {
  568. if !strings.HasSuffix(trimPath, "/") {
  569. trimPath += "/"
  570. }
  571. if strings.HasPrefix(sPath, trimPath) {
  572. return path[len(trimPath):]
  573. }
  574. }
  575. return path
  576. }
  577. func indentation(line string) int {
  578. column := 0
  579. for _, c := range line {
  580. if c == ' ' {
  581. column++
  582. } else if c == '\t' {
  583. column++
  584. for column%8 != 0 {
  585. column++
  586. }
  587. } else {
  588. break
  589. }
  590. }
  591. return column
  592. }