An SoC team profiles a slow regression. The top of the profile is not the DUT, not the scoreboards, not constraint solving: it is uvm_objection::m_raise and m_drop, called millions of times. Every bus agent's monitor raises and drops an objection for every transaction it sees, "so the test cannot end mid-transfer". Turning on +UVM_OBJECTION_TRACE to debug a hang produces a 14 GB log.
Objections are cheap once and expensive a million times. This lesson gives you the cost model and the tools that keep end of test correct without paying for it on every transaction.
Think of the tallies from Objection Hierarchy Propagation as a chain of people passing a message upstairs. Each raise is a message carried from the raiser to uvm_top, stopping at every floor to update a tally and to tell that floor's manager (raised() callback). A drop carries another message the same way, and when a total reaches zero it also schedules a drain and an all_dropped callback at that level. In a bench ten levels deep, one raise-and-drop pair is twenty tally updates, twenty callback calls, and up to ten scheduled drains. Done once per test, that is nothing. Done once per transaction across 40 agents, it is a measurable share of the run.
Work per raise or drop | Propagate mode 1 (default) | Propagate mode 0 |
|---|---|---|
Tally updates | Every level from source to uvm_top | Source and uvm_top only |
| On every component on the path | On the source and uvm_top only |
Drain and | At every level on the path | At the source and uvm_top |
Trace lines with tracing on | Two or more per level | About two per raise or drop |
| Yes | No: middle levels are not updated |
The cheapest objection is the one you do not raise. Monitors never raise objections at all; end of test belongs to the test and to checkers. Drivers do not either: they live in forever loops that end with the phase. The per-transaction raise usually comes from a good intention ("don't end in the middle of a burst") that is better served by a single hold: the scoreboard's owed-work count from phase_ready_to_end costs one raise per test, not one per item.
If a component truly must hold the phase while work is in flight, raise when its outstanding count goes from 0 to 1 and drop when it returns to 0. The count lives in your component; the objection changes only at the edges.
// snippet
// Raise on the 0 -> 1 edge, drop on the 1 -> 0 edge: two objection calls per busy period.
function void item_started(uvm_phase phase);
if (outstanding++ == 0) phase.raise_objection(this, "SB: items in flight");
endfunction
function void item_finished(uvm_phase phase);
if (--outstanding == 0) phase.drop_objection(this, "SB: idle");
endfunctionset_propagate_mode(0) on an objection tells UVM to skip the middle levels: a raise or drop updates the source and uvm_top only. The phase still ends correctly, because the decision uses uvm_top's total. What you lose is everything in between: get_objection_total(env) no longer reflects the env's subtree, and raised()/dropped() callbacks on intermediate components stop firing. The library refuses to change the mode while the objection is raised or draining (it reports UVM/BASE/OBJTN/PROP_MODE), so set it before any raise, for example in start_of_simulation_phase on the run phase's objection.
// snippet
function void start_of_simulation_phase(uvm_phase phase);
uvm_phase run_ph = phase.find_by_name("run"); // the run phase node in this schedule
if (run_ph != null) run_ph.get_objection().set_propagate_mode(0);
endfunctionUse it when profiling shows objection traffic in the top entries and nothing in the bench relies on intermediate totals or callbacks. Do not use it as a reflex: a hang dump that shows only the source and uvm_top is harder to read in a deep hierarchy.
Components get three hooks on every objection that passes through them: raised() and dropped() (functions) and all_dropped() (a task, called after that level's drain when its total reaches zero). They are the right place for bookkeeping that would otherwise poll the counts. For objection-wide hooks that are not tied to one component, register a uvm_objection_callback with uvm_objection_cbs_t::add.
// snippet
class dma_env_dbg extends dma_env;
`uvm_component_utils(dma_env_dbg)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// Called whenever this env or anything below it raises.
virtual function void raised(uvm_objection objection, uvm_object source_obj,
string description, int count);
`uvm_info("ENV_OBJ", $sformatf("%s raised %0d (%s)",
source_obj.get_full_name(), count, description), UVM_HIGH)
endfunction
// Called after the env's drain time, when its whole subtree has dropped.
virtual task all_dropped(uvm_objection objection, uvm_object source_obj,
string description, int count);
`uvm_info("ENV_OBJ", "env subtree idle", UVM_MEDIUM)
endtask
endclassKeep callback bodies cheap: they run on every raise and drop that passes through. With propagate mode 0, intermediate components do not see these calls at all.
Component raiser | Sequence raiser | |
|---|---|---|
Path to uvm_top | Component parents | Its sequencer, then the sequencer's parents |
Lifetime | Whole simulation | One start() call; a new object per start |
Typical owner of | The scenario (test) or the tail (scoreboard) | Its own duration, via set_automatic_phase_objection(1) |
Cost if overused | Low: one per component | High if every short sequence raises: one pair per start |
A sequence library that sets set_automatic_phase_objection(1) on every small sequence produces one raise-drop pair per sequence start, which at thousands of starts adds up. Use it on top-level sequences started from the test, not on the sub-sequences they start.
At scale, the query you choose decides whether your tools work. get_objection_count(obj) is the object's own raises; get_objection_total(obj) is its subtree; with no argument, both ask about uvm_top. A watchdog or dashboard should read get_objection_total(), the number that decides the phase. A hang dump should call display_objections(), which lists every object with a non-zero total, sequences included.
+UVM_OBJECTION_TRACE prints every raise and drop at every level. On the DMA bench, the drain test produces 18 trace lines for two raises, because each event is reported at the scoreboard, the env, the test and uvm_top, for dropped and all_dropped separately. Two of them:
UVM_INFO @ 365000: run_objection [OBJTN_TRC] Object uvm_test_top.env.sb raised 1 run_objection objection(s) (1 word(s) still owed by the DMA): count=1 total=1
UVM_INFO @ 365000: run_objection [OBJTN_TRC] Object uvm_top added 1 run_objection objection(s) to its total (raised from source object uvm_test_top.env.sb, 1 word(s) still owed by the DMA): count=0 total=1Multiply by a per-transaction raiser in a deep SoC tree and the trace becomes unusable. Use it on a short reproducer, or switch it on at a chosen time with phase.get_objection().trace_mode(1) just before the window you care about.
The opening SoC team had 40 AXI and APB monitors, each raising and dropping per transaction, eight levels deep. Profiling attributed about a fifth of the run time to objection bookkeeping and callbacks. Worse, the raises did not even help: a monitor sees a transaction after it completes on the bus, so its raise covered nothing still in flight.
They removed every monitor objection, gave each subsystem scoreboard an owed-work hold in phase_ready_to_end, and kept the test's scenario objection. The regression got faster, the end-of-test became exact, and +UVM_OBJECTION_TRACE went back to a few dozen lines per test, short enough to read during a hang.
env.sb raises. What do get_objection_total(env) and get_objection_total() return?get_objection_total(env) returns 0, because the middle levels are skipped. get_objection_total() (uvm_top) returns 1, because the source and uvm_top are always updated. The phase is held correctly; only the intermediate view is gone.
write path rarely protect the end of a test?A monitor publishes a transaction after it has completed on the pins, so its raise and drop bracket nothing that is still in flight inside the DUT. It adds cost without covering the real tail. Outstanding work is better counted by the checker, from what the DUT was asked to do.
set_automatic_phase_objection(1). What does that cost, and what would you change?5,000 raise-drop pairs, each walking from the sequence through its sequencer to uvm_top with callbacks at every level. Set automatic objection only on the top-level virtual sequence started by the test; the sub-sequences finish inside its duration and need no objection of their own.
Next: the counts are only half of debugging. Objection triage and hang forensics turns these tools into a playbook for the test that will not end.