Skip to content

UVM

Runtime Customization

Altering behavior dynamically during simulation — adding and removing callbacks at run time so they are active only in a window, enabling and disabling them with callback_mode without removing them, per-phase and per-test customization, turning one reusable environment into many behaviors selected at run time without recompiling, and why runtime changes must be timed at clean boundaries.

Callbacks · Module 22 · Page 22.4

The Engineering Problem

Callbacks are registered — but when? The natural assumption is build time: you add your callbacks as the testbench is constructed, and they stay for the whole run. But that's only the static picture. Callbacks can be added, removed, enabled, and disabled during the simulation — so a customization can be active only in a window, toggled per phase, switched per test, all without recompiling. This runtime dimension is powerful: a single, reusable environment, built once, can exhibit many behaviors selected at run time by which callbacks are active when — error injection on during a stress phase and off elsewhere, a test enabling its callbacks in run_phase and disabling them at the end, a plusarg selecting a scenario in the same compiled binary. But the power comes with a hazard: changing the callback set at the wrong momentmid-transaction — can leave a half-applied customization or a stale stateful callback, producing inconsistent, non-reproducible glitches. The problem this chapter solves is runtime customization: how to add/remove and enable/disable callbacks dynamically, per phase and per test, to make one environment behave many ways — and why runtime changes must be timed at clean boundaries.

Runtime customization is altering behavior dynamically during simulation through the callback (and wider customization) mechanisms. Dynamic registration: uvm_callbacks#(T,CB)::add(...) and ::delete(...) can be called at run time (in a phase, on an event), so a callback is active only in a window — enable error injection during a stress phase, remove it after. Enable/disable: callback_mode(0/1) toggles a registered callback's execution without removing it — keep the registration, flip it off and on. Per-phase / per-test: each phase or test can have different active callbacks, so the same environment behaves differently depending on which callbacks are active when. The payoff is one reusable environment, many behaviors selected at run time — a plusarg or config selects the runtime customization (+enable_err_inject), so the same compiled testbench runs different scenarios without recompiling. The caveat is timing: adding, removing, or toggling a callback mid-transaction can leave inconsistent state, so runtime changes must be done at clean boundaries (phase boundaries, between transactions). This chapter is runtime customization: dynamic registration, enable/disable, per-phase/per-test control, one-environment-many-behaviors, and the timing discipline.

How do you customize behavior at run time — add and remove callbacks dynamically so they're active only in a window, enable and disable them with callback_mode without removing them, vary customization per phase and per test to make one environment behave many ways without recompiling, and why must runtime changes be timed at clean boundaries?

Motivation — why customize at run time, not just at build

Build-time customization is fixed for the whole run — but real verification needs behavior that varies over time. The reasons runtime customization matters:

  • Behavior should vary across the run. Error injection might be wanted only during a stress phase, logging only during a suspect window, a tweak only for part of a test. Build-time registration is all-or-nothing for the whole run; runtime control gives windows.
  • One build should serve many scenarios. Recompiling a large testbench is slow. If the same compiled binary can run different behaviors — selected by a plusarg or config at run time — regression is faster and more flexible: one build, many behaviors.
  • Per-phase and per-test behavior is natural. UVM's phases and per-test structure invite different customization at different times — a reset phase with no injection, a main phase with it; a test that enables its callbacks for its window. Runtime control aligns customization with the phase/test structure.
  • Enable/disable is lighter than add/remove. Sometimes you want a callback registered (set up once) but active only sometimestoggling it (callback_mode) is cheaper and cleaner than re-registering each time. The two controls fit different needs.
  • But timing is a hazard. A callback toggled mid-transaction can leave an inconsistent state — the customization half-applied. Runtime power requires timing discipline: change the callback set at clean points, not arbitrary moments.

The motivation, in one line: build-time customization is fixed for the whole run, but verification needs behavior that varies over timewindows of injection, one build serving many scenarios, per-phase/per-test behavior — so runtime customization (add/remove, enable/disable) makes one reusable environment exhibit many behaviors selected at run time without recompiling, with the discipline that runtime changes be timed at clean boundaries to avoid inconsistent state.

Mental Model

Hold runtime customization as hot-plugging and switching accessories while the appliance runs — one appliance does many jobs by live configuration, but you hot-plug at safe moments:

