...

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

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

     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  
    15  package driver
    16  
    17  import (
    18  	"bytes"
    19  	"flag"
    20  	"fmt"
    21  	"net"
    22  	_ "net/http/pprof"
    23  	"os"
    24  	"reflect"
    25  	"regexp"
    26  	"runtime"
    27  	"strconv"
    28  	"strings"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/google/pprof/internal/plugin"
    33  	"github.com/google/pprof/internal/proftest"
    34  	"github.com/google/pprof/internal/symbolz"
    35  	"github.com/google/pprof/profile"
    36  )
    37  
    38  var updateFlag = flag.Bool("update", false, "Update the golden files")
    39  
    40  func TestParse(t *testing.T) {
    41  	// Override weblist command to collect output in buffer
    42  	pprofCommands["weblist"].postProcess = nil
    43  
    44  	// Our mockObjTool.Open will always return success, causing
    45  	// driver.locateBinaries to "find" the binaries below in a non-existent
    46  	// directory. As a workaround, point the search path to the fake
    47  	// directory containing out fake binaries.
    48  	savePath := os.Getenv("PPROF_BINARY_PATH")
    49  	os.Setenv("PPROF_BINARY_PATH", "/path/to")
    50  	defer os.Setenv("PPROF_BINARY_PATH", savePath)
    51  	testcase := []struct {
    52  		flags, source string
    53  	}{
    54  		{"text,functions,flat", "cpu"},
    55  		{"text,functions,noinlines,flat", "cpu"},
    56  		{"text,filefunctions,noinlines,flat", "cpu"},
    57  		{"text,addresses,noinlines,flat", "cpu"},
    58  		{"tree,addresses,flat,nodecount=4", "cpusmall"},
    59  		{"text,functions,flat,nodecount=5,call_tree", "unknown"},
    60  		{"text,alloc_objects,flat", "heap_alloc"},
    61  		{"text,files,flat", "heap"},
    62  		{"text,files,flat,focus=[12]00,taghide=[X3]00", "heap"},
    63  		{"text,inuse_objects,flat", "heap"},
    64  		{"text,lines,cum,hide=line[X3]0", "cpu"},
    65  		{"text,lines,cum,show=[12]00", "cpu"},
    66  		{"text,lines,cum,hide=line[X3]0,focus=[12]00", "cpu"},
    67  		{"topproto,lines,cum,hide=mangled[X3]0", "cpu"},
    68  		{"topproto,lines", "cpu"},
    69  		{"tree,lines,cum,focus=[24]00", "heap"},
    70  		{"tree,relative_percentages,cum,focus=[24]00", "heap"},
    71  		{"tree,lines,cum,show_from=line2", "cpu"},
    72  		{"callgrind", "cpu"},
    73  		{"callgrind,call_tree", "cpu"},
    74  		{"callgrind", "heap"},
    75  		{"dot,functions,flat", "cpu"},
    76  		{"dot,functions,flat,call_tree", "cpu"},
    77  		{"dot,lines,flat,focus=[12]00", "heap"},
    78  		{"dot,unit=minimum", "heap_sizetags"},
    79  		{"dot,addresses,flat,ignore=[X3]002,focus=[X1]000", "contention"},
    80  		{"dot,files,cum", "contention"},
    81  		{"comments,add_comment=some-comment", "cpu"},
    82  		{"comments", "heap"},
    83  		{"tags", "cpu"},
    84  		{"tags,tagignore=tag[13],tagfocus=key[12]", "cpu"},
    85  		{"tags", "heap"},
    86  		{"tags,unit=bytes", "heap"},
    87  		{"traces", "cpu"},
    88  		{"traces,addresses", "cpu"},
    89  		{"traces", "heap_tags"},
    90  		{"dot,alloc_space,flat,focus=[234]00", "heap_alloc"},
    91  		{"dot,alloc_space,flat,tagshow=[2]00", "heap_alloc"},
    92  		{"dot,alloc_space,flat,hide=line.*1?23?", "heap_alloc"},
    93  		{"dot,inuse_space,flat,tagfocus=1mb:2gb", "heap"},
    94  		{"dot,inuse_space,flat,tagfocus=30kb:,tagignore=1mb:2mb", "heap"},
    95  		{"disasm=line[13],addresses,flat", "cpu"},
    96  		{"peek=line.*01", "cpu"},
    97  		{"weblist=line(1000|3000)$,addresses,flat", "cpu"},
    98  		{"tags,tagfocus=400kb:", "heap_request"},
    99  		{"tags,tagfocus=+400kb:", "heap_request"},
   100  		{"dot", "long_name_funcs"},
   101  		{"text", "long_name_funcs"},
   102  	}
   103  
   104  	baseConfig := currentConfig()
   105  	defer setCurrentConfig(baseConfig)
   106  	for _, tc := range testcase {
   107  		t.Run(tc.flags+":"+tc.source, func(t *testing.T) {
   108  			// Reset config before processing
   109  			setCurrentConfig(baseConfig)
   110  
   111  			testUI := &proftest.TestUI{T: t, AllowRx: "Generating report in|Ignoring local file|expression matched no samples|Interpreted .* as range, not regexp"}
   112  
   113  			f := baseFlags()
   114  			f.args = []string{tc.source}
   115  
   116  			flags := strings.Split(tc.flags, ",")
   117  
   118  			// Encode profile into a protobuf and decode it again.
   119  			protoTempFile, err := os.CreateTemp("", "profile_proto")
   120  			if err != nil {
   121  				t.Errorf("cannot create tempfile: %v", err)
   122  			}
   123  			defer os.Remove(protoTempFile.Name())
   124  			defer protoTempFile.Close()
   125  			f.strings["output"] = protoTempFile.Name()
   126  
   127  			if flags[0] == "topproto" {
   128  				f.bools["proto"] = false
   129  				f.bools["topproto"] = true
   130  				f.bools["addresses"] = true
   131  			}
   132  
   133  			// First pprof invocation to save the profile into a profile.proto.
   134  			// Pass in flag set hen setting defaults, because otherwise default
   135  			// transport will try to add flags to the default flag set.
   136  			o1 := setDefaults(&plugin.Options{Flagset: f})
   137  			o1.Fetch = testFetcher{}
   138  			o1.Sym = testSymbolizer{}
   139  			o1.UI = testUI
   140  			if err := PProf(o1); err != nil {
   141  				t.Fatalf("%s %q:  %v", tc.source, tc.flags, err)
   142  			}
   143  			// Reset config after the proto invocation
   144  			setCurrentConfig(baseConfig)
   145  
   146  			// Read the profile from the encoded protobuf
   147  			outputTempFile, err := os.CreateTemp("", "profile_output")
   148  			if err != nil {
   149  				t.Errorf("cannot create tempfile: %v", err)
   150  			}
   151  			defer os.Remove(outputTempFile.Name())
   152  			defer outputTempFile.Close()
   153  
   154  			f = baseFlags()
   155  			f.strings["output"] = outputTempFile.Name()
   156  			f.args = []string{protoTempFile.Name()}
   157  
   158  			delete(f.bools, "proto")
   159  			addFlags(&f, flags)
   160  			solution := solutionFilename(tc.source, &f)
   161  			// Apply the flags for the second pprof run, and identify name of
   162  			// the file containing expected results
   163  			if flags[0] == "topproto" {
   164  				addFlags(&f, flags)
   165  				solution = solutionFilename(tc.source, &f)
   166  				delete(f.bools, "topproto")
   167  				f.bools["text"] = true
   168  			}
   169  
   170  			// Second pprof invocation to read the profile from profile.proto
   171  			// and generate a report.
   172  			// Pass in flag set hen setting defaults, because otherwise default
   173  			// transport will try to add flags to the default flag set.
   174  			o2 := setDefaults(&plugin.Options{Flagset: f})
   175  			o2.Sym = testSymbolizeDemangler{}
   176  			o2.Obj = new(mockObjTool)
   177  			o2.UI = testUI
   178  
   179  			if err := PProf(o2); err != nil {
   180  				t.Errorf("%s: %v", tc.source, err)
   181  			}
   182  			b, err := os.ReadFile(outputTempFile.Name())
   183  			if err != nil {
   184  				t.Errorf("Failed to read profile %s: %v", outputTempFile.Name(), err)
   185  			}
   186  
   187  			// Read data file with expected solution
   188  			solution = "testdata/" + solution
   189  			sbuf, err := os.ReadFile(solution)
   190  			if err != nil {
   191  				t.Fatalf("reading solution file %s: %v", solution, err)
   192  			}
   193  			if runtime.GOOS == "windows" {
   194  				if flags[0] == "dot" {
   195  					// The .dot test has the paths inside strings, so \ must be escaped.
   196  					sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte(`testdata\\`), -1)
   197  					sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte(`\\path\\to\\`), -1)
   198  				} else {
   199  					sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte(`testdata\`), -1)
   200  					sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte(`\path\to\`), -1)
   201  				}
   202  			}
   203  
   204  			if flags[0] == "svg" {
   205  				b = removeScripts(b)
   206  				sbuf = removeScripts(sbuf)
   207  			}
   208  
   209  			if string(b) != string(sbuf) {
   210  				t.Errorf("diff %s %s", solution, tc.source)
   211  				d, err := proftest.Diff(sbuf, b)
   212  				if err != nil {
   213  					t.Fatalf("diff %s %v", solution, err)
   214  				}
   215  				t.Errorf("%s\n%s\n", solution, d)
   216  				if *updateFlag {
   217  					err := os.WriteFile(solution, b, 0644)
   218  					if err != nil {
   219  						t.Errorf("failed to update the solution file %q: %v", solution, err)
   220  					}
   221  				}
   222  			}
   223  		})
   224  	}
   225  }
   226  
   227  // removeScripts removes <script > .. </script> pairs from its input
   228  func removeScripts(in []byte) []byte {
   229  	beginMarker := []byte("<script")
   230  	endMarker := []byte("</script>")
   231  
   232  	if begin := bytes.Index(in, beginMarker); begin > 0 {
   233  		if end := bytes.Index(in[begin:], endMarker); end > 0 {
   234  			in = append(in[:begin], removeScripts(in[begin+end+len(endMarker):])...)
   235  		}
   236  	}
   237  	return in
   238  }
   239  
   240  // addFlags parses flag descriptions and adds them to the testFlags
   241  func addFlags(f *testFlags, flags []string) {
   242  	for _, flag := range flags {
   243  		fields := strings.SplitN(flag, "=", 2)
   244  		switch len(fields) {
   245  		case 1:
   246  			f.bools[fields[0]] = true
   247  		case 2:
   248  			if i, err := strconv.Atoi(fields[1]); err == nil {
   249  				f.ints[fields[0]] = i
   250  			} else {
   251  				f.strings[fields[0]] = fields[1]
   252  			}
   253  		}
   254  	}
   255  }
   256  
   257  func testSourceURL(port int) string {
   258  	return fmt.Sprintf("http://%s/", net.JoinHostPort(testSourceAddress, strconv.Itoa(port)))
   259  }
   260  
   261  // solutionFilename returns the name of the solution file for the test
   262  func solutionFilename(source string, f *testFlags) string {
   263  	name := []string{"pprof", strings.TrimPrefix(source, testSourceURL(8000))}
   264  	name = addString(name, f, []string{"flat", "cum"})
   265  	name = addString(name, f, []string{"functions", "filefunctions", "files", "lines", "addresses"})
   266  	name = addString(name, f, []string{"noinlines"})
   267  	name = addString(name, f, []string{"inuse_space", "inuse_objects", "alloc_space", "alloc_objects"})
   268  	name = addString(name, f, []string{"relative_percentages"})
   269  	name = addString(name, f, []string{"seconds"})
   270  	name = addString(name, f, []string{"call_tree"})
   271  	name = addString(name, f, []string{"text", "tree", "callgrind", "dot", "svg", "tags", "dot", "traces", "disasm", "peek", "weblist", "topproto", "comments"})
   272  	if f.strings["focus"] != "" || f.strings["tagfocus"] != "" {
   273  		name = append(name, "focus")
   274  	}
   275  	if f.strings["ignore"] != "" || f.strings["tagignore"] != "" {
   276  		name = append(name, "ignore")
   277  	}
   278  	if f.strings["show_from"] != "" {
   279  		name = append(name, "show_from")
   280  	}
   281  	name = addString(name, f, []string{"hide", "show"})
   282  	if f.strings["unit"] != "minimum" {
   283  		name = addString(name, f, []string{"unit"})
   284  	}
   285  	return strings.Join(name, ".")
   286  }
   287  
   288  func addString(name []string, f *testFlags, components []string) []string {
   289  	for _, c := range components {
   290  		if f.bools[c] || f.strings[c] != "" || f.ints[c] != 0 {
   291  			return append(name, c)
   292  		}
   293  	}
   294  	return name
   295  }
   296  
   297  // testFlags implements the plugin.FlagSet interface.
   298  type testFlags struct {
   299  	bools       map[string]bool
   300  	ints        map[string]int
   301  	floats      map[string]float64
   302  	strings     map[string]string
   303  	args        []string
   304  	stringLists map[string][]string
   305  }
   306  
   307  func (testFlags) ExtraUsage() string { return "" }
   308  
   309  func (testFlags) AddExtraUsage(eu string) {}
   310  
   311  func (f testFlags) Bool(s string, d bool, c string) *bool {
   312  	if b, ok := f.bools[s]; ok {
   313  		return &b
   314  	}
   315  	return &d
   316  }
   317  
   318  func (f testFlags) Int(s string, d int, c string) *int {
   319  	if i, ok := f.ints[s]; ok {
   320  		return &i
   321  	}
   322  	return &d
   323  }
   324  
   325  func (f testFlags) Float64(s string, d float64, c string) *float64 {
   326  	if g, ok := f.floats[s]; ok {
   327  		return &g
   328  	}
   329  	return &d
   330  }
   331  
   332  func (f testFlags) String(s, d, c string) *string {
   333  	if t, ok := f.strings[s]; ok {
   334  		return &t
   335  	}
   336  	return &d
   337  }
   338  
   339  func (f testFlags) StringList(s, d, c string) *[]*string {
   340  	if t, ok := f.stringLists[s]; ok {
   341  		// convert slice of strings to slice of string pointers before returning.
   342  		tp := make([]*string, len(t))
   343  		for i, v := range t {
   344  			tp[i] = &v
   345  		}
   346  		return &tp
   347  	}
   348  	return &[]*string{}
   349  }
   350  
   351  func (f testFlags) Parse(func()) []string {
   352  	return f.args
   353  }
   354  
   355  func baseFlags() testFlags {
   356  	return testFlags{
   357  		bools: map[string]bool{
   358  			"proto":          true,
   359  			"trim":           true,
   360  			"compact_labels": true,
   361  		},
   362  		ints: map[string]int{
   363  			"nodecount": 20,
   364  		},
   365  		floats: map[string]float64{
   366  			"nodefraction": 0.05,
   367  			"edgefraction": 0.01,
   368  			"divide_by":    1.0,
   369  		},
   370  		strings: map[string]string{
   371  			"unit": "minimum",
   372  		},
   373  	}
   374  }
   375  
   376  const testStart = 0x1000
   377  const testOffset = 0x5000
   378  
   379  type testFetcher struct{}
   380  
   381  func (testFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) {
   382  	var p *profile.Profile
   383  	switch s {
   384  	case "cpu", "unknown":
   385  		p = cpuProfile()
   386  	case "cpusmall":
   387  		p = cpuProfileSmall()
   388  	case "heap":
   389  		p = heapProfile()
   390  	case "heap_alloc":
   391  		p = heapProfile()
   392  		p.SampleType = []*profile.ValueType{
   393  			{Type: "alloc_objects", Unit: "count"},
   394  			{Type: "alloc_space", Unit: "bytes"},
   395  		}
   396  	case "heap_request":
   397  		p = heapProfile()
   398  		for _, s := range p.Sample {
   399  			s.NumLabel["request"] = s.NumLabel["bytes"]
   400  		}
   401  	case "heap_sizetags":
   402  		p = heapProfile()
   403  		tags := []int64{2, 4, 8, 16, 32, 64, 128, 256}
   404  		for _, s := range p.Sample {
   405  			numValues := append(s.NumLabel["bytes"], tags...)
   406  			s.NumLabel["bytes"] = numValues
   407  		}
   408  	case "heap_tags":
   409  		p = heapProfile()
   410  		for i := 0; i < len(p.Sample); i += 2 {
   411  			s := p.Sample[i]
   412  			if s.Label == nil {
   413  				s.Label = make(map[string][]string)
   414  			}
   415  			s.NumLabel["request"] = s.NumLabel["bytes"]
   416  			s.Label["key1"] = []string{"tag"}
   417  		}
   418  	case "contention":
   419  		p = contentionProfile()
   420  	case "symbolz":
   421  		p = symzProfile()
   422  	case "long_name_funcs":
   423  		p = longNameFuncsProfile()
   424  	default:
   425  		return nil, "", fmt.Errorf("unexpected source: %s", s)
   426  	}
   427  	return p, testSourceURL(8000) + s, nil
   428  }
   429  
   430  type testSymbolizer struct{}
   431  
   432  func (testSymbolizer) Symbolize(_ string, _ plugin.MappingSources, _ *profile.Profile) error {
   433  	return nil
   434  }
   435  
   436  type testSymbolizeDemangler struct{}
   437  
   438  func (testSymbolizeDemangler) Symbolize(_ string, _ plugin.MappingSources, p *profile.Profile) error {
   439  	for _, fn := range p.Function {
   440  		if fn.Name == "" || fn.SystemName == fn.Name {
   441  			fn.Name = fakeDemangler(fn.SystemName)
   442  		}
   443  	}
   444  	return nil
   445  }
   446  
   447  func testFetchSymbols(source, post string) ([]byte, error) {
   448  	var buf bytes.Buffer
   449  
   450  	switch source {
   451  	case testSourceURL(8000) + "symbolz":
   452  		for _, address := range strings.Split(post, "+") {
   453  			a, _ := strconv.ParseInt(address, 0, 64)
   454  			fmt.Fprintf(&buf, "%v\t", address)
   455  			if a-testStart > testOffset {
   456  				fmt.Fprintf(&buf, "wrong_source_%v_", address)
   457  				continue
   458  			}
   459  			fmt.Fprintf(&buf, "%#x\n", a-testStart)
   460  		}
   461  		return buf.Bytes(), nil
   462  	case testSourceURL(8001) + "symbolz":
   463  		for _, address := range strings.Split(post, "+") {
   464  			a, _ := strconv.ParseInt(address, 0, 64)
   465  			fmt.Fprintf(&buf, "%v\t", address)
   466  			if a-testStart < testOffset {
   467  				fmt.Fprintf(&buf, "wrong_source_%v_", address)
   468  				continue
   469  			}
   470  			fmt.Fprintf(&buf, "%#x\n", a-testStart-testOffset)
   471  		}
   472  		return buf.Bytes(), nil
   473  	default:
   474  		return nil, fmt.Errorf("unexpected source: %s", source)
   475  	}
   476  }
   477  
   478  type testSymbolzSymbolizer struct{}
   479  
   480  func (testSymbolzSymbolizer) Symbolize(variables string, sources plugin.MappingSources, p *profile.Profile) error {
   481  	return symbolz.Symbolize(p, false, sources, testFetchSymbols, nil)
   482  }
   483  
   484  func fakeDemangler(name string) string {
   485  	switch name {
   486  	case "mangled1000":
   487  		return "line1000"
   488  	case "mangled2000":
   489  		return "line2000"
   490  	case "mangled2001":
   491  		return "line2001"
   492  	case "mangled3000":
   493  		return "line3000"
   494  	case "mangled3001":
   495  		return "line3001"
   496  	case "mangled3002":
   497  		return "line3002"
   498  	case "mangledNEW":
   499  		return "operator new"
   500  	case "mangledMALLOC":
   501  		return "malloc"
   502  	default:
   503  		return name
   504  	}
   505  }
   506  
   507  // longNameFuncsProfile returns a profile with function names which should be
   508  // shortened in graph and flame views.
   509  func longNameFuncsProfile() *profile.Profile {
   510  	var longNameFuncsM = []*profile.Mapping{
   511  		{
   512  			ID:              1,
   513  			Start:           0x1000,
   514  			Limit:           0x4000,
   515  			File:            "/path/to/testbinary",
   516  			HasFunctions:    true,
   517  			HasFilenames:    true,
   518  			HasLineNumbers:  true,
   519  			HasInlineFrames: true,
   520  		},
   521  	}
   522  
   523  	var longNameFuncsF = []*profile.Function{
   524  		{ID: 1, Name: "path/to/package1.object.function1", SystemName: "path/to/package1.object.function1", Filename: "path/to/package1.go"},
   525  		{ID: 2, Name: "(anonymous namespace)::Bar::Foo", SystemName: "(anonymous namespace)::Bar::Foo", Filename: "a/long/path/to/package2.cc"},
   526  		{ID: 3, Name: "java.bar.foo.FooBar.run(java.lang.Runnable)", SystemName: "java.bar.foo.FooBar.run(java.lang.Runnable)", Filename: "FooBar.java"},
   527  	}
   528  
   529  	var longNameFuncsL = []*profile.Location{
   530  		{
   531  			ID:      1000,
   532  			Mapping: longNameFuncsM[0],
   533  			Address: 0x1000,
   534  			Line: []profile.Line{
   535  				{Function: longNameFuncsF[0], Line: 1},
   536  			},
   537  		},
   538  		{
   539  			ID:      2000,
   540  			Mapping: longNameFuncsM[0],
   541  			Address: 0x2000,
   542  			Line: []profile.Line{
   543  				{Function: longNameFuncsF[1], Line: 4},
   544  			},
   545  		},
   546  		{
   547  			ID:      3000,
   548  			Mapping: longNameFuncsM[0],
   549  			Address: 0x3000,
   550  			Line: []profile.Line{
   551  				{Function: longNameFuncsF[2], Line: 9},
   552  			},
   553  		},
   554  	}
   555  
   556  	return &profile.Profile{
   557  		PeriodType:    &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
   558  		Period:        1,
   559  		DurationNanos: 10e9,
   560  		SampleType: []*profile.ValueType{
   561  			{Type: "samples", Unit: "count"},
   562  			{Type: "cpu", Unit: "milliseconds"},
   563  		},
   564  		Sample: []*profile.Sample{
   565  			{
   566  				Location: []*profile.Location{longNameFuncsL[0], longNameFuncsL[1], longNameFuncsL[2]},
   567  				Value:    []int64{1000, 1000},
   568  			},
   569  			{
   570  				Location: []*profile.Location{longNameFuncsL[0], longNameFuncsL[1]},
   571  				Value:    []int64{100, 100},
   572  			},
   573  			{
   574  				Location: []*profile.Location{longNameFuncsL[2]},
   575  				Value:    []int64{10, 10},
   576  			},
   577  		},
   578  		Location: longNameFuncsL,
   579  		Function: longNameFuncsF,
   580  		Mapping:  longNameFuncsM,
   581  	}
   582  }
   583  
   584  func cpuProfile() *profile.Profile {
   585  	var cpuM = []*profile.Mapping{
   586  		{
   587  			ID:              1,
   588  			Start:           0x1000,
   589  			Limit:           0x4000,
   590  			File:            "/path/to/testbinary",
   591  			HasFunctions:    true,
   592  			HasFilenames:    true,
   593  			HasLineNumbers:  true,
   594  			HasInlineFrames: true,
   595  		},
   596  	}
   597  
   598  	var cpuF = []*profile.Function{
   599  		{ID: 1, Name: "mangled1000", SystemName: "mangled1000", Filename: "testdata/file1000.src"},
   600  		{ID: 2, Name: "mangled2000", SystemName: "mangled2000", Filename: "testdata/file2000.src"},
   601  		{ID: 3, Name: "mangled2001", SystemName: "mangled2001", Filename: "testdata/file2000.src"},
   602  		{ID: 4, Name: "mangled3000", SystemName: "mangled3000", Filename: "testdata/file3000.src"},
   603  		{ID: 5, Name: "mangled3001", SystemName: "mangled3001", Filename: "testdata/file3000.src"},
   604  		{ID: 6, Name: "mangled3002", SystemName: "mangled3002", Filename: "testdata/file3000.src"},
   605  	}
   606  
   607  	var cpuL = []*profile.Location{
   608  		{
   609  			ID:      1000,
   610  			Mapping: cpuM[0],
   611  			Address: 0x1000,
   612  			Line: []profile.Line{
   613  				{Function: cpuF[0], Line: 1},
   614  			},
   615  		},
   616  		{
   617  			ID:      2000,
   618  			Mapping: cpuM[0],
   619  			Address: 0x2000,
   620  			Line: []profile.Line{
   621  				{Function: cpuF[2], Line: 9},
   622  				{Function: cpuF[1], Line: 4},
   623  			},
   624  		},
   625  		{
   626  			ID:      3000,
   627  			Mapping: cpuM[0],
   628  			Address: 0x3000,
   629  			Line: []profile.Line{
   630  				{Function: cpuF[5], Line: 2},
   631  				{Function: cpuF[4], Line: 5},
   632  				{Function: cpuF[3], Line: 6},
   633  			},
   634  		},
   635  		{
   636  			ID:      3001,
   637  			Mapping: cpuM[0],
   638  			Address: 0x3001,
   639  			Line: []profile.Line{
   640  				{Function: cpuF[4], Line: 8},
   641  				{Function: cpuF[3], Line: 9},
   642  			},
   643  		},
   644  		{
   645  			ID:      3002,
   646  			Mapping: cpuM[0],
   647  			Address: 0x3002,
   648  			Line: []profile.Line{
   649  				{Function: cpuF[5], Line: 5},
   650  				{Function: cpuF[3], Line: 9},
   651  			},
   652  		},
   653  	}
   654  
   655  	return &profile.Profile{
   656  		PeriodType:    &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
   657  		Period:        1,
   658  		DurationNanos: 10e9,
   659  		SampleType: []*profile.ValueType{
   660  			{Type: "samples", Unit: "count"},
   661  			{Type: "cpu", Unit: "milliseconds"},
   662  		},
   663  		Sample: []*profile.Sample{
   664  			{
   665  				Location: []*profile.Location{cpuL[0], cpuL[1], cpuL[2]},
   666  				Value:    []int64{1000, 1000},
   667  				Label: map[string][]string{
   668  					"key1": {"tag1"},
   669  					"key2": {"tag1"},
   670  				},
   671  			},
   672  			{
   673  				Location: []*profile.Location{cpuL[0], cpuL[3]},
   674  				Value:    []int64{100, 100},
   675  				Label: map[string][]string{
   676  					"key1": {"tag2"},
   677  					"key3": {"tag2"},
   678  				},
   679  			},
   680  			{
   681  				Location: []*profile.Location{cpuL[1], cpuL[4]},
   682  				Value:    []int64{10, 10},
   683  				Label: map[string][]string{
   684  					"key1": {"tag3"},
   685  					"key2": {"tag2"},
   686  				},
   687  			},
   688  			{
   689  				Location: []*profile.Location{cpuL[2]},
   690  				Value:    []int64{10, 10},
   691  				Label: map[string][]string{
   692  					"key1": {"tag4"},
   693  					"key2": {"tag1"},
   694  				},
   695  			},
   696  		},
   697  		Location: cpuL,
   698  		Function: cpuF,
   699  		Mapping:  cpuM,
   700  	}
   701  }
   702  
   703  func cpuProfileSmall() *profile.Profile {
   704  	var cpuM = []*profile.Mapping{
   705  		{
   706  			ID:              1,
   707  			Start:           0x1000,
   708  			Limit:           0x4000,
   709  			File:            "/path/to/testbinary",
   710  			HasFunctions:    true,
   711  			HasFilenames:    true,
   712  			HasLineNumbers:  true,
   713  			HasInlineFrames: true,
   714  		},
   715  	}
   716  
   717  	var cpuL = []*profile.Location{
   718  		{
   719  			ID:      1000,
   720  			Mapping: cpuM[0],
   721  			Address: 0x1000,
   722  		},
   723  		{
   724  			ID:      2000,
   725  			Mapping: cpuM[0],
   726  			Address: 0x2000,
   727  		},
   728  		{
   729  			ID:      3000,
   730  			Mapping: cpuM[0],
   731  			Address: 0x3000,
   732  		},
   733  		{
   734  			ID:      4000,
   735  			Mapping: cpuM[0],
   736  			Address: 0x4000,
   737  		},
   738  		{
   739  			ID:      5000,
   740  			Mapping: cpuM[0],
   741  			Address: 0x5000,
   742  		},
   743  	}
   744  
   745  	return &profile.Profile{
   746  		PeriodType:    &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
   747  		Period:        1,
   748  		DurationNanos: 10e9,
   749  		SampleType: []*profile.ValueType{
   750  			{Type: "samples", Unit: "count"},
   751  			{Type: "cpu", Unit: "milliseconds"},
   752  		},
   753  		Sample: []*profile.Sample{
   754  			{
   755  				Location: []*profile.Location{cpuL[0], cpuL[1], cpuL[2]},
   756  				Value:    []int64{1000, 1000},
   757  			},
   758  			{
   759  				Location: []*profile.Location{cpuL[3], cpuL[1], cpuL[4]},
   760  				Value:    []int64{1000, 1000},
   761  			},
   762  			{
   763  				Location: []*profile.Location{cpuL[2]},
   764  				Value:    []int64{1000, 1000},
   765  			},
   766  			{
   767  				Location: []*profile.Location{cpuL[4]},
   768  				Value:    []int64{1000, 1000},
   769  			},
   770  		},
   771  		Location: cpuL,
   772  		Function: nil,
   773  		Mapping:  cpuM,
   774  	}
   775  }
   776  
   777  func heapProfile() *profile.Profile {
   778  	var heapM = []*profile.Mapping{
   779  		{
   780  			ID:              1,
   781  			BuildID:         "buildid",
   782  			Start:           0x1000,
   783  			Limit:           0x4000,
   784  			HasFunctions:    true,
   785  			HasFilenames:    true,
   786  			HasLineNumbers:  true,
   787  			HasInlineFrames: true,
   788  		},
   789  	}
   790  
   791  	var heapF = []*profile.Function{
   792  		{ID: 1, Name: "pruneme", SystemName: "pruneme", Filename: "prune.h"},
   793  		{ID: 2, Name: "mangled1000", SystemName: "mangled1000", Filename: "testdata/file1000.src"},
   794  		{ID: 3, Name: "mangled2000", SystemName: "mangled2000", Filename: "testdata/file2000.src"},
   795  		{ID: 4, Name: "mangled2001", SystemName: "mangled2001", Filename: "testdata/file2000.src"},
   796  		{ID: 5, Name: "mangled3000", SystemName: "mangled3000", Filename: "testdata/file3000.src"},
   797  		{ID: 6, Name: "mangled3001", SystemName: "mangled3001", Filename: "testdata/file3000.src"},
   798  		{ID: 7, Name: "mangled3002", SystemName: "mangled3002", Filename: "testdata/file3000.src"},
   799  		{ID: 8, Name: "mangledMALLOC", SystemName: "mangledMALLOC", Filename: "malloc.h"},
   800  		{ID: 9, Name: "mangledNEW", SystemName: "mangledNEW", Filename: "new.h"},
   801  	}
   802  
   803  	var heapL = []*profile.Location{
   804  		{
   805  			ID:      1000,
   806  			Mapping: heapM[0],
   807  			Address: 0x1000,
   808  			Line: []profile.Line{
   809  				{Function: heapF[0], Line: 100},
   810  				{Function: heapF[7], Line: 100},
   811  				{Function: heapF[1], Line: 1},
   812  			},
   813  		},
   814  		{
   815  			ID:      2000,
   816  			Mapping: heapM[0],
   817  			Address: 0x2000,
   818  			Line: []profile.Line{
   819  				{Function: heapF[8], Line: 100},
   820  				{Function: heapF[3], Line: 2},
   821  				{Function: heapF[2], Line: 3},
   822  			},
   823  		},
   824  		{
   825  			ID:      3000,
   826  			Mapping: heapM[0],
   827  			Address: 0x3000,
   828  			Line: []profile.Line{
   829  				{Function: heapF[8], Line: 100},
   830  				{Function: heapF[6], Line: 3},
   831  				{Function: heapF[5], Line: 2},
   832  				{Function: heapF[4], Line: 4},
   833  			},
   834  		},
   835  		{
   836  			ID:      3001,
   837  			Mapping: heapM[0],
   838  			Address: 0x3001,
   839  			Line: []profile.Line{
   840  				{Function: heapF[0], Line: 100},
   841  				{Function: heapF[8], Line: 100},
   842  				{Function: heapF[5], Line: 2},
   843  				{Function: heapF[4], Line: 4},
   844  			},
   845  		},
   846  		{
   847  			ID:      3002,
   848  			Mapping: heapM[0],
   849  			Address: 0x3002,
   850  			Line: []profile.Line{
   851  				{Function: heapF[6], Line: 3},
   852  				{Function: heapF[4], Line: 4},
   853  			},
   854  		},
   855  	}
   856  
   857  	return &profile.Profile{
   858  		Comments:   []string{"comment", "#hidden comment"},
   859  		PeriodType: &profile.ValueType{Type: "allocations", Unit: "bytes"},
   860  		Period:     524288,
   861  		SampleType: []*profile.ValueType{
   862  			{Type: "inuse_objects", Unit: "count"},
   863  			{Type: "inuse_space", Unit: "bytes"},
   864  		},
   865  		Sample: []*profile.Sample{
   866  			{
   867  				Location: []*profile.Location{heapL[0], heapL[1], heapL[2]},
   868  				Value:    []int64{10, 1024000},
   869  				NumLabel: map[string][]int64{"bytes": {102400}},
   870  			},
   871  			{
   872  				Location: []*profile.Location{heapL[0], heapL[3]},
   873  				Value:    []int64{20, 4096000},
   874  				NumLabel: map[string][]int64{"bytes": {204800}},
   875  			},
   876  			{
   877  				Location: []*profile.Location{heapL[1], heapL[4]},
   878  				Value:    []int64{40, 65536000},
   879  				NumLabel: map[string][]int64{"bytes": {1638400}},
   880  			},
   881  			{
   882  				Location: []*profile.Location{heapL[2]},
   883  				Value:    []int64{80, 32768000},
   884  				NumLabel: map[string][]int64{"bytes": {409600}},
   885  			},
   886  		},
   887  		DropFrames: ".*operator new.*|malloc",
   888  		Location:   heapL,
   889  		Function:   heapF,
   890  		Mapping:    heapM,
   891  	}
   892  }
   893  
   894  func contentionProfile() *profile.Profile {
   895  	var contentionM = []*profile.Mapping{
   896  		{
   897  			ID:              1,
   898  			BuildID:         "buildid-contention",
   899  			Start:           0x1000,
   900  			Limit:           0x4000,
   901  			HasFunctions:    true,
   902  			HasFilenames:    true,
   903  			HasLineNumbers:  true,
   904  			HasInlineFrames: true,
   905  		},
   906  	}
   907  
   908  	var contentionF = []*profile.Function{
   909  		{ID: 1, Name: "mangled1000", SystemName: "mangled1000", Filename: "testdata/file1000.src"},
   910  		{ID: 2, Name: "mangled2000", SystemName: "mangled2000", Filename: "testdata/file2000.src"},
   911  		{ID: 3, Name: "mangled2001", SystemName: "mangled2001", Filename: "testdata/file2000.src"},
   912  		{ID: 4, Name: "mangled3000", SystemName: "mangled3000", Filename: "testdata/file3000.src"},
   913  		{ID: 5, Name: "mangled3001", SystemName: "mangled3001", Filename: "testdata/file3000.src"},
   914  		{ID: 6, Name: "mangled3002", SystemName: "mangled3002", Filename: "testdata/file3000.src"},
   915  	}
   916  
   917  	var contentionL = []*profile.Location{
   918  		{
   919  			ID:      1000,
   920  			Mapping: contentionM[0],
   921  			Address: 0x1000,
   922  			Line: []profile.Line{
   923  				{Function: contentionF[0], Line: 1},
   924  			},
   925  		},
   926  		{
   927  			ID:      2000,
   928  			Mapping: contentionM[0],
   929  			Address: 0x2000,
   930  			Line: []profile.Line{
   931  				{Function: contentionF[2], Line: 2},
   932  				{Function: contentionF[1], Line: 3},
   933  			},
   934  		},
   935  		{
   936  			ID:      3000,
   937  			Mapping: contentionM[0],
   938  			Address: 0x3000,
   939  			Line: []profile.Line{
   940  				{Function: contentionF[5], Line: 2},
   941  				{Function: contentionF[4], Line: 3},
   942  				{Function: contentionF[3], Line: 5},
   943  			},
   944  		},
   945  		{
   946  			ID:      3001,
   947  			Mapping: contentionM[0],
   948  			Address: 0x3001,
   949  			Line: []profile.Line{
   950  				{Function: contentionF[4], Line: 3},
   951  				{Function: contentionF[3], Line: 5},
   952  			},
   953  		},
   954  		{
   955  			ID:      3002,
   956  			Mapping: contentionM[0],
   957  			Address: 0x3002,
   958  			Line: []profile.Line{
   959  				{Function: contentionF[5], Line: 4},
   960  				{Function: contentionF[3], Line: 3},
   961  			},
   962  		},
   963  	}
   964  
   965  	return &profile.Profile{
   966  		PeriodType: &profile.ValueType{Type: "contentions", Unit: "count"},
   967  		Period:     524288,
   968  		SampleType: []*profile.ValueType{
   969  			{Type: "contentions", Unit: "count"},
   970  			{Type: "delay", Unit: "nanoseconds"},
   971  		},
   972  		Sample: []*profile.Sample{
   973  			{
   974  				Location: []*profile.Location{contentionL[0], contentionL[1], contentionL[2]},
   975  				Value:    []int64{10, 10240000},
   976  			},
   977  			{
   978  				Location: []*profile.Location{contentionL[0], contentionL[3]},
   979  				Value:    []int64{20, 40960000},
   980  			},
   981  			{
   982  				Location: []*profile.Location{contentionL[1], contentionL[4]},
   983  				Value:    []int64{40, 65536000},
   984  			},
   985  			{
   986  				Location: []*profile.Location{contentionL[2]},
   987  				Value:    []int64{80, 32768000},
   988  			},
   989  		},
   990  		Location: contentionL,
   991  		Function: contentionF,
   992  		Mapping:  contentionM,
   993  		Comments: []string{"Comment #1", "Comment #2"},
   994  	}
   995  }
   996  
   997  func symzProfile() *profile.Profile {
   998  	var symzM = []*profile.Mapping{
   999  		{
  1000  			ID:    1,
  1001  			Start: testStart,
  1002  			Limit: 0x4000,
  1003  			File:  "/path/to/testbinary",
  1004  		},
  1005  	}
  1006  
  1007  	var symzL = []*profile.Location{
  1008  		{ID: 1, Mapping: symzM[0], Address: testStart},
  1009  		{ID: 2, Mapping: symzM[0], Address: testStart + 0x1000},
  1010  		{ID: 3, Mapping: symzM[0], Address: testStart + 0x2000},
  1011  	}
  1012  
  1013  	return &profile.Profile{
  1014  		PeriodType:    &profile.ValueType{Type: "cpu", Unit: "milliseconds"},
  1015  		Period:        1,
  1016  		DurationNanos: 10e9,
  1017  		SampleType: []*profile.ValueType{
  1018  			{Type: "samples", Unit: "count"},
  1019  			{Type: "cpu", Unit: "milliseconds"},
  1020  		},
  1021  		Sample: []*profile.Sample{
  1022  			{
  1023  				Location: []*profile.Location{symzL[0], symzL[1], symzL[2]},
  1024  				Value:    []int64{1, 1},
  1025  			},
  1026  		},
  1027  		Location: symzL,
  1028  		Mapping:  symzM,
  1029  	}
  1030  }
  1031  
  1032  func largeProfile(tb testing.TB) *profile.Profile {
  1033  	tb.Helper()
  1034  	input := proftest.LargeProfile(tb)
  1035  	prof, err := profile.Parse(bytes.NewBuffer(input))
  1036  	if err != nil {
  1037  		tb.Fatal(err)
  1038  	}
  1039  	return prof
  1040  }
  1041  
  1042  var autoCompleteTests = []struct {
  1043  	in  string
  1044  	out string
  1045  }{
  1046  	{"", ""},
  1047  	{"xyz", "xyz"},                        // no match
  1048  	{"dis", "disasm"},                     // single match
  1049  	{"t", "t"},                            // many matches
  1050  	{"top abc", "top abc"},                // no function name match
  1051  	{"top mangledM", "top mangledMALLOC"}, // single function name match
  1052  	{"top cmd cmd mangledM", "top cmd cmd mangledMALLOC"},
  1053  	{"top mangled", "top mangled"},                      // many function name matches
  1054  	{"cmd mangledM", "cmd mangledM"},                    // invalid command
  1055  	{"top mangledM cmd", "top mangledM cmd"},            // cursor misplaced
  1056  	{"top edMA", "top mangledMALLOC"},                   // single infix function name match
  1057  	{"top -mangledM", "top -mangledMALLOC"},             // ignore sign handled
  1058  	{"lin", "lines"},                                    // single variable match
  1059  	{"EdGeF", "edgefraction"},                           // single capitalized match
  1060  	{"help dis", "help disasm"},                         // help command match
  1061  	{"help relative_perc", "help relative_percentages"}, // help variable match
  1062  	{"help coMpa", "help compact_labels"},               // help variable capitalized match
  1063  }
  1064  
  1065  func TestAutoComplete(t *testing.T) {
  1066  	complete := newCompleter(functionNames(heapProfile()))
  1067  
  1068  	for _, test := range autoCompleteTests {
  1069  		if out := complete(test.in); out != test.out {
  1070  			t.Errorf("autoComplete(%s) = %s; want %s", test.in, out, test.out)
  1071  		}
  1072  	}
  1073  }
  1074  
  1075  func TestTagFilter(t *testing.T) {
  1076  	var tagFilterTests = []struct {
  1077  		desc, value string
  1078  		tags        map[string][]string
  1079  		want        bool
  1080  	}{
  1081  		{
  1082  			"1 key with 1 matching value",
  1083  			"tag2",
  1084  			map[string][]string{"value1": {"tag1", "tag2"}},
  1085  			true,
  1086  		},
  1087  		{
  1088  			"1 key with no matching values",
  1089  			"tag3",
  1090  			map[string][]string{"value1": {"tag1", "tag2"}},
  1091  			false,
  1092  		},
  1093  		{
  1094  			"two keys, each with value matching different one value in list",
  1095  			"tag1,tag3",
  1096  			map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}},
  1097  			true,
  1098  		},
  1099  		{"two keys, all value matching different regex value in list",
  1100  			"t..[12],t..3",
  1101  			map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}},
  1102  			true,
  1103  		},
  1104  		{
  1105  			"one key, not all values in list matched",
  1106  			"tag2,tag3",
  1107  			map[string][]string{"value1": {"tag1", "tag2"}},
  1108  			false,
  1109  		},
  1110  		{
  1111  			"key specified, list of tags where all tags in list matched",
  1112  			"key1=tag1,tag2",
  1113  			map[string][]string{"key1": {"tag1", "tag2"}},
  1114  			true,
  1115  		},
  1116  		{"key specified, list of tag values where not all are matched",
  1117  			"key1=tag1,tag2",
  1118  			map[string][]string{"key1": {"tag1"}},
  1119  			true,
  1120  		},
  1121  		{
  1122  			"key included for regex matching, list of values where all values in list matched",
  1123  			"key1:tag1,tag2",
  1124  			map[string][]string{"key1": {"tag1", "tag2"}},
  1125  			true,
  1126  		},
  1127  		{
  1128  			"key included for regex matching, list of values where not only second value matched",
  1129  			"key1:tag1,tag2",
  1130  			map[string][]string{"key1": {"tag2"}},
  1131  			false,
  1132  		},
  1133  		{
  1134  			"key included for regex matching, list of values where not only first value matched",
  1135  			"key1:tag1,tag2",
  1136  			map[string][]string{"key1": {"tag1"}},
  1137  			false,
  1138  		},
  1139  	}
  1140  	for _, test := range tagFilterTests {
  1141  		t.Run(test.desc, func(t *testing.T) {
  1142  			filter, err := compileTagFilter(test.desc, test.value, nil, &proftest.TestUI{T: t}, nil)
  1143  			if err != nil {
  1144  				t.Fatalf("tagFilter %s:%v", test.desc, err)
  1145  			}
  1146  			s := profile.Sample{
  1147  				Label: test.tags,
  1148  			}
  1149  			if got := filter(&s); got != test.want {
  1150  				t.Errorf("tagFilter %s: got %v, want %v", test.desc, got, test.want)
  1151  			}
  1152  		})
  1153  	}
  1154  }
  1155  
  1156  func TestIdentifyNumLabelUnits(t *testing.T) {
  1157  	var tagFilterTests = []struct {
  1158  		desc               string
  1159  		tagVals            []map[string][]int64
  1160  		tagUnits           []map[string][]string
  1161  		wantUnits          map[string]string
  1162  		allowedRx          string
  1163  		wantIgnoreErrCount int
  1164  	}{
  1165  		{
  1166  			"Multiple keys, no units for all keys",
  1167  			[]map[string][]int64{{"keyA": {131072}, "keyB": {128}}},
  1168  			[]map[string][]string{{"keyA": {}, "keyB": {""}}},
  1169  			map[string]string{"keyA": "keyA", "keyB": "keyB"},
  1170  			"",
  1171  			0,
  1172  		},
  1173  		{
  1174  			"Multiple keys, different units for each key",
  1175  			[]map[string][]int64{{"keyA": {131072}, "keyB": {128}}},
  1176  			[]map[string][]string{{"keyA": {"bytes"}, "keyB": {"kilobytes"}}},
  1177  			map[string]string{"keyA": "bytes", "keyB": "kilobytes"},
  1178  			"",
  1179  			0,
  1180  		},
  1181  		{
  1182  			"Multiple keys with multiple values, different units for each key",
  1183  			[]map[string][]int64{{"keyC": {131072, 1}, "keyD": {128, 252}}},
  1184  			[]map[string][]string{{"keyC": {"bytes", "bytes"}, "keyD": {"kilobytes", "kilobytes"}}},
  1185  			map[string]string{"keyC": "bytes", "keyD": "kilobytes"},
  1186  			"",
  1187  			0,
  1188  		},
  1189  		{
  1190  			"Multiple keys with multiple values, some units missing",
  1191  			[]map[string][]int64{{"key1": {131072, 1}, "A": {128, 252}, "key3": {128}, "key4": {1}}, {"key3": {128}, "key4": {1}}},
  1192  			[]map[string][]string{{"key1": {"", "bytes"}, "A": {"kilobytes", ""}, "key3": {""}, "key4": {"hour"}}, {"key3": {"seconds"}, "key4": {""}}},
  1193  			map[string]string{"key1": "bytes", "A": "kilobytes", "key3": "seconds", "key4": "hour"},
  1194  			"",
  1195  			0,
  1196  		},
  1197  		{
  1198  			"One key with three units in same sample",
  1199  			[]map[string][]int64{{"key": {8, 8, 16}}},
  1200  			[]map[string][]string{{"key": {"bytes", "megabytes", "kilobytes"}}},
  1201  			map[string]string{"key": "bytes"},
  1202  			`(For tag key used unit bytes, also encountered unit\(s\) kilobytes, megabytes)`,
  1203  			1,
  1204  		},
  1205  		{
  1206  			"One key with four units in same sample",
  1207  			[]map[string][]int64{{"key": {8, 8, 16, 32}}},
  1208  			[]map[string][]string{{"key": {"bytes", "kilobytes", "a", "megabytes"}}},
  1209  			map[string]string{"key": "bytes"},
  1210  			`(For tag key used unit bytes, also encountered unit\(s\) a, kilobytes, megabytes)`,
  1211  			1,
  1212  		},
  1213  		{
  1214  			"One key with two units in same sample",
  1215  			[]map[string][]int64{{"key": {8, 8}}},
  1216  			[]map[string][]string{{"key": {"bytes", "seconds"}}},
  1217  			map[string]string{"key": "bytes"},
  1218  			`(For tag key used unit bytes, also encountered unit\(s\) seconds)`,
  1219  			1,
  1220  		},
  1221  		{
  1222  			"One key with different units in different samples",
  1223  			[]map[string][]int64{{"key1": {8}}, {"key1": {8}}, {"key1": {8}}},
  1224  			[]map[string][]string{{"key1": {"bytes"}}, {"key1": {"kilobytes"}}, {"key1": {"megabytes"}}},
  1225  			map[string]string{"key1": "bytes"},
  1226  			`(For tag key1 used unit bytes, also encountered unit\(s\) kilobytes, megabytes)`,
  1227  			1,
  1228  		},
  1229  		{
  1230  			"Key alignment, unit not specified",
  1231  			[]map[string][]int64{{"alignment": {8}}},
  1232  			[]map[string][]string{nil},
  1233  			map[string]string{"alignment": "bytes"},
  1234  			"",
  1235  			0,
  1236  		},
  1237  		{
  1238  			"Key request, unit not specified",
  1239  			[]map[string][]int64{{"request": {8}}, {"request": {8, 8}}},
  1240  			[]map[string][]string{nil, nil},
  1241  			map[string]string{"request": "bytes"},
  1242  			"",
  1243  			0,
  1244  		},
  1245  		{
  1246  			"Check units not over-written for keys with default units",
  1247  			[]map[string][]int64{{
  1248  				"alignment": {8},
  1249  				"request":   {8},
  1250  				"bytes":     {8},
  1251  			}},
  1252  			[]map[string][]string{{
  1253  				"alignment": {"seconds"},
  1254  				"request":   {"minutes"},
  1255  				"bytes":     {"hours"},
  1256  			}},
  1257  			map[string]string{
  1258  				"alignment": "seconds",
  1259  				"request":   "minutes",
  1260  				"bytes":     "hours",
  1261  			},
  1262  			"",
  1263  			0,
  1264  		},
  1265  	}
  1266  	for _, test := range tagFilterTests {
  1267  		t.Run(test.desc, func(t *testing.T) {
  1268  			p := profile.Profile{Sample: make([]*profile.Sample, len(test.tagVals))}
  1269  			for i, numLabel := range test.tagVals {
  1270  				s := profile.Sample{
  1271  					NumLabel: numLabel,
  1272  					NumUnit:  test.tagUnits[i],
  1273  				}
  1274  				p.Sample[i] = &s
  1275  			}
  1276  			testUI := &proftest.TestUI{T: t, AllowRx: test.allowedRx}
  1277  			units := identifyNumLabelUnits(&p, testUI)
  1278  			if !reflect.DeepEqual(test.wantUnits, units) {
  1279  				t.Errorf("got %v units, want %v", units, test.wantUnits)
  1280  			}
  1281  			if got, want := testUI.NumAllowRxMatches, test.wantIgnoreErrCount; want != got {
  1282  				t.Errorf("got %d errors logged, want %d errors logged", got, want)
  1283  			}
  1284  		})
  1285  	}
  1286  }
  1287  
  1288  func TestNumericTagFilter(t *testing.T) {
  1289  	var tagFilterTests = []struct {
  1290  		desc, value     string
  1291  		tags            map[string][]int64
  1292  		identifiedUnits map[string]string
  1293  		want            bool
  1294  	}{
  1295  		{
  1296  			"Match when unit conversion required",
  1297  			"128kb",
  1298  			map[string][]int64{"key1": {131072}, "key2": {128}},
  1299  			map[string]string{"key1": "bytes", "key2": "kilobytes"},
  1300  			true,
  1301  		},
  1302  		{
  1303  			"Match only when values equal after unit conversion",
  1304  			"512kb",
  1305  			map[string][]int64{"key1": {512}, "key2": {128}},
  1306  			map[string]string{"key1": "bytes", "key2": "kilobytes"},
  1307  			false,
  1308  		},
  1309  		{
  1310  			"Match when values and units initially equal",
  1311  			"10bytes",
  1312  			map[string][]int64{"key1": {10}, "key2": {128}},
  1313  			map[string]string{"key1": "bytes", "key2": "kilobytes"},
  1314  			true,
  1315  		},
  1316  		{
  1317  			"Match range without lower bound, no unit conversion required",
  1318  			":10bytes",
  1319  			map[string][]int64{"key1": {8}},
  1320  			map[string]string{"key1": "bytes"},
  1321  			true,
  1322  		},
  1323  		{
  1324  			"Match range without lower bound, unit conversion required",
  1325  			":10kb",
  1326  			map[string][]int64{"key1": {8}},
  1327  			map[string]string{"key1": "bytes"},
  1328  			true,
  1329  		},
  1330  		{
  1331  			"Match range without upper bound, unit conversion required",
  1332  			"10b:",
  1333  			map[string][]int64{"key1": {8}},
  1334  			map[string]string{"key1": "kilobytes"},
  1335  			true,
  1336  		},
  1337  		{
  1338  			"Match range without upper bound, no unit conversion required",
  1339  			"10b:",
  1340  			map[string][]int64{"key1": {12}},
  1341  			map[string]string{"key1": "bytes"},
  1342  			true,
  1343  		},
  1344  		{
  1345  			"Don't match range without upper bound, no unit conversion required",
  1346  			"10b:",
  1347  			map[string][]int64{"key1": {8}},
  1348  			map[string]string{"key1": "bytes"},
  1349  			false,
  1350  		},
  1351  		{
  1352  			"Multiple keys with different units, don't match range without upper bound",
  1353  			"10kb:",
  1354  			map[string][]int64{"key1": {8}},
  1355  			map[string]string{"key1": "bytes", "key2": "kilobytes"},
  1356  			false,
  1357  		},
  1358  		{
  1359  			"Match range without upper bound, unit conversion required",
  1360  			"10b:",
  1361  			map[string][]int64{"key1": {8}},
  1362  			map[string]string{"key1": "kilobytes"},
  1363  			true,
  1364  		},
  1365  		{
  1366  			"Don't match range without lower bound, no unit conversion required",
  1367  			":10b",
  1368  			map[string][]int64{"key1": {12}},
  1369  			map[string]string{"key1": "bytes"},
  1370  			false,
  1371  		},
  1372  		{
  1373  			"Match specific key, key present, one of two values match",
  1374  			"bytes=5b",
  1375  			map[string][]int64{"bytes": {10, 5}},
  1376  			map[string]string{"bytes": "bytes"},
  1377  			true,
  1378  		},
  1379  		{
  1380  			"Match specific key, key present and value matches",
  1381  			"bytes=1024b",
  1382  			map[string][]int64{"bytes": {1024}},
  1383  			map[string]string{"bytes": "kilobytes"},
  1384  			false,
  1385  		},
  1386  		{
  1387  			"Match specific key, matching key present and value matches, also non-matching key",
  1388  			"bytes=1024b",
  1389  			map[string][]int64{"bytes": {1024}, "key2": {5}},
  1390  			map[string]string{"bytes": "bytes", "key2": "bytes"},
  1391  			true,
  1392  		},
  1393  		{
  1394  			"Match specific key and range of values, value matches",
  1395  			"bytes=512b:1024b",
  1396  			map[string][]int64{"bytes": {780}},
  1397  			map[string]string{"bytes": "bytes"},
  1398  			true,
  1399  		},
  1400  		{
  1401  			"Match specific key and range of values, value too large",
  1402  			"key1=1kb:2kb",
  1403  			map[string][]int64{"key1": {4096}},
  1404  			map[string]string{"key1": "bytes"},
  1405  			false,
  1406  		},
  1407  		{
  1408  			"Match specific key and range of values, value too small",
  1409  			"key1=1kb:2kb",
  1410  			map[string][]int64{"key1": {256}},
  1411  			map[string]string{"key1": "bytes"},
  1412  			false,
  1413  		},
  1414  		{
  1415  			"Match specific key and value, unit conversion required",
  1416  			"bytes=1024b",
  1417  			map[string][]int64{"bytes": {1}},
  1418  			map[string]string{"bytes": "kilobytes"},
  1419  			true,
  1420  		},
  1421  		{
  1422  			"Match specific key and value, key does not appear",
  1423  			"key2=256bytes",
  1424  			map[string][]int64{"key1": {256}},
  1425  			map[string]string{"key1": "bytes"},
  1426  			false,
  1427  		},
  1428  		{
  1429  			"Match negative key and range of values, value matches",
  1430  			"bytes=-512b:-128b",
  1431  			map[string][]int64{"bytes": {-256}},
  1432  			map[string]string{"bytes": "bytes"},
  1433  			true,
  1434  		},
  1435  		{
  1436  			"Match negative key and range of values, value outside range",
  1437  			"bytes=-512b:-128b",
  1438  			map[string][]int64{"bytes": {-2048}},
  1439  			map[string]string{"bytes": "bytes"},
  1440  			false,
  1441  		},
  1442  		{
  1443  			"Match exact value, unitless tag",
  1444  			"pid=123",
  1445  			map[string][]int64{"pid": {123}},
  1446  			nil,
  1447  			true,
  1448  		},
  1449  		{
  1450  			"Match range, unitless tag",
  1451  			"pid=123:123",
  1452  			map[string][]int64{"pid": {123}},
  1453  			nil,
  1454  			true,
  1455  		},
  1456  		{
  1457  			"Don't match range, unitless tag",
  1458  			"pid=124:124",
  1459  			map[string][]int64{"pid": {123}},
  1460  			nil,
  1461  			false,
  1462  		},
  1463  		{
  1464  			"Match range without upper bound, unitless tag",
  1465  			"pid=100:",
  1466  			map[string][]int64{"pid": {123}},
  1467  			nil,
  1468  			true,
  1469  		},
  1470  		{
  1471  			"Don't match range without upper bound, unitless tag",
  1472  			"pid=200:",
  1473  			map[string][]int64{"pid": {123}},
  1474  			nil,
  1475  			false,
  1476  		},
  1477  		{
  1478  			"Match range without lower bound, unitless tag",
  1479  			"pid=:200",
  1480  			map[string][]int64{"pid": {123}},
  1481  			nil,
  1482  			true,
  1483  		},
  1484  		{
  1485  			"Don't match range without lower bound, unitless tag",
  1486  			"pid=:100",
  1487  			map[string][]int64{"pid": {123}},
  1488  			nil,
  1489  			false,
  1490  		},
  1491  	}
  1492  	for _, test := range tagFilterTests {
  1493  		t.Run(test.desc, func(t *testing.T) {
  1494  			wantErrMsg := strings.Join([]string{"(", test.desc, ":Interpreted '", test.value[strings.Index(test.value, "=")+1:], "' as range, not regexp", ")"}, "")
  1495  			filter, err := compileTagFilter(test.desc, test.value, test.identifiedUnits, &proftest.TestUI{T: t,
  1496  				AllowRx: wantErrMsg}, nil)
  1497  			if err != nil {
  1498  				t.Fatalf("%v", err)
  1499  			}
  1500  			s := profile.Sample{
  1501  				NumLabel: test.tags,
  1502  			}
  1503  			if got := filter(&s); got != test.want {
  1504  				t.Fatalf("got %v, want %v", got, test.want)
  1505  			}
  1506  		})
  1507  	}
  1508  }
  1509  
  1510  // TestOptionsHaveHelp tests that a help message is supplied for every
  1511  // selectable option.
  1512  func TestOptionsHaveHelp(t *testing.T) {
  1513  	for _, f := range configFields {
  1514  		// Check all choices if this is a group, else check f.name.
  1515  		names := f.choices
  1516  		if len(names) == 0 {
  1517  			names = []string{f.name}
  1518  		}
  1519  		for _, name := range names {
  1520  			if _, ok := configHelp[name]; !ok {
  1521  				t.Errorf("missing help message for %q", name)
  1522  			}
  1523  		}
  1524  	}
  1525  }
  1526  
  1527  type testSymbolzMergeFetcher struct{}
  1528  
  1529  func (testSymbolzMergeFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) {
  1530  	var p *profile.Profile
  1531  	switch s {
  1532  	case testSourceURL(8000) + "symbolz":
  1533  		p = symzProfile()
  1534  	case testSourceURL(8001) + "symbolz":
  1535  		p = symzProfile()
  1536  		p.Mapping[0].Start += testOffset
  1537  		p.Mapping[0].Limit += testOffset
  1538  		for i := range p.Location {
  1539  			p.Location[i].Address += testOffset
  1540  		}
  1541  	default:
  1542  		return nil, "", fmt.Errorf("unexpected source: %s", s)
  1543  	}
  1544  	return p, s, nil
  1545  }
  1546  
  1547  func TestSymbolzAfterMerge(t *testing.T) {
  1548  	baseConfig := currentConfig()
  1549  	defer setCurrentConfig(baseConfig)
  1550  
  1551  	f := baseFlags()
  1552  	f.args = []string{
  1553  		testSourceURL(8000) + "symbolz",
  1554  		testSourceURL(8001) + "symbolz",
  1555  	}
  1556  
  1557  	o := setDefaults(nil)
  1558  	o.Flagset = f
  1559  	o.Obj = new(mockObjTool)
  1560  	src, cmd, err := parseFlags(o)
  1561  	if err != nil {
  1562  		t.Fatalf("parseFlags: %v", err)
  1563  	}
  1564  
  1565  	if len(cmd) != 1 || cmd[0] != "proto" {
  1566  		t.Fatalf("parseFlags returned command %v, want [proto]", cmd)
  1567  	}
  1568  
  1569  	o.Fetch = testSymbolzMergeFetcher{}
  1570  	o.Sym = testSymbolzSymbolizer{}
  1571  	p, err := fetchProfiles(src, o)
  1572  	if err != nil {
  1573  		t.Fatalf("fetchProfiles: %v", err)
  1574  	}
  1575  	if len(p.Location) != 3 {
  1576  		t.Errorf("Got %d locations after merge, want %d", len(p.Location), 3)
  1577  	}
  1578  	for i, l := range p.Location {
  1579  		if len(l.Line) != 1 {
  1580  			t.Errorf("Number of lines for symbolz %#x in iteration %d, got %d, want %d", l.Address, i, len(l.Line), 1)
  1581  			continue
  1582  		}
  1583  		address := l.Address - l.Mapping.Start
  1584  		if got, want := l.Line[0].Function.Name, fmt.Sprintf("%#x", address); got != want {
  1585  			t.Errorf("symbolz %#x, got %s, want %s", address, got, want)
  1586  		}
  1587  	}
  1588  }
  1589  
  1590  func TestProfileCopier(t *testing.T) {
  1591  	type testCase struct {
  1592  		name string
  1593  		prof *profile.Profile
  1594  	}
  1595  	for _, c := range []testCase{
  1596  		{"cpu", cpuProfile()},
  1597  		{"heap", heapProfile()},
  1598  		{"contention", contentionProfile()},
  1599  		{"symbolz", symzProfile()},
  1600  		{"long_name_funcs", longNameFuncsProfile()},
  1601  		{"large", largeProfile(t)},
  1602  	} {
  1603  		t.Run(c.name, func(t *testing.T) {
  1604  			copier := makeProfileCopier(c.prof)
  1605  
  1606  			// Muck with one copy to check that fresh copies are unaffected
  1607  			tmp := copier.newCopy()
  1608  			tmp.Sample = tmp.Sample[:0]
  1609  
  1610  			// Get new copy and check it is same as the original.
  1611  			want := c.prof.String()
  1612  			got := copier.newCopy().String()
  1613  			if got != want {
  1614  				t.Errorf("New copy is not same as original profile")
  1615  				diff, err := proftest.Diff([]byte(want), []byte(got))
  1616  				if err != nil {
  1617  					t.Fatalf("Diff: %v", err)
  1618  				}
  1619  				t.Logf("Diff:\n%s\n", string(diff))
  1620  			}
  1621  		})
  1622  	}
  1623  }
  1624  
  1625  type mockObjTool struct{}
  1626  
  1627  func (*mockObjTool) Open(file string, start, limit, offset uint64, relocationSymbol string) (plugin.ObjFile, error) {
  1628  	return &mockFile{file, "abcdef", 0}, nil
  1629  }
  1630  
  1631  func (m *mockObjTool) Disasm(file string, start, end uint64, intelSyntax bool) ([]plugin.Inst, error) {
  1632  	const fn1 = "line1000"
  1633  	const fn3 = "line3000"
  1634  	const file1 = "testdata/file1000.src"
  1635  	const file3 = "testdata/file3000.src"
  1636  	data := []plugin.Inst{
  1637  		{Addr: 0x1000, Text: "instruction one", Function: fn1, File: file1, Line: 1},
  1638  		{Addr: 0x1001, Text: "instruction two", Function: fn1, File: file1, Line: 1},
  1639  		{Addr: 0x1002, Text: "instruction three", Function: fn1, File: file1, Line: 2},
  1640  		{Addr: 0x1003, Text: "instruction four", Function: fn1, File: file1, Line: 1},
  1641  		{Addr: 0x3000, Text: "instruction one", Function: fn3, File: file3},
  1642  		{Addr: 0x3001, Text: "instruction two", Function: fn3, File: file3},
  1643  		{Addr: 0x3002, Text: "instruction three", Function: fn3, File: file3},
  1644  		{Addr: 0x3003, Text: "instruction four", Function: fn3, File: file3},
  1645  		{Addr: 0x3004, Text: "instruction five", Function: fn3, File: file3},
  1646  	}
  1647  	var result []plugin.Inst
  1648  	for _, inst := range data {
  1649  		if inst.Addr >= start && inst.Addr <= end {
  1650  			result = append(result, inst)
  1651  		}
  1652  	}
  1653  	return result, nil
  1654  }
  1655  
  1656  type mockFile struct {
  1657  	name, buildID string
  1658  	base          uint64
  1659  }
  1660  
  1661  // Name returns the underlyinf file name, if available
  1662  func (m *mockFile) Name() string {
  1663  	return m.name
  1664  }
  1665  
  1666  // ObjAddr returns the objdump address corresponding to a runtime address.
  1667  func (m *mockFile) ObjAddr(addr uint64) (uint64, error) {
  1668  	return addr - m.base, nil
  1669  }
  1670  
  1671  // BuildID returns the GNU build ID of the file, or an empty string.
  1672  func (m *mockFile) BuildID() string {
  1673  	return m.buildID
  1674  }
  1675  
  1676  // SourceLine reports the source line information for a given
  1677  // address in the file. Due to inlining, the source line information
  1678  // is in general a list of positions representing a call stack,
  1679  // with the leaf function first.
  1680  func (*mockFile) SourceLine(addr uint64) ([]plugin.Frame, error) {
  1681  	// Return enough data to support the SourceLine() calls needed for
  1682  	// weblist on cpuProfile() contents.
  1683  	frame := func(fn, file string, num int) plugin.Frame {
  1684  		// Reuse the same num for line number and column number.
  1685  		return plugin.Frame{Func: fn, File: file, Line: num, Column: num}
  1686  	}
  1687  	switch addr {
  1688  	case 0x1000:
  1689  		return []plugin.Frame{
  1690  			frame("mangled1000", "testdata/file1000.src", 1),
  1691  		}, nil
  1692  	case 0x1001:
  1693  		return []plugin.Frame{
  1694  			frame("mangled1000", "testdata/file1000.src", 1),
  1695  		}, nil
  1696  	case 0x1002:
  1697  		return []plugin.Frame{
  1698  			frame("mangled1000", "testdata/file1000.src", 2),
  1699  		}, nil
  1700  	case 0x1003:
  1701  		return []plugin.Frame{
  1702  			frame("mangled1000", "testdata/file1000.src", 1),
  1703  		}, nil
  1704  	case 0x2000:
  1705  		return []plugin.Frame{
  1706  			frame("mangled2001", "testdata/file2000.src", 9),
  1707  			frame("mangled2000", "testdata/file2000.src", 4),
  1708  		}, nil
  1709  	case 0x3000:
  1710  		return []plugin.Frame{
  1711  			frame("mangled3002", "testdata/file3000.src", 2),
  1712  			frame("mangled3001", "testdata/file3000.src", 5),
  1713  			frame("mangled3000", "testdata/file3000.src", 6),
  1714  		}, nil
  1715  	case 0x3001:
  1716  		return []plugin.Frame{
  1717  			frame("mangled3001", "testdata/file3000.src", 8),
  1718  			frame("mangled3000", "testdata/file3000.src", 9),
  1719  		}, nil
  1720  	case 0x3002:
  1721  		return []plugin.Frame{
  1722  			frame("mangled3002", "testdata/file3000.src", 5),
  1723  			frame("mangled3000", "testdata/file3000.src", 9),
  1724  		}, nil
  1725  	}
  1726  
  1727  	return nil, nil
  1728  }
  1729  
  1730  // Symbols returns a list of symbols in the object file.
  1731  // If r is not nil, Symbols restricts the list to symbols
  1732  // with names matching the regular expression.
  1733  // If addr is not zero, Symbols restricts the list to symbols
  1734  // containing that address.
  1735  func (m *mockFile) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) {
  1736  	switch r.String() {
  1737  	case "line[13]":
  1738  		return []*plugin.Sym{
  1739  			{
  1740  				Name: []string{"line1000"}, File: m.name,
  1741  				Start: 0x1000, End: 0x1003,
  1742  			},
  1743  			{
  1744  				Name: []string{"line3000"}, File: m.name,
  1745  				Start: 0x3000, End: 0x3004,
  1746  			},
  1747  		}, nil
  1748  	}
  1749  	return nil, fmt.Errorf("unimplemented")
  1750  }
  1751  
  1752  // Close closes the file, releasing associated resources.
  1753  func (*mockFile) Close() error {
  1754  	return nil
  1755  }
  1756  

View as plain text