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 */