The earlier model was a sealed appliance with ports and a library of accessories you plug in. Runtime customization adds the dynamic dimension: you can plug in, unplug, and flip the switch on accessories while the appliance is running, without turning it off or rebuilding it. Want error injection only during the stress phase? Plug in the injector accessory when that phase starts and unplug it when it ends. Want a logger registered but quiet most of the time? Leave it plugged in and just flip its switch off, flipping it on only in the window you care about. The same appliance, by live configuration, does many different jobs over the course of one run — and a single setting chosen at startup (a plusarg) selects which configuration without rebuilding the appliance. The one rule of hot-plugging is timing: you don't yank an accessory out in the middle of an operation it's participating in, or you get a glitch — a half-finished action, a dangling state. You hot-plug at safe moments, between operations or at phase boundaries, when nothing is mid-flight. So runtime customization is live reconfiguration of a running, reusable appliance — powerful because one build does many behaviors, disciplined because you reconfigure only at clean moments. Recall the sealed appliance with ports and a library of accessories (Modules 22.1, 22.3). Runtime customization adds the dynamic dimension: you can plug in, unplug, and flip the switch on accessories while the appliance is running, without turning it off or rebuilding it. Want error injection only during the stress phase? Plug in the injector when that phase starts and unplug it when it ends (dynamic add/delete). Want a logger registered but quiet most of the time? Leave it plugged in and just flip its switch off (callback_mode(0)), flipping it on only in the window you care about (enable/disable). The same appliance, by live configuration, does many different jobs over one run — and a single setting chosen at startup (a plusarg) selects which configuration without rebuilding. The one rule of hot-plugging is timing: you don't yank an accessory out in the middle of an operation it's participating in, or you get a glitch — a half-finished action, a dangling state. You hot-plug at safe moments, between operations or at phase boundaries, when nothing is mid-flight. So runtime customization is live reconfiguration of a running, reusable appliancepowerful because one build does many behaviors, disciplined because you reconfigure only at clean moments.

So runtime customization is hot-plugging and switching accessories on the running appliance: plug in/unplug (dynamic add/delete — active only in a window), flip the switch (enable/disable — toggle without unplugging), and select the configuration at startup (plusarg/config — one build, many behaviors). The same appliance does many jobs by live configuration over one run — but you hot-plug at safe moments (phase boundaries, between transactions), never yanking an accessory mid-operation (or you get a glitch — a half-applied customization). Reconfigure the running appliance live to get many behaviors from one build — but only at clean moments.

Visual Explanation — the runtime customization controls

The defining picture is the controls: dynamic add/remove, enable/disable, and selection — together turning one environment into many behaviors at run time.

Runtime controls: dynamic add/remove, enable/disable, selection, yielding one env many behaviorsDynamic add / removeuvm_callbacks::add / ::delete at run time — the callback is active only in a window (plug in / unplug)uvm_callbacks::add / ::delete at run time — the callback is active only in a window (plug in / unplug)Enable / disable (callback_mode)toggle a registered callback's execution without removing it — keep the registration, flip the switchtoggle a registered callback's execution without removing it — keep the registration, flip the switchSelection (plusarg / config)a setting read at startup chooses which runtime customization applies this run — no recompilea setting read at startup chooses which runtime customization applies this run — no recompileOne environment, many behaviorsa single reusable build exhibits many behaviors selected at run time — fast, flexible regressiona single reusable build exhibits many behaviors selected at run time — fast, flexible regression
Figure 1 — the runtime customization controls turn one environment into many behaviors. Dynamic add/remove registers or deletes a callback during the run, so it's active only in a window. Enable/disable (callback_mode) toggles a registered callback's execution without removing it. Selection (a plusarg or config read at startup) chooses which runtime customization applies in this run. Together, these controls let a single reusable environment exhibit many behaviors selected at run time, without recompiling — one build, many scenarios.

The figure shows the runtime customization controls. Dynamic add/remove (the brand-colored top): uvm_callbacks::add / ::delete at run time — the callback is active only in a window (plug in / unplug). Enable/disable (callback_mode): toggle a registered callback's execution without removing it — keep the registration, flip the switch. Selection (the success-coloredplusarg / config): a setting read at startup chooses which runtime customization applies this runno recompile. Together, these yield one environment, many behaviors (the warning-colored outcome): a single reusable build exhibits many behaviors selected at run timefast, flexible regression. The crucial reading is that the three controls operate at different granularities and combine: add/remove changes what's registered (the coarsest — the callback is present or absent), enable/disable changes whether a registered callback runs (a toggle — registration kept), and selection changes which configuration is chosen (the run-level choice — at startup). Layering them gives full runtime control: select a scenario at startup (plusarg), add the relevant callbacks, enable/disable them per phase. The brand-colored add/remove and enable/disable are the dynamic mechanisms; the success-colored selection is the run-level choice; and the warning-colored one-env-many-behaviors is the payoff (warning because it's the powerful but hazard-bearing outcome — the timing discipline applies). The deep value is the decoupling of behavior from build: the behavior is no longer fixed at compile time but chosen and varied at run time, so the same compiled testbench serves many scenarios — a huge regression-efficiency win (no recompile per scenario). The diagram is the runtime control stack: add/remove + enable/disable + selection → one environment, many behaviors — the dynamic dimension of customization. Add/remove, enable/disable, and selection together turn one build into many runtime-selected behaviors.

RTL / Simulation Perspective — dynamic registration and callback_mode

In code, the runtime controls are dynamic add/delete and callback_mode — called during a phase. The example shows enabling a callback for a window and toggling it per phase.

runtime customization: add/delete in a window, callback_mode toggle, plusarg selection
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class my_test extends base_test;
  err_inject_cb err_cb;
 
  // SELECTION: a plusarg chooses the runtime customization — same build, different behavior
  task run_phase(uvm_phase phase);
    bit do_err = $test$plusargs("enable_err_inject");   // read at startup, no recompile
 
    if (do_err) begin
      err_cb = err_inject_cb::type_id::create("err_cb");
      // DYNAMIC ADD: register at run time → active from HERE (a window), not the whole run
      uvm_callbacks#(bus_driver, bus_driver_callback)::add(drv, err_cb);   // at a CLEAN point
    end
    // ... main stimulus runs, error injection active if added ...
 
    // DYNAMIC DELETE: remove when the window ends (at a clean boundary, between transactions)
    if (do_err) uvm_callbacks#(bus_driver, bus_driver_callback)::delete(drv, err_cb);
  endtask
endclass
 
// ENABLE/DISABLE: toggle a REGISTERED callback's execution without removing it (callback_mode)
//   err_cb.callback_mode(0);   // disable — registration kept, just doesn't run
//   err_cb.callback_mode(1);   // re-enable later — flip the switch back on
 
// PER-PHASE: different callbacks active in different phases (timed at phase boundaries)
//   reset_phase: no injection   |   main_phase: injection enabled   |   shutdown: disabled
 
// ✗ MISTAKE: ::delete or callback_mode(0) MID-TRANSACTION → half-applied customization, glitch (DebugLab)

The code shows the runtime controls. Selection: $test$plusargs("enable_err_inject") reads a plusarg at startup — the same build runs with or without error injection depending on the plusarg, no recompile. Dynamic add: if selected, create the err_cb and uvm_callbacks#(...)::add(drv, err_cb) at run time (in run_phase) — so the callback is active from here (a window), not the whole run, and added at a clean point. Dynamic delete: uvm_callbacks#(...)::delete(drv, err_cb) when the window ends, at a clean boundary (between transactions). Enable/disable: err_cb.callback_mode(0) disables a registered callback's execution without removing it (registration kept, just doesn't run), and callback_mode(1) re-enablesflip the switch. Per-phase: different callbacks active in different phases (no injection in reset_phase, injection in main_phase, disabled in shutdown) — timed at phase boundaries. The mistake (commented) is ::delete or callback_mode(0) mid-transaction → a half-applied customization, a glitch (the DebugLab). The shape to carry: runtime customization is dynamic add/delete (register/deregister in a window), callback_mode (toggle execution without removing), and plusarg/config selection (choose the scenario at startup) — all called during the run, timed at clean boundaries. The add/delete vs callback_mode distinction is operational: add/delete change the registration set (the callback is present or gone), while callback_mode toggles a still-registered callback (cheaper if you'll re-enable). The plusarg selection is what makes one build serve many behaviors — the behavior is chosen at run time, not compile time. The timing (// at a CLEAN point, // at a clean boundary) is the discipline — and the mistake shows what goes wrong without it. Add/delete in a window, toggle with callback_mode, select with a plusarg — all at clean boundaries.

Verification Perspective — add/remove versus enable/disable

The two runtime mechanisms — add/remove and enable/disable — are distinct tools, and choosing right matters. Seeing the difference clarifies when each fits.

Add/remove changes registration set; enable/disable toggles execution of a registered callbackpresence varies?runs only sometimes?registered or gone;re-create to returnstays registered; flip the switch; keeps statestays registered;flip the switch;…Runtime control needwhich mechanism?Add / deletechanges the registration setEnable / disable(callback_mode)toggles executionGone when deletedre-create + re-add to returnRegistered, just offkeeps state; flip back on12
Figure 2 — add/remove versus enable/disable. Add and delete change the registration set: a deleted callback is gone — not registered, not called, and must be re-created and re-added to come back. Enable and disable (callback_mode) change only whether a registered callback runs: a disabled callback stays registered, keeps its state, and is re-enabled by flipping the switch. Use add/remove when the callback's presence varies and you want it truly gone in some windows; use enable/disable when it stays registered but should run only sometimes — cheaper and stateful-friendly.

The figure shows add/remove versus enable/disable. Add and delete change the registration set: a deleted callback is gonenot registered, not called, and must be re-created and re-added to come back. Enable and disable (callback_mode) change only whether a registered callback runs: a disabled callback stays registered, keeps its state, and is re-enabled by flipping the switch. The verification insight is that these are different tools for different runtime needs. Use add/remove when the callback's presence varies and you want it truly gone in some windows — no registration, no overhead, a clean slate (e.g. a callback that shouldn't exist outside its scenario). Use enable/disable when the callback stays registered but should run only sometimescheaper (no re-registration), stateful-friendly (it keeps its accumulated state across the disabled window), and simpler to toggle (e.g. a logger you flip on and off across phases). The distinction matters for state: a disabled callback retains its internal state (counters, history) — so re-enabling it resumes where it left off; a deleted-and-re-added callback is a fresh instance (or a re-created one) — new state. So if a callback accumulates state you want preserved across a quiet window, disable it (don't delete); if you want a clean slate each window, delete and re-create. The brand-colored mechanisms map to their success-colored behaviorsgone when deleted (re-create to return) versus registered-just-off (keeps state, flip back). The common choice is enable/disable for temporary toggling (lighter, stateful) and add/remove for presence that genuinely varies (truly absent when not wanted). The diagram is the runtime-mechanism choice: presence varies → add/remove (gone when deleted); runs only sometimes → enable/disable (registered, toggled, keeps state) — match the runtime need to the mechanism. Add/remove changes whether the callback exists; enable/disable changes whether a registered callback runs — pick by presence-varies versus runs-sometimes.

Runtime / Execution Flow — per-phase customization over a run

At run time, the per-phase control is visible as a timeline: the same environment's behavior changes as callbacks are toggled across phases. The flow shows it.

Per-phase customization: off in reset, enabled in main/stress, disabled at shutdownreset phase (injection off) → main phase boundary (enable injection) → stress phase (enable extra callbacks) → shutdown (disable all) — toggled at phase boundariesreset phase (injection off) → main phase boundary (enable injection) → stress phase (enable extra callbacks) → shutdown (disable all) — toggled at phase boundaries1Reset phase: injection offno error-injection callback active — clean, correct reset behavior,not perturbed.2Main phase: enable injectionat the phase boundary, enable (or add) the injection callback —main stimulus exercised with errors.3Stress phase: enable extra callbacksenable additional perturbation callbacks for the stress window —more aggressive behavior.4Shutdown: disable alldisable (or remove) the callbacks for a clean drain — toggled atthe boundary, not mid-transaction.
Figure 3 — per-phase runtime customization over one run. In the reset phase, error injection is off — no callback active, clean reset behavior. At the main phase boundary, the injection callback is enabled (or added), so the main stimulus is exercised with errors. At the stress phase, additional callbacks are enabled for extra perturbation. At shutdown, all are disabled (or removed) for clean drain. The same reusable environment exhibits different behavior in each phase, controlled by toggling callbacks at the phase boundaries — one build, many behaviors, changed cleanly at clean points.

The flow shows per-phase runtime customization over one run. Reset phase (step 1): error injection offno callback active, clean, correct reset behavior, not perturbed. Main phase (step 2): at the phase boundary, enable (or add) the injection callback — the main stimulus is exercised with errors. Stress phase (step 3): enable additional perturbation callbacks for the stress windowmore aggressive behavior. Shutdown (step 4): disable (or remove) the callbacks for a clean draintoggled at the boundary, not mid-transaction. The runtime insight is that the same reusable environment exhibits different behavior in each phase, controlled by toggling callbacks at the phase boundaries. This is the operational meaning of "one build, many behaviors"not across runs (different builds), but within one run (different phases): the environment is reconfigured live as it moves through its phases. The phase boundaries are the clean points for the toggles — at a boundary, no transaction is mid-flight, so adding/removing/enabling a callback is safe (the DebugLab shows what happens mid-transaction). The value is aligning customization with the verification structure: reset should be un-perturbed (clean), the main/stress phases should be perturbed (exercise the corners), and shutdown should be clean (drain) — and runtime control achieves this with one environment, toggling the callbacks at the right moments. The brand-colored reset (off) and the success-colored main/stress (enabled) and default-colored shutdown (disabled) show the behavior varying across the timeline. The flow is the per-phase timeline: reset (off) → main (enable) → stress (enable more) → shutdown (disable) — the same environment, different behavior per phase, toggled at clean boundaries. The same environment is reconfigured live across phases — off in reset, enabled in main/stress, disabled at shutdown — toggled at the clean phase boundaries.

Waveform Perspective — a callback enabled only in a window

The runtime toggle is visible on a timeline: a callback is enabled only in a window (a stress phase), injecting there and not elsewhere — toggled at clean boundaries. The waveform shows error injection active only during a stress window.

Error injection is enabled only during the stress window; toggled at clean boundaries, off elsewhere

12 cycles
Error injection is enabled only during the stress window; toggled at clean boundaries, off elsewherereset/normal window: cb_enabled low → transactions T0,T1 run clean (no inject)reset/normal window: c…stress window boundary: callback ENABLED → T2,T3 are injected (inject)stress window boundary…stress window ends at a clean boundary: callback DISABLED → T4+ clean againstress window ends at …one environment, injection only in the stress window — toggled at boundariesone environment, injec…clkstress_wincb_enabledtxnT0--T1T2--T3--T4--T5--T6injectt0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — a callback active only in a runtime window. The run moves through phases. The injection callback is disabled during the reset/normal window (cb_enabled low) — no injection, clean behavior. At the stress-window boundary, the callback is enabled (cb_enabled high), so injections occur (inject) on transactions in that window. At the end of the stress window, the callback is disabled again at a clean boundary — injection stops. The toggles happen at the window boundaries, never mid-transaction, so each transaction is either fully injected or fully clean. One environment, different behavior per window, controlled at run time.

The waveform shows a callback active only in a runtime window. The run moves through phases. The injection callback is disabled during the reset/normal window (cb_enabled low) — no injection, clean behavior (T0, T1 run clean). At the stress-window boundary, the callback is enabled (cb_enabled high), so injections occur (inject) on transactions in that window (T2, T3 injected). At the end of the stress window, the callback is disabled again at a clean boundaryinjection stops (T4+ clean). The crucial reading is the windowed behavior with boundary-aligned toggles: the same environment injects errors only during the stress window and runs clean elsewhere, and the toggles (cb_enabled rising and falling) happen at the window boundariesnever mid-transaction. Because the toggles are at clean boundaries, each transaction is either fully injected or fully cleanno transaction is half-injected (the consistency the timing discipline guarantees; the DebugLab shows the opposite). The picture to carry is that runtime customization produces windowed behavior: the callback's effect (inject) is bounded by the window (cb_enabled), and the window boundaries are the clean toggle points. One environment exhibits different behavior per windowclean in reset/normal, injected in stress — controlled at run time by toggling the callback. Reading the timeline this way — when is the callback enabled, and is each transaction wholly inside or outside the window? — is seeing runtime customization done right. The injection bounded to the stress window, toggled at clean boundaries is the signature of correct runtime control: windowed behavior, no half-applied transactions. The callback is enabled only in its window and toggled at clean boundaries, so each transaction is wholly clean or wholly injected — one environment, behavior varying by window.

DebugLab — the half-injected transaction from a mid-transaction toggle

A non-reproducible glitch from disabling a callback in the middle of a transaction

Symptom

A team used a paired callback on a multi-beat bus transaction: a pre_burst hook tagged the transaction with an injected error marker, and a post_burst hook recorded the expected corrupted result for the scoreboard. To end error injection promptly when a budget was reached, they disabled the callback (callback_mode(0)) from a parallel run_phase thread the moment the budget hit — whenever that happened, including mid-burst. The result was an intermittent, non-reproducible failure: occasionally, a burst would have its pre_burst hook run (tagging the error) but its post_burst hook skipped (because the callback was disabled in between) — leaving a transaction that was corrupted but had no recorded expected result, so the scoreboard saw a mismatch it couldn't explain. The failure appeared only when the budget happened to fall mid-burstrare, timing-dependent, and impossible to reproduce on demand.

Root cause

The callback was toggled mid-transactionbetween its paired pre_burst and post_burst hooks — leaving the customization half-applied (error injected, but its expected result not recorded), a classic runtime-timing hazard:

why disabling a callback mid-transaction left a half-injected, unexplained mismatch
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ DISABLE the callback at an ARBITRARY moment (mid-burst):
  // parallel thread: the instant the budget hits, err_cb.callback_mode(0);  // could be MID-BURST
  // burst in flight: pre_burst ran (injected error + tagged) ... THEN disabled ...
  //   post_burst SKIPPED (callback now off) → expected corrupted result NOT recorded
  // → scoreboard sees a corrupted txn with no matching expected → UNEXPLAINED, NON-REPRODUCIBLE mismatch
 
✓ TOGGLE only at a CLEAN boundary (between transactions / at a phase boundary):
  // disable at the END of the current burst, or at the next phase boundary — never mid-burst:
  forever begin
    wait_for_burst_boundary();              // a clean point: no burst in flight
    if (budget_reached) begin err_cb.callback_mode(0); break; end
  end
  // now each burst is WHOLLY injected (pre AND post) or wholly clean → consistent, reproducible

This is the mid-transaction-toggle bug — the cardinal runtime-customization hazard. The callback had paired hooks (pre_burst injects and tags, post_burst records the expected result), so its customization spans the transaction — and disabling it between the paired hooks left the customization half-applied: the error was injected (pre ran) but its expected result was never recorded (post skipped), so the scoreboard saw a corrupted transaction with no matching expected — an unexplained mismatch. Because the disable happened whenever the budget hit (a parallel thread, arbitrary timing), the failure occurred only when the budget happened to fall mid-burstrare, timing-dependent, non-reproducible (the worst kind of bug). The root error is changing the callback set at an arbitrary momentmid-transactionviolating the timing discipline. The fix is to toggle only at a clean boundary: wait for a burst boundary (a point with no burst in flight), then check the budget and disable — so each burst is wholly injected (pre and post) or wholly clean, never half. The general lesson, and the chapter's thesis: runtime callback changes must be timed at clean boundariesadding, removing, or toggling a callback mid-transaction can leave the customization half-applied (a hook ran on one side of the transaction but not the other) or a stateful callback in an inconsistent state, causing spurious, non-reproducible failures; so coordinate runtime customization with the transaction and phase boundariestoggle at clean points where nothing is mid-flight — because the power of runtime control is only safe when changes happen between operations, not during them. Runtime customization is hot-plugging — never yank a callback out in the middle of an operation it's participating in; wait for the clean boundary, then toggle.

Diagnosis

The tell is an intermittent, timing-dependent failure correlated with a runtime callback change. Diagnose a mid-transaction toggle:

  1. Correlate the failure with a callback toggle. An intermittent mismatch that appears only when a callback is added/removed/disabled points at a mid-transaction change.
  2. Check for paired or multi-hook callbacks. A callback whose effect spans hooks (pre and post) is half-applied if toggled between them.
  3. Find where the toggle happens. A toggle from a parallel thread at an arbitrary moment, rather than at a boundary, is the cause.
  4. Confirm non-reproducibility tracks timing. A failure that reproduces only when the toggle falls mid-transaction is the signature of the hazard.
Prevention

Toggle only at clean boundaries:

  1. Change the callback set at phase or transaction boundaries. Wait for a clean point with nothing mid-flight before adding, removing, or toggling.
  2. Never toggle mid-transaction from a parallel thread. Synchronize the toggle to a boundary rather than acting the instant a condition hits.
  3. Be careful with paired/multi-hook callbacks. Ensure a callback whose effect spans hooks is wholly active or wholly inactive for each transaction.
  4. Mind stateful callbacks across toggles. A disabled callback keeps its state; ensure re-enabling resumes consistently and stale state doesn't corrupt later behavior.

The one-sentence lesson: runtime callback changes must be timed at clean boundaries — adding, removing, or toggling a callback mid-transaction can leave the customization half-applied (a hook ran on one side of the transaction but not the other) or a stateful callback inconsistent, causing spurious, non-reproducible failures, so coordinate runtime customization with the transaction and phase boundaries and toggle only at clean points where nothing is mid-flight.

Common Mistakes

  • Toggling a callback mid-transaction. Adding, removing, or disabling between a callback's paired hooks leaves the customization half-applied; toggle at clean boundaries.
  • Forgetting to disable/remove after the window. A callback left active past its window leaks into later phases or tests, contaminating them; scope and tear down runtime callbacks.
  • Confusing add/remove with enable/disable. Delete removes the callback (re-create to return); callback_mode toggles a registered one (keeps state) — pick by presence-varies versus runs-sometimes.
  • Recompiling for each scenario. A plusarg or config selecting the runtime customization runs many behaviors from one build; don't rebuild per scenario.
  • Ignoring stateful-callback state across toggles. A disabled callback retains state; re-enabling resumes it — ensure that's intended, not stale contamination.
  • Toggling from a parallel thread at an arbitrary moment. Synchronize runtime changes to a boundary, not the instant a condition fires, to avoid mid-transaction changes.

Senior Design Review Notes

Interview Insights

Runtime customization is altering behavior dynamically during the simulation through the callback and wider customization mechanisms, and it enables one reusable environment to exhibit many behaviors selected at run time without recompiling. The static picture of callbacks is that you register them at build time and they stay for the whole run. The runtime dimension adds dynamism: callbacks can be added, removed, enabled, and disabled during the simulation. There are three controls. Dynamic add and delete: uvm_callbacks add and delete can be called at run time, in a phase or on an event, so a callback is active only in a window — you add the error injector when the stress phase starts and delete it when it ends. Enable and disable, via callback_mode: this toggles a registered callback's execution without removing it — you keep it registered and just flip the switch off, flipping it on only in the window you want. And selection: a plusarg or config read at startup chooses which runtime customization applies this run. Together these let the behavior vary across the run — error injection on during stress and off in reset, a test enabling its callbacks for its window — and let a single compiled build serve many scenarios, because the behavior is chosen at run time rather than fixed at compile time. The payoff is fast, flexible regression: one build, many behaviors, no recompile per scenario, which matters because recompiling a large testbench is slow. The mental model is hot-plugging accessories on a running appliance: you plug in, unplug, and flip switches while it runs, so one appliance does many jobs by live configuration. The crucial caveat is timing — you hot-plug at safe moments, between operations or at phase boundaries, never mid-operation, because changing the callback set mid-transaction can leave a half-applied customization. So runtime customization is the dynamic dimension of callbacks: live reconfiguration of a reusable environment, powerful for getting many behaviors from one build, disciplined by doing the changes only at clean boundaries.

Add and delete change the registration set — whether the callback exists in the pool at all — while callback_mode enable and disable change only whether a registered callback runs, keeping it registered. With add, you register a callback so it starts being called at its hooks; with delete, you remove it from the pool, so it's gone — not registered, not called, and to bring it back you must re-create and re-add it. With callback_mode of zero, you disable a registered callback so it stays in the pool but its methods aren't executed; with callback_mode of one, you re-enable it, flipping the switch back on. The practical differences are several. State: a disabled callback retains its internal state — counters, accumulated history — so re-enabling resumes where it left off, whereas a deleted-and-re-added callback is a fresh or re-created instance with new state. So if a callback accumulates state you want preserved across a quiet window, disable it rather than delete it; if you want a clean slate each window, delete and re-create. Cost: enable/disable is cheaper — no re-registration overhead — so for frequent toggling, callback_mode is lighter than repeated add/delete. Presence: delete makes the callback truly absent, with no overhead and no presence in the pool, which is right when the callback genuinely shouldn't exist outside its scenario; disable keeps it present but inert. So the choice is by the need. Use add/remove when the callback's presence varies and you want it truly gone in some windows — a clean slate, no registration. Use enable/disable when the callback stays registered but should run only sometimes — cheaper, stateful-friendly, simpler to toggle, like a logger you flip on and off across phases. In both cases the timing discipline applies: whether you delete or disable, do it at a clean boundary, not mid-transaction, or you risk a half-applied customization. The mechanisms are complementary tools, and matching the runtime need — presence-varies versus runs-sometimes — to the right one is the skill.

Runtime customization gives one build many behaviors by deferring the choice of behavior from compile time to run time — a plusarg or config read at startup selects which callbacks are active, and dynamic add/remove and enable/disable vary them across the run, so the same compiled testbench exhibits different scenarios without recompiling. Normally, the behavior of a testbench is fixed when you compile it: the components, the customizations, all baked in. If you wanted a different behavior, you'd change the code and recompile, which for a large testbench is slow. Runtime customization breaks this coupling. The behavior becomes a function of which callbacks are active when, and that's controlled at run time. At startup, a plusarg like enable_err_inject is read, and based on it the test adds the error-injection callback or not — same binary, different behavior. Within the run, callbacks are toggled per phase, so the environment behaves one way in reset (clean) and another in the main phase (injected). And across a regression, you launch the same compiled build many times with different plusargs, each producing a different scenario. So one build serves the whole matrix of behaviors, selected at run time. This is a major regression-efficiency win. You compile once and run many configurations, instead of compiling a separate build per configuration. Iteration is faster because you skip the recompile, and the regression is more flexible because you can select and combine customizations via command-line or config without touching code. The deep idea is decoupling behavior from build: the customization is no longer a compile-time property but a run-time selection. The mental model is a single appliance reconfigured live — by what's plugged in and switched on, and by a setting chosen at startup, it does many jobs without being rebuilt. The discipline that makes this safe is timing the runtime changes at clean boundaries, so reconfiguring live doesn't glitch. But the payoff is that a single reusable, compiled environment becomes a flexible platform that runs many behaviors selected at run time, which is exactly what you want for efficient, varied regression.

Because changing the callback set mid-transaction can leave the customization half-applied or a stateful callback inconsistent, producing spurious, non-reproducible failures — so you add, remove, or toggle only at clean boundaries where nothing is mid-flight. The hazard arises when a callback's effect spans more than an instant — for example, a callback with paired hooks, where a pre-hook injects an error and tags the transaction and a post-hook records the expected corrupted result for the scoreboard. The customization spans the transaction. If you disable or remove the callback between the pre and post hooks — mid-transaction — the pre ran but the post is skipped, so the error was injected but its expected result was never recorded. The scoreboard then sees a corrupted transaction with no matching expected and reports an unexplained mismatch. Worse, if the toggle happens at an arbitrary moment, like the instant a budget is reached from a parallel thread, the failure occurs only when that moment happens to fall mid-transaction — rare, timing-dependent, and non-reproducible, the worst kind of bug. Stateful callbacks have a related risk: toggling one mid-operation can leave its internal state inconsistent. The fix is to time runtime changes at clean boundaries — between transactions or at phase boundaries — where no transaction is mid-flight. Instead of disabling the instant the budget hits, you wait for the next transaction boundary, then check the budget and disable. Then each transaction is wholly injected, both pre and post, or wholly clean, never half — consistent and reproducible. The mental model is hot-plugging: you don't yank an accessory out in the middle of an operation it's participating in, or you get a glitch; you hot-plug at safe moments. The phase boundaries are natural clean points because UVM's phasing already synchronizes there. So the discipline is to synchronize runtime customization to the transaction and phase boundaries, never acting at an arbitrary mid-flight moment, because the power of runtime control is only safe when changes happen between operations, not during them.

If you forget to disable or remove a callback after its window, it stays active and leaks into later phases or tests, contaminating them with unexpected behavior — a scoping and teardown failure. Runtime customization is about active windows: you enable error injection for a stress window, or a test enables its callbacks for its scenario. The implicit contract is that the customization is scoped to that window — active during it, inactive outside. If you enable or add a callback but forget to disable or remove it when the window ends, the callback keeps running afterward. The consequences depend on context. Across phases, an injection callback meant for the main phase that isn't disabled at shutdown could perturb the drain, causing spurious shutdown errors. Across tests, this is worse — if a callback is registered globally or on a shared component and not torn down, it bleeds into the next test, which expected a clean environment. That next test, perhaps a basic sanity test with no injection intended, suddenly sees injected errors, producing confusing failures that look like the sanity test is broken when really a previous test's callback leaked in. These contamination bugs are confusing precisely because the cause is in a different test or phase than the symptom. The fix is to scope runtime callbacks to their window and tear them down explicitly: if you enable a callback for a window, disable it when the window ends; if you add one, delete it. For per-test callbacks, ensure they're cleaned up so they don't persist into the next test, which often means registering them on a component whose lifetime matches the test, or explicitly removing them in the test's cleanup. The discipline parallels any resource that's acquired and must be released: an enabled callback is a resource active until you disable it, and leaving it active is a leak. So runtime customization requires not just enabling at the right time but disabling at the right time — scope it to its window and tear it down — or the customization escapes its intended bounds and contaminates the rest of the run or regression. This teardown discipline, along with the boundary-timing discipline, is what keeps runtime customization controlled rather than a source of mysterious cross-phase or cross-test failures.

Exercises

  1. Window a callback. Show how to make an error-injection callback active only during the main phase, added and removed at the boundaries.
  2. Choose the mechanism. For a logger that should keep its accumulated counts across a quiet window, pick add/remove or callback_mode and justify.
  3. Select at run time. Describe how a plusarg makes one compiled build run with and without injection.
  4. Find the timing bug. A callback with pre and post hooks is disabled from a parallel thread when a budget hits. Explain the failure and the fix.

Summary

  • Runtime customization is altering behavior dynamically during simulation: add/remove callbacks at run time (uvm_callbacks::add / ::delete) so a callback is active only in a window, enable/disable a registered callback with callback_mode(0/1) (toggling execution without removing it), and vary customization per phase and per test.
  • Add/remove changes the registration set (the callback is gone when deleted, re-create to return); enable/disable keeps the registration but toggles whether it runs (cheaper, keeps state) — pick by presence-varies versus runs-sometimes.
  • The payoff is one reusable environment, many behaviors selected at run time: a plusarg or config chooses the scenario in the same compiled binary, so one build serves many behaviorsfast, flexible regression, no recompile.
  • The caveat is timing: adding, removing, or toggling a callback mid-transaction can leave a half-applied customization (a hook ran on one side, not the other) or a stale stateful callback, causing spurious, non-reproducible failures — so do runtime changes at clean boundaries (phase boundaries, between transactions).
  • The durable rule of thumb: customize at run time by adding/removing callbacks for a window and toggling registered ones with callback_mode, so one compiled build exhibits many behaviors selected by plusargs/config without recompiling — but time every runtime change at a clean boundary (phase or transaction), never mid-transaction, and scope-and-tear-down each runtime callback when its window ends, because a mid-transaction toggle leaves a half-applied customization and a leaked callback contaminates later phases and tests.

Next — Resource Database: the next module turns to UVM's lower-level configuration substrate — the resource database. The uvm_config_db you have used is a convenience layer over a more general uvm_resource_db: a typed, hierarchical, scoped store of named resources shared across the testbench. The module examines the resource database directly — how resources are written and read, the scoping and precedence rules, the relationship to the config DB, and when to reach for the lower-level resource API.