XRayXLDocs · 0.4.0

XRayXL — Demo

Ten workbooks that tour XRayXL a feature at a time. Each one tells you, on the sheet, what to press and what to look for in the trace. This page walks through the same tour with real rows from a real run.

64-bit Excel required. XRayXL traces 64-bit Excel only.

What's in here

dist/
  demo/
    01_FirstTrace.xlsm      - VBA and XLL functions in one recalculation, and how to read a row.
    02_Values.xlsm          - Ranges, arrays, Variants and their types.
    03_Callers.xlsm         - What started each call: a cell, an array formula, an event, a button, a timer.
    04_Errors.xlsm          - How calls end: threw, unwound, handled and unhandled, and error values.
    05_VBACurves.xlsm       - A real VBA model, a yield-curve bootstrap, traced end to end.
    06_Objects.xlsm         - A class module's members, and a Range, Worksheet and Workbook as arguments.
    07_CallTree.xlsm        - Recursion, a ByRef argument changing, and End.
    08_Threads.xlsm         - A thread-safe XLL on several calculation threads, and VBA on one.
    09_XLLPricing.xlsm      - An option book built from XLL functions: text, ranges, arrays and references.
    10_Plasma.xlsm          - A plasma painted two ways, cell by cell and in one call, to read in Perfetto.
    DemoBehaviors64.xll     - A demo add-in: ReverseText, MakeSeries, MightDivide, SafeDivide, ThreadSafeSquare,
                              CallCounter.
    DemoFinance64.xll       - A demo add-in: BlackScholes, PresentValue, CompoundReturn, SlowSum, Fibonacci,
                              NetPresentValue, DiscountCurve, WeightedAverage, RangeSize, QuoteLookup.
  docs/                     - This walkthrough and the other guides, to open in a browser.
  LICENSE                   - The licence: GPL-3.0.
  MANIFEST.txt              - The version, tag, commit and SHA256 of every file here.
  THIRD-PARTY-NOTICES.txt   - The licences of the third-party code in XRayXL.
  XRayXL-Perfetto.html      - Send your trace file to Perfetto.
  XRayXL64.xll              - The add-in.

The two demo add-ins are ordinary XLLs that know nothing of XRayXL. They are what you point the tracer at.

Setting up

  1. Load the add-ins. Use File ▸ Open on XRayXL64.xll — in dist\ of a clone, or where you extracted the zip — then on DemoFinance64.xll and DemoBehaviors64.xll in demo\ beside it. Opening an .xll loads it for this Excel session. To load them every time Excel starts, add them through File ▸ Options ▸ Add-ins ▸ Manage: Excel Add-ins ▸ Go ▸ Browse instead.
  2. Show the Developer tab if it is hidden: File ▸ Options ▸ Customize Ribbon, and tick Developer. XRayXL's group is on that tab.
  3. Open a workbook and enable macros.

The XRayXL group on the Developer tab has six buttons:

The XRayXL group at the far right of Excel's Developer tab, with Arm, Disarm,
Tail, Perfetto, Options and Diag buttons

Disarm is greyed until a recording is running, and Perfetto until one has finished.

Button What it does
Arm Starts a recording. Every XLL function and VBA procedure that runs from now on writes rows to a new trace file
Disarm Stops the recording and closes the file
Tail Opens PowerShell on the trace file and shows each row as it is written. Live from the first arm
Perfetto Shows the last trace on a timeline, in Perfetto UI in your browser. Live after Disarm. See On a timeline
Options The settings: what to capture, where the trace goes and in what format, the buffer and the log
Diag Diagnostics: what is loaded into this Excel: its modules and their versions, the environment and the process. Read-only

The buttons appear once a workbook is open. Expect your antivirus to take an interest in XRayXL64.xll: it hooks Excel's internals, which is what it is for.

The loop

Every demo is the same four steps:

  1. Arm.
  2. Do the thing the sheet says: recalculate, edit a cell, or press a button.
  3. Disarm.
  4. Read the trace. Open Options ▸ Output, right-click Trace file and choose Reveal in File Explorer. It is a CSV, so Excel opens it. Or press Perfetto to see it on a timeline.

To watch rows arrive while you work, press Tail on the ribbon after step 1.

