Created
February 5, 2026 18:04
-
-
Save defufna/6d5fac538d7bcbd61722b46f8bd9915e to your computer and use it in GitHub Desktop.
WinDBG extension
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use strict"; | |
| function initializeScript() { | |
| return [ | |
| new host.functionAlias(findBlockers, "findblockers"), | |
| new host.functionAlias(clrThreads, "clrthreads"), | |
| new host.functionAlias(groupThreads, "groupthreads") | |
| ]; | |
| } | |
| function findBlockers() { | |
| const currentProcess = host.currentProcess; | |
| // Maintainable list of blocking functions | |
| const waitSymbols = [ | |
| "NtWaitForSingleObject", "WaitForSingleObject", | |
| "NtWaitForMultipleObjects", "WaitForMultipleObjects", | |
| "NtDelayExecution", "Sleep", "KiSwapThread", "CriticalSection", | |
| "NtRemoveIoCompletion", "NtWaitForWorkViaWorkerFactory", "DbgBreakPoint" | |
| ]; | |
| let stats = { waiting: 0, running: 0 }; | |
| let waitingIds = []; | |
| let runningIds = []; | |
| host.diagnostics.debugLog("--- THREAD STATUS REPORT ---\n\n"); | |
| for (var thread of currentProcess.Threads) { | |
| const stack = thread.Stack.Frames; | |
| const dbgId = thread.Index; // This is the WinDbg ID (e.g., 0, 1, 2) | |
| const osTid = thread.Id.toString(16); | |
| let isWaiting = false; | |
| let waitFunction = ""; | |
| // Check top frames for blockers | |
| const depth = Math.min(stack.Count(), 2); | |
| for (let i = 0; i < depth; i++) { | |
| let frameName = stack[i].toString(); | |
| if (waitSymbols.some(s => frameName.includes(s))) { | |
| isWaiting = true; | |
| waitFunction = frameName; | |
| break; | |
| } | |
| } | |
| if (isWaiting) { | |
| stats.waiting++; | |
| waitingIds.push(dbgId); | |
| // host.diagnostics.debugLog(`[#${dbgId}] (TID: 0x${osTid}) STATUS: WAITING\n`); | |
| // host.diagnostics.debugLog(` Wait API: ${waitFunction}\n\n`); | |
| } else { | |
| stats.running++; | |
| runningIds.push(dbgId); | |
| const topFrame = stack.Count() > 0 ? stack[0].toString() : "No Stack Frames"; | |
| host.diagnostics.debugLog(`[#${dbgId}] (TID: 0x${osTid}) STATUS: RUNNING\n`); | |
| host.diagnostics.debugLog(` Top Call: ${topFrame}\n\n`); | |
| } | |
| } | |
| // Summary block | |
| host.diagnostics.debugLog("--- FINAL SUMMARY ---\n"); | |
| host.diagnostics.debugLog(`Total Threads: ${stats.waiting + stats.running}\n`); | |
| host.diagnostics.debugLog(`Waiting: ${stats.waiting}\n`); | |
| host.diagnostics.debugLog(`Running: ${stats.running}\n`); | |
| host.diagnostics.debugLog("----------------------\n"); | |
| host.diagnostics.debugLog("WAITING: " + waitingIds.join(",") + "\n"); | |
| host.diagnostics.debugLog("RUNNING: " + runningIds.join(",") + "\n"); | |
| host.diagnostics.debugLog("----------------------\n"); | |
| } | |
| /** | |
| * Accepts a comma-separated string of WinDbg thread IDs. | |
| * Switches to each thread and executes !clrstack. | |
| * Usage: !clrthreads "1,5,12" | |
| */ | |
| function clrThreads(idListString) { | |
| if (!idListString) { | |
| host.diagnostics.debugLog("Error: Please provide a comma-separated list of thread IDs. Example: !clrthreads \"0,2,3\"\n"); | |
| return; | |
| } | |
| // Split by comma and trim whitespace | |
| const ids = idListString.split(",").map(id => id.trim()); | |
| const control = host.namespace.Debugger.Utility.Control; | |
| for (let id of ids) { | |
| if (id === "") continue; | |
| host.diagnostics.debugLog(`\n>>> ANALYZING THREAD #${id} <<<\n`); | |
| try { | |
| // Execute the command: ~[id]e !clrstack | |
| // This switches context internally for that command only | |
| let output = control.ExecuteCommand(`~${id}e !clrstack`); | |
| for (let line of output) { | |
| host.diagnostics.debugLog(line + "\n"); | |
| } | |
| } catch (e) { | |
| host.diagnostics.debugLog(`Could not run !clrstack on thread ${id}. (Is it a managed thread?)\n`); | |
| } | |
| } | |
| } | |
| /** | |
| * Groups threads by their return address stack signature and prints them sorted by frequency. | |
| */ | |
| function groupThreads(idListString) { | |
| if (!idListString) { | |
| host.diagnostics.debugLog("Usage: !groupthreads \"0,1,2,3\"\n"); | |
| return; | |
| } | |
| const ids = idListString.split(',').map(id => id.trim()).filter(id => id !== ""); | |
| const currentProcess = host.currentProcess; | |
| const control = host.namespace.Debugger.Utility.Control; | |
| let groups = new Map(); | |
| // Using ToArray() as you suggested to ensure stable indexing | |
| let threads = currentProcess.Threads.ToArray(); | |
| for (let id of ids) { | |
| try { | |
| let thread = threads[parseInt(id)]; | |
| if (!thread) { | |
| host.diagnostics.debugLog(`Thread #${id} not found.\n`); | |
| continue; | |
| } | |
| // Generate a signature based on return addresses | |
| let stackSignature = ""; | |
| for (let frame of thread.Stack.Frames) { | |
| stackSignature += frame.Attributes.ReturnOffset.toString(16) + "|"; | |
| } | |
| if (groups.has(stackSignature)) { | |
| let entry = groups.get(stackSignature); | |
| entry.count++; | |
| entry.threads.push(id); | |
| } else { | |
| groups.set(stackSignature, { | |
| count: 1, | |
| representativeId: id, | |
| threads: [id] | |
| }); | |
| } | |
| } catch (e) { | |
| host.diagnostics.debugLog(`Could not process thread ${id}: ${e}\n`); | |
| } | |
| } | |
| // Convert Map to Array and sort by count (Descending) | |
| let sortedGroups = Array.from(groups.values()).sort((a, b) => b.count - a.count); | |
| host.diagnostics.debugLog(`\n=== STACK GROUPING REPORT (${sortedGroups.length} Unique Groups) ===\n`); | |
| host.diagnostics.debugLog(`Sorted by thread frequency (highest first)\n\n`); | |
| for (let data of sortedGroups) { | |
| host.diagnostics.debugLog(`[Group: ${data.count} thread(s)]\n`); | |
| host.diagnostics.debugLog(`IDs: ${data.threads.join(", ")}\n`); | |
| host.diagnostics.debugLog(`Representative Managed Stack (Thread #${data.representativeId}):\n`); | |
| try { | |
| // Indent the !clrstack output for readability | |
| let output = control.ExecuteCommand(`~${data.representativeId}e !clrstack`); | |
| for (let line of output) { | |
| host.diagnostics.debugLog(" " + line + "\n"); | |
| } | |
| } catch (e) { | |
| host.diagnostics.debugLog(" [No managed stack found for this group]\n"); | |
| } | |
| host.diagnostics.debugLog("-".repeat(60) + "\n\n"); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment