rtos-views
Version:
RTOS views for microcontrollers
527 lines (503 loc) • 24.5 kB
text/typescript
/* eslint-disable @typescript-eslint/naming-convention */
import * as vscode from 'vscode';
import { DebugProtocol } from '@vscode/debugprotocol';
import * as RTOSCommon from './rtos-common';
// We will have two rows of headers for FreeRTOS and the table below describes
// the columns headers for the two rows and the width of each column as a fraction
// of the overall space.
enum DisplayFields {
ID,
Address,
TaskName,
Status,
Priority,
StackStart,
StackTop,
StackEnd,
StackSize,
StackUsed,
StackFree,
StackPeak,
Runtime
}
const numType = RTOSCommon.ColTypeEnum.colTypeNumeric;
const FreeRTOSItems: { [key: string]: RTOSCommon.DisplayColumnItem } = {};
FreeRTOSItems[DisplayFields[DisplayFields.ID]] = { width: 1, headerRow1: '', headerRow2: 'ID', colType: numType };
FreeRTOSItems[DisplayFields[DisplayFields.Address]] = {
width: 3,
headerRow1: 'Thread',
headerRow2: 'Address',
colGapBefore: 1
};
FreeRTOSItems[DisplayFields[DisplayFields.TaskName]] = { width: 4, headerRow1: '', headerRow2: 'Task Name' };
FreeRTOSItems[DisplayFields[DisplayFields.Status]] = { width: 3, headerRow1: '', headerRow2: 'Status' };
FreeRTOSItems[DisplayFields[DisplayFields.Priority]] = {
width: 1.5,
headerRow1: 'Prio',
headerRow2: 'rity',
colType: numType
};
FreeRTOSItems[DisplayFields[DisplayFields.StackStart]] = {
width: 3,
headerRow1: 'Stack',
headerRow2: 'Start',
colType: RTOSCommon.ColTypeEnum.colTypeLink,
colGapBefore: 1
};
FreeRTOSItems[DisplayFields[DisplayFields.StackTop]] = { width: 3, headerRow1: 'Stack', headerRow2: 'Top' };
FreeRTOSItems[DisplayFields[DisplayFields.StackEnd]] = { width: 3, headerRow1: 'Stack', headerRow2: 'End' };
FreeRTOSItems[DisplayFields[DisplayFields.StackSize]] = {
width: 2,
headerRow1: 'Stack',
headerRow2: 'Size',
colType: numType
};
FreeRTOSItems[DisplayFields[DisplayFields.StackUsed]] = {
width: 2,
headerRow1: 'Stack',
headerRow2: 'Used',
colType: numType
};
FreeRTOSItems[DisplayFields[DisplayFields.StackFree]] = {
width: 2,
headerRow1: 'Stack',
headerRow2: 'Free',
colType: numType
};
FreeRTOSItems[DisplayFields[DisplayFields.StackPeak]] = {
width: 2,
headerRow1: 'Stack',
headerRow2: 'Peak',
colType: numType
};
FreeRTOSItems[DisplayFields[DisplayFields.Runtime]] = {
width: 2,
headerRow1: '',
headerRow2: 'Runtime',
colType: numType
};
const DisplayFieldNames: string[] = Object.keys(FreeRTOSItems);
export class RTOSFreeRTOS extends RTOSCommon.RTOSBase {
// We keep a bunch of variable references (essentially pointers) that we can use to query for values
// Since all of them are global variable, we only need to create them once per session. These are
// similar to Watch/Hover variables
private uxCurrentNumberOfTasks: RTOSCommon.RTOSVarHelperMaybe;
private uxCurrentNumberOfTasksVal = 0;
private pxReadyTasksLists: RTOSCommon.RTOSVarHelperMaybe;
private xDelayedTaskList1: RTOSCommon.RTOSVarHelperMaybe;
private xDelayedTaskList2: RTOSCommon.RTOSVarHelperMaybe;
private xPendingReadyList: RTOSCommon.RTOSVarHelperMaybe;
private pxCurrentTCB: RTOSCommon.RTOSVarHelperMaybe;
private xSuspendedTaskList: RTOSCommon.RTOSVarHelperMaybe;
private xTasksWaitingTermination: RTOSCommon.RTOSVarHelperMaybe;
private ulTotalRunTime: RTOSCommon.RTOSVarHelperMaybe;
private ulTotalRunTimeVal = 0;
private stale = true;
private curThreadAddr = 0;
private foundThreads: RTOSCommon.RTOSThreadInfo[] = [];
private finalThreads: RTOSCommon.RTOSThreadInfo[] = [];
private timeInfo = '';
private readonly maxThreads = 1024;
private helpHtml: string | undefined;
// Need to do a TON of testing for stack growing the other direction
private stackIncrements = -1;
constructor(public session: vscode.DebugSession) {
super(session, 'FreeRTOS');
}
public async tryDetect(useFrameId: number): Promise<RTOSCommon.RTOSBase> {
this.progStatus = 'stopped';
try {
if (this.status === 'none') {
// We only get references to all the interesting variables. Note that any one of the following can fail
// and the caller may try again until we know that it definitely passed or failed. Note that while we
// re-try everything, we do remember what already had succeeded and don't waste time trying again. That
// is how this.getVarIfEmpty() works
this.uxCurrentNumberOfTasks = await this.getVarIfEmpty(
this.uxCurrentNumberOfTasks,
useFrameId,
'uxCurrentNumberOfTasks'
);
this.pxReadyTasksLists = await this.getVarIfEmpty(
this.pxReadyTasksLists,
useFrameId,
'pxReadyTasksLists'
);
this.xDelayedTaskList1 = await this.getVarIfEmpty(
this.xDelayedTaskList1,
useFrameId,
'xDelayedTaskList1'
);
this.xDelayedTaskList2 = await this.getVarIfEmpty(
this.xDelayedTaskList2,
useFrameId,
'xDelayedTaskList2'
);
this.xPendingReadyList = await this.getVarIfEmpty(
this.xPendingReadyList,
useFrameId,
'xPendingReadyList'
);
this.pxCurrentTCB = await this.getVarIfEmpty(this.pxCurrentTCB, useFrameId, 'pxCurrentTCB');
this.xSuspendedTaskList = await this.getVarIfEmpty(
this.xSuspendedTaskList,
useFrameId,
'xSuspendedTaskList',
true
);
this.xTasksWaitingTermination = await this.getVarIfEmpty(
this.xTasksWaitingTermination,
useFrameId,
'xTasksWaitingTermination',
true
);
this.ulTotalRunTime = await this.getVarIfEmpty(this.ulTotalRunTime, useFrameId, 'ulTotalRunTime', true);
this.status = 'initialized';
}
return this;
} catch (e) {
if (e instanceof RTOSCommon.ShouldRetry) {
console.error(e.message);
} else {
this.status = 'failed';
this.failedWhy = e;
}
return this;
}
}
protected createHmlHelp(th: RTOSCommon.RTOSThreadInfo, thInfo: RTOSCommon.RTOSStrToValueMap) {
function strong(s: string) {
return `<strong>${s}</strong>`;
}
if (this.helpHtml === undefined) {
this.helpHtml = '';
try {
let ret = '';
if (!thInfo['uxTCBNumber'].val) {
ret += `Thread ID missing......: Enable macro ${strong('configUSE_TRACE_FACILITY')} in FW<br>`;
}
if (!th.stackInfo.stackEnd) {
ret += `Stack End missing......: Enable macro
${strong('configRECORD_STACK_HIGH_ADDRESS')} in FW<br>`;
}
if (thInfo['pcTaskName'].val === '[0]' || thInfo['pcTaskName'].val === '[1]') {
ret += `Thread Name missing....: Set macro
${strong('configMAX_TASK_NAME_LEN')} to something greater than 1 in FW<br>`;
}
if (!this.ulTotalRunTime) {
ret += /*html*/ `<br>Missing Runtime stats..:<br>
/* To get runtime stats, modify the following macro in FreeRTOSConfig.h */<br>
#define ${strong('configGENERATE_RUN_TIME_STATS')} 1 /* 1: generate runtime statistics; 0: no runtime statistics */<br>
/* Also, add the following two macros to provide a high speed counter -- something at least 10x faster than<br>
** your RTOS scheduler tick. One strategy could be to use a HW counter and sample its current value when needed<br>
*/<br>
#define ${strong('portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()')} /* Define this to initialize your timer/counter */<br>
#define ${strong('portGET_RUN_TIME_COUNTER_VALUE()')}${' '.repeat(9)}
/* Define this to sample the timer/counter */<br>
`;
}
if (ret) {
ret +=
'<br>Note: Make sure you consider the performance/resources impact for any changes to your FW.<br>\n';
ret =
'<button class="help-button">Hints to get more out of the FreeRTOS viewer</button>\n' +
`<div class="help"><p>\n${ret}\n</p></div>\n`;
this.helpHtml = ret;
}
} catch (e) {
console.log(e);
}
}
}
private updateCurrentThreadAddr(frameId: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.pxCurrentTCB?.getValue(frameId).then(
(ret) => {
this.curThreadAddr = parseInt(ret || '');
resolve();
},
(e) => {
reject(e);
}
);
});
}
private updateTotalRuntime(frameId: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!this.ulTotalRunTime) {
resolve();
return;
}
this.ulTotalRunTime.getValue(frameId).then(
(ret) => {
this.ulTotalRunTimeVal = parseInt(ret || '');
resolve();
},
(e) => {
reject(e);
}
);
});
}
public refresh(frameId: number): Promise<void> {
return new Promise<void>((resolve) => {
if (this.progStatus !== 'stopped') {
resolve();
return;
}
const timer = new RTOSCommon.HrTimer();
this.stale = true;
this.timeInfo = new Date().toISOString();
// uxCurrentNumberOfTasks can go invalid anytime. Like when a reset/restart happens
this.uxCurrentNumberOfTasksVal = Number.MAX_SAFE_INTEGER;
this.foundThreads = [];
this.uxCurrentNumberOfTasks?.getValue(frameId).then(
async (str) => {
try {
this.uxCurrentNumberOfTasksVal = str ? parseInt(str) : Number.MAX_SAFE_INTEGER;
if (this.uxCurrentNumberOfTasksVal > 0 && this.uxCurrentNumberOfTasksVal <= this.maxThreads) {
let promises = [];
const ary = await this.pxReadyTasksLists?.getVarChildren(frameId);
for (const v of ary || []) {
promises.push(this.getThreadInfo(v.variablesReference, 'READY', frameId));
}
promises.push(this.updateCurrentThreadAddr(frameId));
promises.push(this.updateTotalRuntime(frameId));
// Update in bulk, but broken up into three chunks, if the number of threads are already fulfilled, then
// not much happens
await Promise.all(promises);
promises = [];
promises.push(this.getThreadInfo(this.xDelayedTaskList1, 'BLOCKED', frameId));
promises.push(this.getThreadInfo(this.xDelayedTaskList2, 'BLOCKED', frameId));
promises.push(this.getThreadInfo(this.xPendingReadyList, 'PENDING', frameId));
await Promise.all(promises);
promises = [];
promises.push(this.getThreadInfo(this.xSuspendedTaskList, 'SUSPENDED', frameId));
promises.push(this.getThreadInfo(this.xTasksWaitingTermination, 'TERMINATED', frameId));
await Promise.all(promises);
promises = [];
if (this.foundThreads.length > 0) {
const th = this.foundThreads[0];
if (th.display['ID'].text !== '??') {
this.foundThreads.sort(
(a, b) => parseInt(a.display['ID'].text) - parseInt(b.display['ID'].text)
);
} else {
this.foundThreads.sort(
(a, b) =>
parseInt(a.display['Address'].text) - parseInt(b.display['Address'].text)
);
}
}
this.finalThreads = [...this.foundThreads];
// console.table(this.finalThreads);
} else {
this.finalThreads = [];
}
this.stale = false;
this.timeInfo += ' in ' + timer.deltaMs() + ' ms';
resolve();
} catch (e) {
resolve();
console.error('FreeRTOS.refresh() failed: ', e);
}
},
(reason) => {
resolve();
console.error('FreeRTOS.refresh() failed: ', reason);
}
);
});
}
private getThreadInfo(
varRef: RTOSCommon.RTOSVarHelperMaybe | number,
state: string,
frameId: number
): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (
!varRef ||
(typeof varRef !== 'number' && !varRef.varReference) ||
this.foundThreads.length >= this.uxCurrentNumberOfTasksVal
) {
resolve();
return;
}
if (this.progStatus !== 'stopped') {
reject(new Error('Busy'));
return;
}
let promise;
if (typeof varRef !== 'number') {
promise = varRef.getVarChildrenObj(frameId);
} else {
promise = this.getVarChildrenObj(varRef, 'task-list');
}
promise.then(
async (obj: any) => {
const threadCount = parseInt(obj['uxNumberOfItems']?.val);
const listEndRef = obj['xListEnd']?.ref;
if (threadCount <= 0 || !listEndRef) {
resolve();
return;
}
try {
const listEndObj = (await this.getVarChildrenObj(listEndRef, 'xListEnd')) || {};
let curRef = listEndObj['pxPrevious']?.ref;
for (let thIx = 0; thIx < threadCount; thIx++) {
const element = (await this.getVarChildrenObj(curRef, 'pxPrevious')) || {};
const threadId = parseInt(element['pvOwner']?.val);
const thInfo = await this.getExprValChildrenObj(
`((TCB_t*)${RTOSCommon.hexFormat(threadId)})`,
frameId
);
const threadRunning = threadId === this.curThreadAddr;
const tmpThName =
(await this.getExprVal('(char *)' + thInfo['pcTaskName']?.exp, frameId)) || '';
const match = tmpThName.match(/"([^*]*)"$/);
const thName = match ? match[1] : tmpThName;
const stackInfo = await this.getStackInfo(thInfo, 0xA5);
// This is the order we want stuff in
const display: { [key: string]: RTOSCommon.DisplayRowItem } = {};
const mySetter = (x: DisplayFields, text: string, value?: any) => {
display[DisplayFieldNames[x]] = { text, value };
};
mySetter(DisplayFields.ID, thInfo['uxTCBNumber']?.val || '??');
mySetter(DisplayFields.Address, RTOSCommon.hexFormat(threadId));
mySetter(DisplayFields.TaskName, thName);
mySetter(DisplayFields.Status, threadRunning ? 'RUNNING' : state);
mySetter(DisplayFields.StackStart, RTOSCommon.hexFormat(stackInfo.stackStart));
mySetter(DisplayFields.StackTop, RTOSCommon.hexFormat(stackInfo.stackTop));
mySetter(
DisplayFields.StackEnd,
stackInfo.stackEnd ? RTOSCommon.hexFormat(stackInfo.stackEnd) : '0x????????'
);
if (thInfo['uxBasePriority']?.val) {
mySetter(
DisplayFields.Priority,
`${thInfo['uxPriority']?.val},${thInfo['uxBasePriority']?.val}`
);
} else {
mySetter(DisplayFields.Priority, `${thInfo['uxPriority']?.val}`);
}
const func = (x: any) => (x === undefined ? '???' : x.toString());
mySetter(DisplayFields.StackSize, func(stackInfo.stackSize));
mySetter(DisplayFields.StackUsed, func(stackInfo.stackUsed));
mySetter(DisplayFields.StackFree, func(stackInfo.stackFree));
mySetter(DisplayFields.StackPeak, func(stackInfo.stackPeak));
if (thInfo['ulRunTimeCounter']?.val && this.ulTotalRunTimeVal) {
const tmp = (parseInt(thInfo['ulRunTimeCounter']?.val) / this.ulTotalRunTimeVal) * 100;
mySetter(DisplayFields.Runtime, tmp.toFixed(2).padStart(5, '0') + '%');
} else {
mySetter(DisplayFields.Runtime, '??.??%');
}
const thread: RTOSCommon.RTOSThreadInfo = {
display: display,
stackInfo: stackInfo,
running: threadRunning
};
this.foundThreads.push(thread);
this.createHmlHelp(thread, thInfo);
curRef = element['pxPrevious']?.ref;
}
resolve();
} catch (e) {
console.log('FreeRTOS read thread info error', e);
}
},
(e) => {
reject(e);
}
);
});
}
protected async getStackInfo(thInfo: RTOSCommon.RTOSStrToValueMap, waterMark: number) {
const pxStack = thInfo['pxStack']?.val;
const pxTopOfStack = thInfo['pxTopOfStack']?.val;
const pxEndOfStack = thInfo['pxEndOfStack']?.val;
const stackInfo: RTOSCommon.RTOSStackInfo = {
stackStart: parseInt(pxStack),
stackTop: parseInt(pxTopOfStack)
};
const stackDelta = Math.abs(stackInfo.stackTop - stackInfo.stackStart);
if (this.stackIncrements < 0) {
stackInfo.stackFree = stackDelta;
} else {
stackInfo.stackUsed = stackDelta;
}
if (pxEndOfStack) {
stackInfo.stackEnd = parseInt(pxEndOfStack);
stackInfo.stackSize = Math.abs(stackInfo.stackStart - stackInfo.stackEnd);
if (this.stackIncrements < 0) {
stackInfo.stackUsed = stackInfo.stackSize - stackDelta;
} else {
stackInfo.stackFree = stackInfo.stackSize - stackDelta;
}
if (!RTOSCommon.RTOSBase.disableStackPeaks) {
const memArg: DebugProtocol.ReadMemoryArguments = {
memoryReference: RTOSCommon.hexFormat(Math.min(stackInfo.stackStart, stackInfo.stackEnd)),
count: stackInfo.stackSize
};
try {
const stackData = await this.session.customRequest('readMemory', memArg);
const buf = Buffer.from(stackData.data, 'base64');
stackInfo.bytes = new Uint8Array(buf);
let start = this.stackIncrements < 0 ? 0 : stackInfo.bytes.length - 1;
const end = this.stackIncrements < 0 ? stackInfo.bytes.length : -1;
let peak = 0;
while (start !== end) {
if (stackInfo.bytes[start] !== waterMark) {
break;
}
start -= this.stackIncrements;
peak++;
}
stackInfo.stackPeak = stackInfo.stackSize - peak;
} catch (e) {
console.log(e);
}
}
}
return stackInfo;
}
public lastValidHtmlContent: RTOSCommon.HtmlInfo = { html: '', css: '' };
public getHTML(): RTOSCommon.HtmlInfo {
const htmlContent: RTOSCommon.HtmlInfo = { html: '', css: '' };
// WARNING: This stuff is super fragile. Once we know how this works, then we should refactor this
let msg = '';
if (this.status === 'none') {
htmlContent.html = '<p>RTOS not yet fully initialized. Will occur next time program pauses</p>\n';
return htmlContent;
} else if (this.stale) {
const lastHtmlInfo = this.lastValidHtmlContent;
if (this.uxCurrentNumberOfTasksVal === Number.MAX_SAFE_INTEGER) {
msg = ' Could not read "uxCurrentNumberOfTasks". Perhaps program is busy or did not stop long enough';
lastHtmlInfo.html = '';
lastHtmlInfo.css = '';
} else if (this.uxCurrentNumberOfTasksVal > this.maxThreads) {
msg = ` FreeRTOS variable uxCurrentNumberOfTasks = ${this.uxCurrentNumberOfTasksVal} seems invalid`;
lastHtmlInfo.html = '';
lastHtmlInfo.css = '';
} else if (lastHtmlInfo.html) {
// TODO check if this condition is ok
msg = ' Following info from last query may be stale.';
}
htmlContent.html = `<p>Unable to collect full RTOS information.${msg}</p>\n` + lastHtmlInfo.html;
htmlContent.css = lastHtmlInfo.css;
return htmlContent;
} else if (
this.uxCurrentNumberOfTasksVal !== Number.MAX_SAFE_INTEGER &&
this.finalThreads.length !== this.uxCurrentNumberOfTasksVal
) {
msg += `<p>Expecting ${this.uxCurrentNumberOfTasksVal} threads, found ${this.finalThreads.length}. Thread data may be unreliable<p>\n`;
} else if (this.finalThreads.length === 0) {
htmlContent.html = `<p>No ${this.name} threads detected, perhaps RTOS not yet initialized or tasks yet to be created!</p>\n`;
return htmlContent;
}
const ret = this.getHTMLCommon(DisplayFieldNames, FreeRTOSItems, this.finalThreads, this.timeInfo);
htmlContent.html = msg + ret.html + (this.helpHtml || '');
htmlContent.css = ret.css;
this.lastValidHtmlContent = htmlContent;
// console.log(this.lastValidHtmlContent.html);
return this.lastValidHtmlContent;
}
}