Don't re-pay the cost of unresolvable watch expressions on every stop - #1411
Closed
zhuwanhong wants to merge 2 commits into
Closed
zhuwanhong wants to merge 2 commits into
zhuwanhong wants to merge 2 commits into
Conversation
A watch expression is re-evaluated every time the debuggee stops. When one of
its identifiers does not resolve in the current frame, nat_eval() falls through
FindVariable() into two lookups that are expensive in a large program:
- SBFrame::FindValue() with eValueTypeVariableGlobal/Static materializes the
compile unit's globals and statics;
- SBFrame::EvaluateExpression() brings up the C++ expression parser, which
must exhaust every lookup path before it can report an undeclared identifier.
Both were paid again on every single step. Measured on an MFC/boost application
whose stale watch expression made stepping unusable:
MISS nmod=110 find=0.013 findvalue=3.384 varpath=0.026 eval=2.670
HIT nmod=110 find=0.000
Remember the failures per debug session, keyed by the expression, the enclosing
function (unqualified lookup depends on the class scope) and the module count (a
name may become resolvable once a module is loaded). Stepping in that session
went from ~2.9s to ~0.15s per step once the cache warms up.
The cache is consulted only after FindVariable() has failed, so it cannot mask a
local that has come into scope. It is sound only because nat_eval() is always
handed a name: the simple-expression dialect evaluates operators, indexing and
member access itself. Native expressions are arbitrary C++ whose failures may
depend on program state, and are not cached.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nfm9PxpvSxsYYMTMZiRzMD
Watch expressions are re-evaluated every time the debuggee stops, so a slow one adds its full cost to every step. Nothing connects the two for the user: all they observe is that stepping has become sluggish, with no indication of which expression is responsible or that an expression is responsible at all. Print a console warning, once per expression, when an automatically re-evaluated expression takes more than half a second. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nfm9PxpvSxsYYMTMZiRzMD
Owner
|
I don't like this change - the extra complexity isn't worth it. Just delete the watch expression, or collapse the Watch panel. |
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.
Problem
A watch expression is re-evaluated every time the debuggee stops, so its cost is added to
every step. When one of its identifiers does not resolve in the current frame,
nat_eval()falls through
SBFrame::FindVariable()into two lookups that are expensive in a large program:SBFrame::FindValue()witheValueTypeVariableGlobal/Staticmaterializes the compileunit's globals and statics;
SBFrame::EvaluateExpression()brings up the C++ expression parser, which has to exhaustevery lookup path before it can report an undeclared identifier.
Both were paid again on every single step.
This came out of a report of CodeLLDB stepping far more slowly than the MS C/C++ extension on
a large MFC/boost/OSG application (Windows, MSVC/PDB, ~110 loaded modules). Each step took over
2.5 s. The cause turned out to be a single stale watch expression left over from another
function — nothing in the UI connected the two.
Timing added inside
nat_eval()(nmodisSBTarget::GetNumModules()):From the DAP log, one step before the change:
next→stoppedthreadsstackTrace(top frame)evaluate(the stale watch)stackTrace(19 more frames)variables(locals)The evaluation also occupies the adapter's event loop, so
scopesand the secondstackTracesat in the queue behind it.
Changes
Cache identifiers that cannot be resolved, per debug session, keyed by the expression, the
enclosing function and the module count. Stepping in that session went from ~2.9 s to ~0.15 s
once the cache warms up.
Warn about slow watch and hover expressions — a console message, once per expression, when
an automatically re-evaluated expression takes more than half a second. Until now there was
nothing to connect sluggish stepping to its cause; this whole investigation was needed to find
what the adapter could simply have said.
Why the cache is sound
FindVariable()has failed, andFindVariable()still runs onevery evaluation — so a local that has come into scope is still found.
that can only change when modules are loaded or unloaded, hence the module count in the key.
m_fooresolving via an implicitthis), hence the function name in the key.nat_eval()is always handed a name: the simple-expression dialectevaluates operators, indexing and member access itself. Native expressions are arbitrary C++
whose failures may depend on program state, and are deliberately not cached.
Not addressed
The underlying LLDB costs are untouched. A single failed lookup still takes seconds on a large
MSVC/PDB target, and watch expressions written with the
/natprefix still pay it on everystop. That looks like an LLDB issue rather than a CodeLLDB one: a valid native expression
(
1+1) evaluates instantly, andlog timersaccounts for only ~2 ms, so the time is spent inuninstrumented name-lookup / expression-parser code.
🤖 Generated with Claude Code