Parse deep plans on a large-stack thread (#589) - #591
Merged
Merged
Conversation
A plan about 450 operators deep overflowed the 1 MB stack of the UI thread and the CLI's main thread before MaxParseDepth (1,000) could refuse it, and the process ended. Parse and ParseAsync now walk the tree on a 32 MB thread and join it, so the depth limit stops a deep plan. Same fix as PerformanceMonitor#4551. The web viewer (WebAssembly) still parses inline. ScopedDescendants and ResultMapper.MapNode use loops with their own stacks, and HtmlExporter keeps its per-node text out of the recursive method, so every step after the parse fits a 1 MB thread at the depth limit. JSON output, limited to about 500 operator levels, now says the plan is too deeply nested instead of "a possible object cycle" (CLI, Robot Advice, MCP, web share). The CLI stops with the parse error when a plan does not parse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
- get_query_store_top reports a plan the parser refuses in load_error. It returned the plan as loaded, with no warnings. - The web viewer's share button maps only the serializer's JsonException to TooDeepMessage. A reply from the server that isn't JSON gets the usual "Share failed" message again. - HtmlExporter.WriteOperatorNode drops a parameter it never used. - New tests: the HTML exporter at 1,000 levels on a 384 KB thread (the old exporter overflows there), the search's document order and RelOp skipping, the CLI's parse-failure message, and the too-deep messages from the CLI and the MCP tools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR
|
Reviewed the parse-thread rewrite and the traversal changes. No correctness issues found. Specifically verified:
No untrusted-input or T-SQL concerns — this PR doesn't touch SQL generation. No |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fixes #589.
A plan with about 450 or more nested operators ended the process with a stack overflow. The parser walked the plan tree on the caller's thread. The app's UI thread and the CLI's main thread have 1 MB stacks. The walk ran out of stack at about 450 levels, so the depth limit of 1,000 (
MaxParseDepth) never refused the plan. A .NET process cannot catch a stack overflow, so the app closed with no message.The changes:
ShowPlanParser.ParseandParseAsyncwalk the tree on a new thread with a 32 MB stack, and wait for it. The public signature does not change. Now the depth limit stops a deep plan: 1,000 levels parse, and 1,001 levels give a parse error. This is the same fix as Plan analysis: a deeply nested plan no longer crashes the process that parses it (#4512) PerformanceMonitor#4551.ScopedDescendantssearches the elements inside one operator. It used a recursive iterator, and no depth guard counts those elements, so deep nesting there overflowed even the 32 MB stack. It now uses a loop with its own stack.ResultMapperneeded more than 768 KB of a 1 MB stack, andHtmlExporterneeded 512 KB.ResultMappernow uses a loop.HtmlExporterwrites each operator's line in a separate method, so its recursive method has a small frame. Now each step needs 384 KB or less.AnalysisJson.MaxDepth, 1,024). Each operator level uses two JSON levels, so the limit allows about 500 operator levels. Before this change, the parser crashed before a plan got that deep. Now a plan with more levels reaches the limit, and the serializer's message blames "a possible object cycle". A newAnalysisJson.TooDeepMessagegives the real cause. The CLI, Robot Advice, the MCP tools and the share button in the web viewer show it.query-storewrote an empty or partial analysis and reported OK.get_query_store_topreports a plan that does not parse inload_error. Before, it listed the plan as loaded, with no warnings.Which component(s) does this affect?
The web viewer (PlanViewer.Web) changes too, in its share error message.
How was this tested?
DeepPlanStackTests, 11 cases. The depth cases run on a real thread with a small stack:ParseAsyncparses a plan at the depth limit on a 1 MB thread.JsonException.planview.exeanalyzed plans 150, 999, 1,000 and 1,001 levels deep:-o textsucceeds up to 1,000 levels.-o jsonexits 1 with the new message at 999 and 1,000 levels. At 1,001 levels, both formats exit 1 with the parse error.Taskfrom the parse thread, so the caller resumed on the thread pool. Two MCP tests then timed out in the full suite. The parse now waits for its thread withJoin, in the same way that the walk ran on the caller's thread before.Review round 1
get_query_store_topdid not check the parse error (above).JsonExceptionon depth, but a reply from the server that is not JSON throws one too. Now only the serializer's exception gets the depth message.HtmlExporter.WriteOperatorNodepassed down a parameter that it never used.Checklist
--no-incremental)dotnet test)🤖 Generated with Claude Code
https://claude.ai/code/session_01G625JBNh45iTR1hpT4CxNR