XRayXL Armed

A flight recorder for the code running inside Excel.

When a spreadsheet is slow, freezes, or crashes, XRayXL tells you which XLL add-in and VBA functions actually ran — in what order, on which thread, triggered by which cell or button, with what arguments, returning what, how long each took, which functions threw errors and which handled them.

It changes nothing about your workbook: no cells touched, no macros added, no VBA source read. You arm it, recalculate or run your macros, then read the trace file to see what really happened.

64-bit Excel on Windows One native DLL, no installer No .NET, no runtime GPL-3.0
One recalculation, as XRayXL recorded it

Four cells, in the order Excel calculated them rather than the order they sit on the sheet. A3 calls VbaOuter, which calls an add-in function through Application.Run; then A4, A2 and A1.

seqkindsourcemodulefunctioncallercallerrefargsretoutcometicks
1entryVBA[OneTimeline_39248.xlsm]ProbeVbaOutercellSheet1!A3a1:Double=3
2entryXLLTracedAddin64.xllTxEcellSheet1!A3a1:E=3
3exitXLLTracedAddin64.xllTxE3returned113
4exitVBA[OneTimeline_39248.xlsm]ProbeVbaOuter4returned2766
5entryXLLTracedAddin64.xllTxEcellSheet1!A4a1:E=4.5
6exitXLLTracedAddin64.xllTxE4.5returned116
7entryVBA[OneTimeline_39248.xlsm]ProbeVbaPlaincellSheet1!A2a1:Double=2
8exitVBA[OneTimeline_39248.xlsm]ProbeVbaPlain6returned181
9entryXLLTracedAddin64.xllTxEcellSheet1!A1a1:E=1.5
10exitXLLTracedAddin64.xllTxE1.5returned159

Abridged for width. The file is a plain CSV of 22 columns, and also carries input, span, parent, depth, thread, qpc, proc, typetext, argcount, rettype and trust.

Read row 1 as: the VBA function VbaOuter, in module Probe of OneTimeline_39248.xlsm, called from Sheet1!A3, declared Double, given 3. The callerref is the same address Excel's own Range.Address(,,,True) gives — paste it straight back into a formula.

What a row tells you
order

The real sequence

Every entry and exit in the order Excel actually ran them — not the order the sheet is laid out in, and not the order you assumed.

attribution

Who called it

The cell, button or event that started the chain, as a reference you can paste back into a formula. Multi-threaded recalculation is recorded per thread.

values

Arguments and returns

What went in and what came back, with declared types — so a wrong answer leads to the call that produced it, not just the cell showing it.

duration

Cost per call

Ticks from a high-resolution counter on every exit. Nested and recursive calls each carry their own, so the expensive one is the one that reads expensive.

structure

Depth and nesting

A per-thread shadow stack supplies depth and parent, so a flat file reconstructs into a call tree.

outcome

Errors, in full

Which procedure raised, which ones the error passed through, and which one finally caught it.

VBA parameters, going in and coming back

Every VBA call records what it was actually given, decoded and typed — not a rendering of what the source says it takes. A Variant is reported by what it holds; an array carries its real bounds; an object is named by its class.

a1:String="GBP-SONIA"The declared type, where VBA's metadata names one. Strings are quoted and escaped, so the value survives the CSV field intact.
a1:Variant=Integer(42)A Variant names the type it is holding. A Double is written bare; everything else is named.
a1:Variant=Variant[1..2,1..2]{{1,"x"},{2,TRUE}}An array keeps its real bounds and nests rows first, mixed element types and all.
a1:Variant=Range@0x000001E2…('[Book1]Sheet1'!A1:C2)=Variant[1..2,1..3]{{11,12,13},{21,22,23}}An object is named by its class and its address is kept, so one object can be followed from row to row. A Range is addressed and its contents read.
a1:Variant=#DIV/0!A worksheet error is spelt the way Excel spells it, so a bad value can be matched to the cell showing it.
a2:Variant=MissingAn omitted optional argument is recorded as omitted, rather than as whatever was left in the slot.

The number is the frame slot, not the parameter ordinal — a ByVal Variant occupies three, so the parameter after one reads a4, while argcount still counts arguments.

timing

Read before the body runs

When a frame opens, the arguments are still sitting in the interpreter's working memory. XRayXL reads them there — before the procedure's first statement has a chance to overwrite them.

byref

Re-read on the way out

A by-reference parameter is noted at the entry and read again at the exit, and the exit row carries it only if it changed. A row with arguments on it is saying "these moved".

byval

ByVal is never reported at an exit

The callee's copy may well differ by then, but the caller never sees it. An "after" value would assert an effect that does not exist, so none is written.

Where VBA's metadata does not name a type, a ? marks the spot rather than a guess, and a slot that decodes to nothing truthful is written out as its raw bytes — never a coerced value.