Traces are written to %TEMP%\XRayXL\TraceFiles\, one file per arm, named XRayXL_Trace_<id>_<pid>.csv. XRayXL writes nothing to your workbook: no cells, no VBA, no names.


01 — Your first trace

Try it: Arm, press Ctrl+Alt+F9 to recalculate everything, then Disarm.

Five cells: two VBA functions and three XLL functions from DemoFinance.

Cell Formula What it is
B11 =RiskWeighted(1000000,3) VBA, calling a VBA helper WeightFor
B12 =OptionBook(100) VBA, calling the XLL BlackScholes through Application.Run
B13 =BlackScholes(100,100,1,0.05,0.2) XLL
B14 =SlowSum(2,3) a deliberately slow XLL
B15 =Fibonacci(30) a busy XLL

The trace has fourteen rows. Here they are with the most useful columns:

seq kind  source span parent depth function      callerref  args                                         ret               ticks
1   entry XLL    1    0      1     BlackScholes  ...!B13    a1:B=100 a2:B=100 a3:B=1 a4:B=0.05 a5:B=0.2
2   exit  XLL    1    0      1     BlackScholes                                                         10.4505835721856    477
3   entry XLL    2    0      1     Fibonacci     ...!B15    a1:B=30
4   exit  XLL    2    0      1     Fibonacci                                                            832040            34952
5   entry VBA    3    0      1     OptionBook    ...!B12    a1:Double=100
6   entry XLL    4    0      1     BlackScholes  ...!B12    a1:B=100 a2:B=100 a3:B=1 a4:B=0.05 a5:B=0.2
7   exit  XLL    4    0      1     BlackScholes                                                         10.4505835721856    162
8   exit  VBA    3    0      1     OptionBook                                                           1045.05835721856   2815
9   entry XLL    5    0      1     SlowSum       ...!B14    a1:B=2 a2:B=3
10  exit  XLL    5    0      1     SlowSum                                                              5                432132
11  entry VBA    6    0      1     RiskWeighted  ...!B11    a1:Double=1000000 a2:Long=3
12  entry VBA    7    6      2     WeightFor     ...!B11    a1:Long=3
13  exit  VBA    7    6      2     WeightFor                                                            1                   243
14  exit  VBA    6    0      1     RiskWeighted                                                         1000000             742

... stands for '[01_FirstTrace.xlsm]First trace'. Your ticks will differ.

Reading a row

Every call writes two rows: an entry when it starts and an exit when it ends. The two share a span number, which is how you pair them.

Column Says
kind entry or exit
source XLL for an add-in function, VBA for a VBA procedure
span this call's number. Its entry and exit rows share it
parent, depth where the call sits: parent is the span of the call that made it, 0 at the top
module, function the add-in file or VBA module, and the function's name
typetext the parameter types: the XLL's registration codes (B is a number), or VBA's declared types
caller, callerref what started the call. Here every caller is a cell, and callerref is its address
args on the entry row, the values going in, each as aN:type=value: a1 is the first argument (on VBA rows N counts frame slots, so it can skip; see the row model)
ret, rettype on the exit row, the value that came back and its type
outcome how it ended. Everything here returned. Demo 04 shows the rest
ticks how long it took, in QueryPerformanceCounter ticks. On most machines that is 10,000 to a millisecond
trust whether ticks is exact (exit) or an upper bound. See demo 04

The full list is in the row model.

What to notice

The Options dialog

Open Options from the ribbon:

The XRayXL Options dialog on its Capture page: XLL add-in functions and VBA
procedures each with a depth dropdown set to All calls, tickboxes for capturing
argument and return values, and for VBA a tickbox for describing objects

Page What is on it
Capture For XLL and VBA separately: the depth (off, the top-level call only, or everything), whether to capture argument and return values, and for VBA whether to describe objects such as a Range
Events Which of Excel's own events to record, each with its own tick box, or a preset: None, Calc, Selection, Calc & Selection, All. Calc is the default. Demo 03 shows them
Output The format (CSV or JSON Lines), the output folder, and the current trace file
Advanced The buffer between Excel and the file, what to do when it fills, and the log level and log file
About, Notices The version, the build time and commit, the licence and third-party notices

