14.8. Breakpoints

The Breakpoints section lists every breakpoint the debugger knows about, with two columns:

Rows whose source file no longer exists on disk are drawn with a warning icon, gray text, and a disabled Active checkbox; their tooltip reads File not found: <path> so you can spot stale entries that the loader will never resolve.

Breakpoints are created and removed in several ways:

Looking for conditional breakpoints, hit counts, or logpoints? Edit the Location cell of a breakpoint row to attach any of those — see Section 14.8.1, “Conditions, hit counts, and logpoints”.

Double-clicking a row in the Breakpoints table opens the corresponding file in the editor and jumps to the line.

The section header carries four small controls to the right of the title rule, in left-to-right order:

The Breakpoints table supports Ctrl / Shift click for multi-row selection. A right-click context menu groups its entries by scope — first the actions that target the row under the cursor, then the actions that target the whole table:

Breakpoints survive Wireshark restarts. Adding a breakpoint automatically turns the debugger on if it was off — running with no line hook cannot trigger a pause.

14.8.1. Conditions, hit counts, and logpoints

The Location cell of a breakpoint row is editable (double-click, F2, or right-click → Edit) and lets you attach one of three extras to the breakpoint. A white core inside the breakpoint dot — in the Breakpoints list and in the editor gutter — marks rows that carry an extra:

  • Expression — a Lua expression evaluated each time control reaches the line. The breakpoint pauses only when the expression is truthy in the current frame; locals, upvalues, and globals are visible exactly as they are in Watch / Evaluate. Runtime errors are treated as false (no pause) and surface as a warning icon on the row.
  • Hit Count — gate the pause on a hit-counter. 0 disables the gate. A small dropdown next to the integer picks the comparison mode:

    from

    Pause on every hit from N onwards — inclusive of the N-th hit (default; "skip the first N-1, then pause forever").

    every

    Pause on hits N, 2N, 3N, … — sample a noisy loop without stopping on every iteration.

    once

    One-shot. Pause on the N-th hit and deactivate the breakpoint so subsequent hits go through. The slot is consumed the moment the counter reaches N, even if the row’s Expression evaluates falsy or the row is a logpoint that emits and resumes — the row deactivates either way. The row’s Active checkbox visibly clears AND the runtime counter resets to zero in the same step, so re-ticking Active is enough to arm the next N-hit cycle without a separate Reset Hit Count.

    The counter is per breakpoint and is not persisted across Wireshark restarts.

  • Log Message — a template that is written to the Evaluate output (and to Wireshark’s debug log) each time the breakpoint fires — that is, after the Hit Count gate and any Expression allow it. By default execution continues without pausing; click the pause toggle on the editor row (the icon next to the Log Message field) to also pause after emitting (useful for "log-then-inspect" without duplicating the breakpoint). The line is emitted verbatim — there is no automatic file:line prefix; include the origin via the tags below if you want it.

The Log Message template uses {…​} placeholders. {{ and }} produce literal { / }. Anything inside {} that is not one of the reserved tags below is evaluated as a Lua expression in the paused frame and converted to text with the same coercion tostring() performs; per-placeholder evaluation errors substitute <error: …​> without aborting the line.

The reserved tags below shadow any same-named Lua local / upvalue / global. To log a Lua variable that happens to share a name with a tag, wrap it: {tostring(filename)}.

Origin

{filename}

Source file path (canonicalized).

{basename}

Last path component of {filename}, e.g. printer.lua. Empty when the chunk has no on-disk path.

{line}

1-based line number of the breakpoint.

{function}

Running function’s name (debug.getinfo n), or ? for anonymous, tail, and main-chunk frames.

{what}

Frame kind: Lua, C, main, or tail.

Counters and scope

{hits}

This breakpoint’s cumulative hit counter — the number of times the line has been reached since the row was created or the counter was last reset. The counter advances on every line hit, including hits skipped by the hit-count gate or a falsy Expression; the log line itself only emits on hits the gates pass, so the value rendered is the counter at the firing moment (e.g. every 100 shows 100, 200, 300, …).

{depth}

Lua-frame stack depth at the fire site.

{thread}

main for the main thread, coro@<ptr> for coroutines (the pointer is stable per coroutine within a session).

Time

{timestamp}

Local wall-clock HH:MM:SS.mmm at fire time.

{datetime}

Local YYYY-MM-DD HH:MM:SS.mmm at fire time.

{epoch}

Unix time, seconds with millisecond fraction.

{epoch_ms}

Unix time as integer milliseconds.

{elapsed}

Milliseconds since the debugger was last attached.

{delta}

Milliseconds since this breakpoint last fired (0 on the first fire and after Reset Hit Count / Reset All Hit Counts).

14.8.1.1. Examples

The mini-blocks below resolve against the names defined by the printer.lua postdissector in Section 14.3, “Getting Started”: locals tvb, pinfo, tree, offset inside printer.dissector, upvalues sessions, last_seen, opcode_names, handshake, ip_src_F, tcp_flags_F. Drop a breakpoint on any line of printer.dissector and edit its Location cell to attach the extras shown.

Expression — gate the pause on a Lua predicate.

-- Pause only after frame 100:
pinfo.number > 100