An error, read outwards from the throw
entryE_Outerdepth 1 · parent 0
entryE_Middledepth 2 · parent 1
entryE_Throwerdepth 3 · parent 2
exitE_Throwerthrew4657 ticks
exitE_Middleunwound6585 ticks
exitE_Outerhandled10111 ticks
threwThe procedure that raised the error.
unwoundThe error passed through it — it ran nothing after the raise, so it did not handle it.
handledThe one that caught it. Everything else reads returned.
How it works — two tracers, one output
XLL

Add-in functions

Excel's registration table names every registered add-in function and its address — a documented API. XRayXL wraps each one with an inline detour that records the entry, calls the original, and records the exit.

Functions registered after arming are picked up by watching Excel's registration callback. Nothing is derived; nothing is guessed.

VBA

VBA procedures

The VBA interpreter reaches every opcode handler through one table of function pointers inside VBE7.DLL. XRayXL finds that table by its shape — not by a byte signature — and swaps a handful of entries for its own.

So it sees each procedure start and stop without touching a byte of code. The calling cell comes from asking Excel.

Because the interpreter hook is global, VBA outside the calculation engine is traced too — macros, buttons and event handlers.

Scope, stated plainly

Traced

  • XLL functions from any add-in, including ones registered after arming
  • VBA procedures — nested, recursive, and across threads
  • VBA outside the calculation engine: macros, buttons, event handlers
  • Errors: where they were thrown, what they unwound through, who caught them

Not traced

  • Excel's own built-in functions — SUM, XLOOKUP and the rest. XRayXL follows add-in and VBA code, not the calculation engine's internals
  • COM and RTD add-ins. =RTD(...) is a COM mechanism rather than the XLL C API
  • JavaScript custom functions from Office Add-ins, which run in their own runtime rather than as XLL exports or VBA p-code
  • Asynchronous XLL functions are marked in the trace but carry no duration — they return before their answer exists
Before you run it

Honest caveats, in roughly the order they will matter to you.

It hooks Excel's internalsIt patches function entry points in a live Excel process, using undocumented details Microsoft can change in any update. Every hook is fault-guarded and the VBA hooks stand down after repeated faults — but the blast radius of a bug is somebody's Excel.
Antivirus and EDR will plausibly noticeInline hooking of excel.exe is malware-shaped behaviour. On a managed or corporate machine, expect a conversation with whoever owns security before it runs at all.
Check it against your licence termsInstrumenting Office internals may sit in tension with the Office licence. If in doubt, ask legal or compliance before running it.
The trace file contains your dataArgument values, return values, cell references and function names all land in it, in the clear. It is a plain file in %TEMP% — treat it as sensitive as the spreadsheet itself.
64-bit Excel onlyThe two hooking thunks are hand-written x64 assembly, and the argument and return-value decoders read the x64 calling convention directly. 32-bit is a port and a re-measurement, not a build flag.
Durability across Excel updates is unprovenNothing is hardcoded — every address is derived at runtime, which is designed to survive an update. If one breaks, the add-in refuses to arm and says so, rather than producing a wrong trace.
It leaves nothing behindRibbon controls can only be served to a COM add-in, so XRayXL writes three registry entries under HKEY_CURRENT_USER, connects itself, and deletes all three immediately.
Getting started

Download it

No build required. dist/ is committed, so the latest release — or a plain clone — already contains a working tool, plus two demo add-ins and a guided tour in nine workbooks.

Point Excel at it

Drag XRayXL64.xll onto an open Excel window for that session, or add it permanently through File → Options → Add-ins. An XRayXL group appears on the Developer tab.

Arm, run, disarm

Press Arm, recalculate or run your macros, press Disarm. Each button is also a registered command, so a session can be driven from VBA or any automation client with no window and no focus.

Excel's Developer tab, with an XRayXL group added at the far right holding three buttons: Arm, Disarm and Options. Disarm is greyed out because no session is running.
The XRayXL group, at the far right of the Developer tab. Three buttons — Arm, Disarm, Options — and Disarm stays greyed until a session is running. Excel hides the Developer tab by default, and the group appears once a workbook is open rather than on the start screen.
The XRayXL Options dialog, open on its Capture page. XLL add-in functions and VBA procedures each have a Depth dropdown set to All calls, with tickboxes for capturing argument values and return values, and for VBA a further tickbox for describing Range, Worksheet and Workbook objects.
Options → Capture. Each source carries its own depth — every call, or only the one Excel called directly — and its own switches for argument values, return values and object description.Everything is on by default, and the defaults suit most sessions. Drop argument or object capture when you want tighter timings.The settings are read once, at arm, so they grey out until you disarm.
trace → %TEMP%\XRayXL\TraceFiles\XRayXL_Trace_<id>_<pid>.csv
log   → %TEMP%\XRayXL\Logs\XRayXL_<pid>.log