Skip to content

Commit da1f7e9

Browse files
authored
Add doc URL to profile format and use it display help link. (#888)
* Add doc URL to profile format and use it display help link. * Add test for Report.DocURL * Update new proto field comment
1 parent fa2c70b commit da1f7e9

11 files changed

Lines changed: 162 additions & 0 deletions

File tree

internal/driver/html/common.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ a {
148148
right: 2px;
149149
}
150150

151+
.help {
152+
padding-left: 1em;
153+
}
154+
151155
{{/* Used to disable events when a modal dialog is displayed */}}
152156
#dialog-overlay {
153157
display: none;

internal/driver/html/header.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ <h1><a href="./">pprof</a></h1>
8383
{{range .Legend}}<div>{{.}}</div>{{end}}
8484
</div>
8585
</div>
86+
87+
{{if .DocURL}}
88+
<div class="menu-item">
89+
<div class="help menu-name"><a title="Profile documentation" href="{{.DocURL}}" target="_blank">Help&nbsp;⤇</a></div>
90+
</div>
91+
{{end}}
8692
</div>
8793

8894
<div id="dialog-overlay"></div>

internal/driver/webui.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type webArgs struct {
7979
Total int64
8080
SampleTypes []string
8181
Legend []string
82+
DocURL string
8283
Standalone bool // True for command-line generation of HTML
8384
Help map[string]string
8485
Nodes []string
@@ -290,6 +291,7 @@ func renderHTML(dst io.Writer, tmpl string, rpt *report.Report, errList, legend
290291
data.Title = file + " " + profile
291292
data.Errors = errList
292293
data.Total = rpt.Total()
294+
data.DocURL = rpt.DocURL()
293295
data.Legend = legend
294296
return getHTMLTemplates().ExecuteTemplate(dst, tmpl, data)
295297
}

internal/report/report.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package report
1919
import (
2020
"fmt"
2121
"io"
22+
"net/url"
2223
"path/filepath"
2324
"regexp"
2425
"sort"
@@ -1331,6 +1332,22 @@ func (rpt *Report) Total() int64 { return rpt.total }
13311332
// OutputFormat returns the output format for the report.
13321333
func (rpt *Report) OutputFormat() int { return rpt.options.OutputFormat }
13331334

1335+
// DocURL returns the documentation URL for Report, or "" if not available.
1336+
func (rpt *Report) DocURL() string {
1337+
u := rpt.prof.DocURL
1338+
if u == "" || !absoluteURL(u) {
1339+
return ""
1340+
}
1341+
return u
1342+
}
1343+
1344+
func absoluteURL(str string) bool {
1345+
// Avoid returning relative URLs to prevent unwanted local navigation
1346+
// within pprof server.
1347+
u, err := url.Parse(str)
1348+
return err == nil && (u.Scheme == "https" || u.Scheme == "http")
1349+
}
1350+
13341351
func abs64(i int64) int64 {
13351352
if i < 0 {
13361353
return -i

internal/report/report_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,3 +547,32 @@ func TestPrintAssemblyErrorMessage(t *testing.T) {
547547
}
548548
}
549549
}
550+
551+
func TestDocURL(t *testing.T) {
552+
type testCase struct {
553+
input string
554+
want string
555+
}
556+
for name, c := range map[string]testCase{
557+
"empty": {"", ""},
558+
"http": {"http://example.com/pprof-help", "http://example.com/pprof-help"},
559+
"https": {"https://example.com/pprof-help", "https://example.com/pprof-help"},
560+
"relative": {"/foo", ""},
561+
"nonhttp": {"mailto:nobody@example.com", ""},
562+
} {
563+
t.Run(name, func(t *testing.T) {
564+
profile := testProfile.Copy()
565+
profile.DocURL = c.input
566+
rpt := New(profile, &Options{
567+
OutputFormat: Dot,
568+
Symbol: regexp.MustCompile(`.`),
569+
TrimPath: "/some/path",
570+
SampleValue: func(v []int64) int64 { return v[1] },
571+
SampleUnit: testProfile.SampleType[1].Unit,
572+
})
573+
if got := rpt.DocURL(); got != c.want {
574+
t.Errorf("bad doc URL %q, expecting %q", got, c.want)
575+
}
576+
})
577+
}
578+
}

profile/encode.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ func (p *Profile) preEncode() {
122122
}
123123

124124
p.defaultSampleTypeX = addString(strings, p.DefaultSampleType)
125+
p.docURLX = addString(strings, p.DocURL)
125126

126127
p.stringTable = make([]string, len(strings))
127128
for s, i := range strings {
@@ -156,6 +157,7 @@ func (p *Profile) encode(b *buffer) {
156157
encodeInt64Opt(b, 12, p.Period)
157158
encodeInt64s(b, 13, p.commentX)
158159
encodeInt64(b, 14, p.defaultSampleTypeX)
160+
encodeInt64Opt(b, 15, p.docURLX)
159161
}
160162

161163
var profileDecoder = []decoder{
@@ -237,6 +239,8 @@ var profileDecoder = []decoder{
237239
func(b *buffer, m message) error { return decodeInt64s(b, &m.(*Profile).commentX) },
238240
// int64 defaultSampleType = 14
239241
func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).defaultSampleTypeX) },
242+
// string doc_link = 15;
243+
func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).docURLX) },
240244
}
241245

