github.com/grafana/pyroscope@v1.18.0/pkg/pprof/merge_test.go (about)

     1  package pprof
     2  
     3  import (
     4  	"crypto/md5"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"net"
     8  	"os"
     9  	"path/filepath"
    10  	"sort"
    11  	"strings"
    12  	"sync"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/google/pprof/profile"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    20  	"github.com/grafana/pyroscope/pkg/testhelper"
    21  )
    22  
    23  func Test_Merge_Single(t *testing.T) {
    24  	p, err := OpenFile("testdata/go.cpu.labels.pprof")
    25  	require.NoError(t, err)
    26  	var m ProfileMerge
    27  	require.NoError(t, m.Merge(p.CloneVT(), true))
    28  	sortLabels(p.Profile)
    29  	act := m.Profile()
    30  	exp := p.Profile
    31  	testhelper.EqualProto(t, exp, act)
    32  }
    33  
    34  func sortLabels(p *profilev1.Profile) {
    35  	for _, s := range p.Sample {
    36  		sort.Sort(LabelsByKeyValue(s.Label))
    37  	}
    38  }
    39  
    40  type fuzzEvent byte
    41  
    42  const (
    43  	fuzzEventUnknown fuzzEvent = iota
    44  	fuzzEventPostDecode
    45  	fuzzEventPostMerge
    46  )
    47  
    48  type eventSocket struct {
    49  	lck  sync.Mutex
    50  	fMap map[string]net.Conn
    51  }
    52  
    53  var eventWriter = &eventSocket{
    54  	fMap: make(map[string]net.Conn),
    55  }
    56  
    57  func eventWrite(t *testing.T, msg []byte) {
    58  	eventWriter.lck.Lock()
    59  	c, ok := eventWriter.fMap[eventName(t)]
    60  	if !ok {
    61  		var err error
    62  		c, err = net.Dial("unix", eventPath(t))
    63  		if err != nil {
    64  			eventWriter.lck.Unlock()
    65  			t.Fatalf("error connecting: %v", err)
    66  			return
    67  		}
    68  		eventWriter.fMap[eventName(t)] = c
    69  	}
    70  	eventWriter.lck.Unlock()
    71  	_, err := c.Write(msg)
    72  
    73  	require.NoError(t, err)
    74  }
    75  
    76  func eventName(t testing.TB) string {
    77  	return strings.Split(t.Name(), "/")[0]
    78  }
    79  
    80  func eventPath(t testing.TB) string {
    81  	hash := md5.Sum([]byte(eventName(t)))
    82  	p := filepath.Join(os.TempDir(), hex.EncodeToString(hash[:])+"-fuzz-events.sock")
    83  	return p
    84  }
    85  
    86  func isFuzzWorker() bool {
    87  	for _, arg := range os.Args {
    88  		if arg == "-test.fuzzworker" {
    89  			return true
    90  		}
    91  		if arg == "-fuzzworker" {
    92  			return true
    93  		}
    94  	}
    95  	return false
    96  }
    97  
    98  // runEventsGatherer starts a server that listens for events from the fuzzing worker processes. This allows us to gather additional metrics on how successful the fuzzing is with finding valid profiles.
    99  func runEventsGatherer(t testing.TB) {
   100  	fPath := eventPath(t)
   101  	_ = os.Remove(fPath)
   102  	socket, err := net.Listen("unix", fPath)
   103  	require.NoError(t, err)
   104  	t.Cleanup(func() {
   105  		socket.Close()
   106  		_ = os.Remove(fPath)
   107  	})
   108  
   109  	eventCh := make(chan fuzzEvent, 32)
   110  	go func() {
   111  		for {
   112  			conn, err := socket.Accept()
   113  			if err != nil {
   114  				return
   115  			}
   116  			go func(conn net.Conn) {
   117  				defer conn.Close()
   118  				buf := make([]byte, 1024)
   119  				for {
   120  					n, err := conn.Read(buf)
   121  					if err != nil {
   122  						return
   123  					}
   124  					for _, b := range buf[:n] {
   125  						eventCh <- fuzzEvent(b)
   126  					}
   127  				}
   128  			}(conn)
   129  		}
   130  	}()
   131  
   132  	go func() {
   133  		ticker := time.NewTicker(3 * time.Second)
   134  		stdout := os.Stdout
   135  		defer ticker.Stop()
   136  		var totalPostDecode, totalPostMerge int64
   137  		var lastPostDecode, lastPostMerge int64
   138  		for {
   139  			select {
   140  			case <-ticker.C:
   141  				fmt.Fprintf(stdout, "postDecode: %d/%d (last 3s, total) postMerge %d/%d (last 3s, total)\n", totalPostDecode-lastPostDecode, totalPostDecode, totalPostMerge-lastPostMerge, totalPostMerge)
   142  				lastPostDecode = totalPostDecode
   143  				lastPostMerge = totalPostMerge
   144  			case event := <-eventCh:
   145  				switch event {
   146  				case fuzzEventPostDecode:
   147  					totalPostDecode += 1
   148  				case fuzzEventPostMerge:
   149  					totalPostMerge += 1
   150  				}
   151  			}
   152  		}
   153  	}()
   154  }
   155  
   156  func Fuzz_Merge_Single(f *testing.F) {
   157  	// setup event handler (only in main process)
   158  	if !isFuzzWorker() {
   159  		runEventsGatherer(f)
   160  	}
   161  
   162  	for _, file := range []string{
   163  		"testdata/go.cpu.labels.pprof",
   164  		"testdata/heap",
   165  		"testdata/profile_java",
   166  		"testdata/profile_rust",
   167  	} {
   168  		raw, err := OpenFile(file)
   169  		require.NoError(f, err)
   170  		data, err := raw.MarshalVT()
   171  		require.NoError(f, err)
   172  		f.Add(data)
   173  	}
   174  
   175  	f.Fuzz(func(t *testing.T, data []byte) {
   176  		var p profilev1.Profile
   177  		err := p.UnmarshalVT(data)
   178  		if err != nil {
   179  			return
   180  		}
   181  
   182  		eventWrite(t, []byte{byte(fuzzEventPostDecode)})
   183  		var m ProfileMerge
   184  		err = m.Merge(&p, true)
   185  		if err != nil {
   186  			return
   187  		}
   188  		eventWrite(t, []byte{byte(fuzzEventPostMerge)})
   189  	})
   190  }
   191  
   192  func Test_Merge_Self(t *testing.T) {
   193  	p, err := OpenFile("testdata/go.cpu.labels.pprof")
   194  	require.NoError(t, err)
   195  	var m ProfileMerge
   196  	require.NoError(t, m.Merge(p.CloneVT(), true))
   197  	require.NoError(t, m.Merge(p.CloneVT(), true))
   198  	for i := range p.Sample {
   199  		s := p.Sample[i]
   200  		for j := range s.Value {
   201  			s.Value[j] *= 2
   202  		}
   203  	}
   204  	p.DurationNanos *= 2
   205  	sortLabels(p.Profile)
   206  	testhelper.EqualProto(t, p.Profile, m.Profile())
   207  }
   208  
   209  func Test_Merge_Halves(t *testing.T) {
   210  	p, err := OpenFile("testdata/go.cpu.labels.pprof")
   211  	require.NoError(t, err)
   212  
   213  	a := p.CloneVT()
   214  	b := p.CloneVT()
   215  	n := len(p.Sample) / 2
   216  	a.Sample = a.Sample[:n]
   217  	b.Sample = b.Sample[n:]
   218  
   219  	var m ProfileMerge
   220  	require.NoError(t, m.Merge(a, true))
   221  	require.NoError(t, m.Merge(b, true))
   222  
   223  	// Merge with self for normalisation.
   224  	var sm ProfileMerge
   225  	require.NoError(t, sm.Merge(p.CloneVT(), true))
   226  	p.DurationNanos *= 2
   227  
   228  	sortLabels(p.Profile)
   229  	testhelper.EqualProto(t, p.Profile, m.Profile())
   230  }
   231  
   232  func Test_Merge_Sample(t *testing.T) {
   233  	stringTable := []string{
   234  		"",
   235  		"samples",
   236  		"count",
   237  		"cpu",
   238  		"nanoseconds",
   239  		"foo",
   240  		"bar",
   241  		"profile_id",
   242  		"c717c11b87121639",
   243  		"function",
   244  		"slow",
   245  		"8c946fa4ae322f7f",
   246  		"fast",
   247  		"main.work",
   248  		"/Users/kolesnikovae/Documents/src/pyroscope/examples/golang-push/simple/main.go",
   249  		"main.slowFunction.func1",
   250  		"runtime/pprof.Do",
   251  		"/usr/local/go/src/runtime/pprof/runtime.go",
   252  		"main.slowFunction",
   253  		"main.main.func2",
   254  		"github.com/pyroscope-io/client/pyroscope.TagWrapper.func1",
   255  		"/Users/kolesnikovae/go/pkg/mod/github.com/pyroscope-io/client@v0.2.4-0.20220607180407-0ba26860ce5b/pyroscope/api.go",
   256  		"github.com/pyroscope-io/client/pyroscope.TagWrapper",
   257  		"main.main",
   258  		"runtime.main",
   259  		"/usr/local/go/src/runtime/proc.go",
   260  		"main.fastFunction.func1",
   261  		"main.fastFunction",
   262  	}
   263  
   264  	a := &profilev1.Profile{
   265  		SampleType: []*profilev1.ValueType{
   266  			{
   267  				Type: 1,
   268  				Unit: 2,
   269  			},
   270  			{
   271  				Type: 3,
   272  				Unit: 4,
   273  			},
   274  		},
   275  		Sample: []*profilev1.Sample{
   276  			{
   277  				LocationId: []uint64{1, 2, 3},
   278  				Value:      []int64{1, 10000000},
   279  				Label: []*profilev1.Label{
   280  					{Key: 5, Str: 6},
   281  					{Key: 7, Str: 8},
   282  					{Key: 9, Str: 10},
   283  				},
   284  			},
   285  		},
   286  		Mapping: []*profilev1.Mapping{
   287  			{
   288  				Id:           1,
   289  				HasFunctions: true,
   290  			},
   291  		},
   292  		Location: []*profilev1.Location{
   293  			{
   294  				Id:        1,
   295  				MappingId: 1,
   296  				Address:   19497668,
   297  				Line:      []*profilev1.Line{{FunctionId: 1, Line: 19}},
   298  			},
   299  			{
   300  				Id:        2,
   301  				MappingId: 1,
   302  				Address:   19498429,
   303  				Line:      []*profilev1.Line{{FunctionId: 2, Line: 43}},
   304  			},
   305  			{
   306  				Id:        3,
   307  				MappingId: 1,
   308  				Address:   19267106,
   309  				Line:      []*profilev1.Line{{FunctionId: 3, Line: 40}},
   310  			},
   311  		},
   312  		Function: []*profilev1.Function{
   313  			{
   314  				Id:         1,
   315  				Name:       13,
   316  				SystemName: 13,
   317  				Filename:   14,
   318  			},
   319  			{
   320  				Id:         2,
   321  				Name:       15,
   322  				SystemName: 15,
   323  				Filename:   14,
   324  			},
   325  			{
   326  				Id:         3,
   327  				Name:       16,
   328  				SystemName: 16,
   329  				Filename:   17,
   330  			},
   331  		},
   332  		StringTable:   stringTable,
   333  		TimeNanos:     1654798932062349000,
   334  		DurationNanos: 10123363553,
   335  		PeriodType: &profilev1.ValueType{
   336  			Type: 3,
   337  			Unit: 4,
   338  		},
   339  		Period: 10000000,
   340  	}
   341  
   342  	b := &profilev1.Profile{
   343  		SampleType: []*profilev1.ValueType{
   344  			{
   345  				Type: 1,
   346  				Unit: 2,
   347  			},
   348  			{
   349  				Type: 3,
   350  				Unit: 4,
   351  			},
   352  		},
   353  		Sample: []*profilev1.Sample{
   354  			{
   355  				LocationId: []uint64{1},
   356  				Value:      []int64{1, 10000000},
   357  				Label: []*profilev1.Label{
   358  					{Key: 5, Str: 6},
   359  					{Key: 7, Str: 11},
   360  					{Key: 9, Str: 12},
   361  				},
   362  			},
   363  			{
   364  				LocationId: []uint64{2, 3, 4}, // Same
   365  				Value:      []int64{1, 10000000},
   366  				Label: []*profilev1.Label{
   367  					{Key: 5, Str: 6},
   368  					{Key: 7, Str: 8},
   369  					{Key: 9, Str: 10},
   370  				},
   371  			},
   372  		},
   373  		Mapping: []*profilev1.Mapping{
   374  			{
   375  				Id:           1,
   376  				HasFunctions: true,
   377  			},
   378  		},
   379  		Location: []*profilev1.Location{
   380  			{
   381  				Id:        1,
   382  				MappingId: 1,
   383  				Address:   19499013,
   384  				Line:      []*profilev1.Line{{FunctionId: 1, Line: 42}},
   385  			},
   386  			{
   387  				Id:        2,
   388  				MappingId: 1,
   389  				Address:   19497668,
   390  				Line:      []*profilev1.Line{{FunctionId: 2, Line: 19}},
   391  			},
   392  			{
   393  				Id:        3,
   394  				MappingId: 1,
   395  				Address:   19498429,
   396  				Line:      []*profilev1.Line{{FunctionId: 3, Line: 43}},
   397  			},
   398  			{
   399  				Id:        4,
   400  				MappingId: 1,
   401  				Address:   19267106,
   402  				Line:      []*profilev1.Line{{FunctionId: 4, Line: 40}},
   403  			},
   404  		},
   405  		Function: []*profilev1.Function{
   406  			{
   407  				Id:         1,
   408  				Name:       18,
   409  				SystemName: 18,
   410  				Filename:   14,
   411  			},
   412  			{
   413  				Id:         2,
   414  				Name:       13,
   415  				SystemName: 13,
   416  				Filename:   14,
   417  			},
   418  			{
   419  				Id:         3,
   420  				Name:       15,
   421  				SystemName: 15,
   422  				Filename:   14,
   423  			},
   424  			{
   425  				Id:         4,
   426  				Name:       16,
   427  				SystemName: 16,
   428  				Filename:   17,
   429  			},
   430  		},
   431  		StringTable:   stringTable,
   432  		TimeNanos:     1654798932062349000,
   433  		DurationNanos: 10123363553,
   434  		PeriodType: &profilev1.ValueType{
   435  			Type: 3,
   436  			Unit: 4,
   437  		},
   438  		Period: 10000000,
   439  	}
   440  
   441  	expected := &profilev1.Profile{
   442  		SampleType: []*profilev1.ValueType{
   443  			{
   444  				Type: 1,
   445  				Unit: 2,
   446  			},
   447  			{
   448  				Type: 3,
   449  				Unit: 4,
   450  			},
   451  		},
   452  		Sample: []*profilev1.Sample{
   453  			{
   454  				LocationId: []uint64{1, 2, 3},
   455  				Value:      []int64{2, 20000000},
   456  				Label: []*profilev1.Label{
   457  					{Key: 5, Str: 6},
   458  					{Key: 7, Str: 8},
   459  					{Key: 9, Str: 10},
   460  				},
   461  			},
   462  			{
   463  				LocationId: []uint64{4},
   464  				Value:      []int64{1, 10000000},
   465  				Label: []*profilev1.Label{
   466  					{Key: 5, Str: 6},
   467  					{Key: 7, Str: 11},
   468  					{Key: 9, Str: 12},
   469  				},
   470  			},
   471  		},
   472  		Mapping: []*profilev1.Mapping{
   473  			{
   474  				Id:           1,
   475  				HasFunctions: true,
   476  			},
   477  		},
   478  		Location: []*profilev1.Location{
   479  			{
   480  				Id:        1,
   481  				MappingId: 1,
   482  				Address:   19497668,
   483  				Line:      []*profilev1.Line{{FunctionId: 1, Line: 19}},
   484  			},
   485  			{
   486  				Id:        2,
   487  				MappingId: 1,
   488  				Address:   19498429,
   489  				Line:      []*profilev1.Line{{FunctionId: 2, Line: 43}},
   490  			},
   491  			{
   492  				Id:        3,
   493  				MappingId: 1,
   494  				Address:   19267106,
   495  				Line:      []*profilev1.Line{{FunctionId: 3, Line: 40}},
   496  			},
   497  			{
   498  				Id:        4,
   499  				MappingId: 1,
   500  				Address:   19499013,
   501  				Line:      []*profilev1.Line{{FunctionId: 4, Line: 42}},
   502  			},
   503  		},
   504  		Function: []*profilev1.Function{
   505  			{
   506  				Id:         1,
   507  				Name:       13,
   508  				SystemName: 13,
   509  				Filename:   14,
   510  			},
   511  			{
   512  				Id:         2,
   513  				Name:       15,
   514  				SystemName: 15,
   515  				Filename:   14,
   516  			},
   517  			{
   518  				Id:         3,
   519  				Name:       16,
   520  				SystemName: 16,
   521  				Filename:   17,
   522  			},
   523  			{
   524  				Id:         4,
   525  				Name:       18,
   526  				SystemName: 18,
   527  				Filename:   14,
   528  			},
   529  		},
   530  		StringTable:   stringTable,
   531  		TimeNanos:     1654798932062349000,
   532  		DurationNanos: 20246727106,
   533  		PeriodType: &profilev1.ValueType{
   534  			Type: 3,
   535  			Unit: 4,
   536  		},
   537  		Period: 10000000,
   538  	}
   539  
   540  	var m ProfileMerge
   541  	require.NoError(t, m.Merge(a, true))
   542  	require.NoError(t, m.Merge(b, true))
   543  
   544  	testhelper.EqualProto(t, expected, m.Profile())
   545  }
   546  
   547  func TestMergeEmpty(t *testing.T) {
   548  	var m ProfileMerge
   549  
   550  	err := m.Merge(&profilev1.Profile{
   551  		SampleType: []*profilev1.ValueType{
   552  			{
   553  				Type: 2,
   554  				Unit: 1,
   555  			},
   556  		},
   557  		PeriodType: &profilev1.ValueType{
   558  			Type: 2,
   559  			Unit: 1,
   560  		},
   561  		StringTable: []string{"", "nanoseconds", "cpu"},
   562  	}, true)
   563  	require.NoError(t, err)
   564  	err = m.Merge(&profilev1.Profile{
   565  		Sample: []*profilev1.Sample{
   566  			{
   567  				LocationId: []uint64{1},
   568  				Value:      []int64{1},
   569  			},
   570  		},
   571  		Location: []*profilev1.Location{
   572  			{
   573  				Id:        1,
   574  				MappingId: 1,
   575  				Line:      []*profilev1.Line{{FunctionId: 1, Line: 1}},
   576  			},
   577  		},
   578  		Function: []*profilev1.Function{
   579  			{
   580  				Id:   1,
   581  				Name: 1,
   582  			},
   583  		},
   584  		SampleType: []*profilev1.ValueType{
   585  			{
   586  				Type: 3,
   587  				Unit: 2,
   588  			},
   589  		},
   590  		PeriodType: &profilev1.ValueType{
   591  			Type: 3,
   592  			Unit: 2,
   593  		},
   594  		Mapping: []*profilev1.Mapping{
   595  			{
   596  				Id: 1,
   597  			},
   598  		},
   599  		StringTable: []string{"", "bar", "nanoseconds", "cpu"},
   600  	}, true)
   601  	require.NoError(t, err)
   602  }
   603  
   604  // Benchmark_Merge_self/pprof.Merge-10                	    2722	    421419 ns/op
   605  // Benchmark_Merge_self/profile.Merge-10              	     802	   1417907 ns/op
   606  func Benchmark_Merge_self(b *testing.B) {
   607  	d, err := os.ReadFile("testdata/go.cpu.labels.pprof")
   608  	require.NoError(b, err)
   609  
   610  	b.Run("pprof.Merge", func(b *testing.B) {
   611  		p, err := RawFromBytes(d)
   612  		require.NoError(b, err)
   613  		b.ResetTimer()
   614  		for i := 0; i < b.N; i++ {
   615  			var m ProfileMerge
   616  			require.NoError(b, m.Merge(p.CloneVT(), true))
   617  		}
   618  	})
   619  
   620  	b.Run("profile.Merge", func(b *testing.B) {
   621  		p, err := profile.ParseData(d)
   622  		require.NoError(b, err)
   623  		b.ResetTimer()
   624  		for i := 0; i < b.N; i++ {
   625  			_, err = profile.Merge([]*profile.Profile{p.Copy()})
   626  			require.NoError(b, err)
   627  		}
   628  	})
   629  }