Skip to content
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

Add auto-refresh capabilities for memory inspector windows #115

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

martin-fleck-at
Copy link
Contributor

@martin-fleck-at martin-fleck-at commented Mar 22, 2024

Extend Auto Refresh capabilities with 'After Delay' and 'On Focus'

  • Rework 'refreshOnStop' boolean setting to 'autoRefresh' enumerable
    -- On Stop (previously: 'on' for 'refreshOnStop')
    -- On Focus (previously: always implicit on view state change)
    -- After Delay (new)
    -- Off (previously: 'off' for 'refreshOnStop')

  • On Stop
    -- Rework global setting to be local for each Memory Inspector
    -- Listen to debug session stopped event and propagate to view

  • On Focus
    -- Rework implicit refresh update to option in setting
    -- Listen to view state changes and propagate to view

  • After Delay
    -- New option to explicitly define a delay when re-fetching the memory
    -- Minimum: 500ms, default: 500, input step size: 250ms

Refactoring:

  • Split debug session tracking into dedicated class with session events
    -- Convert debug events into session events with additional data
  • Split context tracking from memory provider into dedicated class
  • Move manifest to common for default values and avoid duplication
    -- Align 'Min' with 'Minimal' value from manifest

Minor:

  • Add title toolbar item for C/C++ file to access open memory inspector
  • Improve debugging experience by using inline source maps
  • Align creation of option enums to use const objects
  • Additionally guard 'body' on debug responses for safety
  • Avoid functional React state update where unnecessary

Fixes #91

How to test

  • Enable auto-refresh in the display options
  • If your DAP provides logging you can watch the request/response pairs in the output

auto-refresh
(Note: Options have been replaced by enum and look a little bit different now)

Review checklist

Reminder for reviewers

@colin-grant-work
Copy link
Contributor

We already have the preference "memory-inspector.refreshOnStop", with which this new preference clashes a bit. I'd suggest that we generalize to something like the autosave preference with various triggers:

image

So that we could have, for now off onStop afterDelay + the delay preference.

@martin-fleck-at
Copy link
Contributor Author

We already have the preference "memory-inspector.refreshOnStop", with which this new preference clashes a bit. [...]
So that we could have, for now off onStop afterDelay + the delay preference.

You definitely raise a very interesting point! My reasoning for a dedicated boolean flag (auto refresh enabled) was that I wanted to give the user the option to disable the auto-refresh without the need to change (and lose) the value to something "invalid" (like -1). So in my mind auto refresh and refresh on stop were definitely not mutually exclusive. I don't know if there really is a use case for onStop + afterDelay but maybe @jreineckearm has some experience with this.

Curiously, the refresh on stop option is one of the few "global" setting that we have, i.e., it cannot be changed on a per-view basis. I wonder how that should play out when we combine it with the auto-refresh which I believe should definitely remain on a per-view basis since multiple open views might otherwise flood the DAP with requests. What would you expect to happen if you switch globally from afterDelay to onStop? Should we automatically disable the checkbox and input field for the auto refresh UI? Should it still be enabled (even the checkbox) but simply not have an effect? I feel like we need to carefully think about that as otherwise this might get confusing.

@martin-fleck-at
Copy link
Contributor Author

(force-pushed to solve merge conflict)

@jreineckearm
Copy link
Contributor

@colin-grant-work , @martin-fleck-at , a couple of very good points. Let's not rush into merging this one before the release and spend a little more though on it. I'll get back to this next week.

@colin-grant-work
Copy link
Contributor

Curiously, the refresh on stop option is one of the few "global" setting that we have, i.e., it cannot be changed on a per-view basis.

I suggest that we allow it - or the new preference that combines it with auto-refresh - to be configured on a per-view basis, as you've done in your PR. Then there can't be a conflict: the options would be encapsulated in a single piece of configuration, and that configuration would configurable in the usual places (globally and per view).

Should we automatically disable the checkbox and input field for the auto refresh UI? Should it still be enabled (even the checkbox) but simply not have an effect?