242246
// postDecode takes the unexported fields populated by decode (with
@@ -384,6 +388,7 @@ func (p *Profile) postDecode() error {
384388

385389
p.commentX = nil
386390
p.DefaultSampleType, err = getString(p.stringTable, &p.defaultSampleTypeX, err)
391+
p.DocURL, err = getString(p.stringTable, &p.docURLX, err)
387392
p.stringTable = nil
388393
return err
389394
}

profile/merge.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ func combineHeaders(srcs []*Profile) (*Profile, error) {
476476
var timeNanos, durationNanos, period int64
477477
var comments []string
478478
seenComments := map[string]bool{}
479+
var docURL string
479480
var defaultSampleType string
480481
for _, s := range srcs {
481482
if timeNanos == 0 || s.TimeNanos < timeNanos {
@@ -494,6 +495,9 @@ func combineHeaders(srcs []*Profile) (*Profile, error) {
494495
if defaultSampleType == "" {
495496
defaultSampleType = s.DefaultSampleType
496497
}
498+
if docURL == "" {
499+
docURL = s.DocURL
500+
}
497501
}
498502

499503
p := &Profile{
@@ -509,6 +513,7 @@ func combineHeaders(srcs []*Profile) (*Profile, error) {
509513

510514
Comments: comments,
511515
DefaultSampleType: defaultSampleType,
516+
DocURL: docURL,
512517
}
513518
copy(p.SampleType, srcs[0].SampleType)
514519
return p, nil

profile/merge_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package profile
1717
import (
1818
"bytes"
1919
"fmt"
20+
"reflect"
2021
"testing"
2122

2223
"github.com/google/pprof/internal/proftest"
@@ -443,3 +444,63 @@ func TestCompatibilizeSampleTypes(t *testing.T) {
443444
})
444445
}
445446
}
447+
448+
func TestDocURLMerge(t *testing.T) {
449+
const url1 = "http://example.com/url1"
450+
const url2 = "http://example.com/url2"
451+
type testCase struct {
452+
name string
453+
profiles []*Profile
454+
want string
455+
}
456+
profile := func(url string) *Profile {
457+
return &Profile{
458+
PeriodType: &ValueType{Type: "cpu", Unit: "seconds"},
459+
DocURL: url,
460+
}
461+
}
462+
for _, test := range []testCase{
463+
{
464+
name: "nolinks",
465+
profiles: []*Profile{
466+
profile(""),
467+
profile(""),
468+
},
469+
want: "",
470+
},
471+
{
472+
name: "single",
473+
profiles: []*Profile{
474+
profile(url1),
475+
},
476+
want: url1,
477+
},
478+
{
479+
name: "mix",
480+
profiles: []*Profile{
481+
profile(""),
482+
profile(url1),
483+
},
484+
want: url1,
485+
},
486+
{
487+
name: "different",
488+
profiles: []*Profile{
489+
profile(url1),
490+
profile(url2),
491+
},
492+
want: url1,
493+
},
494+
} {
495+
t.Run(test.name, func(t *testing.T) {
496+
merged, err := combineHeaders(test.profiles)
497+
if err != nil {
498+
t.Fatal(err)
499+
}
500+
got := merged.DocURL
501+
if !reflect.DeepEqual(test.want, got) {
502+
t.Errorf("unexpected links; want: %#v, got: %#v", test.want, got)
503+
}
504+
})
505+
}
506+
}

profile/profile.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Profile struct {
3939
Location []*Location
4040
Function []*Function
4141
Comments []string
42+
DocURL string
4243

4344
DropFrames string
4445
KeepFrames string
@@ -53,6 +54,7 @@ type Profile struct {
5354
encodeMu sync.Mutex
5455

5556
commentX []int64
57+
docURLX int64
5658
dropFramesX int64
5759
keepFramesX int64
5860
stringTable []string
@@ -555,6 +557,9 @@ func (p *Profile) String() string {
555557
for _, c := range p.Comments {
556558
ss = append(ss, "Comment: "+c)
557559
}
560+
if url := p.DocURL; url != "" {
561+
ss = append(ss, fmt.Sprintf("Doc: %s", url))
562+
}
558563
if pt := p.PeriodType; pt != nil {
559564
ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit))
560565
}

profile/profile_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,6 +1869,28 @@ func TestParseKernelRelocation(t *testing.T) {
18691869
}
18701870
}
18711871

1872+
func TestEncodeDecodeDocURL(t *testing.T) {
1873+
input := testProfile1.Copy()
1874+
input.DocURL = "http://example.comp/url"
1875+
1876+
// Encode/decode.
1877+
var buf bytes.Buffer
1878+
if err := input.Write(&buf); err != nil {
1879+
t.Fatal("encode: ", err)
1880+
}
1881+
output, err := Parse(&buf)
1882+
if err != nil {
1883+
t.Fatal("decode: ", err)
1884+
}
1885+
if want, got := input.String(), output.String(); want != got {
1886+
d, err := proftest.Diff([]byte(want), []byte(got))
1887+
if err != nil {
1888+
t.Fatal(err)
1889+
}
1890+
t.Errorf("wrong result of encode/decode (-want,+got):\n%s\n", string(d))
1891+
}
1892+
}
1893+
18721894
// parallel runs n copies of fn in parallel.
18731895
func parallel(n int, fn func()) {
18741896
var wg sync.WaitGroup

0 commit comments

Comments
 (0)