github.com/grafana/pyroscope@v1.18.0/pkg/test/integration/ingest_pprof_test.go (about) 1 package integration 2 3 import ( 4 "fmt" 5 "os" 6 "testing" 7 "time" 8 9 "connectrpc.com/connect" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1" 14 pprof2 "github.com/grafana/pyroscope/pkg/og/convert/pprof" 15 "github.com/grafana/pyroscope/pkg/og/convert/pprof/bench" 16 "github.com/grafana/pyroscope/pkg/og/convert/pprof/strprofile" 17 "github.com/grafana/pyroscope/pkg/og/ingestion" 18 "github.com/grafana/pyroscope/pkg/pprof" 19 "github.com/grafana/pyroscope/pkg/pprof/testhelper" 20 ) 21 22 const repoRoot = "../../../" 23 24 var ( 25 golangHeap = []expectedMetric{ 26 {"memory:alloc_objects:count:space:bytes", nil, 0}, 27 {"memory:alloc_space:bytes:space:bytes", nil, 1}, 28 {"memory:inuse_objects:count:space:bytes", nil, 2}, 29 {"memory:inuse_space:bytes:space:bytes", nil, 3}, 30 } 31 golangCPU = []expectedMetric{ 32 {"process_cpu:samples:count:cpu:nanoseconds", nil, 0}, 33 {"process_cpu:cpu:nanoseconds:cpu:nanoseconds", nil, 1}, 34 } 35 _ = golangHeap 36 _ = golangCPU 37 testdata = []pprofTestData{ 38 { 39 profile: repoRoot + "pkg/pprof/testdata/heap", 40 expectStatusIngest: 200, 41 expectStatusPush: 200, 42 metrics: golangHeap, 43 needsGoHeapFix: true, 44 }, 45 { 46 profile: repoRoot + "pkg/pprof/testdata/heap_delta", 47 expectStatusPush: 200, 48 expectStatusIngest: 200, 49 metrics: golangHeap, 50 needsGoHeapFix: true, 51 }, 52 { 53 profile: repoRoot + "pkg/pprof/testdata/profile_java", 54 expectStatusIngest: 200, 55 expectStatusPush: 200, 56 metrics: []expectedMetric{ 57 {"process_cpu:cpu:nanoseconds:cpu:nanoseconds", nil, 0}, 58 }, 59 }, 60 { 61 profile: repoRoot + "pkg/pprof/testdata/go.cpu.labels.pprof", 62 expectStatusIngest: 200, 63 expectStatusPush: 200, 64 metrics: golangCPU, 65 }, 66 { 67 profile: repoRoot + "pkg/og/convert/testdata/cpu.pprof", 68 expectStatusIngest: 200, 69 expectStatusPush: 200, 70 71 metrics: golangCPU, 72 }, 73 { 74 profile: repoRoot + "pkg/og/convert/testdata/cpu.pprof", 75 prevProfile: repoRoot + "pkg/og/convert/testdata/cpu.pprof", 76 expectStatusIngest: 422, 77 }, 78 79 { 80 profile: repoRoot + "pkg/og/convert/pprof/testdata/cpu.pb.gz", 81 prevProfile: "", 82 expectStatusIngest: 200, 83 expectStatusPush: 200, 84 metrics: golangCPU, 85 }, 86 { 87 profile: repoRoot + "pkg/og/convert/pprof/testdata/cpu-exemplars.pb.gz", 88 expectStatusIngest: 200, 89 expectStatusPush: 200, 90 metrics: golangCPU, 91 }, 92 { 93 profile: repoRoot + "pkg/og/convert/pprof/testdata/cpu-js.pb.gz", 94 expectStatusIngest: 200, 95 expectStatusPush: 200, 96 metrics: []expectedMetric{ 97 {"wall:sample:count:wall:microseconds", nil, 0}, 98 {"wall:wall:microseconds:wall:microseconds", nil, 1}, 99 }, 100 }, 101 { 102 profile: repoRoot + "pkg/og/convert/pprof/testdata/heap.pb", 103 expectStatusIngest: 200, 104 expectStatusPush: 200, 105 metrics: golangHeap, 106 needsGoHeapFix: true, 107 }, 108 { 109 profile: repoRoot + "pkg/og/convert/pprof/testdata/heap.pb.gz", 110 expectStatusIngest: 200, 111 expectStatusPush: 200, 112 metrics: golangHeap, 113 needsGoHeapFix: true, 114 }, 115 { 116 profile: repoRoot + "pkg/og/convert/pprof/testdata/heap-js.pprof", 117 expectStatusIngest: 200, 118 expectStatusPush: 200, 119 metrics: []expectedMetric{ 120 {"memory:space:bytes:space:bytes", nil, 1}, 121 {"memory:objects:count:space:bytes", nil, 0}, 122 }, 123 }, 124 { 125 profile: repoRoot + "pkg/og/convert/pprof/testdata/nodejs-heap.pb.gz", 126 expectStatusIngest: 200, 127 expectStatusPush: 200, 128 metrics: []expectedMetric{ 129 {"memory:inuse_space:bytes:inuse_space:bytes", nil, 1}, 130 {"memory:inuse_objects:count:inuse_space:bytes", nil, 0}, 131 }, 132 }, 133 { 134 profile: repoRoot + "pkg/og/convert/pprof/testdata/nodejs-wall.pb.gz", 135 expectStatusIngest: 200, 136 expectStatusPush: 200, 137 metrics: []expectedMetric{ 138 {"wall:samples:count:wall:microseconds", nil, 0}, 139 {"wall:wall:microseconds:wall:microseconds", nil, 1}, 140 }, 141 }, 142 { 143 profile: repoRoot + "pkg/og/convert/pprof/testdata/req_2.pprof", 144 sampleTypeConfig: repoRoot + "pkg/og/convert/pprof/testdata/req_2.st.json", 145 expectStatusIngest: 200, 146 expectStatusPush: 200, 147 metrics: []expectedMetric{ 148 {"goroutines:goroutine:count:goroutine:count", nil, 0}, 149 }, 150 }, 151 { 152 profile: repoRoot + "pkg/og/convert/pprof/testdata/req_3.pprof", 153 sampleTypeConfig: repoRoot + "pkg/og/convert/pprof/testdata/req_3.st.json", 154 expectStatusIngest: 200, 155 expectStatusPush: 200, 156 metrics: []expectedMetric{ 157 {"block:delay:nanoseconds:contentions:count", nil, 1}, 158 {"block:contentions:count:contentions:count", nil, 0}, 159 }, 160 }, 161 { 162 profile: repoRoot + "pkg/og/convert/pprof/testdata/req_4.pprof", 163 sampleTypeConfig: repoRoot + "pkg/og/convert/pprof/testdata/req_4.st.json", 164 expectStatusIngest: 200, 165 expectStatusPush: 200, 166 metrics: []expectedMetric{ 167 {"mutex:contentions:count:contentions:count", nil, 0}, 168 {"mutex:delay:nanoseconds:contentions:count", nil, 1}, 169 }, 170 }, 171 { 172 profile: repoRoot + "pkg/og/convert/pprof/testdata/req_5.pprof", 173 sampleTypeConfig: repoRoot + "pkg/og/convert/pprof/testdata/req_5.st.json", 174 expectStatusIngest: 200, 175 expectStatusPush: 200, 176 metrics: []expectedMetric{ 177 {"memory:alloc_objects:count:space:bytes", nil, 0}, 178 {"memory:alloc_space:bytes:space:bytes", nil, 1}, 179 }, 180 }, 181 { 182 // this one have milliseconds in Profile.TimeNanos 183 // https://github.com/grafana/pyroscope/pull/2376/files 184 profile: repoRoot + "pkg/og/convert/pprof/testdata/pyspy-1.pb.gz", 185 expectStatusIngest: 200, 186 expectStatusPush: 200, 187 metrics: []expectedMetric{ 188 {"process_cpu:samples:count::milliseconds", nil, 0}, 189 }, 190 spyName: pprof2.SpyNameForFunctionNameRewrite(), 191 }, 192 { 193 // this one is broken dotnet pprof 194 // it has function.id == 0 for every function 195 profile: repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-3.pb.gz", 196 sampleTypeConfig: repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-3.st.json", 197 expectStatusIngest: 200, 198 expectStatusPush: 400, 199 expectedError: "function id is 0", 200 metrics: []expectedMetric{ 201 {"process_cpu:cpu:nanoseconds::nanoseconds", nil, 0}, 202 }, 203 needFunctionIDFix: true, 204 spyName: "dotnetspy", 205 }, 206 { 207 // this one is broken dotnet pprof 208 // it has function.id == 0 for every function 209 // it also has "-" in sample type name 210 profile: repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-73.pb.gz", 211 sampleTypeConfig: repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-3.st.json", 212 expectStatusIngest: 200, 213 expectStatusPush: 400, 214 expectedError: "function id is 0", 215 metrics: []expectedMetric{ 216 // notice how they all use process_cpu metric 217 {"process_cpu:cpu:nanoseconds::nanoseconds", nil, 0}, 218 {"process_cpu:alloc_samples:count::nanoseconds", nil, 2}, // this was rewriten by ingest handler to replace - 219 {"process_cpu:alloc_size:bytes::nanoseconds", nil, 3}, // this was rewriten by ingest handler to replace - 220 }, 221 needFunctionIDFix: true, 222 spyName: "dotnetspy", 223 }, 224 { 225 // this is a fixed dotnet pprof 226 profile: repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-211.pb.gz", 227 sampleTypeConfig: repoRoot + "pkg/og/convert/pprof/testdata/dotnet-pprof-211.st.json", 228 expectStatusIngest: 200, 229 expectStatusPush: 200, 230 metrics: []expectedMetric{ 231 {"process_cpu:cpu:nanoseconds::nanoseconds", nil, 0}, 232 {"process_cpu:alloc_samples:count::nanoseconds", nil, 2}, 233 {"process_cpu:alloc_size:bytes::nanoseconds", nil, 3}, 234 {"process_cpu:alloc_size:bytes::nanoseconds", nil, 3}, 235 }, 236 spyName: "dotnetspy", 237 }, 238 { 239 240 profile: repoRoot + "pkg/og/convert/pprof/testdata/invalid_utf8.pb.gz", 241 expectStatusPush: 400, 242 expectStatusIngest: 422, 243 metrics: []expectedMetric{ 244 {"process_cpu:cpu:nanoseconds::nanoseconds", nil, 0}, 245 }, 246 }, 247 } 248 ) 249 250 func TestIngest(t *testing.T) { 251 EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) { 252 for _, td := range testdata { 253 t.Run(td.profile, func(t *testing.T) { 254 rb := p.NewRequestBuilder(t). 255 Spy(td.spyName) 256 req := rb.IngestPPROFRequest(td.profile, td.prevProfile, td.sampleTypeConfig) 257 p.Ingest(t, req, td.expectStatusIngest) 258 259 if td.expectStatusIngest == 200 { 260 for _, metric := range td.metrics { 261 rb.Render(metric.name) 262 profile := rb.SelectMergeProfile(metric.name, metric.query) 263 assertPPROF(t, profile, metric, td, td.fixAtIngest) 264 } 265 } 266 }) 267 } 268 }) 269 } 270 271 func TestIngestPPROFFixPythonLinenumbers(t *testing.T) { 272 EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) { 273 274 profile := pprof.RawFromProto(&profilev1.Profile{ 275 SampleType: []*profilev1.ValueType{{ 276 Type: 5, 277 Unit: 6, 278 }}, 279 PeriodType: &profilev1.ValueType{ 280 Type: 5, Unit: 6, 281 }, 282 StringTable: []string{"", "main", "func1", "func2", "qwe.py", "cpu", "nanoseconds"}, 283 Period: 1000000000, 284 Function: []*profilev1.Function{ 285 {Id: 1, Name: 1, Filename: 4, SystemName: 1, StartLine: 239}, 286 {Id: 2, Name: 2, Filename: 4, SystemName: 2, StartLine: 42}, 287 {Id: 3, Name: 3, Filename: 4, SystemName: 3, StartLine: 7}, 288 }, 289 Location: []*profilev1.Location{ 290 {Id: 1, Line: []*profilev1.Line{{FunctionId: 1, Line: 242}}}, 291 {Id: 2, Line: []*profilev1.Line{{FunctionId: 2, Line: 50}}}, 292 {Id: 3, Line: []*profilev1.Line{{FunctionId: 3, Line: 8}}}, 293 }, 294 Sample: []*profilev1.Sample{ 295 {LocationId: []uint64{2, 1}, Value: []int64{10}}, 296 {LocationId: []uint64{3, 1}, Value: []int64{13}}, 297 }, 298 }) 299 300 tempProfileFile, err := os.CreateTemp("", "profile") 301 require.NoError(t, err) 302 _, err = profile.WriteTo(tempProfileFile) 303 assert.NoError(t, err) 304 tempProfileFile.Close() 305 defer os.Remove(tempProfileFile.Name()) 306 307 rb := p.NewRequestBuilder(t). 308 Spy("pyspy") 309 req := rb.IngestPPROFRequest(tempProfileFile.Name(), "", "") 310 p.Ingest(t, req, 200) 311 312 renderedProfile := rb.SelectMergeProfile("process_cpu:cpu:nanoseconds:cpu:nanoseconds", nil) 313 actual := bench.StackCollapseProto(renderedProfile.Msg, 0, 1) 314 expected := []string{ 315 "qwe.py main;qwe.py func1 10", 316 "qwe.py main;qwe.py func2 13", 317 } 318 assert.Equal(t, expected, actual) 319 }) 320 } 321 322 func TestIngestPPROFSanitizeOtelLabels(t *testing.T) { 323 EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) { 324 325 p1 := testhelper.NewProfileBuilder(time.Now().Add(-time.Second).UnixNano()). 326 CPUProfile(). 327 ForStacktraceString("my", "other"). 328 AddSamples(239) 329 p1.Sample[0].Label = []*profilev1.Label{ 330 { 331 Key: p1.AddString("foo.bar"), 332 Str: p1.AddString("qwe.asd"), 333 }, 334 } 335 p1bs, err := p1.MarshalVT() 336 require.NoError(t, err) 337 338 rb := p.NewRequestBuilder(t) 339 rb.Push(rb.PushPPROFRequestFromBytes(p1bs, "process_cpu"), 200, "") 340 341 renderedProfile := rb.SelectMergeProfile("process_cpu:cpu:nanoseconds:cpu:nanoseconds", map[string]string{ 342 "foo_bar": "qwe.asd", 343 }) 344 actual, err := strprofile.Stringify(renderedProfile.Msg, strprofile.Options{ 345 NoTime: true, 346 NoDuration: true, 347 }) 348 require.NoError(t, err) 349 expected := `{ 350 "sample_types": [ 351 { 352 "type": "cpu", 353 "unit": "nanoseconds" 354 } 355 ], 356 "samples": [ 357 { 358 "locations": [ 359 { 360 "address": "0x0", 361 "lines": [ 362 "my[]@:0" 363 ], 364 "mapping": "0x0-0x0@0x0 ()" 365 }, 366 { 367 "address": "0x0", 368 "lines": [ 369 "other[]@:0" 370 ], 371 "mapping": "0x0-0x0@0x0 ()" 372 } 373 ], 374 "values": "239" 375 } 376 ], 377 "period": "1000000000" 378 }` 379 assert.JSONEq(t, expected, actual) 380 }) 381 } 382 383 func TestGodeltaprofRelabelPush(t *testing.T) { 384 const blockSize = 1024 385 const metric = "godeltaprof_memory" 386 387 EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) { 388 389 p1, _ := testhelper.NewProfileBuilder(time.Now().Add(-time.Second).UnixNano()). 390 MemoryProfile(). 391 ForStacktraceString("my", "other"). 392 AddSamples(239, 239*blockSize, 1000, 1000*blockSize). 393 MarshalVT() 394 395 p2, _ := testhelper.NewProfileBuilder(time.Now().UnixNano()). 396 MemoryProfile(). 397 ForStacktraceString("my", "other"). 398 AddSamples(3, 3*blockSize, 1000, 1000*blockSize). 399 MarshalVT() 400 401 rb := p.NewRequestBuilder(t) 402 rb.Push(rb.PushPPROFRequestFromBytes(p1, metric), 200, "") 403 rb.Push(rb.PushPPROFRequestFromBytes(p2, metric), 200, "") 404 renderedProfile := rb.SelectMergeProfile("memory:alloc_objects:count:space:bytes", nil) 405 actual := bench.StackCollapseProto(renderedProfile.Msg, 0, 1) 406 expected := []string{ 407 "other;my 242", 408 } 409 assert.Equal(t, expected, actual) 410 }) 411 } 412 413 func TestPushStringTableOOBSampleType(t *testing.T) { 414 const blockSize = 1024 415 const metric = "godeltaprof_memory" 416 417 EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) { 418 419 testdata := []struct { 420 name string 421 corrupt func(p *testhelper.ProfileBuilder) 422 expectedErr string 423 }{ 424 { 425 name: "sample type", 426 corrupt: func(p *testhelper.ProfileBuilder) { 427 p.SampleType[0].Type = 100500 428 }, 429 expectedErr: "sample type type string index out of range", 430 }, 431 { 432 name: "function name", 433 corrupt: func(p *testhelper.ProfileBuilder) { 434 p.Function[0].Name = 100500 435 }, 436 expectedErr: "function name string index out of range", 437 }, 438 { 439 name: "mapping", 440 corrupt: func(p *testhelper.ProfileBuilder) { 441 p.Mapping[0].Filename = 100500 442 }, 443 expectedErr: "mapping file name string index out of range", 444 }, 445 { 446 name: "Sample label", 447 corrupt: func(p *testhelper.ProfileBuilder) { 448 p.Sample[0].Label = []*profilev1.Label{{ 449 Key: 100500, 450 }} 451 }, 452 expectedErr: "sample label string index out of range", 453 }, 454 { 455 name: "String 0 not empty", 456 corrupt: func(p *testhelper.ProfileBuilder) { 457 p.StringTable[0] = "hmmm" 458 }, 459 expectedErr: "string 0 should be empty string", 460 }, 461 } 462 for _, td := range testdata { 463 t.Run(td.name, func(t *testing.T) { 464 p1 := testhelper.NewProfileBuilder(time.Now().Add(-time.Second).UnixNano()). 465 MemoryProfile(). 466 ForStacktraceString("my", "other"). 467 AddSamples(239, 239*blockSize, 1000, 1000*blockSize) 468 td.corrupt(p1) 469 p1bs, err := p1.MarshalVT() 470 require.NoError(t, err) 471 472 rb := p.NewRequestBuilder(t) 473 rb.Push(rb.PushPPROFRequestFromBytes(p1bs, metric), 400, td.expectedErr) 474 }) 475 } 476 }) 477 } 478 479 func TestPush(t *testing.T) { 480 EachPyroscopeTest(t, func(p *PyroscopeTest, t *testing.T) { 481 482 for _, td := range testdata { 483 if td.prevProfile != "" { 484 continue 485 } 486 t.Run(td.profile, func(t *testing.T) { 487 rb := p.NewRequestBuilder(t) 488 489 req := rb.PushPPROFRequestFromFile(td.profile, td.metrics[0].name) 490 rb.Push(req, td.expectStatusPush, td.expectedError) 491 492 if td.expectStatusPush == 200 { 493 for _, metric := range td.metrics { 494 rb.Render(metric.name) 495 profile := rb.SelectMergeProfile(metric.name, metric.query) 496 497 assertPPROF(t, profile, metric, td, td.fixAtPush) 498 } 499 } 500 }) 501 } 502 }) 503 } 504 505 func assertPPROF(t *testing.T, resp *connect.Response[profilev1.Profile], metric expectedMetric, testdatum pprofTestData, fix func(*pprof.Profile) *pprof.Profile) { 506 507 assert.Equal(t, 1, len(resp.Msg.SampleType)) 508 509 profileBytes, err := os.ReadFile(testdatum.profile) 510 require.NoError(t, err) 511 expectedProfile, err := pprof.RawFromBytes(profileBytes) 512 require.NoError(t, err) 513 514 if fix != nil { 515 expectedProfile = fix(expectedProfile) 516 } 517 518 actualStacktraces := bench.StackCollapseProto(resp.Msg, 0, 1) 519 expectedStacktraces := bench.StackCollapseProto(expectedProfile.Profile, metric.valueIDX, 1) 520 521 for i, valueType := range expectedProfile.SampleType { 522 fmt.Println(i, expectedProfile.StringTable[valueType.Type]) 523 } 524 require.Equal(t, expectedStacktraces, actualStacktraces) 525 } 526 527 type pprofTestData struct { 528 profile string 529 prevProfile string 530 sampleTypeConfig string 531 spyName string 532 expectStatusIngest int 533 expectStatusPush int 534 expectedError string 535 metrics []expectedMetric 536 needFunctionIDFix bool 537 needsGoHeapFix bool 538 } 539 540 func (d *pprofTestData) fixAtPush(p *pprof.Profile) *pprof.Profile { 541 if d.needsGoHeapFix { 542 p.Profile = pprof.FixGoProfile(p.Profile) 543 } 544 return p 545 } 546 547 func (d *pprofTestData) fixAtIngest(p *pprof.Profile) *pprof.Profile { 548 if d.spyName == pprof2.SpyNameForFunctionNameRewrite() { 549 pprof2.FixFunctionNamesForScriptingLanguages(p, ingestion.Metadata{SpyName: d.spyName}) 550 } 551 if d.needFunctionIDFix { 552 pprof2.FixFunctionIDForBrokenDotnet(p.Profile) 553 } 554 if d.needsGoHeapFix { 555 p.Profile = pprof.FixGoProfile(p.Profile) 556 } 557 return p 558 } 559 560 type expectedMetric struct { 561 name string 562 query map[string]string 563 valueIDX int 564 }