Skip to content

fix(mcp): resolve parent node to its function in the profile call graph - #11952

Open
lhy-dcf wants to merge 1 commit into
deepflowio:mainfrom
lhy-dcf:fix/mcp-profile-parent-function
Open

lhy-dcf wants to merge 1 commit into
deepflowio:mainfrom
lhy-dcf:fix/mcp-profile-parent-function

Conversation

@lhy-dcf

@lhy-dcf lhy-dcf commented Nov 4, 2026

Copy link
Copy Markdown

Fixes #10176.

Problem

generateFunctionMermaidDiagram in server/mcp/handle/profile.go builds the function-level call graph out of profileData.NodeValues. Those rows are [function_id, parent_node_id, self_value, total_value] — see server/querier/profile/service/profile.go:

result.NodeValues.Values = append(result.NodeValues.Values, []int{locationID, node.ParentNodeID, node.SelfValue, node.TotalValue})
result.NodeValues.Columns = []string{"function_id", "parent_node_id", "self_value", "total_value"}

parent_node_id indexes the node list (i.e. the row order), while function_id indexes FunctionNames. The builder used the parent node id to index functions:

if parentID != -1 && parentID < len(profileData.FunctionNames) {
    parentFunction := profileData.FunctionNames[parentID]

The two index spaces only coincide while every function owns exactly one node. A location that shows up in two stacks gets two nodes, so node indexes run ahead of location ids and every later parent resolves to a different function.

It never panics, which is why this survived: a location id is assigned before its node is appended, so locationID <= nodeIndex always holds. The wrong lookup therefore always stays inside the bounds check — the graph just gains an edge that does not exist instead of failing loudly.

Concrete case (the shape of the new test):

node function_id parent_node_id actual meaning
0 0 rootFn -1 root
1 1 alphaFn 0 rootFn -> alphaFn
2 1 alphaFn 0 same function, a second stack
3 3 gammaFn 2 alphaFn -> gammaFn

node 3 is a child of node 2, which is alphaFn. The old code looks up FunctionNames[2] = betaFn and emits betaFn --> gammaFn.

Fix

Build a node id -> function name table from the row order once, then use it for the node's own function and for its parent. function_id keeps indexing FunctionNames as before; only the parent lookup changes.

A second defect in the same function

The legend line passed the translated template as an argument to an unrelated format string:

fmt.Sprintf("\n    info[\"%s\"]\n", translation("显示了 %d 个函数,%d 条调用关系"), len(functionInfos), relationCount)

The counts never reach the output and Sprintf writes info["显示了 %d 个函数,%d 条调用关系"]%!(EXTRA int=2, int=1) into the mermaid block, which breaks the diagram. go test runs vet, which rejects this call (fmt.Sprintf call needs 1 arg but has 3 args) as soon as the package contains a test file, so it has to be corrected for the tests below to run at all. Both locales keep their %d placeholders, so the translated string is the format.

Verification

Three tests in server/mcp/handle/profile_test.go:

  • TestDiagramResolvesParentThroughNodeIndex — the table above; asserts alphaFn->gammaFn exists and betaFn->gammaFn does not.
  • TestDiagramKeepsLinearStackEdges — a linear stack where node index and function id still coincide; guards the working shape against regressions.
  • TestDiagramLegendCarriesCounts — asserts the legend carries the counts and that no %! marker leaks into the block.

Counter-proof: putting main's version of profile.go back and keeping the tests produces

--- FAIL: TestDiagramResolvesParentThroughNodeIndex
    want edge alphaFn->gammaFn (parent node 2 is alphaFn), got edges: map[betaFn->gammaFn:true rootFn->alphaFn:true]
--- FAIL: TestDiagramLegendCarriesCounts
    legend contains Sprintf error markers

so the tests catch exactly these two defects and not something incidental. With the fix: ok ... 3 passed, gofmt -l clean, go vet clean.

Evidence boundary

The tests run against the real profile.go, the real model and common packages, and the real mcp-go / go-simplejson modules. server/mcp/config, server/libs/logger and common.CURLPerform were replaced by minimal stand-ins, because this checkout has no vendored tree and the network here cannot fetch the full module: config only needs the MConfig pointer to stay nil, logger only needs the three methods profile.go calls, and CURLPerform is never reached by these tests. That means the package itself compiles and the tests pass against the file being changed, but make for the whole server module was not executed locally — CI's verify server will confirm it.

Not covered: no end-to-end MCP profile tool call against a live querier, and no visual check of the rendered diagram. The fix only changes which function each parent resolves to; the mermaid emission itself is untouched.

node_values rows are [function_id, parent_node_id, self_value,
total_value]. parent_node_id indexes the row order of node_values while
function_id indexes the functions list, and the mermaid builder used
parent_node_id to index functions directly. The two only coincide while
every function owns exactly one node: as soon as one location shows up in
two stacks the parent resolves to an unrelated function, so the graph gets
an edge that does not exist. It never panics, because a location id cannot
exceed the node index, so the mistake stays inside the bounds check and
silently claims the wrong caller.

Map node ids to function names from the row order once, and use that table
for the node's own function and its parent.

The same function also passed the translated legend template as an
argument to an unrelated format string, so the counts never reached the
output and Sprintf appended an %!(EXTRA ...) marker inside the mermaid
block. vet rejects that call as soon as the package has a test, so it is
corrected together with the tests that expose both problems.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Some bugs in MCP Data Process

1 participant