Repository navigation
Self CPU: subtract whole batch zones, and sum threads for an operator's own CPU - #662
Conversation
…'s own CPU The CLI's "Expensive operators" list and the GUI's advice cards each did their own row-mode subtraction. It took only a batch child's own time, so a row-mode parent was charged the rest of the batch zone, for elapsed and CPU both. On param-sniffing-posttypeid2.sqlplan the Stream Aggregate (node 4) showed 14,771 ms CPU and 14,720 ms elapsed. PlanAnalyzer already gave it 3,059 ms of elapsed. The listed CPU shares added up to 159%. - PlanAnalyzer.GetOperatorOwnCpuMs (new): own CPU summed across threads, per-thread subtraction, batch zones summed. OperatorResult gets OwnCpuMs and OwnElapsedMs (own_cpu_ms / own_elapsed_ms in JSON) from ResultMapper, and the text formatter and advice cards read those instead of subtracting again. - GetEffectiveChildCpuMs sums a batch child's zone, like the elapsed path does. This also fixes the serial path of GetOperatorMaxThreadOwnCpuMs. - GetOperatorMaxThreadOwnCpuMs returns a batch operator's busiest thread without subtracting children, like GetOperatorOwnElapsedMs. - NodeTimeAttribution.GetOwnCpuMs sums batch zones too. After: node 4 shows 3,112 ms CPU and 3,059 ms elapsed, and the shares add up to 98%. Across the 60 test plans, the Expensive operators text changes on 10. Two plans listed CPU shares over 105% before, and none do now. Four new tests in OperatorSelfTimeTests. Core tests: 1,503 total, 0 failed, 40 skipped (1,499 and 40 before). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The advice cards no longer mirror TextFormatter's attribution, only its filtering, ranking and formatting; both read OwnCpuMs and OwnElapsedMs from OperatorResult. NodeTimeAttribution's remark now names GetOperatorOwnCpuMs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoaQ2xKMpZEXvm4HncT8ND
| if (node.ActualExecutionMode == "Batch") | ||
| return node.ActualCPUMs; | ||
|
|
||
| if (node.PerThreadStats.Count > 1) |
There was a problem hiding this comment.
Low: the batch check here (and in GetOperatorMaxThreadOwnCpuMs) reads node.ActualExecutionMode only. GetEffectiveChildCpuMs, AddEffectiveChildByThread and SumBatchSubtreeCpuMs use ActualExecutionMode ?? ExecutionMode. A node with no actual mode attribute but an estimated mode of Batch is subtracted as a child but treated as row mode when it is the parent. That mismatch would double-subtract. Use the same ?? ExecutionMode expression in both places, or put it in a small helper.
Tests are fine for the changed behavior. They cover the serial, parallel and batch cases and the CLI text.
GetOperatorOwnElapsedMs, GetOperatorMaxThreadOwnCpuMs and GetOperatorOwnCpuMs read only ActualExecutionMode for the node itself, while the child paths read ActualExecutionMode ?? ExecutionMode. The parser fills ActualExecutionMode from a per-thread attribute that can be missing, so a batch node without it was subtracted as a batch child but treated as row mode as a parent. All three now use the same fallback as the child paths and NodeTimeAttribution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoaQ2xKMpZEXvm4HncT8ND
| // Batch mode reports each thread's own CPU directly. Subtracting the | ||
| // children would take their standalone CPU off a number that never held it. | ||
| if ((node.ActualExecutionMode ?? node.ExecutionMode) == "Batch") | ||
| return WorkThreads(node).Select(t => t.ActualCPUMs).DefaultIfEmpty(0).Max(); |
There was a problem hiding this comment.
Regression risk: when a batch-mode node has no PerThreadStats (serial plan, or per-thread info absent), WorkThreads(node) is empty and DefaultIfEmpty(0).Max() returns 0. Previously the code fell through to the serial path and returned ActualCPUMs. GetOperatorOwnCpuMs right below handles this by returning node.ActualCPUMs. Fall back to node.ActualCPUMs when there are no threads, e.g. var threads = WorkThreads(node); if (threads.Count == 0) return node.ActualCPUMs;. The new tests only cover a parallel plan, so none of them catches this.
GetOperatorMaxThreadOwnCpuMs returned 0 for a batch node with no PerThreadStats. Parsed plans always have at least one thread row when ActualCPUMs is above 0, but a node built in code may not, so fall back to ActualCPUMs as GetOperatorOwnCpuMs does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoaQ2xKMpZEXvm4HncT8ND
|
Reviewed the diff. I found no blocking issues.
|
The CLI's "Expensive operators" list and the GUI's advice cards each did their own row-mode subtraction. It took only a batch child's own time, so a row-mode parent was charged the rest of the batch zone, for elapsed and CPU both. On param-sniffing-posttypeid2.sqlplan the Stream Aggregate (node 4) showed 14,771 ms CPU and 14,720 ms elapsed. PlanAnalyzer already gave it 3,059 ms of elapsed. The listed CPU shares added up to 159%.
Changes
Testing
The same two CPU problems are fixed in the sqlserver-query-plans plugin (claude-plugins, version 1.5.6).
🤖 Generated with Claude Code
https://claude.ai/code/session_01UoaQ2xKMpZEXvm4HncT8ND