rtos-views
Version:
RTOS views for microcontrollers
731 lines (619 loc) • 29.2 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 embOS 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,
StackPercent,
StackPeakPercent
}
const RTOSEMBOSItems: { [key: string]: RTOSCommon.DisplayColumnItem } = {};
RTOSEMBOSItems[DisplayFields[DisplayFields.ID_Address]] = { width: 2, headerRow1: '', headerRow2: 'ID / Address' };
RTOSEMBOSItems[DisplayFields[DisplayFields.TaskName]] = {
width: 4,
headerRow1: '',
headerRow2: 'Name',
colGapBefore: 1
};
RTOSEMBOSItems[DisplayFields[DisplayFields.Status]] = {
width: 4,
headerRow1: 'Thread',
headerRow2: 'Status',
colType: RTOSCommon.ColTypeEnum.colTypeCollapse
};
RTOSEMBOSItems[DisplayFields[DisplayFields.Priority]] = {
width: 2,
headerRow1: 'Priority',
headerRow2: 'cur,base',
colType: RTOSCommon.ColTypeEnum.colTypeNumeric,
colGapAfter: 1
};
RTOSEMBOSItems[DisplayFields[DisplayFields.StackPercent]] = {
width: 4,
headerRow1: 'Stack Usage',
headerRow2: '% (Used B / Size B)',
colType: RTOSCommon.ColTypeEnum.colTypePercentage
};
RTOSEMBOSItems[DisplayFields[DisplayFields.StackPeakPercent]] = {
width: 4,
headerRow1: 'Stack Peak Usage',
headerRow2: '% (Peak B / Size B)',
colType: RTOSCommon.ColTypeEnum.colTypePercentage
};
// TODO Maybe add a column with NumActivations and/or NumPreemptions which is available when OS_SUPPORT_STAT is set (not 0)
// TODO Maybe add a column with Load / ExecTotal / ExecLast which is available when OS_SUPPORT_PROFILE is set (not 0)
const DisplayFieldNames: string[] = Object.keys(RTOSEMBOSItems);
export class RTOSEmbOS 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 OSGlobal: RTOSCommon.RTOSVarHelperMaybe;
private OSGlobalVal: any;
private OSGlobalpTask: RTOSCommon.RTOSVarHelperMaybe; /* start of task linked list */
private OSGlobalpObjNameRoot: RTOSCommon.RTOSVarHelperMaybe; /* start of object name linked list */
private pCurrentTaskVal: number = 0;
private taskCount: number = 0;
private stale: boolean = true;
private foundThreads: RTOSCommon.RTOSThreadInfo[] = [];
private finalThreads: RTOSCommon.RTOSThreadInfo[] = [];
private timeInfo: string = '';
private readonly maxThreads = 1024;
private stackPattern = 0xCD; /* Seems that OS_TASK_CREATE() does initialize the stack to 0xCD */
private stackIncrements = -1; /* negative numbers => high to low address growth on stack (OS_STACK_GROWS_TOWARD_HIGHER_ADDR = 0) */
private helpHtml: string | undefined;
constructor(public session: vscode.DebugSession) {
super(session, 'embOS');
if (session.configuration.rtosViewConfig) {
if (session.configuration.rtosViewConfig.stackPattern) {
this.stackPattern = parseInt(session.configuration.rtosViewConfig.stackPattern);
}
if (session.configuration.rtosViewConfig.stackGrowth) {
this.stackIncrements = parseInt(session.configuration.rtosViewConfig.stackGrowth);
}
}
}
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.OSGlobal = await this.getVarIfEmpty(this.OSGlobal, useFrameId, 'OS_Global', false);
this.OSGlobalpTask = await this.getVarIfEmpty(this.OSGlobalpTask, useFrameId, 'OS_Global.pTask', false);
this.OSGlobalpObjNameRoot = await this.getVarIfEmpty(
this.OSGlobalpObjNameRoot,
useFrameId,
'OS_Global.pObjNameRoot',
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(text: string) {
return `<strong>${text}</strong>`;
}
if (this.helpHtml === undefined) {
this.helpHtml = '';
try {
let ret: string = '';
if (!thInfo['sName']?.val) {
ret += `Thread name missing: Enable
${strong('OS_SUPPORT_TRACKNAME')} or use library mode that enables it and
use ${strong('sName')} parameter on task creation in FW<br><br>`;
}
if (!th.stackInfo.stackSize) {
ret += `Stack Size & Peak missing: Enable
${strong('OS_SUPPORT_STACKCHECK')} or use library mode that enables it<br><br>`;
}
if (ret) {
ret +=
'Note: Make sure you consider the performance/resources impact for any changes to your FW.<br>\n';
this.helpHtml =
'<button class="help-button">Hints to get more out of the embOS RTOS View</button>\n' +
`<div class="help"><p>\n${ret}\n</p></div>\n`;
}
} catch (e) {
console.log(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();
this.taskCount = Number.MAX_SAFE_INTEGER;
this.foundThreads = [];
this.OSGlobal?.getVarChildrenObj(frameId).then(
async (varObj) => {
try {
this.OSGlobalVal = varObj;
let isRunning: any = '0';
if (Object.hasOwn(this.OSGlobalVal, 'IsRunning')) {
isRunning = this.OSGlobalVal['IsRunning']?.val;
}
else {
/* older embOS versions do not have IsRunning struct member */
isRunning = '1';
}
if (undefined !== isRunning && !isNaN(isRunning) && 0 !== parseInt(isRunning)) {
const taskList = this.OSGlobalVal['pTask']?.val;
if (undefined !== taskList && 0 !== parseInt(taskList)) {
if (this.OSGlobalVal['pCurrentTask']?.val) {
this.pCurrentTaskVal = parseInt(this.OSGlobalVal['pCurrentTask']?.val);
} else {
this.pCurrentTaskVal = Number.MAX_SAFE_INTEGER;
}
const objectNameEntries = await this.getObjectNameEntries(frameId);
await this.getThreadInfo(this.OSGlobalpTask, objectNameEntries, frameId);
this.foundThreads.sort(
(a, b) =>
parseInt(a.display[DisplayFieldNames[DisplayFields.ID_Address]].text) -
parseInt(b.display[DisplayFieldNames[DisplayFields.ID_Address]].text)
);
this.finalThreads = [...this.foundThreads];
} else {
this.finalThreads = [];
}
} else {
this.finalThreads = [];
}
this.stale = false;
this.timeInfo += ' in ' + timer.deltaMs() + ' ms';
resolve();
} catch (e) {
resolve();
console.error('RTOSEMBOS.refresh() failed: ', e);
}
},
(reason) => {
resolve();
console.error('RTOSEMBOS.refresh() failed: ', reason);
}
);
});
}
private getThreadInfo(
taskListEntry: RTOSCommon.RTOSVarHelperMaybe,
objectNameEntries: Map<number, string>,
frameId: number
): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (!taskListEntry || !taskListEntry.varReference) {
resolve();
return;
}
if (this.progStatus !== 'stopped') {
reject(new Error('Busy'));
return;
}
taskListEntry.getVarChildrenObj(frameId).then(
async (obj: RTOSCommon.RTOSStrToValueMap) => {
try {
let curTaskObj = obj;
let thAddress = parseInt(taskListEntry.value || '');
let threadCount = 1;
do {
let thName = '???';
if (Object.hasOwn(curTaskObj, 'sName')) {
const matchName = curTaskObj['sName']?.val.match(/"([^*]*)"$/);
thName = matchName ? matchName[1] : curTaskObj['sName']?.val;
}
else if (Object.hasOwn(curTaskObj, 'Name')) {
/* older embOS versions used Name */
const matchName = curTaskObj['Name']?.val.match(/"([^*]*)"$/);
thName = matchName ? matchName[1] : curTaskObj['Name']?.val;
}
const threadRunning = thAddress === this.pCurrentTaskVal;
const thStateObject = await this.analyzeTaskState(curTaskObj, objectNameEntries);
const stackInfo = await this.getStackInfo(curTaskObj, this.stackPattern);
const display: { [key: string]: RTOSCommon.DisplayRowItem } = {};
const mySetter = (x: DisplayFields, text: string, value?: any) => {
display[DisplayFieldNames[x]] = { text, value };
};
mySetter(DisplayFields.ID_Address, RTOSCommon.hexFormat(thAddress));
mySetter(DisplayFields.TaskName, thName);
mySetter(
DisplayFields.Status,
threadRunning ? 'RUNNING' : thStateObject.describe(),
thStateObject.fullData()
);
const myHexNumStrCon = (hexNumberString: string): string => {
return parseInt(hexNumberString).toString();
};
const prioString = `${myHexNumStrCon(curTaskObj['Priority']?.val)},${myHexNumStrCon(
curTaskObj['BasePrio']?.val
)}`;
mySetter(DisplayFields.Priority, prioString);
if (stackInfo.stackUsed !== undefined && stackInfo.stackSize !== undefined) {
const stackPercentVal = Math.round((stackInfo.stackUsed / stackInfo.stackSize) * 100);
const stackPercentText = `${stackPercentVal} % (${stackInfo.stackUsed} / ${stackInfo.stackSize})`;
mySetter(DisplayFields.StackPercent, stackPercentText, stackPercentVal);
} else {
mySetter(DisplayFields.StackPercent, '?? %');
}
if (stackInfo.stackPeak !== undefined && stackInfo.stackSize !== undefined) {
const stackPeakPercentVal = Math.round(
(stackInfo.stackPeak / stackInfo.stackSize) * 100
);
const stackPeakPercentText = `${stackPeakPercentVal.toString().padStart(3)} % (${stackInfo.stackPeak
} / ${stackInfo.stackSize})`;
mySetter(DisplayFields.StackPeakPercent, stackPeakPercentText, stackPeakPercentVal);
} else {
mySetter(DisplayFields.StackPeakPercent, '?? %');
}
const thread: RTOSCommon.RTOSThreadInfo = {
display: display,
stackInfo: stackInfo,
running: threadRunning,
};
this.foundThreads.push(thread);
this.createHmlHelp(thread, curTaskObj);
thAddress = parseInt(curTaskObj['pNext']?.val);
if (0 !== thAddress) {
const nextThreadObj = await this.getVarChildrenObj(curTaskObj['pNext']?.ref, 'pNext');
curTaskObj = nextThreadObj || {};
threadCount++;
}
if (threadCount > this.maxThreads) {
console.error(`Exceeded maximum number of allowed threads (${this.maxThreads})`);
break;
}
} while (0 !== thAddress);
this.taskCount = threadCount;
resolve();
} catch (e) {
console.log('RTOSEMBOS.getThreadInfo() error', e);
}
},
(e) => {
reject(e);
}
);
});
}
protected async analyzeTaskState(
curTaskObj: RTOSCommon.RTOSStrToValueMap,
objectNameEntries: Map<number, string>
): Promise<TaskState> {
const state = parseInt(curTaskObj['Stat']?.val);
const suspendCount = state & OS_TASK_STATE_SUSPEND_MASK;
if (suspendCount !== 0) {
return new TaskSuspended(suspendCount);
}
let pendTimeout = Number.MAX_SAFE_INTEGER;
let TimeoutActive = false;
if (state & OS_TASK_STATE_TIMEOUT_ACTIVE) {
pendTimeout = parseInt(curTaskObj['Timeout']?.val);
TimeoutActive = true;
}
const maskedState = state & OS_TASK_STATE_MASK;
switch (maskedState) {
case OsTaskPendingState.READY:
if (pendTimeout) {
return new TaskDelayed(pendTimeout);
} else {
return new TaskReady();
}
case OsTaskPendingState.TASK_EVENT:
const resultState = new TaskPending();
resultState.addEventType(maskedState);
if (curTaskObj['EventMask']?.val) {
const eventMask = parseInt(curTaskObj['EventMask']?.val); // Waiting bits
const event = parseInt(curTaskObj['Events']?.val); // Set bits
const eventInfo: EventInfo = {
address: eventMask,
eventType: state,
name: `mask ${eventMask} - set ${event}`,
};
if (TimeoutActive) {
eventInfo.timeOut = pendTimeout;
}
resultState.addEvent(eventInfo);
}
return resultState;
default: {
const resultState = new TaskPending();
resultState.addEventType(maskedState);
if (curTaskObj['pWaitList']?.val) {
const waitListEntryAddress = parseInt(curTaskObj['pWaitList']?.val);
if (waitListEntryAddress !== 0) {
const waitListEntry = await this.getVarChildrenObj(curTaskObj['pWaitList']?.ref, 'pWaitList');
const waitObject = parseInt(waitListEntry ? waitListEntry['pWaitObj']?.val : '');
const eventInfo: EventInfo = { address: waitObject, eventType: state };
if (objectNameEntries.has(waitObject)) {
eventInfo.name = objectNameEntries.get(waitObject);
}
if (TimeoutActive) {
eventInfo.timeOut = pendTimeout;
}
resultState.addEvent(eventInfo);
}
}
return resultState;
}
}
}
protected async getStackInfo(
thInfo: RTOSCommon.RTOSStrToValueMap,
stackPattern: number
): Promise<RTOSCommon.RTOSStackInfo> {
const TopOfStack = thInfo['pStack']?.val;
/* only available with #if (OS_SUPPORT_STACKCHECK != 0) || (OS_SUPPORT_MPU != 0) (optional) */
const StackSize = thInfo['StackSize']?.val;
let EndOfStack: any;
if (Object.hasOwn(thInfo, 'pStackBase')) {
EndOfStack = thInfo['pStackBase']?.val;
} else {
/* older embOS versions used pStackBot instead of pStackBase */
EndOfStack = thInfo['pStackBot']?.val;
}
let Stack = 0;
if (EndOfStack && StackSize) {
if (this.stackIncrements < 0) {
Stack = parseInt(EndOfStack) + parseInt(StackSize);
} else {
Stack = parseInt(EndOfStack) - parseInt(StackSize);
}
} else {
/* As stackStart is mandatory, we need to set it to some reasonable value */
Stack = parseInt(TopOfStack);
}
const stackInfo: RTOSCommon.RTOSStackInfo = {
stackStart: Stack,
stackTop: parseInt(TopOfStack),
};
if (EndOfStack && StackSize) {
stackInfo.stackEnd = parseInt(EndOfStack);
stackInfo.stackSize = parseInt(StackSize);
if (this.stackIncrements < 0) {
const stackDelta = stackInfo.stackStart - stackInfo.stackTop;
stackInfo.stackFree = stackInfo.stackSize - stackDelta;
stackInfo.stackUsed = stackDelta;
} else {
const stackDelta = stackInfo.stackTop - stackInfo.stackStart;
stackInfo.stackFree = stackDelta;
stackInfo.stackUsed = stackInfo.stackSize - stackDelta;
}
/* check stack peak */
const memArg: DebugProtocol.ReadMemoryArguments = {
memoryReference: RTOSCommon.hexFormat(Math.min(stackInfo.stackTop, stackInfo.stackEnd)),
count: stackInfo.stackFree,
};
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] !== stackPattern) {
break;
}
start -= this.stackIncrements;
peak++;
}
stackInfo.stackPeak = stackInfo.stackSize - peak;
} catch (e) {
console.log(e);
}
}
return stackInfo;
}
protected async getObjectNameEntries(frameId: number): Promise<Map<number, string>> {
const result: Map<number, string> = new Map();
if (this.OSGlobalpObjNameRoot) {
await this.OSGlobalpObjNameRoot.getValue(frameId);
/* Follow the linked list of object identifier nodes */
if (0 !== parseInt(this.OSGlobalpObjNameRoot.value || '')) {
let entry: RTOSCommon.RTOSStrToValueMap | null = await this.OSGlobalpObjNameRoot.getVarChildrenObj(
frameId
);
while (entry) {
const objectId = parseInt(entry['pOSObjID']?.val);
if (!objectId || objectId === 0) {
break;
}
const matchName = entry['sName']?.val.match(/"([^*]*)"$/);
const objectName = matchName ? matchName[1] : entry['sName']?.val;
if (objectName && !result.has(objectId)) {
result.set(objectId, objectName);
}
const nextEntryAddr = parseInt(entry['pNext']?.val);
if (nextEntryAddr === 0) {
break;
} else {
entry = await this.getVarChildrenObj(entry['pNext']?.ref, 'pNext');
}
}
}
}
return result;
}
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.taskCount === Number.MAX_SAFE_INTEGER) {
msg =
' Could not read any task from "OS_Global.pTask". Perhaps program is busy or did not stop long enough';
lastHtmlInfo.html = '';
lastHtmlInfo.css = '';
} else if (this.taskCount > this.maxThreads) {
msg = ` embOS variable "OS_Global.pTask" holds ${this.taskCount} tasks which seems invalid for us`;
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.taskCount !== Number.MAX_SAFE_INTEGER && this.finalThreads.length !== this.taskCount) {
msg += `<p>Expecting ${this.taskCount} 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, RTOSEMBOSItems, 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;
}
}
const OS_TASK_STATE_SUSPEND_MASK = 0x03; /* Task suspend count (bit 0 - 1) */
const OS_TASK_STATE_TIMEOUT_ACTIVE = 0x04; /* Task timeout active (bit 2) */
const OS_TASK_STATE_MASK = 0xF8; /* Task state mask (bit 3 - bit 7) */
enum OsTaskPendingState {
READY = 0x00,
TASK_EVENT = 0x08, /* flag group "assigned" to one task */
MUTEX = 0x10,
UNKNOWN = 0x18, /* Not sure when this value is set */
SEMAPHORE = 0x20,
MEMPOOL = 0x28,
QUEUE_NOT_EMPTY = 0x30,
MAILBOX_NOT_FULL = 0x38,
MAILBOX_NOT_EMPTY = 0x40,
EVENT_OBJECT = 0x48, /* flag group without task "assignment" */
QUEUE_NOT_FULL = 0x50
}
abstract class TaskState {
public abstract describe(): string;
public abstract fullData(): any;
}
class TaskReady extends TaskState {
public describe(): string {
return 'READY';
}
public fullData(): any {
return null;
}
}
class TaskDelayed extends TaskState {
protected delayTicks: number;
constructor(delayTicks: number) {
super();
this.delayTicks = delayTicks;
}
public describe(): string {
return `DELAYED by ${this.delayTicks}`; // TODO Not sure what unit this variable holds
}
public fullData(): any {
return null;
}
}
class TaskSuspended extends TaskState {
private suspendCount: number;
constructor(suspendCount: number) {
super();
this.suspendCount = suspendCount;
}
public describe(): string {
return `SUSPENDED (count: ${this.suspendCount})`;
}
public fullData(): any {
return null;
}
}
class TaskPending extends TaskState {
private pendingInfo: Map<OsTaskPendingState, EventInfo[]>;
constructor() {
super();
this.pendingInfo = new Map();
}
public addEvent(event: EventInfo) {
this.addEventType(event.eventType);
const val = this.pendingInfo.get(event.eventType);
if (val) {
val.push(event);
}
}
public addEventType(eventType: OsTaskPendingState) {
if (!this.pendingInfo.has(eventType)) {
this.pendingInfo.set(eventType, []);
}
}
public describe(): string {
// Converting to an array here is inefficient, but JS has no builtin iterator map/reduce feature
const eventCount = [...this.pendingInfo.values()].reduce((acc, events) => acc + events.length, 0);
if (eventCount <= 1) {
let event: EventInfo | undefined;
for (const events of this.pendingInfo.values()) {
if (events.length > 0) {
event = events[0];
}
}
if (event) {
const eventTypeStr = OsTaskPendingState[event.eventType]
? OsTaskPendingState[event.eventType]
: 'Unknown';
const eventTimeoutString = event.timeOut ? ` with timeout in ${event.timeOut}` : ''; // TODO Not sure what unit this variable holds
return `PEND ${eventTypeStr}: ${describeEvent(event)}${eventTimeoutString}`;
} else {
// This should not happen, but we still keep it as a fallback
return 'PEND Unknown';
}
} else {
return 'PEND MULTI';
}
}
public fullData() {
// Build an object containing mapping event types to event descriptions
const result: any = {};
const eventTypes = [...this.pendingInfo.keys()];
eventTypes.sort();
for (const eventType of eventTypes) {
result[OsTaskPendingState[eventType]] = [];
for (const event of this.pendingInfo.get(eventType) || []) {
result[OsTaskPendingState[eventType]].push(describeEvent(event));
}
}
return result;
}
}
interface EventInfo {
name?: string;
timeOut?: number;
address: number;
eventType: OsTaskPendingState;
}
function describeEvent(event: EventInfo): string {
if (event.name && event.name !== '?') {
return event.name;
} else {
return `0x${event.address.toString(16)}`;
}
}