UNPKG

rtos-views

Version:
610 lines (533 loc) 24.2 kB
/* eslint-disable @typescript-eslint/naming-convention */ import * as vscode from 'vscode'; import * as RTOSCommon from './rtos-common'; // We will have two rows of headers for ChibiOS 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, THREAD_DESCRIPTION, FLAGS, REFS, TIME, WTOBJP, STATS_N, STATS_WORST, STATS_CUMULATIVE, STACK_CURRENT_USAGE, STACK_PEAK_USAGE } enum chThreadState { READY = 0, CURRENT, STARTED, SUSPENDED, QUEUED, WTSEM, WTMTX, WTCOND, SLEEPING, WTEXIT, WTOREVT, WTANDEVT, SNDMSGQ, SNDMSG, WTMSG, FINAL, UNKNOWN, _SIZE } const colNumType = RTOSCommon.ColTypeEnum.colTypeNumeric; const ChibiOSItems: { [key: string]: RTOSCommon.DisplayColumnItem } = {}; ChibiOSItems[DisplayFields[DisplayFields.ID]] = { width: 2, headerRow1: '', headerRow2: 'id', colType: colNumType }; ChibiOSItems[DisplayFields[DisplayFields.THREAD_DESCRIPTION]] = { width: 14, headerRow1: '', headerRow2: 'Thread', colGapBefore: 1 }; ChibiOSItems[DisplayFields[DisplayFields.FLAGS]] = { width: 2, headerRow1: '', headerRow2: 'Flags', colGapAfter: 1 }; ChibiOSItems[DisplayFields[DisplayFields.REFS]] = { width: 2, headerRow1: '', headerRow2: 'Refs', colType: colNumType }; ChibiOSItems[DisplayFields[DisplayFields.TIME]] = { width: 2, headerRow1: '', headerRow2: 'Time', colType: colNumType }; ChibiOSItems[DisplayFields[DisplayFields.WTOBJP]] = { width: 4, headerRow1: 'Wait', headerRow2: 'Obj/Msg', colGapBefore: 1 }; ChibiOSItems[DisplayFields[DisplayFields.STATS_N]] = { width: 4, headerRow1: 'Stats', headerRow2: 'Switches', colType: colNumType }; ChibiOSItems[DisplayFields[DisplayFields.STATS_WORST]] = { width: 4, headerRow1: '', headerRow2: 'Worst Path', colType: colNumType }; ChibiOSItems[DisplayFields[DisplayFields.STATS_CUMULATIVE]] = { width: 4, headerRow1: '', headerRow2: 'Cumulative Time', colType: colNumType }; ChibiOSItems[DisplayFields[DisplayFields.STACK_CURRENT_USAGE]] = { width: 3, headerRow1: 'Stack', headerRow2: '', colType: colNumType }; ChibiOSItems[DisplayFields[DisplayFields.STACK_PEAK_USAGE]] = { width: 3, headerRow1: '', headerRow2: '', colType: colNumType }; const DisplayFieldNames: string[] = Object.keys(ChibiOSItems); function getThreadStateName(s: number): string { if (s < chThreadState._SIZE) { return chThreadState[s]; } return chThreadState[chThreadState._SIZE - 1]; } function getCString(s: string, nullValue: string = ''): string { const matchName = s.match(/"([^*]*)"$/); return matchName ? matchName[1] : nullValue; } function getNumber(s: string): number { return s ? parseInt(s) : 0; } function getNumberNVL(s: string | null | undefined, nullValue: number): number { return s ? parseInt(s) : nullValue; } function nvl(v: any, nullValue: any) { if (v === undefined || v === null) { return nullValue; } return v; } function getStackDisplayPercentage(s?: number, v?: number) { let text = '-'; let percent = 0; if (s !== undefined && v !== undefined) { if (v === -1) { text = 'overflow'; percent = 100; } else { percent = Math.round((v / s) * 100); text = `${percent.toString()}% (${v} / ${s})`; } } return { text: text, percent: percent }; } function getStackDisplayValue(v?: number): string { let text = '-'; if (v) { if (v === -1) { text = 'overflow'; } else { text = v.toString(); } } return text; } export class RTOSChibiOS 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 chRlistCurrent: RTOSCommon.RTOSVarHelperMaybe; private chReglist: RTOSCommon.RTOSVarHelperMaybe; private rlistCurrent: number = 0; private threadOffset: number = 0; private threadSize: number = 0; private smp: boolean = false; private hasWAEND: boolean = false; private stale: boolean = true; private foundThreads: RTOSCommon.RTOSThreadInfo[] = []; private finalThreads: RTOSCommon.RTOSThreadInfo[] = []; private timeInfo: string = ''; private helpHtml: string | undefined; // Need to do a TON of testing for stack growing the other direction private stackIncrements = -1; private readonly maxThreads = 1024; constructor(public session: vscode.DebugSession) { super(session, 'ChibiOS'); } private async scanStackUnused(stackTop: number, stackEnd: number, s: number) { const stackData = await this.session.customRequest('readMemory', { memoryReference: RTOSCommon.hexFormat(Math.min(stackTop, stackEnd)), count: s }); const bytes = new Uint8Array(Buffer.from(stackData.data, 'base64')); let unused = 0; while (unused < bytes.length && bytes[unused] === 0x55) { unused++; } return unused; } private async getStackPointer(threadInfo: RTOSCommon.RTOSStrToValueMap) { let sp = 0; const currentThreadCtx = await this.getVarChildrenObj(threadInfo['ctx']?.ref, 'ctx'); const currentThreadCtxRegs = currentThreadCtx ? await this.getVarChildrenObj(currentThreadCtx['sp']?.ref, 'sp') : null; if (currentThreadCtxRegs && currentThreadCtx) { sp = getNumberNVL(Object.hasOwn(currentThreadCtxRegs, 'r13') ? currentThreadCtxRegs['r13']?.val : currentThreadCtx['sp']?.val, 0); } return sp; } private getStackPeak(stackInfo: RTOSCommon.RTOSStackInfo, unused: number) { let peak = undefined; if (this.hasWAEND) { // Calculate stack peak if (stackInfo.stackSize && stackInfo.stackSize !== 0) { peak = Math.max(0, stackInfo.stackSize - unused); } } else { // Assign stack min free size peak = unused; } return peak; } 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 try { this.chReglist = await this.getVarIfEmpty( this.chReglist, useFrameId, '(uint32_t) &ch_system.reglist', false ); this.smp = true; } catch (e) { if (e instanceof RTOSCommon.ShouldRetry) { throw e; } this.chReglist = await this.getVarIfEmpty( this.chReglist, useFrameId, '(uint32_t) &ch0.reglist', false ); } this.chRlistCurrent = await this.getVarIfEmpty( this.chRlistCurrent, useFrameId, 'ch0.rlist.current', false ); const chRlistCurrentWAEND = await this.getVarIfEmpty( undefined, useFrameId, 'ch0.rlist.current.waend', true ); this.threadOffset = parseInt( (await this.getExprVal('((char *)(&((thread_t *)0)->rqueue) - (char *)0)', useFrameId)) || '' ); this.threadSize = parseInt((await this.getExprVal('sizeof(thread_t)', useFrameId)) || ''); this.status = 'initialized'; if (!chRlistCurrentWAEND) { // old version without waend ChibiOSItems[DisplayFields[DisplayFields.STACK_CURRENT_USAGE]] = { width: 3, headerRow1: 'Stack', headerRow2: 'Current free', colType: colNumType }; ChibiOSItems[DisplayFields[DisplayFields.STACK_PEAK_USAGE]] = { width: 3, headerRow1: '', headerRow2: 'Min. free', colType: colNumType }; } else { // new version with waend ChibiOSItems[DisplayFields[DisplayFields.STACK_CURRENT_USAGE]] = { width: 4, headerRow1: 'Stack', headerRow2: 'Current %<br><small>(Used B / Size B)</small>', colType: RTOSCommon.ColTypeEnum.colTypePercentage }; ChibiOSItems[DisplayFields[DisplayFields.STACK_PEAK_USAGE]] = { width: 4, headerRow1: '', headerRow2: 'Peak %<br><small>(Peak B / Size B)</small>', colType: RTOSCommon.ColTypeEnum.colTypePercentage }; this.hasWAEND = true; } } 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, thread: RTOSCommon.RTOSStrToValueMap, threadStats: RTOSCommon.RTOSStrToValueMap, _threadStack: RTOSCommon.RTOSStackInfo ) { function strong(text: string) { return `<strong>${text}</strong>`; } if (this.helpHtml === undefined) { this.helpHtml = ''; try { let ret: string = ''; if (!getNumberNVL(thread['wabase']?.val, 0)) { ret += `Thread stack debug information is not enabled: to enable set ${strong('CH_DBG_ENABLE_STACK_CHECK')} and ${strong('CH_DBG_FILL_THREADS')} to ${strong('TRUE')} in chconf.h<br><br>`; } if (!threadStats['n']?.val || !threadStats['worst']?.val || !threadStats['cumulative']?.val) { ret += `Kernel statistics are not enabled: to enable set ${strong('CH_DBG_STATISTICS')} to ${strong('TRUE')} in chconf.h<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 ChibiOS 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.foundThreads = []; this.finalThreads = []; this.chRlistCurrent?.getValue(frameId).then( async (rlistCurrentStr) => { try { this.rlistCurrent = getNumberNVL(rlistCurrentStr, 0); if (0 !== this.rlistCurrent) { // TODO: add global info: panic message, irs cnt... await this.getThreadInfo(this.chReglist, frameId); this.finalThreads = [...this.foundThreads]; } else { this.finalThreads = []; } this.stale = false; this.timeInfo += ' in ' + timer.deltaMs() + ' ms'; resolve(); } catch (e) { resolve(); console.error('ChibiOS.refresh() failed: ', e); } }, (reason) => { resolve(); console.error('ChibiOS.refresh() failed: ', reason); } ); }); } private getThreadInfo(reglist: RTOSCommon.RTOSVarHelperMaybe, frameId: number): Promise<void> { return new Promise<void>((resolve, reject) => { if (!reglist) { resolve(); return; } if (this.progStatus !== 'stopped') { reject(new Error('Busy')); return; } reglist.getValue(frameId).then( async (obj) => { try { const reglistHeader = getNumberNVL(obj, 0); if (reglistHeader && 0 !== reglistHeader) { let nextEntry = await this.getExprValChildrenObj('(ch_queue_t *)' + reglistHeader, frameId); let currentReglist = getNumber(nextEntry['next'].val); let i = 0; // TODO: add reglist integrity check do { const currentThreadAddr = currentReglist - this.threadOffset; const currentThread = await this.getExprValChildrenObj( '(thread_t *) ' + currentThreadAddr, frameId ); const currentThreadPqueue = await this.getExprValChildrenObj( '((thread_t *) ' + currentThreadAddr + ')->hdr.pqueue', frameId ); const currentThreadStateDetails = (await this.getVarChildrenObj(currentThread['u']?.ref, 'u')) || {}; const currentThreadStats = (await this.getVarChildrenObj(currentThread['stats']?.ref, 'stats')) || {}; const threadRunning = currentThreadAddr === this.rlistCurrent; const threadName = getCString(currentThread['name'].val, '[NO NAME]'); const threadState = getThreadStateName( getNumberNVL(currentThread['state']?.val, chThreadState._SIZE) ); const threadFlags = getNumberNVL(currentThread['flags']?.val, 0); const threadPrio = getNumberNVL(currentThreadPqueue['prio']?.val, 0); const threadRefs = getNumberNVL(currentThread['refs']?.val, 0); const threadTime = nvl(currentThread['time']?.val, '-'); const threadWaitForObj = currentThreadStateDetails['wtobjp']?.val; const threadStatsN = nvl(currentThreadStats['n']?.val, '-'); const threadStatsWorst = nvl(currentThreadStats['worst']?.val, '-'); const threadStatsCumulative = nvl(currentThreadStats['cumulative']?.val, '-'); const stackInfo = await this.getStackInfo(currentThread); i++; const display: { [key: string]: RTOSCommon.DisplayRowItem } = {}; const mySetter = (x: DisplayFields, text: string, value?: any) => { display[DisplayFieldNames[x]] = { text, value }; }; mySetter(DisplayFields.ID, i.toString()); mySetter( DisplayFields.THREAD_DESCRIPTION, threadName + '@' + RTOSCommon.hexFormat(currentThreadAddr) + ' ' + threadState + ' [P:' + threadPrio + ']' ); mySetter(DisplayFields.FLAGS, RTOSCommon.hexFormat(threadFlags, 2)); mySetter(DisplayFields.REFS, threadRefs.toString()); mySetter(DisplayFields.TIME, threadTime); mySetter(DisplayFields.WTOBJP, RTOSCommon.hexFormat(parseInt(threadWaitForObj))); mySetter(DisplayFields.STATS_N, threadStatsN); mySetter(DisplayFields.STATS_WORST, threadStatsWorst); mySetter(DisplayFields.STATS_CUMULATIVE, threadStatsCumulative); if (this.hasWAEND) { const currentStackUsage = getStackDisplayPercentage( stackInfo.stackSize, stackInfo.stackUsed ); const peakStackUsage = getStackDisplayPercentage( stackInfo.stackSize, stackInfo.stackPeak ); mySetter( DisplayFields.STACK_CURRENT_USAGE, currentStackUsage.text, currentStackUsage.percent ); mySetter( DisplayFields.STACK_PEAK_USAGE, peakStackUsage.text, peakStackUsage.percent ); } else { mySetter( DisplayFields.STACK_CURRENT_USAGE, getStackDisplayValue(stackInfo.stackFree) ); mySetter(DisplayFields.STACK_PEAK_USAGE, getStackDisplayValue(stackInfo.stackPeak)); } const threadInfo: RTOSCommon.RTOSThreadInfo = { display: display, stackInfo: stackInfo, running: threadRunning, }; this.foundThreads.push(threadInfo); this.createHmlHelp(threadInfo, currentThread, currentThreadStats, stackInfo); nextEntry = await this.getExprValChildrenObj( '(ch_queue_t *)' + currentReglist, frameId ); currentReglist = getNumberNVL(nextEntry['next']?.val, 0); } while (reglistHeader !== currentReglist); } else { // TODO: add error message - reglist header not found } resolve(); } catch (e) { console.log('ChibiOS.getThreadInfo() error', e); } }, (e) => { reject(e); } ); }); } protected async getStackInfo(threadInfo: RTOSCommon.RTOSStrToValueMap) { const stackInfo: RTOSCommon.RTOSStackInfo = { stackStart: 0, stackTop: 0 }; stackInfo.stackEnd = getNumberNVL(threadInfo['wabase']?.val, 0); stackInfo.stackTop = await this.getStackPointer(threadInfo); if (this.hasWAEND) { stackInfo.stackStart = Math.max(getNumberNVL(threadInfo['waend']?.val, 0) - this.threadSize); if (stackInfo.stackStart !== 0 && stackInfo.stackEnd !== 0) { stackInfo.stackSize = Math.abs(stackInfo.stackStart - stackInfo.stackEnd); if (stackInfo.stackTop === 0) { stackInfo.stackTop = stackInfo.stackStart; } } } else { stackInfo.stackStart = stackInfo.stackTop; } if (stackInfo.stackTop === 0 || stackInfo.stackEnd === 0) { // unknown stack stackInfo.stackFree = stackInfo.stackPeak = stackInfo.stackUsed = undefined; } else if (stackInfo.stackTop < stackInfo.stackEnd) { // stack overflow stackInfo.stackFree = stackInfo.stackPeak = stackInfo.stackUsed = -1; } else { stackInfo.stackFree = Math.abs(stackInfo.stackTop - stackInfo.stackEnd); if (stackInfo.stackSize && stackInfo.stackSize !== 0) { stackInfo.stackUsed = Math.max(0, stackInfo.stackSize - stackInfo.stackFree); } // get stack peak const unused = await this.scanStackUnused(stackInfo.stackTop, stackInfo.stackEnd, stackInfo.stackFree); stackInfo.stackPeak = this.getStackPeak(stackInfo, unused); } 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 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; htmlContent.html = '<p>Unable to collect full RTOS information.</p>\n' + lastHtmlInfo.html; htmlContent.css = lastHtmlInfo.css; return htmlContent; } 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, ChibiOSItems, this.finalThreads, this.timeInfo); htmlContent.html = ret.html + (this.helpHtml || ''); htmlContent.css = ret.css; this.lastValidHtmlContent = htmlContent; // console.log(this.lastValidHtmlContent.html); return this.lastValidHtmlContent; } }