github.com/stealthrocket/wzprof@v0.2.1-0.20230830205924-5fa86be5e5b3/cmd/wzprof/main_test.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path/filepath"
     7  	"testing"
     8  
     9  	"github.com/google/pprof/profile"
    10  )
    11  
    12  // This test file performs end-to-end validation of the profiler on actual wasm
    13  // binary files. They are located in testdata. Tests are very sensitive to the
    14  // content of those files. If you rebuild them, you will likely need to rebuild
    15  // the expected samples below. Use the printSamples() function to help you with
    16  // that.
    17  
    18  func TestDataCSimple(t *testing.T) {
    19  	p := program{filePath: "../../testdata/c/simple.wasm"}
    20  	testMemoryProfiler(t, p, []sample{
    21  		{
    22  			[]int64{1, 10},
    23  			[]frame{
    24  				{"malloc", 0, false},
    25  				{"func1", 6, false},
    26  				{"main", 34, false},
    27  				{"__main_void", 0, false},
    28  				{"_start", 0, false},
    29  			},
    30  		},
    31  		{
    32  			[]int64{1, 20},
    33  			[]frame{
    34  				{"malloc", 0, false},
    35  				{"func21", 12, false},
    36  				{"func2", 18, false},
    37  				{"main", 35, false},
    38  				{"__main_void", 0, false},
    39  				{"_start", 0, false},
    40  			},
    41  		},
    42  		{
    43  			[]int64{1, 30},
    44  			[]frame{
    45  				{"malloc", 0, false},
    46  				{"func31", 29, true},
    47  				{"func3", 23, false},
    48  				{"main", 36, false},
    49  				{"__main_void", 0, false},
    50  				{"_start", 0, false},
    51  			},
    52  		},
    53  	})
    54  }
    55  
    56  func TestCBench(t *testing.T) {
    57  	p := program{filePath: "../../testdata/c/bench.wasm"}
    58  
    59  	testCpuProfiler(t, p, []sample{
    60  		{ // ensure isDir is inlined
    61  			[]int64{1},
    62  			[]frame{
    63  				{"strlen", 0, false},      // strlen
    64  				{"isDir", 89, true},       // isDir
    65  				{"joinPath", 17, false},   // joinPath
    66  				{"main", 115, false},      // __main_argc_argv
    67  				{"__main_void", 0, false}, // __main_void
    68  				{"_start", 0, false},      // _start
    69  			},
    70  		},
    71  		{ // ensure appendCleanPath is child of joinPath
    72  			[]int64{1},
    73  			[]frame{
    74  				{"appendCleanPath", 22, false}, // appendCleanPath
    75  				{"joinPath", 83, false},        // joinPath
    76  				{"main", 115, false},           // __main_argc_argv
    77  				{"__main_void", 0, false},      // __main_void
    78  				{"_start", 0, false},           // _start
    79  			},
    80  		},
    81  	})
    82  }
    83  
    84  func TestDataRustSimple(t *testing.T) {
    85  	p := program{filePath: "../../testdata/rust/simple/target/wasm32-wasi/debug/simple.wasm"}
    86  	testMemoryProfiler(t, p, []sample{
    87  		{
    88  			[]int64{1, 120},
    89  			[]frame{
    90  				{"malloc", 0, false},                                                                        // malloc
    91  				{"std:sys:wasi:alloc:{impl#0}:alloc", 381, true},                                            // _ZN3std3sys4wasi5alloc81_$LT$impl$u20$core..alloc..global..GlobalAlloc$u20$for$u20$std..alloc..System$GT$5alloc17hf06d843ee28c936eE
    92  				{"std:alloc:__default_lib_allocator:__rdl_alloc", 14, false},                                // std:alloc:__default_lib_allocator:__rdl_alloc
    93  				{"__rust_alloc", 0, false},                                                                  // __rust_alloc
    94  				{"alloc:alloc:alloc_impl", 95, false},                                                       // _ZN5alloc5alloc6Global10alloc_impl17h579ac88351552cb7E
    95  				{"alloc:alloc:{impl#1}:allocate", 237, false},                                               // _ZN63_$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$8allocate17hcb9ff3e2ca003c84E
    96  				{"alloc:raw_vec:allocate_in<i32, alloc::alloc::Global>", 185, false},                        // _ZN5alloc7raw_vec19RawVec$LT$T$C$A$GT$11allocate_in17hec12f02c19409feeE
    97  				{"alloc:vec:with_capacity_in<i32, alloc::alloc::Global>", 483, true},                        // _ZN5alloc3vec16Vec$LT$T$C$A$GT$16with_capacity_in17h574658446754b2caE
    98  				{"alloc:vec:with_capacity<i32>", 131, false},                                                // _ZN5alloc3vec12Vec$LT$T$GT$13with_capacity17hea1d94514f4fb20fE
    99  				{"simple:allocate_more_memory", 19, false},                                                  // _ZN6simple20allocate_more_memory17h4594ee16911b70d7E
   100  				{"simple:allocate_memory", 13, false},                                                       // _ZN6simple15allocate_memory17hb0084bacecc50a31E
   101  				{"simple:main", 4, false},                                                                   // _ZN6simple4main17h7c6bec49f74488e8E
   102  				{"core:ops:function:FnOnce:call_once<fn(), ()>", 250, false},                                // _ZN4core3ops8function6FnOnce9call_once17h65afd749b06e87d3E
   103  				{"std:sys_common:backtrace:__rust_begin_short_backtrace<fn(), ()>", 121, false},             // _ZN3std10sys_common9backtrace28__rust_begin_short_backtrace17h46f307b03ffe9605E
   104  				{"std:rt:lang_start:{closure#0}<()>", 166, false},                                           // _ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h820e14cd6a99f492E
   105  				{"std:panic:catch_unwind<std::rt::lang_start_internal::{closure_env#2}, isize>", 148, true}, // _ZN3std5panic12catch_unwind17h09dbc99d0be4be1fE
   106  				{"std:rt:lang_start_internal", 287, false},                                                  // _ZN3std2rt19lang_start_internal17h38aaea5d7881ae71E
   107  				{"std:rt:lang_start<()>", 165, false},                                                       // _ZN3std2rt10lang_start17hb2321e0751704c7cE
   108  				{"__main_void", 0, false},                                                                   // __main_void
   109  				{"_start", 0, false},                                                                        // _start
   110  			},
   111  		},
   112  	})
   113  }
   114  
   115  func TestPyTwoCalls(t *testing.T) {
   116  	pyd := t.TempDir()
   117  	pyzip := filepath.Join(pyd, "/usr/local/lib/python311.zip")
   118  	pyscript := filepath.Join(pyd, "script.py")
   119  	os.MkdirAll(filepath.Dir(pyzip), os.ModePerm)
   120  	os.Link("../../.python/python311.zip", pyzip)
   121  	os.Link("../../testdata/python/simple.py", pyscript)
   122  
   123  	p := program{
   124  		filePath: "../../.python/python.wasm",
   125  		args:     []string{"/script.py"},
   126  		mounts:   []string{pyd + ":/"},
   127  	}
   128  
   129  	testCpuProfiler(t, p, []sample{
   130  		{ // deepest script.py call stack
   131  			[]int64{1},
   132  			[]frame{
   133  				{"script.a", 2, false},
   134  				{"script.b", 7, false},
   135  				{"script.c", 11, false},
   136  				{"script", 15, false},
   137  			},
   138  		},
   139  	})
   140  
   141  	testMemoryProfiler(t, p, []sample{
   142  		// byterray(100) allocates 28 bytes for the object, and 100+1 byte for
   143  		// the content because in python byte arrays are null-terminated. It
   144  		// first calls PyObject_Malloc(28), then PyObject_Realloc(101).
   145  		{
   146  			[]int64{2, 129},
   147  			[]frame{
   148  				{"script.a", 3, false},
   149  				{"script.b", 7, false},
   150  				{"script.c", 11, false},
   151  				{"script", 15, false},
   152  			},
   153  		},
   154  	})
   155  }
   156  
   157  func TestGoTwoCalls(t *testing.T) {
   158  	p := program{filePath: "../../testdata/go/twocalls.wasm"}
   159  
   160  	testCpuProfiler(t, p, []sample{
   161  		{ // first call to myalloc1() from main.
   162  			[]int64{1},
   163  			[]frame{
   164  				{"runtime.mallocgc", 948, false},  // runtime.mallocgc
   165  				{"runtime.makeslice", 103, false}, // runtime.makeslice
   166  				{"main.myalloc1", 5, false},       // main.myalloc1
   167  				{"main.main", 23, false},          // main.main
   168  				{"runtime.main", 267, false},      // runtime.main
   169  				{"runtime.goexit", 401, false},    // runtime.goexit
   170  			},
   171  		},
   172  		{ // call to myalloc1() through intermediate() that is inlined in main
   173  			[]int64{1},
   174  			[]frame{
   175  				{"runtime.mallocgc", 948, false},  // runtime.mallocgc
   176  				{"runtime.makeslice", 103, false}, // runtime.makeslice
   177  				{"main.myalloc1", 5, false},       // main.myalloc1
   178  				{"main.main", 18, true},           // main.main
   179  				{"main.main", 25, false},          // main.main
   180  				{"runtime.main", 267, false},      // runtime.main
   181  				{"runtime.goexit", 401, false},    // runtime.goexit
   182  			},
   183  		},
   184  		{ // call to myalloc2()
   185  			[]int64{1},
   186  			[]frame{
   187  				{"runtime.mallocgc", 948, false},  // runtime.mallocgc
   188  				{"runtime.makeslice", 103, false}, // runtime.makeslice
   189  				{"main.myalloc2", 13, false},      // main.myalloc2
   190  				{"main.main", 28, false},          // main.main
   191  				{"runtime.main", 267, false},      // runtime.main
   192  				{"runtime.goexit", 401, false},    // runtime.goexit
   193  			},
   194  		},
   195  	})
   196  
   197  	testMemoryProfiler(t, p, []sample{
   198  		{ // first call to myalloc1() from main.
   199  			[]int64{1, 41},
   200  			[]frame{
   201  				{"runtime.mallocgc", 948, false},  // runtime.mallocgc
   202  				{"runtime.makeslice", 103, false}, // runtime.makeslice
   203  				{"main.myalloc1", 5, false},       // main.myalloc1
   204  				{"main.main", 23, false},          // main.main
   205  				{"runtime.main", 267, false},      // runtime.main
   206  				{"runtime.goexit", 401, false},    // runtime.goexit
   207  			},
   208  		},
   209  		{ // call to myalloc1() through intermediate() that is inlined in main
   210  			[]int64{1, 50},
   211  			[]frame{
   212  				{"runtime.mallocgc", 948, false},  // runtime.mallocgc
   213  				{"runtime.makeslice", 103, false}, // runtime.makeslice
   214  				{"main.myalloc1", 5, false},       // main.myalloc1
   215  				{"main.main", 18, true},           // main.main
   216  				{"main.main", 25, false},          // main.main
   217  				{"runtime.main", 267, false},      // runtime.main
   218  				{"runtime.goexit", 401, false},    // runtime.goexit
   219  			},
   220  		},
   221  		{ // call to myalloc2()
   222  			[]int64{1, 100},
   223  			[]frame{
   224  				{"runtime.mallocgc", 948, false},  // runtime.mallocgc
   225  				{"runtime.makeslice", 103, false}, // runtime.makeslice
   226  				{"main.myalloc2", 13, false},      // main.myalloc2
   227  				{"main.main", 28, false},          // main.main
   228  				{"runtime.main", 267, false},      // runtime.main
   229  				{"runtime.goexit", 401, false},    // runtime.goexit
   230  			},
   231  		},
   232  	})
   233  }
   234  
   235  func testCpuProfiler(t *testing.T, prog program, expectedSamples []sample) {
   236  	prog.sampleRate = 1
   237  	prog.cpuProfile = filepath.Join(t.TempDir(), "cpu.pprof")
   238  
   239  	expectedTypes := []string{
   240  		"samples",
   241  		"cpu",
   242  	}
   243  
   244  	p := execForProfile(t, &prog, prog.cpuProfile)
   245  	assertSamples(t, expectedTypes, expectedSamples, p)
   246  }
   247  
   248  func testMemoryProfiler(t *testing.T, prog program, expectedSamples []sample) {
   249  	prog.sampleRate = 1
   250  	prog.memProfile = filepath.Join(t.TempDir(), "mem.pprof")
   251  
   252  	expectedTypes := []string{
   253  		"alloc_objects",
   254  		"alloc_space",
   255  	}
   256  
   257  	p := execForProfile(t, &prog, prog.memProfile)
   258  	assertSamples(t, expectedTypes, expectedSamples, p)
   259  }
   260  
   261  func execForProfile(t *testing.T, prog *program, out string) *profile.Profile {
   262  	err := prog.run(context.Background())
   263  	if err != nil {
   264  		t.Fatal(err)
   265  	}
   266  	f, err := os.Open(out)
   267  	if err != nil {
   268  		t.Fatalf("can't open profile file %s: %s", prog.memProfile, err)
   269  	}
   270  	defer f.Close()
   271  	p, err := profile.Parse(f)
   272  	if err != nil {
   273  		t.Fatalf("error parsing profile: %s", err)
   274  	}
   275  	if err := p.CheckValid(); err != nil {
   276  		t.Fatalf("invalid profile: %s", err)
   277  	}
   278  	return p
   279  }
   280  
   281  func assertSamples(t *testing.T, expectedTypes []string, expectedSamples []sample, p *profile.Profile) {
   282  	if len(p.SampleType) != len(expectedTypes) {
   283  		t.Errorf("expected %d sample types; got %d", len(expectedTypes), len(p.SampleType))
   284  	}
   285  
   286  	for i, e := range expectedTypes {
   287  		if p.SampleType[i].Type != e {
   288  			t.Fatalf("expected sample type %d to be %s; was %s", i, e, p.SampleType[i].Type)
   289  		}
   290  	}
   291  
   292  	// TODO: pre-process samples to assess faster.
   293  expected:
   294  	for esi, expected := range expectedSamples {
   295  	sample:
   296  		for si, actual := range p.Sample {
   297  			stack := expected.stack
   298  			for _, loc := range actual.Location {
   299  				for i, line := range loc.Line {
   300  					if len(stack) == 0 {
   301  						continue sample
   302  					}
   303  					if line.Function.Name == stack[0].name && line.Line == stack[0].line {
   304  						inline := i < len(loc.Line)-1
   305  						if inline != stack[0].inlined {
   306  							t.Errorf("stack frame was supposed to be inlined %t; was %t", stack[0].inlined, inline)
   307  						}
   308  						stack = stack[1:]
   309  					} else {
   310  						continue sample
   311  					}
   312  				}
   313  			}
   314  			if len(stack) == 0 {
   315  				// TODO: test "inuse_*" samples
   316  				for i, e := range expected.values {
   317  					if e != actual.Value[i] {
   318  						t.Errorf("expected sample matched %d, but value %d was %d; expected %d", si, i, actual.Value[i], e)
   319  					}
   320  				}
   321  				continue expected
   322  			}
   323  		}
   324  		t.Errorf("expected sample %d not found in profile", esi)
   325  	}
   326  }
   327  
   328  type frame struct {
   329  	name    string
   330  	line    int64
   331  	inlined bool
   332  }
   333  
   334  type sample struct {
   335  	values []int64
   336  	stack  []frame
   337  }
   338  
   339  /*
   340  // printSamples outputs the samples list in a way that can be copy-pasted in the
   341  // tests above.
   342  func printSamples(samples []*profile.Sample) {
   343  	for i, s := range samples {
   344  		fmt.Println("{ // Sample", i, "-------------", s.Value)
   345  		fmt.Printf("\t%#v,\n", s.Value)
   346  		fmt.Println("\t[]frame{")
   347  		for _, loc := range s.Location {
   348  			for li, line := range loc.Line {
   349  				inline := li < len(loc.Line)-1
   350  				fmt.Printf("\t\t{\"%s\", %d, %t}, // %s\n", line.Function.Name, line.Line, inline, line.Function.SystemName)
   351  			}
   352  		}
   353  		fmt.Println("\t},")
   354  		fmt.Println("},")
   355  	}
   356  }
   357  */