I think we have a good model for this in the files.autoSave preference: the files.autoSaveDelay takes effect only if files.autoSave is set to afterDelay and is ignored otherwise. I think with a minimum of documentation, it will make sense to users that memory-inspector.autoRefresh.refreshRate only takes effect if memory-inspector.autoRefresh.behavior (or whatever name we choose) is set to afterDelay.

@martin-fleck-at
Copy link
Contributor Author

Curiously, the refresh on stop option is one of the few "global" setting that we have, i.e., it cannot be changed on a per-view basis.

I suggest that we allow it - or the new preference that combines it with auto-refresh - to be configured on a per-view basis, as you've done in your PR. Then there can't be a conflict: the options would be encapsulated in a single piece of configuration, and that configuration would configurable in the usual places (globally and per view).

Ah yes, I somehow misinterpreted your previous statement, having all as a per-view basis that absolutely makes sense. Thank you for the clarification!

@martin-fleck-at
Copy link
Contributor Author

martin-fleck-at commented Mar 25, 2024

Just throwing that one out there as well: Currently we always refresh the memory inspector when the view state (active/focussed or visible) changes and the view is visible. That can result in quite a few refreshes as during debugging the code editor usually gets focus so clicking between the code editor and the memory inspector also triggers a refresh every time. So while we are at it: Should we also introduce a On Focus Change option?

@colin-grant-work
Copy link
Contributor

So while we are at it: Should we also introduce a On Focus Change option?

Or maybe a not on focus change :-). But yes, I think if that's currently happening as part of the default behavior (and if it's something that we can prevent from happening), then it would make sense to include it among the options.

@jreineckearm
Copy link
Contributor

jreineckearm commented Mar 26, 2024

I agree that we should try to combine the different triggers into a single user visible setting. I think however that a simple drop-down may not be sufficient. It would rather need to be a drop-down with checkbox items (or similar) to selectively allow adding/removing triggers.

Currently we always refresh the memory inspector when the view state (active/focussed or visible) changes and the view is visible.

Is this like in always fetching data from the debug adapter? I am not entirely sure that switching tabs should do a full re-read from the target system. Appreciate though that this may be a result of the WebView not preserving the previous state when hidden for example due to a tab-switch. And that also a debug adapter needs to do its part in caching values where sensible.
I believe however that we shouldn't burden the user with this decision. We may need to investigate how we can get cleverer here. Such cleverness (like only loading visible data) may also help to improve performance.

On onStop vs afterDelay: it really depends on how long the refresh period is for afterDelay. A large refresh period may require an additional onStop update. On the other hand, we also should consider if afterDelay should always be active. Or if it should only refresh while a CPU is running. Traditionally, I am more used to only update while CPU is running. But there were asks in the past to also refresh if the CPU is in stopped state but peripherals and their memory-mapped register interfaces still receiving and processing stimuli from the outside world.

@martin-fleck-at
Copy link
Contributor Author

@jreineckearm @colin-grant-work I pushed an update to the current branch and updated the description. We now have the following options:

  • On Stop (previously: 'on' for 'refreshOnStop')
  • On Focus (previously: always implicit on view state change when the view was visible, now on focus)
  • After Delay (new: every n milliseconds we refresh the view)
  • Off (previously: 'off' for 'refreshOnStop')

All options are now view-local, i.e., scoped per view. This made it necessary for new events to be sent to the webview (e.g., stopped on a debug session and view state change of a webview). I think in the long run, it is definitely a good idea to have that information within the webview so we can be more fine-grained and smarter with memory updates. The refactoring of the session tracking could also serve nicely as a basis for #97.

As discussed before those options are currently mutually exclusive (enum) but there might be a use case where you actually want to combine them (i.e., they become toggles) - as mentioned by @jreineckearm. Unfortunately, I lack the real world application for this use case, so I'd highly value your input on how we should best proceed on this. I think having agreement on that before merging the PR is key as we are already breaking the previous refreshOnStop setting by replacing it and for sure we want to avoid too many breaking updates between releases [1].

[1] Note I also found a very small UX issue in the settings sub-group rendering which may also break settings #119.