-- Only on the first dissection of a frame:
not pinfo.visited

-- Only TLS in either direction:
pinfo.dst_port == 443 or pinfo.src_port == 443

-- Frames whose IP source we have seen before:
last_seen[tostring(pinfo.src)] ~= nil

-- Bail-out invariant: pause if a length assumption breaks:
tvb:len() < 20

A runtime error inside the expression is treated as false and shows a warning icon on the row, so a typo or a nil index silently disables the pause instead of stopping execution. See Section 14.6.3, “Expression patterns” for the full menu of shapes the same evaluator accepts.

Hit Count — gate the pause on a counter.

The integer field carries N; the dropdown next to it picks the comparison mode. The block below sketches the three shapes the gate can take (each line reads as "mode N" — the mode is the dropdown selection, N is what you type into the integer field):

from 10   -- ignore the first 9 hits, pause from hit 10 on
             (default mode; "skip the warm-up")
every 100 -- sample a tight per-packet loop: pause on hits
             100, 200, 300, …
once 5    -- one-shot debug: pause on hit 5 and clear the
             row's Active checkbox; tick it again to re-arm
0         -- disable the gate (any mode); same as clearing
             the field

The counter is per breakpoint and per Wireshark session. It is preserved across edits to the Expression, the Hit Count target or mode, and the Log Message — tuning the threshold or switching from from to every mid-run will not throw away the count you were watching, with one exception: lowering the target below the current counter rolls the counter back to 0 so the breakpoint can resume waiting for "the next N hits" instead of pausing on every line. Right-click the row and choose Reset Hit Count — or Reset All Hit Counts on the empty area of the table — to zero it explicitly. The counter is not persisted across Wireshark restarts. Hit Count, Expression, and Log Message on the same row run in a strict order: hit-count → ExpressionLog Message. The hit-count gate runs first; only when it passes is the Expression evaluated; only when the Expression is truthy (or absent) does the Log Message emit and the row pause. So a logpoint with a condition stays silent while the condition is false, and an every 100 logpoint with tvb:len() > 1500 only logs on the matching every-hundredth packet.

Log Message — show where the line hit.

{filename}:{line}
{basename}:{line} in {function}
{filename}:{line} ({what}) hits={hits} depth={depth}
[{thread}] {function}:{line}

{basename} is the readable form of {filename} — the script’s filename without its directory, useful when most logs come from one or two scripts and the absolute path is just noise. {function} is ? for anonymous, tail, and main-chunk frames; {what} (Lua / C / main / tail) disambiguates them. {thread} is main for the main coroutine and coro@<ptr> otherwise — handy when a dissector is called from a tap callback that runs on its own coroutine.

Log Message — wall-clock and intervals.

[{timestamp}] hit
[{datetime}] entered {function}
{epoch_ms} -- frame seen, ms since the Unix epoch
{elapsed} ms since the debugger was attached
loop iteration {hits} took {delta} ms since the previous fire

Use {timestamp} for short interactive sessions and {datetime} for long-running captures where you may cross midnight. {delta} reports 0 on the very first fire (and after Reset Hit Count / Reset All Hit Counts), so a "took 0 ms" line in the log marks a fresh counter rather than an instant fire.

Log Message — mixing tags with Lua expressions.

Anything inside {} that isn’t a reserved tag is evaluated as a Lua expression in the paused frame and converted to text with the same coercion tostring() performs, just like a Watch row.

frame {pinfo.number}: {tostring(pinfo.src)} -> {tostring(pinfo.dst)}
bytes_left={tvb:len() - offset} at {filename}:{line}
flags=0x{string.format("%02x", tvb:range(13, 1):uint())}
{tostring(pinfo.src)} last seen at frame {last_seen[tostring(pinfo.src)] or "never"}
{datetime} hit #{hits}: ports {pinfo.src_port}->{pinfo.dst_port}

Wireshark userdata classes (Address, Pinfo, Tvb, …) expose their canonical text via __tostring, so wrap them in tostring(…​) instead of relying on a :tostring() method (the class doesn’t have one). { is the only character that needs escaping; {{ and }} produce literal braces:

state = {{ src={tostring(pinfo.src)}, dst={tostring(pinfo.dst)} }}

A per-placeholder runtime error becomes <error: …> inline, so one bad expression in a long template doesn’t drop the rest of the line. Edit the row’s Location cell to clear the bad placeholder.

[Note]Logpoints on every packet of a large capture

A logpoint that matches every packet of a multi-thousand-frame capture can fire thousands of times per second. The debugger coalesces those fires onto the GUI thread in a single drain per event-loop tick, and the Evaluate output keeps only the last ~5000 lines (older lines are evicted as new ones arrive), so the dialog stays responsive — but each fire still runs the template formatter on the Lua thread.

To keep per-packet logpoints cheap on big captures:

  • Throttle with Hit Count: pick every N (e.g. every 100) to sample one out of every N matches instead of all of them.
  • Filter with Expression: a predicate like pinfo.dst_port == 443 or tvb:len() > 1500 skips the format-and-emit path entirely on every non-matching frame.
  • Keep templates lean: {depth} walks the Lua call stack and {thread} queries the running coroutine on every fire. Tags the template doesn’t reference are skipped, so omit ones you don’t need.