Skip to main content
Glama

mcp-pprof-anaylzer

flamegraph_test.go8.95 kB
package analyzer_test import ( "encoding/json" "testing" "github.com/ZephyrDeng/pprof-analyzer-mcp/analyzer" "github.com/google/pprof/profile" ) func TestBuildFlameGraphTree(t *testing.T) { // Create a simple test profile with a call stack testProfile := &profile.Profile{ SampleType: []*profile.ValueType{ {Type: "samples", Unit: "count"}, {Type: "cpu", Unit: "nanoseconds"}, }, Sample: []*profile.Sample{ { Location: []*profile.Location{ { ID: 1, Line: []profile.Line{ { Function: &profile.Function{ ID: 1, Name: "main", Filename: "main.go", }, Line: 10, }, }, }, { ID: 2, Line: []profile.Line{ { Function: &profile.Function{ ID: 2, Name: "foo", Filename: "foo.go", }, Line: 20, }, }, }, { ID: 3, Line: []profile.Line{ { Function: &profile.Function{ ID: 3, Name: "bar", Filename: "bar.go", }, Line: 30, }, }, }, }, Value: []int64{1, 1000}, // 1 sample, 1000 nanoseconds }, }, } // Test building a flame graph tree for CPU samples t.Run("CPUFlameGraph", func(t *testing.T) { // Use the second value (nanoseconds) flameGraph, err := analyzer.BuildFlameGraphTree(testProfile, 1) if err != nil { t.Fatalf("Error building flame graph tree: %v", err) } // Convert to JSON for inspection jsonBytes, err := json.Marshal(flameGraph) if err != nil { t.Fatalf("Error marshaling flame graph to JSON: %v", err) } // Parse the JSON var result map[string]interface{} if err := json.Unmarshal(jsonBytes, &result); err != nil { t.Fatalf("Error parsing flame graph JSON: %v", err) } // Check the root node if name, ok := result["name"].(string); !ok || name != "root" { t.Errorf("Expected root node name to be 'root', but got '%v'", result["name"]) } // Check the value if value, ok := result["value"].(float64); !ok || value != 1000 { t.Errorf("Expected root node value to be 1000, but got %v", result["value"]) } // Check that there are children children, ok := result["children"].([]interface{}) if !ok { t.Errorf("Expected root node to have children, but it doesn't") } else if len(children) == 0 { t.Errorf("Expected root node to have at least one child, but it has none") } // Check that there's at least one child firstChild := children[0].(map[string]interface{}) if name, ok := firstChild["name"].(string); !ok { t.Errorf("Expected first child to have a name, but it doesn't") } else { t.Logf("First child name is '%s'", name) } // Check that the child has children childChildren, ok := firstChild["children"].([]interface{}) if !ok || len(childChildren) == 0 { t.Errorf("Expected first child to have children, but it doesn't") } else { // Check that the structure is as expected (a chain of function calls) // Note: We don't check specific names because the order might vary t.Logf("First child has %d children", len(childChildren)) } }) // Test with invalid value index t.Run("InvalidValueIndex", func(t *testing.T) { _, err := analyzer.BuildFlameGraphTree(testProfile, 5) // Index out of bounds if err == nil { t.Error("Expected error for invalid value index, but got nil") } }) // Test with memory profile t.Run("MemoryFlameGraph", func(t *testing.T) { // Create a memory profile memProfile := &profile.Profile{ SampleType: []*profile.ValueType{ {Type: "inuse_space", Unit: "bytes"}, {Type: "inuse_objects", Unit: "count"}, }, Sample: []*profile.Sample{ { Location: []*profile.Location{ { ID: 1, Line: []profile.Line{ { Function: &profile.Function{ ID: 1, Name: "allocator", Filename: "alloc.go", }, Line: 10, }, }, }, }, Value: []int64{1024, 1}, // 1024 bytes, 1 object Label: map[string][]string{ "type": {"TestType"}, }, }, }, } // Use the first value (bytes) flameGraph, err := analyzer.BuildFlameGraphTree(memProfile, 0) if err != nil { t.Fatalf("Error building memory flame graph tree: %v", err) } // Convert to JSON for inspection jsonBytes, err := json.Marshal(flameGraph) if err != nil { t.Fatalf("Error marshaling memory flame graph to JSON: %v", err) } // Parse the JSON var result map[string]interface{} if err := json.Unmarshal(jsonBytes, &result); err != nil { t.Fatalf("Error parsing memory flame graph JSON: %v", err) } // Check the root node if name, ok := result["name"].(string); !ok || name != "root" { t.Errorf("Expected root node name to be 'root', but got '%v'", result["name"]) } // Check the value if value, ok := result["value"].(float64); !ok || value != 1024 { t.Errorf("Expected root node value to be 1024, but got %v", result["value"]) } // Check that there are children children, ok := result["children"].([]interface{}) if !ok { t.Errorf("Expected root node to have children, but it doesn't") } else if len(children) == 0 { t.Errorf("Expected root node to have at least one child, but it has none") } // Check the first child (should be "allocator") firstChild := children[0].(map[string]interface{}) if name, ok := firstChild["name"].(string); !ok || name != "allocator" { t.Errorf("Expected first child name to be 'allocator', but got '%v'", firstChild["name"]) } // Check that the memory-specific fields are present if _, ok := result["valueFormatted"].(string); !ok { t.Errorf("Expected memory flame graph to have 'valueFormatted' field, but it doesn't") } }) // Test with allocs profile t.Run("AllocsFlameGraph", func(t *testing.T) { // Create an allocs profile allocsProfile := &profile.Profile{ SampleType: []*profile.ValueType{ {Type: "alloc_space", Unit: "bytes"}, {Type: "alloc_objects", Unit: "count"}, }, Sample: []*profile.Sample{ { Location: []*profile.Location{ { ID: 1, Line: []profile.Line{ { Function: &profile.Function{ ID: 1, Name: "allocator", Filename: "alloc.go", }, Line: 10, }, }, }, }, Value: []int64{2048, 2}, // 2048 bytes, 2 objects Label: map[string][]string{ "type": {"AllocType"}, }, }, }, } // Use the first value (bytes) flameGraph, err := analyzer.BuildFlameGraphTree(allocsProfile, 0) if err != nil { t.Fatalf("Error building allocs flame graph tree: %v", err) } // Convert to JSON for inspection jsonBytes, err := json.Marshal(flameGraph) if err != nil { t.Fatalf("Error marshaling allocs flame graph to JSON: %v", err) } // Parse the JSON var result map[string]interface{} if err := json.Unmarshal(jsonBytes, &result); err != nil { t.Fatalf("Error parsing allocs flame graph JSON: %v", err) } // Check the root node if name, ok := result["name"].(string); !ok || name != "root" { t.Errorf("Expected root node name to be 'root', but got '%v'", result["name"]) } // Check the value if value, ok := result["value"].(float64); !ok || value != 2048 { t.Errorf("Expected root node value to be 2048, but got %v", result["value"]) } // Check that there are children children, ok := result["children"].([]interface{}) if !ok { t.Errorf("Expected root node to have children, but it doesn't") } else if len(children) == 0 { t.Errorf("Expected root node to have at least one child, but it has none") } // Check the first child (should be "allocator") firstChild := children[0].(map[string]interface{}) if name, ok := firstChild["name"].(string); !ok || name != "allocator" { t.Errorf("Expected first child name to be 'allocator', but got '%v'", firstChild["name"]) } // Check that the memory-specific fields are present if _, ok := result["valueFormatted"].(string); !ok { t.Errorf("Expected allocs flame graph to have 'valueFormatted' field, but it doesn't") } // Check that object count information is present if objectCount, ok := result["objectCount"].(float64); !ok || objectCount != 2 { t.Errorf("Expected root node to have objectCount=2, but got %v", result["objectCount"]) } // Check that average size information is present if avgSize, ok := result["avgSize"].(float64); !ok || avgSize != 1024 { t.Errorf("Expected root node to have avgSize=1024, but got %v", result["avgSize"]) } // Check that type information is present in the child node if typeName, ok := firstChild["type"].(string); !ok || typeName != "AllocType" { t.Errorf("Expected first child to have type='AllocType', but got %v", firstChild["type"]) } }) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ZephyrDeng/pprof-analyzer-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server