- Rework 'refreshOnStop' boolean setting to 'autoRefresh' enumerable
-- On Stop (previously: 'on' for 'refreshOnStop'
-- On Focus (previously: always implicit on view state change)
-- After Delay (new)
-- Off (previously: 'off' for 'refreshOnStop')

- On Stop
-- Rework global setting to be local for each Memory Inspector
-- Listen to debug session stopped event and propagate to view

- On Focus
-- Rework implicit refresh update to option in setting
-- Listen to view state changes and propagate to view

- After Delay
-- New option to explicitly define a delay when re-fetching the memory
-- Minimum: 500ms, default: 500, input step size: 250ms

Refactoring:
- Split debug session tracking into dedicated class with session events
-- Convert debug events into session events with additional data
- Split context tracking from memory provider into dedicated class
- Move manifest to common for default values and avoid duplication
-- Align 'Min' with 'Minimal' value from manifest

Minor:
- Add title toolbar item for C/C++ file to access open memory inspector
- Improve debugging experience by using inline source maps
- Align creation of option enums to use const objects
- Additionally guard 'body' on debug responses for safety
- Avoid functional React state update where unnecessary

Fixes eclipse-cdt-cloud#91
@colin-grant-work
Copy link
Contributor

I'll take a look at this tomorrow.

Comment on lines +206 to +211
"editor/title": [
{
"command": "memory-inspector.show",
"group": "navigation",
"when": "memory-inspector.canRead && (resourceLangId === c || resourceLangId === cpp)"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to the functionality elsewhere in the PR? It looks like the aim is to expose the Memory Inspector functionality more prominently, but the resourceLangId is only a so-so proxy for 'is a source file of a source file for a debug session that can read memory'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, it is not strictly part of this PR and so I only mentioned it as a "minor" in the PR description

Add title toolbar item for C/C++ file to access open memory inspector

Mainly because I used it a lot to open a memory inspector from a source file. I understand that the condition is far from complete in this regard but since we are only opening an empty memory inspector, I don't see the harm. However, I understand if you want me to move this out of this PR as wel.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that if I would put it anywhere, I would put it (if possible) on the toolbar of the debug view rather than the file, since the 'memory' and the 'file' don't have a clear correspondence. For now, perhaps separate from this PR so that it can be discussed separately.

package.json Outdated
"Refresh memory views when when debugger stops (e.g. a breakpoint is hit)",
"Memory view data is manually refreshed by user"
"Refresh when the debugger stops (e.g. a breakpoint is hit)",
"Refresh automatically when the view is newly focussed.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Refresh automatically when the view is newly focussed.",
"Refresh automatically each time the view receives focus.",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much better!


activate(context: vscode.ExtensionContext): void {
context.subscriptions.push(
vscode.debug.registerDebugAdapterTrackerFactory('*', this),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the conceptual separation of jobs between this class and the memory provider, and I like the idea of separating the tracking part from the memory provider part, but I'm not thrilled about both of them registering tracker factories on *. It's a tricky problem: I was looking at this plugin earlier today, and on the one hand, I understood the interest in centralization, but on the other hand, you're likely to still to have the same number of handlers doing the same amount of work, with the only difference being which ones VSCode calls directly and which ones it calls indirectly.

I think for now, I'm basically happy with the division of labor you've implemented, but if you see a way to avoid doubling the session trackers, I might like it even better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that is a very good point and I too was a bit frustrated with the lack of data that VS Code gives about the session, so I definitely see how that plugin came to be. While it centralizes the code nicely (which is a big plus imho), I don't think it will really improve performance since the amount of work will be mostly the same.

I'm not sure how we can reduce the number of session trackers without simply introducing a level of indirection which will lead to a very similar result than the plugin you mentioned. However, I'm also not sure how much impact that will have since currently we have two "global" trackers, one for session information and one for memory. If that number grows and the connections between the trackers become somehow difficult to manage, we should definitely re-think this. But if you agree, I'd postpone that for now and maybe track that in a separate issue if we encounter any problems.

WebviewSelection,
WriteMemoryArguments,
WriteMemoryResult,
writeMemoryType,
} from '../common/messaging';
import { getVisibleColumns, WebviewContext } from '../common/webview-context';
import { AddressPaddingOptions, MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types';
import type { MemoryViewSettings, ScrollingBehavior } from '../webview/utils/view-types';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The annotation here is correct, but the fact that it's correct suggests that these types should be moved to a common area so that we don't have to guard against accidentally importing webview code inappropriately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I 100% agree that it is a good idea to actually move the necessary interfaces into a common area, will do, thanks!

Comment on lines +262 to +265
// we are only interested in the events of the active session
if (!event.session?.active) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true? Suppose that a view wanted to be updated on stopped events, and a background session stopped - would we want to ignore that? (though VSCode might automatically foreground the session...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it is true until we properly implement #97. For now I just went with backwards compatibility and we always just show data from the current session. I believe as soon as we properly tie a view to a particular session this condition no longer holds true. In fact, I hope with the additional data that we send to the session as part of this PR, #97 will not gonna be too much work.

debugCapabilities?: DebugProtocol.Capabilities;
clientCapabilities?: DebugProtocol.InitializeRequestArguments;
active?: boolean;
stopped?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is fairly complex. Consider the code here and my issue here. Do we need to track this as state, or, given that we only set it true when we get a corresponding stopped event, could we treat that as punctual information rather than persistent state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good question! If the session would hold it's state itself (like it does in Theia) we wouldn't have the need to externally track that information and we currently use it whenever we want to update the memory views about changes in the session.

You are arguing that it should be a completely separate event that is sent to the memory views without remembering that we were stopped, right? What is the advantage of that versus remembering the state and sending it from there? I see that in your issue you mentioned that sometimes the event might not (yet) be fired so it could happen that we send "out-dated" information. However, since we always send everything we need to know about the session, that would be corrected as soon as the event is actually fired, or am I missing something?

I'm just a bit hesitant about that because the delay might be true for some sessions but it is not true for all session types and all other Debug Views also rely on the event-based mechanism as far as I can see. I'm not too strongly opposed either, I am just not sure I see the harm for now.

Comment on lines +137 to +138
} else if (isDebugEvent('memory', message)) {
this.fireSessionEvent(session, 'memory-written', message.body);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This illustrates some of the difficulty with the division of labor. This isn't really a matter of status, and the rest of the bookkeeping for memory retrieval and writing is handled in the memory provider. I think it would be preferable to keep this in the memory provider, but I don't feel very strongly about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point. However, I'm not sure we should see it strictly as 'state management' vs 'memory management' but rather 'enriched session management' by the tracker (e.g., more events could be added in the future). Similar to the npm package you shared above, I'd almost argue that the session tracker is just one layer in-between and the reason the memory-provider also needs to register as a debug tracker is because we want to allow external contributions.

Comment on lines +188 to +191
if (this.state.autoRefresh === 'After Delay' && this.state.autoRefreshDelay && this.state.autoRefreshDelay > 0) {
// we do not use an interval here as we only want to schedule another refresh AFTER the previous execution AND the delay has passed
// and not strictly every n milliseconds. Even if 'fetchMemory' fails here, we schedule another auto-refresh.
const scheduleRefresh = () => this.fetchMemory().finally(() => this.updateAutoRefresh());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently in fetchMemory, we are not awaiting doFetchMemory so it will return immediately and we won't have waited for the memory to be fetched.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I now return the doFetchMemory so we are actually working with the correct promise!

// may happen when we initialize empty
return;
}
this.doFetchMemory(completeOptions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.doFetchMemory(completeOptions);
await this.doFetchMemory(completeOptions);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose to return the doFetchMemory instead but if you prefer await, I'm also fine with that.

- Improve wording in setting descriptions
- Move shared types into common area instead of using type imports
- Fix 'fetchMemory' not returning the correct promise
@martin-fleck-at
Copy link
Contributor Author

@colin-grant-work Thank you very much for your feedback! It took me a while to get back to this but I pushed an update now and replied to your comments - some of which may still need some broader discussion but I'm not sure they all need to happen as part of this task.

@jreineckearm
Copy link
Contributor

My apologies for only coming back to this now!

I kind of like the idea of condensing the possible triggers to reduce complexity. But I am a little concerned about losing the ability to mix triggers because of flattening them into an enum. Especially if we need to add more in future. I still have no good idea how to realize this instead. My initial reaction would have been to have a kind of dropdown where you can toggle check marks to indicate which triggers are active. But I couldn't find that pattern yet in VS Code configurations. Often, such things seem to be realized by a bunch of checkboxes.... which we worst case have to accept for now. Let me do some more digging if I find something in the design guidelines...

Regarding onFocus: I am curious if we really should expose this as an option. It feels to me like this is a way to work around the fact that a hidden Memory Inspector instance may not be updated when a different trigger occurred. We probably should rather address this or delay such trigger until the window becomes visible next time. Instead of always updating. This can become quite expensive if the debug adapter lacks caching. Especially if "cycling" through views with Ctrl+Tab.

I think afterDelay isn't really the right name for what's happening. The option was supposed to periodically update the window. What's called delay is in fact the delay between periodic updates.

One original thought about the periodic updates was that they are only active while the CPU is running. Given above discussion, this would become the next option to take into account. Assuming we merge this PR first to get periodic updates in. And address the always-periodic-update vs periodic-update-while-running separately. Which almost could be a dropdown with three options by itself (off being the third)....

Will have a chat with Martin about this again tomorrow. But it feels like we need to keep the options separate and revisit if we can handle the onFocus thing better without the need to make it configurable. Let's not forget that we also have the freeze feature to suppress updates if we don't want them.

@jreineckearm
Copy link
Contributor

jreineckearm commented Apr 23, 2024

I had another look at applying the auto-save settings scheme to the window refresh. Unfortunately, there are some differences because of which I don't think they apply one-to-one to the memory window updates:

  • The triggers for auto-save are rather focused on a document/editor (view). They are all tied to a user interaction/modification within that view. If a user stops interaction with the view, one of these trigger options can fire.
  • One could say that the auto-save triggers "build up" on each other. First you have the afterDelay which fires after inactivity of the user for a while. Then you have the onFocusChange which is more deliberate user action to trigger the update. The next level is to require the onWindowChange which is an even strong user action then just changing focus.
  • The triggers (or better events) for window updates and target reads are less controllable by the user. They can happen whenever something happens in the target system. This sometimes is out of the user's control.
  • However, the user may want or even need to configure sensitivity to each of these events.
    • For example the onStop event is something I expect a user to disable very rarely. That's normally the point in time all debug views are updated for the majority of target systems. There may only be very rare cases where you need to suppress this.
    • The onFocus update should normally be inactive, if exposed at all. Window contents should normally be updated in the background. Reason is that, even if cached, refreshing and re-rendering a large window buffer on each focus change is quite expensive. However, most of the times there is no reason for that. Even worse: if a target architecture doesn't like memory reads while the CPU is running, or if memory reads while running return inconsistent data (for example if an MMU is in play), this may do more harm than good. But as I understand, these refreshes do happen at the moment on a focus change? I think this needs change.
    • Looking at periodic updates, you may want to limit that to a certain CPU state (CPU running or not running) which would give the support for this event already three states to consider. See above about supporting memory reads while CPU runs.

I believe we should keep the trigger/event configuration on the level it was before rather than trying to condense them into a flat list.

Should the settings be only global or also local? I believe also local. Even if I am worried about the advanced window settings getting more and more these days. But this could be handled by a redesign of the in-window settings that distinguishes between common settings and expert settings.

@colin-grant-work
Copy link
Contributor

I'm not sure exactly which state you're referring to when you say

I believe we should keep the trigger/event configuration on the level it was before rather than trying to condense them into a flat list.

Do you just mean that you would like to have two configurations (or three, more likely):

  • onStop: boolean
  • afterDelay: boolean
  • delay: number

And not any form of onViewStateChanged?

If so, it sounds like you have strong feelings about it, and I can live with that arrangement.

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.

Allow periodic refresh, for example for architectures that allow debug memory access while CPU is running
3 participants