-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[debug] Lazily update frames of all threads in all-stop mode #7281
[debug] Lazily update frames of all threads in all-stop mode #7281
Conversation
A re-make of d1678ad. The changed bits: ```diff @@ -181,7 +196,17 @@ export class DebugThread extends DebugThreadData implements TreeElement { return [...result.values()]; } protected clearFrames(): void { + // Clear all frames this._frames.clear(); + + // Cancel all request promises + this.pendingFetchCancel.cancel(); + this.pendingFetchCancel = new CancellationTokenSource(); + + // Empty all current requests + this.pendingFetch = Promise.resolve([]); + this._pendingFetchCount = 0; + this.updateCurrentFrame(); } protected updateCurrentFrame(): void { ``` So what's the difference? Well, if you got multiple stops fast enough, pending frames would be blocking the refresh from happening. Since `updateFrames` was being called from one more place (when switching thread), it forced me to avoid pointless lookups as to not error (GDB complains when you're trying to fetch something that has already been fetched). This new behavior that avoids pointless refreshes didn't take speed into account, and could potentially block an entirely new fetch. To fix this, I cancel all frame fetches when clearing frames (which is done when updating a thread in all-stop mode). Signed-off-by: Samuel Frylmark <samuel.frylmark@ericsson.com>
cc: @tom-shan @akosyakov |
@@ -221,6 +224,9 @@ export class DebugSession implements CompositeTreeElement { | |||
this.fireDidChange(); | |||
if (thread) { | |||
this.toDisposeOnCurrentThread.push(thread.onDidChanged(() => this.fireDidChange())); | |||
|
|||
// If this thread is missing stack frame information, then load that. | |||
this.updateFrames(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to update frames whenever current thread is changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like the comment says,
If this thread is missing stack frame information, then load that.
On a breakpoint or other stop, we clear all threads' frames and only load the current thread's stackframes. When you switch thread it's going to be empty. Which is good, because fetching all stackframes on stop would be slow on a large number of threads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant that we should update only according to stop
logic, if we do it propertly then by the time when someone changes a thread frames will be already there or at least updating. This code should not be necessary.
I plant test this PR by debugging in a few languages: Java, Python, go, cpp |
@marcdumais-work I'm not sure that the original PR was proper. I looked at VS Code logic and they have different logic and timing at least in 2 places. I think the issue is in how we treat |
@akosyakov Actually, we only update in case of all stop mode. See doUpdateThreads and thread.update. I agree it's hard to discover though, I didn't find out about it until you guys commented that there already was stack frame clearing logic. |
@efrlsml |
@akosyakov You're right, unless one remembers to clear |
Is not it already a case?
I don't think a fix requires big changes, but good understanding of how |
Nono, I mean literally taking it as a function argument, not assigning to a class-wide variable before calling. |
VSCode implementation: https://github.com/microsoft/vscode/blob/b62045619a23f134a515b72294b2eb98ca5f1c74/src/vs/workbench/contrib/debug/browser/debugSession.ts#L728-L759 This calls fetchThreads which does a rawUpdate here. Unlike Theia, VSCode lets the session handle the thread updating (like my first PR originally suggested) https://github.com/microsoft/vscode/blob/b62045619a23f134a515b72294b2eb98ca5f1c74/src/vs/workbench/contrib/debug/browser/debugSession.ts#L655-L674. Does this answer any questions or provoke any feedback to how our implementation should look? I'm not familiar enough with how Theia is structured to claim that one way is better than the alternative. |
I don't see any difference in our implementation for update, i.e.
Do you see difference in the logic regardless of structuring? |
@akosyakov Yeah, before this patch upstream theia never clears the stack |
@efrlsml It does on master in The only different which I see that they fetch frames for affected thread, not for the current. Maybe it is a cause of our issues that we are fetching them for not actually stopped thread? |
You're right, I forgot master already got the clearing in place (but not the re-freshing). It also already fetches the affected thread, so with this PR merged we are fetching for both the affected thread and the current. Here seems to be where they fetch the stack frame. Now really, I'm not at all familiar with the VSCode code base so I can't tell you exactly why VSCode works and Theia doesn't. But I can tell you that it does, and I've given you the examples to reproduce this bug in the PR description. I've also made a PR that fixes it, this one. If we disregard VSCode for a second, this PR intuitively makes sense: In upstream Theia you clear the stack frames for all threads, and you never load them again except for the stopped one. You also say that a thread isn't paused when it has no stack frames. So, all other threads except for the last stopped one appear as "not paused" even though they say STOPPED or PAUSED or something. The single-stepping bug was just a data race where frames were still in the queue and therefore weren't re-fetched. This is now fixed by clearing frames before stepping. Does this make sense? |
Sounds reasonable. @marcdumais-work Let's merge it and try again? |
Ok with me but give me an hour or so to at least try it against Python debugging :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested a debug session with Python using the Microsoft extension v2020.1.58038 and the previous issue (#7264) does not happen any more.
Merging. |
What it does
A re-make of d1678ad.
The changed bits:
So what's the difference? Well, if you got multiple stops fast enough,
pending frames would be blocking the refresh from happening. Since
updateFrames
was being called from one more place (when switchingthread), it forced me to avoid pointless lookups as to not error (GDB
complains when you're trying to fetch something that has already been
fetched). This new behavior that avoids pointless refreshes didn't
take speed into account, and could potentially block an entirely new
fetch.
To fix this, I cancel all frame fetches when clearing frames (which is
done when updating a thread in all-stop mode).
How to test
Same as in #6869, but you should also add vscode-python and try debugging a simple python file, to make sure that isn't broken (see 7264). Debugging NodeJS could also be a good idea.
For what it's worth, I apologize for causing a regression.
Review checklist
Reminder for reviewers