Settings are read when a recording starts, so they are locked while you are armed. Everything is on by default. To get the most accurate timings, turn off argument and return values: capturing them is work done inside the call being timed.

On a timeline

After Disarm, press Perfetto. Your browser opens XRayXL-Perfetto.html, the page that sits beside XRayXL64.xll, and it has already read the trace: it shows how many calls, threads and errors it holds, and anything to watch for, such as dropped rows. Press Open Trace in Perfetto. Perfetto opens in a new tab and asks whether to trust "An unknown origin". That is the page, so answer Yes.

The page the Perfetto button opens, here holding demo 10's trace: the file, a summary
of what is in it, and the buttons to open it in Perfetto or download it

In Perfetto (demo 10 has a picture of it):

Opened on its own, the same page takes any trace file, CSV or JSON Lines: press Input XRayXL Trace, or drop the file on it. That is also the way for a trace over 256 MB, which the button refuses.


02 — Arguments and values

Try it: Arm, press Ctrl+Alt+F9, then Disarm.

This workbook is about the args and ret columns: how a range, an array or a Variant is written, so that you can read it back.

function     args / ret
CellCount    a1:Variant=Range@0x23E81224570([02_Values.xlsm]Values!B23:D25)=Variant[1..3,1..3]{{1.5,"text",TRUE},{42,#N/A,Empty},{-2.25,"more",46284}}
MixedTypes   ret Variant[0..6]{Integer(1),2.5,Long(3),Currency(1.5000),Date(46284),"text",TRUE}
TimesTable   ret Long[1..3,1..3]{{1,2,3},{2,4,6},{3,6,9}}
Total        a1:Ref&=Double[1..5]{1,4,9,16,25}
MakeSeries   ret Variant[1..1,1..4]{{1,4,9,16}}

The grammar

An array is written as its element type, its bounds, then its values:

Long[1..3,1..3]{{1,2,3},{2,4,6},{3,6,9}}
^    ^         ^
|    |         the values: one brace level per dimension, row 1 first
|    the bounds of each dimension, as declared (1..3, 0..6, ...)
the element type

There is no truncation: every element is written. The one limit is a single value whose text would pass 4 MB. It is written as its header alone, and the log counts it at disarm.

Declared or recognised

Total's argument reads a1:Ref&=..., where you might have expected a1:Double(). SumOfSquares passes its array to Total(ByRef squares() As Double), and the compiled VBA says only "a reference to eight bytes" for that parameter: Ref&. The tracer then recognised the array from its own descriptor, which names its element type (Double) and bounds (1..5).

A type after a1: is declared when VBA's compiled code names it, and recognised when the tracer worked it out from the value itself, under strict checks. Most of the time XRayXL finds a parameter's type: every argument in demo 05's recalculation has one. A ? marks the rare one it could not. How XRayXL knows a VBA argument's type explains each form.


03 — Who called it

Try it: Arm, then in turn:

  1. Press Ctrl+Alt+F9.
  2. Type a number into the blue Trigger cell.
  3. Press Run a macro.
  4. Press Start a timer and wait a second.

Then Disarm.

The caller and callerref columns say what started each call:

function          caller  callerref                          parent depth
ThreeOf           cell    [03_Callers.xlsm]Callers!B12:D12   0      1
Twice             cell    [03_Callers.xlsm]Callers!B11       0      1
CallCounter       cell    [03_Callers.xlsm]Callers!B13       0      1     <- the recalculation
CallCounter       cell    [03_Callers.xlsm]Callers!B13       0      1     <- again, after the edit
Worksheet_Change  none    ref                                0      1
  Helper          none    ref                                5      2
FromTheButton     name    RunAMacro                          0      1
  Helper          name    RunAMacro                          7      2
StartTimer        name    StartATimer                        0      1
TimerTick         none    ref                                0      1
  Helper          none    ref                                10     2
What started it caller callerref
A formula in one cell cell the cell, ...!B11
An array formula over several cells cell the whole range, ...!B12:D12
A button or shape name the button's name, RunAMacro
An event, or a macro run by Application.OnTime none ref: Excel reported no calling cell

What to notice:

Excel's own events

Between the calls, the trace also records the events Excel raises, one event row each. These are the rows from the recalculation and the edit, with the first calls left out:

seq kind  source function          callerref                      args
1   event XRayXL arm                                             qpcFrequency=10000000 utc="2026-09-27T10:04:45.5364303Z" pid=5976 XLL_DEPTH="ALL" ...
6   entry XLL    CallCounter       ...!B13
7   exit  XLL    CallCounter
8   event Excel  SheetCalculate    [03_Callers.xlsm]Callers       Sh:Object=Worksheet@0x27B83E35C80([03_Callers.xlsm]Callers)
9   event Excel  AfterCalculate
10  entry XLL    CallCounter       ...!B13                        <- the edit's recalculation
11  exit  XLL    CallCounter
12  event Excel  SheetCalculate    [03_Callers.xlsm]Callers       Sh:Object=Worksheet@0x27B83E35C80(...)
13  event Excel  AfterCalculate
14  entry VBA    Worksheet_Change  ref                            a1:Object=Range@0x27B83E34FB0([03_Callers.xlsm]Callers!B14)=7
15  entry VBA    Helper            ref                            a1:String="change event"
16  exit  VBA    Helper
17  exit  VBA    Worksheet_Change
18  event Excel  SheetChange       [03_Callers.xlsm]Callers!B14   Sh:Object=Worksheet@0x27B83E35C80(...) Target:Range=Range@0x27B83E34FB0([03_Callers.xlsm]Callers!B14)=7
19  event XRayXL disarm                                          rowsDropped=0 pauses=0 ... eventsRecorded=5 eventsLost=0

In Perfetto, events are markers on the thread that raised them, among the calls.


04 — Errors and how calls end

Try it: Arm, press Ctrl+Alt+F9, press Run the error chain, then Disarm.

The outcome column says how each call ended: returned, threw, unwound, handled, unhandled or abandoned. ErrorsInTheTrace.md explains each one, and the cases where VBA stops at its error dialog instead of passing an error on.

An error passed up a chain

Run the error chain runs Outer, which calls Middle, which calls Thrower. Thrower raises an error, Middle does not catch it, and Outer does:

function       span parent depth ret  outcome   trust
RunErrorChain  9    0      1          returned  exit
  Outer        10   9      2          handled   exit
    Middle     11   10     3          unwound   backstop
      Thrower  12   11     4          threw     backstop

(Exit rows, reordered to show the chain.) Read it from the bottom up:

trust=backstop means the procedure left without running its normal exit, so the tracer closed it at the next thing it saw. The ticks on such a row is an upper bound, not an exact reading.

Errors in worksheet functions

function     args                             ret      outcome
SafeRatio    a1:Double=1 a2:Double=0          "n/a"    handled
  Divide     a1:Double=1 a2:Double=0                   threw
RawRatio     a1:Double=1 a2:Double=0                   unhandled
  Divide     a1:Double=1 a2:Double=0                   threw
CodeFor      a1:String="GBP"                  #N/A     returned
Scaled       a1:Double=10 a2:Variant=Missing  10       returned
MightDivide  a1:B=10 a2:B=0                   #DIV/0!  returned
SafeDivide   a1:B=10 a2:B=0                   0        returned

05 — A real model: VBACurves

05_VBACurves.xlsm is a working VBA model rather than a demo sheet: a USD SOFR curve bootstrapped from deposits, futures and swaps on Market Data, and zero rates, forward rates and a chart on Curve Analytics. It is one module of ordinary VBA, with no add-in calls.

Try it: Arm, press Ctrl+Alt+F9, then Disarm.

One full recalculation is about 5,350 calls: 10,710 rows, 3.3 MB of CSV. The busiest functions:

calls  function
  973  InterpDF
  969  CurveDF
  852  Demo_AddMonths
  776  Demo_YearFrac
  776  Demo_BasisCode
  224  HandleToIndex

Finding your way around

The same trace as JSON Lines

Open Options ▸ Output, set Format to JSON Lines, then Arm, recalculate and Disarm again. The file is now XRayXL_Trace_<id>_<pid>.jsonl, one JSON object per line, with the same fields as the CSV columns:

{"seq":1,"input":1,"kind":"entry","source":"VBA","span":1,"parent":0,"depth":1,"thread":49172,"qpc":3981638123735,"module":"[05_VBACurves.xlsm]Module1","function":"Demo_TenorNum","proc":"0x18A81C414BC","typetext":"String","caller":"cell","callerref":"'[05_VBACurves.xlsm]Market Data'!C25","argcount":1,"args":[{"slot":1,"type":"String","value":{"t":"String","v":"6M"}}]}
{"seq":4,"input":4,"kind":"exit","source":"VBA","span":1,"parent":0,"depth":1,"thread":49172,"qpc":3981638124839,"module":"[05_VBACurves.xlsm]Module1","function":"Demo_TenorNum","proc":"0x18A81C414BC","ret":{"t":"Long","v":6},"rettype":"Long","outcome":"returned","ticks":1104,"trust":"exit"}

Every value is structured and names its type, including a Double. An array keeps its element type and bounds, and its values are nested one list per dimension:

{"slot":1,"type":"Ref&","value":{"t":"Array","elem":"Double","bounds":[[0,400]],"v":[0,0.0191780821917808,0.0876712328767123,...]}}

That makes JSON Lines the format to choose when a program will read the trace. The price is size: this recalculation is 5.8 MB as JSON Lines, against 3.3 MB of CSV. JSON Lines has the full schema. Set the format back to CSV when you are done.


06 — Objects and classes

Try it: Arm, press Ctrl+Alt+F9, press Describe the objects, then Disarm.

PositionValue(100,2.5) creates a Position from a class module, sets its quantity, values it and lets it go. Every class member is a call of its own:

span parent depth module                      function          args            ret
1    0      1     [06_Objects.xlsm]Objects    PositionValue     a1:Double=100 a2:Double=2.5
2    1      2     [06_Objects.xlsm]Position   Class_Initialize
3    1      2     [06_Objects.xlsm]Position   Qty               a1:Double=100
4    1      2     [06_Objects.xlsm]Position   Value             a1:Double=2.5   250
     ...PositionValue's exit row, ret 250...
5    0      1     [06_Objects.xlsm]Position   Class_Terminate

Excel objects as arguments. With Describe objects on (Options ▸ Capture, the default), an object argument is named and, for a Range, Worksheet, Workbook, Collection or Dictionary, described:

RangeInfo      a1:Object=Range@0x228072FDBE0([06_Objects.xlsm]Objects!B17:C18)=Variant[1..2,1..2]{{1.5,"a"},{2.5,"b"}}
Describe       a1:Object=Worksheet@0x2280464D740([06_Objects.xlsm]Objects)
Describe       a1:Object=Workbook@0x2280464A040([06_Objects.xlsm])
Describe       a1:Object=Collection@0x2281D2B2EF0=Variant[1..2]{"a",Long(2)}
FirstDataCell  ret Range@0x228072FBD20([06_Objects.xlsm]Objects!B17)=1.5

The class comes from the object itself, so a parameter declared As Object still reads Worksheet. A Range shows its address and then its cells, and a single cell is a plain value. A Collection shows its items as a 1-D array; a Dictionary would show one {key,item} row per entry, as a 2-D array. The address after @ lets you follow one object from row to row. Turning Describe objects off leaves only object@0x…, which also stops the tracer asking Excel anything during the call.


07 — The call tree

Try it: Arm, press Ctrl+Alt+F9, press Stop everything, then Disarm.

Recursion. Factorial(5) calls itself four times. Each call gets its own span, and parent points to the call above it:

span parent depth args        ret
12   0      1     a1:Long=5   120
13   12     2     a1:Long=4   24
14   13     3     a1:Long=3   6
15   14     4     a1:Long=2   2
16   15     5     a1:Long=1   1

Branching. VbaFib(4) makes nine calls. Two of them run at depth 3 with different parents, so depth alone cannot tell you which call made which, but parent can:

span parent depth args
3    0      1     a1:Long=4
4    3      2     a1:Long=3
5    4      3     a1:Long=2
6    5      4     a1:Long=1
7    5      4     a1:Long=0
8    4      3     a1:Long=1
9    3      2     a1:Long=2
10   9      3     a1:Long=1
11   9      3     a1:Long=0

A ByRef argument that changed. Clamped(150) passes a variable ByRef to ClampInPlace, which caps it at 100. The exit row carries the argument again, because it changed:

kind  function      args
entry ClampInPlace  a1:Double&=150 a2:Double=0 a3:Double=100
exit  ClampInPlace  a1:Double&=100

Double& means "a Double, passed by reference". Clamped(50) makes the same call, and its exit row has no args, because nothing changed. An exit row only lists ByRef arguments that moved.

End. Stop everything runs StopEverything → Level1 → Level2, which says End. All VBA stops at once and no procedure returns, so all three read outcome abandoned, trust end.


08 — Threads

Try it: Check that File ▸ Options ▸ Advanced ▸ Formulas ▸ Enable multi-threaded calculation is ticked. Then Arm, press Ctrl+Alt+F9, and Disarm.

Columns D and E each hold 500 formulas. ThreadSafeSquare is registered thread-safe (rettype Q$, the $), so Excel spreads it across its calculation threads. VbaSquare is VBA, which Excel always runs on its main thread. Counting entry rows by thread:

calls  function          thread
  43   ThreadSafeSquare  36940
  43   ThreadSafeSquare  57248
  ...  ten more threads, 31 to 44 calls each
 500   VbaSquare         57008
   1   ReverseText       57008

That was twelve threads, the setting on this machine. ReverseText is not thread-safe, so it ran on the main thread with the VBA. It also shows an XLL string going both ways: typetext C%, a1:C%="hello", ret "olleh".

On a multi-threaded recalculation, rows from different threads interleave in the file. The thread column separates them, and span still pairs each entry with its exit.


09 — An option book, all XLL

Try it: Arm, press Ctrl+Alt+F9, then Disarm.

A small priced portfolio with no VBA at all: quotes, ten option positions, portfolio figures, a discount curve and a price grid. It is 45 XLL calls, and each argument type reads differently:

Formula Registered In the trace
=QuoteLookup(A20,Quotes) QC%Q a1:C%="ACME" a2:Q=Variant[1..5,1..2]{{"ACME",102.5},{"BOLT",48.2},...}: the text, then the whole quotes table as values
=BlackScholes(F20,B20,C20,Rate,D20) QBBBBB a1:B=102.5 a2:B=100 ...: five numbers
=WeightedAverage(G20:G28,E20:E28) QK%K% a1:K%=Double[1..9,1..1]{{12.0999...},...} a2:K%=Double[1..9,1..1]{{10},{-5},{20},...}: plain arrays of doubles
=RangeSize(A20:H29) QU a1:U=SRef(R20C1:R29C8): a reference, named and deliberately not read; ret 80
=NetPresentValue(Rate,B37:F37) QBQ a1:B=0.05 a2:Q=Variant[1..1,1..5]{{100,100,100,100,1100}}
=DiscountCurve(Rate,B40:B44) QBQ ret Variant[1..5,1..1]{{0.9759...},{0.9523...},...}: a column in, a column out

What to notice:


10 — A plasma, painted two ways

Try it: Arm, press Start the show and watch for 20 seconds, then Disarm and press Perfetto.

Every cell on this sheet is 10 pixels square, and the 96 × 54 block in the middle is a canvas. The show draws a plasma on it, four sine waves added together with the colours cycling, for two acts of ten seconds each, with a countdown at the top left:

Demo 10 after the show: the plasma on its canvas, the countdown at 0, and the status
line saying act 1 drew 4 frames and act 2 drew 287

The maths is the same in both acts, and so are the cells. On the traced run in Perfetto below, act 1 drew 4 frames in its ten seconds, the last cut short when time ran out, and act 2 drew 245, fewer than the 287 above because the tracer was recording every call: about 2.5 seconds a frame against 40 milliseconds. Each call across to Excel costs far more than the VBA around it, and act 1 makes 5,184 of them a frame where act 2 makes one.

In Perfetto

Demo 10's trace in Perfetto UI: StartShow over two RunAct slices, act 1's wide frames
and act 2's thin ones, with part of act 1 selected and its calls summed by name below

Here 1.75 seconds of act 1 is selected, and the Pivot Table below sums it by name: 3,002 SetPixel calls took 1.5 seconds between them, and the colour maths inside them, Palette with Mix beneath it, 72 milliseconds of that. The rest is Excel colouring cells.


More