Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/debug-session/gdbtarget-debug-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,22 @@ import * as vscode from 'vscode';
import { DebugProtocol } from '@vscode/debugprotocol';
import { logger } from '../logger';
import { CbuildRunReader } from '../cbuild-run';
import { PeriodicRefreshTimer } from './periodic-refresh-timer';

/**
* GDBTargetDebugSession - Wrapper class to provide session state/details
*/
export class GDBTargetDebugSession {
public readonly refreshTimer: PeriodicRefreshTimer<GDBTargetDebugSession>;
private _cbuildRun: CbuildRunReader|undefined;
private _cbuildRunParsePromise: Promise<void>|undefined;

constructor(public session: vscode.DebugSession) {}
constructor(public session: vscode.DebugSession) {
this.refreshTimer = new PeriodicRefreshTimer(this);
if (this.session.configuration.type === 'gdbtarget') {
this.refreshTimer.enabled = this.session.configuration['auxiliaryGdb'] === true;
}
}

public async getCbuildRun(): Promise<CbuildRunReader|undefined> {
if (!this._cbuildRun) {
Expand Down Expand Up @@ -89,7 +96,7 @@ export class GDBTargetDebugSession {
}

public async readMemoryU32(address: number): Promise<number|undefined> {
const data = await this.readMemory(address, 4);
const data = await this.readMemory(address, 8 /* 4 */); // Temporary workaround for GDB servers with extra caching of 4 byte reads
if (!data) {
return undefined;
}
Expand Down
75 changes: 75 additions & 0 deletions src/debug-session/gdbtarget-debug-tracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,79 @@ describe('GDBTargetDebugTracker', () => {

});

describe('refresh timer management', () => {
let adapterFactory: vscode.DebugAdapterTrackerFactory|undefined;
let gdbSession: GDBTargetDebugSession|undefined = undefined;
let tracker: vscode.DebugAdapterTracker|undefined|null = undefined;

beforeEach(async () => {
adapterFactory = undefined;
gdbSession = undefined;
tracker = undefined;
(vscode.debug.registerDebugAdapterTrackerFactory as jest.Mock).mockImplementation((_debugType: string, factory: vscode.DebugAdapterTrackerFactory): vscode.Disposable => {
adapterFactory = factory;
return { dispose: jest.fn() };
});
debugTracker.activate(contextMock);

debugTracker.onWillStartSession(session => gdbSession = session);
tracker = await adapterFactory!.createDebugAdapterTracker(debugSessionFactory(debugConfigurationFactory()));
tracker!.onWillStartSession!();
// Enable refresh timer
gdbSession!.refreshTimer.enabled = true;
});

const sendContinueEvent = () => {
// Send continued event
const continuedEvent: DebugProtocol.ContinuedEvent = {
event: 'continued',
type: 'event',
seq: 1,
body: {
threadId: 1
}
};
tracker!.onDidSendMessage!(continuedEvent);
};

const sendStoppedEvent = () => {
// Send stopped event
const stoppedEvent: DebugProtocol.StoppedEvent = {
event: 'stopped',
type: 'event',
seq: 1,
body: {
reason: 'step'
}
};
tracker!.onDidSendMessage!(stoppedEvent);
};

it.each([
{ eventName: 'stopped', eventData: { event: 'stopped', type: 'event', seq: 1, body: { reason: 'step' } } },
{ eventName: 'terminated', eventData: { event: 'terminated', type: 'event', seq: 1, body: { } } }, // Does't pass optional 'restart' property
{ eventName: 'exited', eventData: { event: 'exited', type: 'event', seq: 1, body: { exitCode: 0 } } },
])('starts refresh timer on session continued event, and stops on $eventName event', async ({ eventData }) => {
expect(gdbSession).toBeDefined();
sendContinueEvent();
expect(gdbSession!.refreshTimer.isRunning).toBe(true);
// Send event supposed to stop the timer
tracker!.onDidSendMessage!(eventData);
expect(gdbSession!.refreshTimer.isRunning).toBe(false);
});

it.each([
{ requestName: 'next', requestArguments: { command: 'next', type: 'request', seq: 1, arguments: { threadId: 1 } } },
{ requestName: 'stepIn', requestArguments: { command: 'stepIn', type: 'request', seq: 1, arguments: { threadId: 1 } } },
{ requestName: 'stepOut', requestArguments: { command: 'stepOut', type: 'request', seq: 1, arguments: { threadId: 1 } } },
])('starts refresh timer on $requestName request, and stops on stoppedEvent', async ({ requestArguments }) => {
expect(gdbSession).toBeDefined();
tracker!.onWillReceiveMessage!(requestArguments);
expect(gdbSession!.refreshTimer.isRunning).toBe(true);
// Send event supposed to stop the timer
sendStoppedEvent();
expect(gdbSession!.refreshTimer.isRunning).toBe(false);
});
});

});
11 changes: 11 additions & 0 deletions src/debug-session/gdbtarget-debug-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,16 @@ export class GDBTargetDebugTracker {
switch (event.event) {
case 'continued':
this._onContinued.fire({ session: gdbTargetSession, event } as ContinuedEvent);
gdbTargetSession?.refreshTimer.start();
break;
case 'stopped':
gdbTargetSession?.refreshTimer.stop();
this._onStopped.fire({ session: gdbTargetSession, event } as StoppedEvent);
break;
case 'terminated':
case 'exited':
gdbTargetSession?.refreshTimer.stop();
break;
}
}

Expand Down Expand Up @@ -183,6 +189,11 @@ export class GDBTargetDebugTracker {
case 'stackTrace':
this.handleStackTraceRequest(gdbTargetSession, request as DebugProtocol.StackTraceRequest);
break;
case 'next':
case 'stepIn':
case 'stepOut':
gdbTargetSession.refreshTimer.start();
break;
}
}
}
Expand Down
83 changes: 83 additions & 0 deletions src/debug-session/periodic-refresh-timer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Copyright 2025 Arm Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { PeriodicRefreshTimer } from './periodic-refresh-timer';

