Skip to content

Commit

Permalink
Add auto-refresh capabilities for memory inspector windows
Browse files Browse the repository at this point in the history
- Add two properties and settings for auto-refresh
-- Enablement: To easily enable/disable auto-refresh (default: false)
-- Refresh Rate: Number of milliseconds after the last auto-refresh
--- Minimum: 500ms, default step size: 250ms, default: null (disabled)

Minor:
- Add entry point to memory inspector from C/C++ file for easy access

Fixes eclipse-cdt-cloud#91
  • Loading branch information
martin-fleck-at committed Mar 22, 2024
1 parent d1c825c commit 70acba8
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 7 deletions.
3 changes: 2 additions & 1 deletion media/options-widget.css
Expand Up @@ -120,7 +120,8 @@
gap: 8px;
}

.advanced-options-dropdown {
.advanced-options-dropdown,
.advanced-options-input {
width: 100%;
}

Expand Down
22 changes: 22 additions & 0 deletions package.json
Expand Up @@ -79,6 +79,7 @@
{
"command": "memory-inspector.show",
"title": "Show Memory Inspector",
"icon": "$(file-binary)",
"category": "Memory"
},
{
Expand Down Expand Up @@ -201,6 +202,13 @@
"group": "display@6",
"when": "webviewId === memory-inspector.memory"
}
],
"editor/title": [
{
"command": "memory-inspector.show",
"group": "navigation",
"when": "memory-inspector.canRead && (resourceLangId === c || resourceLangId === cpp)"
}
]
},
"customEditors": [
Expand Down Expand Up @@ -372,6 +380,20 @@
"type": "boolean",
"default": true,
"description": "Display the radix prefix (e.g., '0x' for hexadecimal, '0b' for binary) before memory addresses."
},
"memory-inspector.autoRefresh.enabled": {
"type": "boolean",
"default": false,
"description": "Enable Auto Refresh"
},
"memory-inspector.autoRefresh.refreshRate": {
"type": [
"number",
"undefined"
],
"default": 500,
"minimum": 500,
"description": "The interval between auto-refresh calls. Leave empty to disable auto-refresh."
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/plugin/manifest.ts
Expand Up @@ -51,6 +51,12 @@ export const DEFAULT_GROUPS_PER_ROW: GroupsPerRowOption = 4;
export const CONFIG_ENDIANNESS = 'endianness';
export const DEFAULT_ENDIANNESS = Endianness.Little;

// Auto Refresh
export const CONFIG_AUTO_REFRESH_ENABLED = 'autoRefresh.enabled';
export const DEFAULT_AUTO_REFRESH_ENABLED = false;
export const CONFIG_AUTO_REFRESH_RATE = 'autoRefresh.refreshRate';
export const DEFAULT_AUTO_REFRESH_RATE = 500;

// Scroll
export const CONFIG_SCROLLING_BEHAVIOR = 'scrollingBehavior';
export const DEFAULT_SCROLLING_BEHAVIOR = 'Paginate';
Expand Down
7 changes: 5 additions & 2 deletions src/plugin/memory-webview-main.ts
Expand Up @@ -205,8 +205,8 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider {

const disposables = [
this.messenger.onNotification(readyType, () => {
this.setInitialSettings(participant, panel.title);
this.setSessionContext(participant, this.memoryProvider.createContext());
this.setInitialSettings(participant, panel.title);
this.refresh(participant, options);
}, { sender: participant }),
this.messenger.onRequest(setOptionsType, o => {
Expand Down Expand Up @@ -266,9 +266,12 @@ export class MemoryWebview implements vscode.CustomReadonlyEditorProvider {
const addressPadding = AddressPaddingOptions[memoryInspectorConfiguration.get(manifest.CONFIG_ADDRESS_PADDING, manifest.DEFAULT_ADDRESS_PADDING)];
const addressRadix = memoryInspectorConfiguration.get<number>(manifest.CONFIG_ADDRESS_RADIX, manifest.DEFAULT_ADDRESS_RADIX);
const showRadixPrefix = memoryInspectorConfiguration.get<boolean>(manifest.CONFIG_SHOW_RADIX_PREFIX, manifest.DEFAULT_SHOW_RADIX_PREFIX);
const autoRefreshEnabled = memoryInspectorConfiguration.get<boolean>(manifest.CONFIG_AUTO_REFRESH_ENABLED, manifest.DEFAULT_AUTO_REFRESH_ENABLED);
const autoRefreshRate = memoryInspectorConfiguration.get<number | undefined>(manifest.CONFIG_AUTO_REFRESH_RATE, manifest.DEFAULT_AUTO_REFRESH_RATE);
return {
messageParticipant, title, bytesPerWord, wordsPerGroup, groupsPerRow,
endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix
endianness, scrollingBehavior, visibleColumns, addressPadding, addressRadix, showRadixPrefix,
autoRefreshRate, autoRefreshEnabled
};
}

Expand Down
4 changes: 4 additions & 0 deletions src/webview/components/memory-widget.tsx
Expand Up @@ -90,6 +90,8 @@ export class MemoryWidget extends React.Component<MemoryWidgetProps, MemoryWidge
bytesPerWord={this.props.bytesPerWord}
wordsPerGroup={this.props.wordsPerGroup}
groupsPerRow={this.props.groupsPerRow}
autoRefreshEnabled={this.props.autoRefreshEnabled}
autoRefreshRate={this.props.autoRefreshRate}
updateMemoryState={this.props.updateMemoryState}
updateRenderOptions={this.props.updateMemoryDisplayConfiguration}
resetRenderOptions={this.props.resetMemoryDisplayConfiguration}
Expand All @@ -115,6 +117,8 @@ export class MemoryWidget extends React.Component<MemoryWidgetProps, MemoryWidge
bytesPerWord={this.props.bytesPerWord}
wordsPerGroup={this.props.wordsPerGroup}
groupsPerRow={this.props.groupsPerRow}
autoRefreshEnabled={this.props.autoRefreshEnabled}
autoRefreshRate={this.props.autoRefreshRate}
effectiveAddressLength={this.props.effectiveAddressLength}
fetchMemory={this.props.fetchMemory}
isMemoryFetching={this.props.isMemoryFetching}
Expand Down
47 changes: 46 additions & 1 deletion src/webview/components/options-widget.tsx
Expand Up @@ -18,6 +18,7 @@ import { Formik, FormikConfig, FormikErrors, FormikProps } from 'formik';
import { Button } from 'primereact/button';
import { Checkbox } from 'primereact/checkbox';
import { Dropdown, DropdownChangeEvent } from 'primereact/dropdown';
import { InputNumber } from 'primereact/inputnumber';
import { InputText } from 'primereact/inputtext';
import { OverlayPanel } from 'primereact/overlaypanel';
import { classNames } from 'primereact/utils';
Expand Down Expand Up @@ -52,6 +53,7 @@ export interface OptionsWidgetProps

interface OptionsWidgetState {
isTitleEditing: boolean;
isEnablingAutoRefresh: boolean;
}

const enum InputId {
Expand All @@ -65,6 +67,8 @@ const enum InputId {
AddressPadding = 'address-padding',
AddressRadix = 'address-radix',
ShowRadixPrefix = 'show-radix-prefix',
AutoRefreshEnabled = 'auto-refresh-enabled',
AutoRefreshRate = 'auto-refresh-rate',
}

interface OptionsForm {
Expand All @@ -77,6 +81,7 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, OptionsWi
protected formConfig: FormikConfig<OptionsForm>;
protected extendedOptions = React.createRef<OverlayPanel>();
protected labelEditInput = React.createRef<HTMLInputElement>();
protected refreshRateInput = React.createRef<InputNumber>();
protected coreOptionsDiv = React.createRef<HTMLDivElement>();
protected optionsMenuContext = createSectionVscodeContext('optionsWidget');
protected advancedOptionsContext = createSectionVscodeContext('advancedOptionsOverlay');
Expand All @@ -100,7 +105,7 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, OptionsWi
this.props.fetchMemory(this.props.configuredReadArguments);
},
};
this.state = { isTitleEditing: false };
this.state = { isTitleEditing: false, isEnablingAutoRefresh: false };
}

protected validate = (values: OptionsForm) => {
Expand All @@ -125,6 +130,11 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, OptionsWi
this.labelEditInput.current?.focus();
this.labelEditInput.current?.select();
}
if (!prevState.isEnablingAutoRefresh && this.state.isEnablingAutoRefresh) {
const input = this.refreshRateInput.current?.getElement().getElementsByTagName('input')[0];
input?.focus();
input?.select();
}
}

override render(): React.ReactNode {
Expand Down Expand Up @@ -397,6 +407,29 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, OptionsWi
/>
<label htmlFor={InputId.ShowRadixPrefix} className='ml-2'>Display Radix Prefix</label>
</div>

<h2>Auto-Refresh</h2>
<div className='flex align-items-center'>
<Checkbox
id={InputId.AutoRefreshEnabled}
onChange={this.handleAdvancedOptionsDropdownChange}
checked={this.props.autoRefreshEnabled}
/>
<InputNumber
id={InputId.AutoRefreshRate}
ref={this.refreshRateInput}
disabled={!this.props.autoRefreshEnabled}
value={this.props.autoRefreshRate}
placeholder='Refresh Rate in Milliseconds'
inputClassName='ml-2 advanced-options-input'
min={500}
step={250}
maxFractionDigits={0}
useGrouping={false}
onBlur={this.handleRefreshRateChange}
onKeyDown={this.handleRefreshRateChange} />
<label htmlFor={InputId.AutoRefreshRate} className='ml-2'>ms</label>
</div>
</div>
</OverlayPanel>
</div>
Expand Down Expand Up @@ -487,6 +520,10 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, OptionsWi
case InputId.ShowRadixPrefix:
this.props.updateRenderOptions({ showRadixPrefix: !!event.target.checked });
break;
case InputId.AutoRefreshEnabled:
this.props.updateRenderOptions({ autoRefreshEnabled: !!event.target.checked });
this.setState({ isEnablingAutoRefresh: !!event.target.checked });
break;
default: {
throw new Error(`${id} can not be handled. Did you call the correct method?`);
}
Expand All @@ -502,6 +539,14 @@ export class OptionsWidget extends React.Component<OptionsWidgetProps, OptionsWi
}
}

protected handleRefreshRateChange: (event: React.FocusEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>) => void = event => this.doHandleRefreshRateChange(event);
doHandleRefreshRateChange(event: React.FocusEvent<HTMLInputElement> | React.KeyboardEvent<HTMLInputElement>): void {
if (!('key' in event) || event.key === 'Enter') {
const autoRefreshRate = tryToNumber(event.currentTarget.value);
this.props.updateRenderOptions({ autoRefreshRate });
}
}

protected handleResetAdvancedOptions: MouseEventHandler<HTMLButtonElement> | undefined = () => this.props.resetRenderOptions();

protected enableTitleEditing = () => this.doEnableTitleEditing();
Expand Down
33 changes: 30 additions & 3 deletions src/webview/memory-webview-view.tsx
Expand Up @@ -80,6 +80,8 @@ const MEMORY_DISPLAY_CONFIGURATION_DEFAULTS: MemoryDisplayConfiguration = {
addressPadding: 'Min',
addressRadix: 16,
showRadixPrefix: true,
autoRefreshEnabled: false,
autoRefreshRate: undefined
};

const DEFAULT_READ_ARGUMENTS: Required<ReadMemoryArguments> = {
Expand All @@ -90,6 +92,7 @@ const DEFAULT_READ_ARGUMENTS: Required<ReadMemoryArguments> = {

class App extends React.Component<{}, MemoryAppState> {
protected memoryWidget = React.createRef<MemoryWidget>();
protected refreshTimer?: NodeJS.Timeout | number;

public constructor(props: {}) {
super(props);
Expand Down Expand Up @@ -135,6 +138,7 @@ class App extends React.Component<{}, MemoryAppState> {
messenger.onRequest(getWebviewSelectionType, () => this.getWebviewSelection());
messenger.onNotification(showAdvancedOptionsType, () => this.showAdvancedOptions());
messenger.sendNotification(readyType, HOST_EXTENSION, undefined);
this.updateAutoRefresh();
}

public componentDidUpdate(_: {}, prevState: MemoryAppState): void {
Expand All @@ -147,9 +151,27 @@ class App extends React.Component<{}, MemoryAppState> {
this.setState({ effectiveAddressLength });
}
}
if (this.state.autoRefreshEnabled !== prevState.autoRefreshEnabled || this.state.autoRefreshRate !== prevState.autoRefreshRate) {
this.updateAutoRefresh();
}
hoverService.setMemoryState(this.state);
}

componentWillUnmount(): void {
clearTimeout(this.refreshTimer);
}

protected updateAutoRefresh = (): void => {
clearTimeout(this.refreshTimer);

if (this.state.autoRefreshEnabled && this.state.autoRefreshRate && this.state.autoRefreshRate > 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());
this.refreshTimer = setTimeout(scheduleRefresh, this.state.autoRefreshRate);
}
};

// use a slight debounce as the same event may come in short succession
protected memoryWritten = debounce((writtenMemory: WrittenMemory): void => {
if (!this.state.memory) {
Expand Down Expand Up @@ -222,6 +244,8 @@ class App extends React.Component<{}, MemoryAppState> {
showRadixPrefix={this.state.showRadixPrefix}
storeMemory={this.storeMemory}
applyMemory={this.applyMemory}
autoRefreshEnabled={this.state.autoRefreshEnabled}
autoRefreshRate={this.state.autoRefreshRate}
/>
</PrimeReactProvider>;
}
Expand All @@ -246,16 +270,19 @@ class App extends React.Component<{}, MemoryAppState> {

protected fetchMemory = async (partialOptions?: MemoryOptions): Promise<void> => this.doFetchMemory(partialOptions);
protected async doFetchMemory(partialOptions?: MemoryOptions): Promise<void> {
if (this.state.isFrozen) {
if (this.state.isFrozen || !this.state.sessionContext.canRead) {
return;
}
this.setState(prev => ({ ...prev, isMemoryFetching: true }));
const completeOptions = {
memoryReference: partialOptions?.memoryReference || this.state.activeReadArguments.memoryReference,
offset: partialOptions?.offset ?? this.state.activeReadArguments.offset,
count: partialOptions?.count ?? this.state.activeReadArguments.count
};

if (completeOptions.memoryReference === '') {
// may happen when we initialize empty
return;
}
this.setState(prev => ({ ...prev, isMemoryFetching: true }));
try {
const response = await messenger.sendRequest(readMemoryType, HOST_EXTENSION, completeOptions);
await Promise.all(Array.from(
Expand Down
2 changes: 2 additions & 0 deletions src/webview/utils/view-types.ts
Expand Up @@ -89,6 +89,8 @@ export interface MemoryDisplayConfiguration {
addressPadding: AddressPadding;
addressRadix: Radix;
showRadixPrefix: boolean;
autoRefreshEnabled: boolean;
autoRefreshRate?: number;
}
export type ScrollingBehavior = 'Paginate' | 'Grow' | 'Auto-Append';

Expand Down

0 comments on commit 70acba8

Please sign in to comment.