...

Source file src/github.com/google/pprof/internal/driver/webui_test.go

Documentation: github.com/google/pprof/internal/driver

     1  // Copyright 2017 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  
    15  package driver
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"net"
    21  	"net/http"
    22  	"net/http/httptest"
    23  	"net/url"
    24  	"os/exec"
    25  	"regexp"
    26  	"runtime"
    27  	"sync"
    28  	"testing"
    29  
    30  	"github.com/google/pprof/internal/plugin"
    31  	"github.com/google/pprof/internal/proftest"
    32  	"github.com/google/pprof/profile"
    33  )
    34  
    35  func makeTestServer(t testing.TB, prof *profile.Profile) *httptest.Server {
    36  	if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
    37  		t.Skip("test assumes tcp available")
    38  	}
    39  
    40  	// Custom http server creator
    41  	var server *httptest.Server
    42  	serverCreated := make(chan bool)
    43  	creator := func(a *plugin.HTTPServerArgs) error {
    44  		server = httptest.NewServer(http.HandlerFunc(
    45  			func(w http.ResponseWriter, r *http.Request) {
    46  				if h := a.Handlers[r.URL.Path]; h != nil {
    47  					h.ServeHTTP(w, r)
    48  				}
    49  			}))
    50  		serverCreated <- true
    51  		return nil
    52  	}
    53  
    54  	// Start server and wait for it to be initialized
    55  	go serveWebInterface("unused:1234", prof, &plugin.Options{
    56  		Obj:        fakeObjTool{},
    57  		UI:         &proftest.TestUI{T: t},
    58  		HTTPServer: creator,
    59  	}, false)
    60  	<-serverCreated
    61  
    62  	// Close the server when the test is done.
    63  	t.Cleanup(server.Close)
    64  
    65  	return server
    66  }
    67  
    68  func TestWebInterface(t *testing.T) {
    69  	prof := makeFakeProfile()
    70  	server := makeTestServer(t, prof)
    71  	haveDot := false
    72  	if _, err := exec.LookPath("dot"); err == nil {
    73  		haveDot = true
    74  	}
    75  
    76  	type testCase struct {
    77  		path    string
    78  		want    []string
    79  		needDot bool
    80  	}
    81  	testcases := []testCase{
    82  		{"/", []string{"F1", "F2", "F3", "testbin", "cpu"}, true},
    83  		{"/top", []string{`"Name":"F2","InlineLabel":"","Flat":200,"Cum":300,"FlatFormat":"200ms","CumFormat":"300ms"}`}, false},
    84  		{"/source?f=" + url.QueryEscape("F[12]"), []string{
    85  			"F1",
    86  			"F2",
    87  			`\. +300ms .*f1:asm`,    // Cumulative count for F1
    88  			"200ms +300ms .*f2:asm", // Flat + cumulative count for F2
    89  		}, false},
    90  		{"/peek?f=" + url.QueryEscape("F[12]"),
    91  			[]string{"300ms.*F1", "200ms.*300ms.*F2"}, false},
    92  		{"/disasm?f=" + url.QueryEscape("F[12]"),
    93  			[]string{"f1:asm", "f2:asm"}, false},
    94  		{"/flamegraph", []string{
    95  			"File: testbin",
    96  			// Check that interesting frames are included.
    97  			`\bF1\b`,
    98  			`\bF2\b`,
    99  			// Check new view JS is included.
   100  			`function stackViewer`,
   101  			// Check new view CSS is included.
   102  			"#stack-chart {",
   103  		}, false},
   104  	}
   105  	for _, c := range testcases {
   106  		if c.needDot && !haveDot {
   107  			t.Log("skipping", c.path, "since dot (graphviz) does not seem to be installed")
   108  			continue
   109  		}
   110  		res, err := http.Get(server.URL + c.path)
   111  		if err != nil {
   112  			t.Error("could not fetch", c.path, err)
   113  			continue
   114  		}
   115  		data, err := io.ReadAll(res.Body)
   116  		if err != nil {
   117  			t.Error("could not read response", c.path, err)
   118  			continue
   119  		}
   120  		result := string(data)
   121  		for _, w := range c.want {
   122  			if match, _ := regexp.MatchString(w, result); !match {
   123  				t.Errorf("response for %s does not match "+
   124  					"expected pattern '%s'; "+
   125  					"actual result:\n%s", c.path, w, result)
   126  			}
   127  		}
   128  	}
   129  
   130  	// Also fetch all the test case URLs in parallel to test thread
   131  	// safety when run under the race detector.
   132  	var wg sync.WaitGroup
   133  	for _, c := range testcases {
   134  		if c.needDot && !haveDot {
   135  			continue
   136  		}
   137  		path := server.URL + c.path
   138  		for count := 0; count < 2; count++ {
   139  			wg.Add(1)
   140  			go func() {
   141  				defer wg.Done()
   142  				res, err := http.Get(path)
   143  				if err != nil {
   144  					t.Error("could not fetch", path, err)
   145  					return
   146  				}
   147  				if _, err = io.ReadAll(res.Body); err != nil {
   148  					t.Error("could not read response", path, err)
   149  				}
   150  			}()
   151  		}
   152  	}
   153  	wg.Wait()
   154  }
   155  
   156  // Implement fake object file support.
   157  
   158  const addrBase = 0x1000
   159  const fakeSource = "testdata/file1000.src"
   160  
   161  type fakeObj struct{}
   162  
   163  func (f fakeObj) Close() error                        { return nil }
   164  func (f fakeObj) Name() string                        { return "testbin" }
   165  func (f fakeObj) ObjAddr(addr uint64) (uint64, error) { return addr, nil }
   166  func (f fakeObj) BuildID() string                     { return "" }
   167  func (f fakeObj) SourceLine(addr uint64) ([]plugin.Frame, error) {
   168  	return nil, fmt.Errorf("SourceLine unimplemented")
   169  }
   170  func (f fakeObj) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) {
   171  	return []*plugin.Sym{
   172  		{
   173  			Name: []string{"F1"}, File: fakeSource,
   174  			Start: addrBase, End: addrBase + 10,
   175  		},
   176  		{
   177  			Name: []string{"F2"}, File: fakeSource,
   178  			Start: addrBase + 10, End: addrBase + 20,
   179  		},
   180  		{
   181  			Name: []string{"F3"}, File: fakeSource,
   182  			Start: addrBase + 20, End: addrBase + 30,
   183  		},
   184  	}, nil
   185  }
   186  
   187  type fakeObjTool struct{}
   188  
   189  func (obj fakeObjTool) Open(file string, start, limit, offset uint64, relocationSymbol string) (plugin.ObjFile, error) {
   190  	return fakeObj{}, nil
   191  }
   192  
   193  func (obj fakeObjTool) Disasm(file string, start, end uint64, intelSyntax bool) ([]plugin.Inst, error) {
   194  	return []plugin.Inst{
   195  		{Addr: addrBase + 10, Text: "f1:asm", Function: "F1", Line: 3},
   196  		{Addr: addrBase + 20, Text: "f2:asm", Function: "F2", Line: 11},
   197  		{Addr: addrBase + 30, Text: "d3:asm", Function: "F3", Line: 22},
   198  	}, nil
   199  }
   200  
   201  func makeFakeProfile() *profile.Profile {
   202  	// Three functions: F1, F2, F3 with three lines, 11, 22, 33.
   203  	funcs := []*profile.Function{
   204  		{ID: 1, Name: "F1", Filename: fakeSource, StartLine: 3},
   205  		{ID: 2, Name: "F2", Filename: fakeSource, StartLine: 5},
   206  		{ID: 3, Name: "F3", Filename: fakeSource, StartLine: 7},
   207  	}
   208  	lines := []profile.Line{
   209  		{Function: funcs[0], Line: 11},
   210  		{Function: funcs[1], Line: 22},
   211  		{Function: funcs[2], Line: 33},
   212  	}
   213  	mapping := []*profile.Mapping{
   214  		{
   215  			ID:             1,
   216  			Start:          addrBase,
   217  			Limit:          addrBase + 100,
   218  			Offset:         0,
   219  			File:           "testbin",
   220  			HasFunctions:   true,
   221  			HasFilenames:   true,
   222  			HasLineNumbers: true,
   223  		},
   224  	}
   225  
   226  	// Three interesting addresses: base+{10,20,30}
   227  	locs := []*profile.Location{
   228  		{ID: 1, Address: addrBase + 10, Line: lines[0:1], Mapping: mapping[0]},
   229  		{ID: 2, Address: addrBase + 20, Line: lines[1:2], Mapping: mapping[0]},
   230  		{ID: 3, Address: addrBase + 30, Line: lines[2:3], Mapping: mapping[0]},
   231  	}
   232  
   233  	// Two stack traces.
   234  	return &profile.Profile{
   235  		PeriodType:    &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
   236  		Period:        1,
   237  		DurationNanos: 10e9,
   238  		SampleType: []*profile.ValueType{
   239  			{Type: "cpu", Unit: "milliseconds"},
   240  		},
   241  		Sample: []*profile.Sample{
   242  			{
   243  				Location: []*profile.Location{locs[2], locs[1], locs[0]},
   244  				Value:    []int64{100},
   245  			},
   246  			{
   247  				Location: []*profile.Location{locs[1], locs[0]},
   248  				Value:    []int64{200},
   249  			},
   250  		},
   251  		Location: locs,
   252  		Function: funcs,
   253  		Mapping:  mapping,
   254  	}
   255  }
   256  
   257  func TestGetHostAndPort(t *testing.T) {
   258  	if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
   259  		t.Skip("test assumes tcp available")
   260  	}
   261  
   262  	type testCase struct {
   263  		hostport       string
   264  		wantHost       string
   265  		wantPort       int
   266  		wantRandomPort bool
   267  	}
   268  
   269  	testCases := []testCase{
   270  		{":", "localhost", 0, true},
   271  		{":4681", "localhost", 4681, false},
   272  		{"localhost:4681", "localhost", 4681, false},
   273  	}
   274  	for _, tc := range testCases {
   275  		host, port, err := getHostAndPort(tc.hostport)
   276  		if err != nil {
   277  			t.Errorf("could not get host and port for %q: %v", tc.hostport, err)
   278  		}
   279  		if got, want := host, tc.wantHost; got != want {
   280  			t.Errorf("for %s, got host %s, want %s", tc.hostport, got, want)
   281  			continue
   282  		}
   283  		if !tc.wantRandomPort {
   284  			if got, want := port, tc.wantPort; got != want {
   285  				t.Errorf("for %s, got port %d, want %d", tc.hostport, got, want)
   286  				continue
   287  			}
   288  		}
   289  	}
   290  }
   291  
   292  func TestIsLocalHost(t *testing.T) {
   293  	for _, s := range []string{"localhost:10000", "[::1]:10000", "127.0.0.1:10000"} {
   294  		host, _, err := net.SplitHostPort(s)
   295  		if err != nil {
   296  			t.Error("unexpected error when splitting", s)
   297  			continue
   298  		}
   299  		if !isLocalhost(host) {
   300  			t.Errorf("host %s from %s not considered local", host, s)
   301  		}
   302  	}
   303  }
   304  
   305  func BenchmarkTop(b *testing.B)   { benchmarkURL(b, "/top", false) }
   306  func BenchmarkFlame(b *testing.B) { benchmarkURL(b, "/flamegraph", false) }
   307  func BenchmarkDot(b *testing.B)   { benchmarkURL(b, "/", true) }
   308  
   309  func benchmarkURL(b *testing.B, path string, needDot bool) {
   310  	if needDot {
   311  		if _, err := exec.LookPath("dot"); err != nil {
   312  			b.Skip("dot not available")
   313  		}
   314  	}
   315  	prof := largeProfile(b)
   316  	server := makeTestServer(b, prof)
   317  	url := server.URL + path
   318  	b.ResetTimer()
   319  	for i := 0; i < b.N; i++ {
   320  		res, err := http.Get(url)
   321  		if err != nil {
   322  			b.Fatal(err)
   323  		}
   324  		data, err := io.ReadAll(res.Body)
   325  		if err != nil {
   326  			b.Fatal(err)
   327  		}
   328  		if i == 0 && testing.Verbose() {
   329  			b.Logf("%-12s : %10d bytes", path, len(data))
   330  		}
   331  	}
   332  }
   333  

View as plain text