describe('PeriodicRefreshTimer', () => {
let refreshTimer: PeriodicRefreshTimer<string>;

beforeEach(() => {
refreshTimer = new PeriodicRefreshTimer<string>('test-timer', 10);
});

afterEach(() => {
// Ensure underlying node timer is cleaned up.
refreshTimer.stop();
});

it('returns correct enabled state through getter function', () => {
refreshTimer.enabled = true;
expect(refreshTimer.enabled).toBe(true);
refreshTimer.enabled = false;
expect(refreshTimer.enabled).toBe(false);
});

it('does not start if not enabled', () => {
// Start timer when not enabled
refreshTimer.start();
expect(refreshTimer.isRunning).toBe(false);
});

it('starts if enabled and stops', () => {
// Start timer when enabled
refreshTimer.enabled = true;
refreshTimer.start();
expect(refreshTimer.isRunning).toBe(true);
// Stop timer
refreshTimer.stop();
expect(refreshTimer.isRunning).toBe(false);
});

it('starts if enabled and stops when disabled', () => {
// Start timer when enabled
refreshTimer.enabled = true;
refreshTimer.start();
expect(refreshTimer.isRunning).toBe(true);
// Disable timer
refreshTimer.enabled = false;
expect(refreshTimer.isRunning).toBe(false);
});

it('fires periodic refresh events', async () => {
// Register listener function
const refreshListener = jest.fn();
refreshTimer.onRefresh(refreshListener);
// Start timer when enabled
refreshTimer.enabled = true;
refreshTimer.start();
expect(refreshTimer.isRunning).toBe(true);
// Wait for a few intervals
await new Promise((resolve) => setTimeout(resolve, 25));
// Stop timer
refreshTimer.stop();
// Between one and two calls should have been made.
// Timing sensitive, so only check we are in an expected range rather
// than full accurracy.
expect(refreshListener.mock.calls.length).toBeGreaterThanOrEqual(1);
expect(refreshListener.mock.calls.length).toBeLessThanOrEqual(2);
expect(refreshListener).toHaveBeenCalledWith('test-timer');
});
});
64 changes: 64 additions & 0 deletions src/debug-session/periodic-refresh-timer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright 2025 Arm Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as vscode from 'vscode';

const DEFAULT_REFRESH_INTERVAL = 500;

export class PeriodicRefreshTimer<T> {
private _enabled: boolean = false;
private _timer: NodeJS.Timeout|undefined;

private readonly _onRefresh: vscode.EventEmitter<T> = new vscode.EventEmitter<T>();
public readonly onRefresh: vscode.Event<T> = this._onRefresh.event;

public get enabled(): boolean {
return this._enabled;
}

public set enabled(value: boolean) {
this._enabled = value;
if (!value) {
this.stop();
}
}

public get isRunning(): boolean {
return !!this._timer;
}

constructor(public session: T, public interval: number = DEFAULT_REFRESH_INTERVAL) {
}

public start(): void {
this.stop();
if (!this._enabled) {
return;
}
const doPeriodicRefresh = () => {
this._onRefresh.fire(this.session);
this._timer = setTimeout(doPeriodicRefresh, this.interval);
};
this._timer = setTimeout(doPeriodicRefresh, this.interval);
}

public stop(): void {
if (this._timer) {
clearTimeout(this._timer);
this._timer = undefined;
}
}
}
13 changes: 13 additions & 0 deletions src/features/cpu-states/__snapshots__/cpu-states.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,16 @@ exports[`CpuStates tests with established connection and CPU states supported ca
"",
]
`;

exports[`CpuStates tests with established connection and CPU states supported captures states for periodic refreshs but does not add to history 1`] = `
[
"",
"ΔT CPU States Reason ",
" 0 0 step ",
"",
"",
"ΔT CPU States Reason ",
" 0 0 step ",
"",
]
`;
36 changes: 36 additions & 0 deletions src/features/cpu-states/cpu-states.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,42 @@ describe('CpuStates', () => {
expect(await cpuStates.getActiveTimeString()).toEqual(' 0 states');
});

it('captures states for periodic refreshs but does not add to history', async () => {
const debugConsoleOutput: string[] = [];
(vscode.debug.activeDebugConsole.appendLine as jest.Mock).mockImplementation(line => debugConsoleOutput.push(line));

// Initial stopped event to capture initial states.
(debugSession.customRequest as jest.Mock).mockResolvedValueOnce({
address: '0xE0001004',
data: new Uint8Array([ 0x01, 0x00, 0x00, 0x00 ]).buffer
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(tracker as any)._onStopped.fire(createStoppedEvent(gdbtargetDebugSession, 'step', 0));
await waitForMs(0);

// Capture initial history and clear console while keeping object unchanged for above callback.
cpuStates.showStatesHistory();
const initialHistoryLength = debugConsoleOutput.length;

// Refresh event to add states but not history entry.
(debugSession.customRequest as jest.Mock).mockResolvedValueOnce({
address: '0xE0001004',
data: new Uint8Array([ 0x04, 0x00, 0x00, 0x00 ]).buffer
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(gdbtargetDebugSession.refreshTimer as any)._onRefresh.fire(gdbtargetDebugSession);
await waitForMs(0);

// Diff is 4 - 1 = 3 states, but no new history entry.
expect(cpuStates.activeCpuStates?.states).toEqual(BigInt(3));
expect(await cpuStates.getActiveTimeString()).toEqual(' 3 states');
cpuStates.showStatesHistory();
// Expecting same output length, i.e. no additional lines due to added history entries.
expect(debugConsoleOutput.length).toEqual(2*initialHistoryLength);
// Match snapshot to notice any unexpected changes.
expect(debugConsoleOutput).toMatchSnapshot();
});

it('fires refresh events on active stack item change', async () => {
const delays: number[] = [];
const listener = (delay: number) => delays.push(delay);
Expand Down
Loading