Skip to content

Instantly share code, notes, and snippets.

@defufna
Created February 5, 2026 18:04
Show Gist options
  • Select an option

  • Save defufna/6d5fac538d7bcbd61722b46f8bd9915e to your computer and use it in GitHub Desktop.

Select an option

Save defufna/6d5fac538d7bcbd61722b46f8bd9915e to your computer and use it in GitHub Desktop.
WinDBG extension
"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