Skip to content

Commit b7e6225

Browse files
Fixed: Symbol path eval, added "early exit" on timeout and Debug Exit (#856)
* Fixed evaluation of path symbols, and added "early exit" (on 10sec timeout, and on exit) * Added tests * Moved execution-cancellation into it's own module
1 parent c8582bc commit b7e6225

18 files changed

Lines changed: 325 additions & 14 deletions

src/views/component-viewer/component-viewer-instance.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ export class ComponentViewerInstance {
222222
return this._statementEngine;
223223
}
224224

225+
/** Cancel any in-progress executeAll run (e.g. when the debug session ends). */
226+
public cancelExecution(reason: string): void {
227+
this._scvdEvalContext?.cancellation.cancel(reason);
228+
}
229+
225230
private set statementEngine(value: StatementEngine | undefined) {
226231
this._statementEngine = value;
227232
}

src/views/component-viewer/component-viewer-main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,15 @@ export class ComponentViewer {
228228
}
229229

230230
private async handleOnWillStopSession(session: GDBTargetDebugSession): Promise<void> {
231+
// Cancel any in-progress executeAll for the session being stopped.
232+
// JS is single-threaded, so this flag will be picked up at the next
233+
// await point (i.e. the next GDB read) inside any running loop.
234+
for (const instance of this._instances) {
235+
if (instance.sessionId === session.session.id) {
236+
instance.componentViewerInstance.cancelExecution('debug session ended');
237+
}
238+
}
239+
231240
// Clear active session if it is the one being stopped
232241
if (this._activeSession?.session.id === session.session.id) {
233242
this._activeSession = undefined;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Copyright 2025-2026 Arm Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
// generated with AI
17+
18+
/**
19+
* Shared cancellation signal for a single `executeAll` run.
20+
*
21+
* Checked at every loop iteration in `StatementList` and `StatementReadList`
22+
* (after each `await` point) so the engine can be stopped promptly both by
23+
* an external caller (e.g. debug-session end) and by a per-run timeout.
24+
*/
25+
export class ExecutionCancellation {
26+
static readonly DEFAULT_TIMEOUT_MS = 10_000;
27+
28+
private _isCancelled = false;
29+
private _reason: string | undefined;
30+
private _timeoutMs = ExecutionCancellation.DEFAULT_TIMEOUT_MS;
31+
private _deadline = Date.now() + this._timeoutMs;
32+
33+
public get isCancelled(): boolean { return this._isCancelled; }
34+
public get reason(): string | undefined { return this._reason; }
35+
36+
/** Prepare for a new `executeAll` run. Clears any previous cancellation and
37+
* sets a fresh wall-clock deadline. */
38+
public reset(timeoutMs?: number): void {
39+
this._isCancelled = false;
40+
this._reason = undefined;
41+
this._timeoutMs = timeoutMs ?? ExecutionCancellation.DEFAULT_TIMEOUT_MS;
42+
this._deadline = Date.now() + this._timeoutMs;
43+
}
44+
45+
/** Cancel immediately (e.g. session ended). Safe to call from an event handler. */
46+
public cancel(reason: string): void {
47+
if (!this._isCancelled) {
48+
this._isCancelled = true;
49+
this._reason = reason;
50+
}
51+
}
52+
53+
/** Check deadline and auto-cancel on expiry. Returns `true` when cancelled. */
54+
public checkDeadline(): boolean {
55+
if (!this._isCancelled && Date.now() > this._deadline) {
56+
this.cancel('executeAll timeout exceeded');
57+
}
58+
return this._isCancelled;
59+
}
60+
}

src/views/component-viewer/scvd-debug-target.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ function normalize(name: string): string {
4646
return name.trim().toUpperCase();
4747
}
4848

49+
/**
50+
* Convert a symbol path notation to GDB qualified symbol syntax.
51+
* Input: "tasks.c/xSchedulerRunning" → Output: "'tasks.c'::xSchedulerRunning"
52+
* Input: "xSchedulerRunning" → Output: "xSchedulerRunning" (unchanged)
53+
*/
54+
export function toGdbSymbol(symbol: string): string {
55+
const slashIndex = symbol.indexOf('/');
56+
if (slashIndex < 0) {
57+
return symbol;
58+
}
59+
const file = symbol.substring(0, slashIndex);
60+
const name = symbol.substring(slashIndex + 1);
61+
if (!file || !name) {
62+
return symbol;
63+
}
64+
return `'${file}'::${name}`;
65+
}
66+
4967
function toUint32(value: number | bigint): number | bigint {
5068
if (typeof value === 'bigint') {
5169
return value & 0xFFFFFFFFn;
@@ -157,7 +175,8 @@ export class ScvdDebugTarget {
157175
}
158176

159177
const addressInfo = await this.symbolCaches.getAddressWithName(symbol, async (symbolName) => {
160-
const symbolAddressStr = await this.targetAccess.evaluateSymbolAddress(symbolName, 'hover', existCheck);
178+
const gdbSymbol = toGdbSymbol(symbolName);
179+
const symbolAddressStr = await this.targetAccess.evaluateSymbolAddress(gdbSymbol, 'hover', existCheck);
161180
if (symbolAddressStr !== undefined) {
162181
const parsed = parseInt(symbolAddressStr as unknown as string, 16);
163182
if (Number.isFinite(parsed)) {
@@ -228,7 +247,8 @@ export class ScvdDebugTarget {
228247
return undefined;
229248
}
230249
return this.symbolCaches.getArrayCount(symbol, async (symbolName) => {
231-
const result = await this.targetAccess.evaluateNumberOfArrayElements(symbolName);
250+
const gdbSymbol = toGdbSymbol(symbolName);
251+
const result = await this.targetAccess.evaluateNumberOfArrayElements(gdbSymbol);
232252
componentViewerLogger.debug(`get Num Array Elements: ${symbolName} resolved to ${result}`);
233253
return result;
234254
});
@@ -260,7 +280,8 @@ export class ScvdDebugTarget {
260280
}
261281

262282
return this.symbolCaches.getSize(symbol, async (symbolName) => {
263-
const size = await this.targetAccess.evaluateSymbolSize(symbolName);
283+
const gdbSymbol = toGdbSymbol(symbolName);
284+
const size = await this.targetAccess.evaluateSymbolSize(gdbSymbol);
264285
if (typeof size === 'number' && size >= 0) {
265286
componentViewerLogger.debug(`get Symbol Size: ${symbolName} resolved to ${size}`);
266287
return size;

src/views/component-viewer/scvd-eval-context.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ScvdFormatSpecifier } from './model/scvd-format-specifier';
2727
import { ScvdDebugTarget } from './scvd-debug-target';
2828
import { ScvdEvalInterface } from './scvd-eval-interface';
2929
import { parsePerf } from './stats-config';
30+
import { ExecutionCancellation } from './execution-cancellation';
3031

3132
export interface ExecutionContext {
3233
memoryHost: MemoryHost;
@@ -35,6 +36,7 @@ export interface ExecutionContext {
3536
debugTarget: ScvdDebugTarget;
3637
evaluator: Evaluator;
3738
parser: ScvdExpressionParser;
39+
cancellation: ExecutionCancellation;
3840
}
3941

4042
export class ScvdExpressionParser {
@@ -76,6 +78,7 @@ export class ScvdEvalContext {
7678
private _model: ScvdComponentViewer;
7779
private _integerModelKind: IntegerModelKind;
7880
private _parserInterface: ScvdExpressionParser;
81+
private _cancellation = new ExecutionCancellation();
7982

8083
constructor(
8184
model: ScvdComponentViewer
@@ -134,6 +137,10 @@ export class ScvdEvalContext {
134137
this.applyIntegerModel();
135138
}
136139

140+
public get cancellation(): ExecutionCancellation {
141+
return this._cancellation;
142+
}
143+
137144
public getExecutionContext(): ExecutionContext {
138145
return {
139146
memoryHost: this.memoryHost,
@@ -144,6 +151,7 @@ export class ScvdEvalContext {
144151
throw new Error('SCVD EvalContext: DebugTarget not initialized');
145152
})(),
146153
parser: this._parserInterface,
154+
cancellation: this._cancellation,
147155
};
148156
}
149157

src/views/component-viewer/scvd-eval-interface.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr
213213
public async resolveColonPath(_container: RefContainer, parts: string[]): Promise<EvalValue> {
214214
// ColonPath in general expression context (not inside __Offset_of)
215215
// Example: typedef:member evaluates to the offset value itself
216-
// This matches C++ behavior where type:member expressions can be used directly
217216

218217
if (parts.length === 2) {
219218
const [typedefName, memberName] = parts;
@@ -391,11 +390,12 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr
391390
public async __size_of(symbol: string): Promise<number | undefined> {
392391
const perfStartTime = perf?.start() ?? 0;
393392
try {
394-
const sizeBytes = await this._debugTarget.getSymbolSize(symbol);
395-
if (sizeBytes !== undefined) {
396-
return sizeBytes;
397-
}
398-
// Legacy fallback: try array element count if size is unavailable
393+
// __size_of must return the number of elements, not byte count:
394+
// - arrays: element count (sizeof(x) / sizeof(x[0]))
395+
// - scalars: 1 (sizeof(x) / sizeof(x[0]) == 1)
396+
// getNumArrayElements evaluates sizeof(x)/sizeof(x[0]) in GDB, which
397+
// is correct for both cases. getSymbolSize evaluates sizeof(x) which
398+
// returns total byte size and must NOT be used here.
399399
const arrayElements = await this._debugTarget.getNumArrayElements(symbol);
400400
if (arrayElements !== undefined) {
401401
return arrayElements;

src/views/component-viewer/statement-engine/statement-engine.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export class StatementEngine {
4949
private _statementTree: StatementBase | undefined;
5050
private _executionContext: ExecutionContext;
5151

52+
/** Wall-clock budget for a single executeAll run (ms). */
53+
public static readonly EXECUTE_ALL_TIMEOUT_MS = 10_000;
54+
5255
constructor(
5356
model: ScvdComponentViewer,
5457
executionContext: ExecutionContext
@@ -162,6 +165,7 @@ export class StatementEngine {
162165
// Execute all statements in the statement tree.
163166
// This is a placeholder implementation.
164167

168+
this._executionContext.cancellation.reset(StatementEngine.EXECUTE_ALL_TIMEOUT_MS);
165169
this._executionContext.memoryHost.clearNonConst();
166170
const evalHost = this._executionContext.evalContext.data as {
167171
resetEvalCaches?: () => void;

src/views/component-viewer/statement-engine/statement-list.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,14 @@ export class StatementList extends StatementBase {
9999

100100
let loopValue = Number(startValue);
101101
let maximumCount = 100000; // prevent infinite loops
102+
102103
while (maximumCount-- > 0) {
104+
// Check for external cancellation (session ended) or global timeout
105+
if (executionContext.cancellation.checkDeadline()) {
106+
componentViewerLogger.warn(`Line: ${this.line}: <list> "${name}" aborted: ${executionContext.cancellation.reason}`);
107+
break;
108+
}
109+
103110
executionContext.memoryHost.setVariable(name, varTargetSize, loopValue, 0, undefined, varTargetSize); // update loop variable in memory
104111

105112
// while-loop

src/views/component-viewer/statement-engine/statement-readList.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,12 @@ export class StatementReadList extends StatementBase {
295295
let readIdx = 0;
296296
const visitedAddresses = new Set<number | bigint>();
297297
while (nextPtrAddr !== undefined) {
298+
// Check for external cancellation (session ended) or global timeout
299+
if (executionContext.cancellation.checkDeadline()) {
300+
componentViewerLogger.warn(`${this.scvdItem.getLineNoStr()}: Executing "readlist": ${scvdReadList.name} aborted: ${executionContext.cancellation.reason}`);
301+
break;
302+
}
303+
298304
const itemAddress: number | bigint | undefined = typeof nextPtrAddr === 'bigint' ? nextPtrAddr : (nextPtrAddr >>> 0);
299305

300306
// Detect cycles: check if we've visited this address before

src/views/component-viewer/test/integration/scvd-debug-target.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
*/
2121

2222
import { componentViewerLogger } from '../../../../logger';
23-
import { ScvdDebugTarget, gdbNameFor, __test__ } from '../../scvd-debug-target';
23+
import { ScvdDebugTarget, gdbNameFor, toGdbSymbol, __test__ } from '../../scvd-debug-target';
2424
import { TargetReadCache } from '../../target-read-cache';
2525
import type { GDBTargetDebugSession, GDBTargetDebugTracker } from '../../../../debug-session';
2626

@@ -74,6 +74,15 @@ describe('scvd-debug-target', () => {
7474
expect(gdbNameFor('unknown')).toBeUndefined();
7575
});
7676

77+
it('converts symbol path notation to GDB qualified syntax', () => {
78+
expect(toGdbSymbol('tasks.c/xSchedulerRunning')).toBe('\'tasks.c\'::xSchedulerRunning');
79+
expect(toGdbSymbol('main.c/myGlobal')).toBe('\'main.c\'::myGlobal');
80+
expect(toGdbSymbol('xSchedulerRunning')).toBe('xSchedulerRunning');
81+
expect(toGdbSymbol('/noFile')).toBe('/noFile');
82+
expect(toGdbSymbol('noSymbol/')).toBe('noSymbol/');
83+
expect(toGdbSymbol('')).toBe('');
84+
});
85+
7786
it('resolves symbol info when session is active', async () => {
7887
accessMock.evaluateSymbolAddress.mockResolvedValue('0x100');
7988
const tracker = { onContinued: jest.fn(), onStopped: jest.fn() } as unknown as GDBTargetDebugTracker;

0 commit comments

Comments
 (0)