Part 2 · Phases & Lifecycle · Intermediate

set_drain_time Deep Dive: Grace Period After Objections Clear

How drain time works internally, set_drain_time API, per-component vs global drain, tuning guidelines, and interaction with phase_ready_to_end.

Why drain time exists

When the last objection drops and count reaches zero, some activity may still be in flight — a monitor may not have sampled the last transaction yet, a scoreboard may not have compared it, or a pipeline may not have flushed. If the phase ended instantly, this trailing activity would be lost.

diagram
[PHASE][RUN] the trailing activity problem

  T=900ns  sequence sends last transaction
  T=900ns  test drops objection (count=0)
  T=905ns  monitor samples last transaction  ← would be MISSED without drain
  T=910ns  scoreboard compares last pair      ← would be MISSED without drain

  drain_time = grace period after count=0 for trailing activity to complete
  • Drain time starts when count reaches zero, not when last drop is called.

  • During drain time, the phase is still alive — monitors/scoreboards still run.

  • After drain time expires, phase_ready_to_end fires, then phase ends.


set_drain_time API

systemverilog
// Set drain time on a specific component's objection contribution
task run_phase(uvm_phase phase);
  // This component's drain time: phase waits this long after THIS component's
  // last drop before considering phase_done
  phase.phase_done.set_drain_time(this, 100ns);

  phase.raise_objection(this, "main seq");
  main_seq.start(env.v_sqr);
  phase.drop_objection(this, "main seq done");
  // Phase ends 100ns AFTER count reaches 0 (if no other objectors)
endtask

// Set drain time globally for the phase (in start_of_simulation or build)
function void start_of_simulation_phase(uvm_phase phase);
  uvm_domain domain = uvm_domain::get_common_domain();
  uvm_phase run_ph = domain.find(uvm_run_phase::get());
  run_ph.get_objection().set_drain_time(500ns);  // global 500ns drain
endfunction
diagram
[PHASE][UVM] drain time timeline

  count=2  drop  count=1  drop  count=0
                                      │
                                      ▼ drain_time starts (e.g., 100ns)
                                      │ monitors still sampling
                                      │ scoreboards still comparing
                                      ▼ drain_time expires
                                      phase_ready_to_end callback
                                      ▼
                                      phase END
  • set_drain_time(this, time) sets per-component drain contribution.

  • Global set_drain_time on the phase objection sets a floor for all.

  • The effective drain is the maximum of all contributors' drain times.


Tuning drain time

Too little drain time

  • Last transactions missed by monitor — not sampled before phase kill.

  • Scoreboard pending queue non-empty at check_phase.

  • Coverage holes for end-of-test scenarios.

Too much drain time

  • Every test pays the cost at every phase end — cumulative regression slowdown.

  • 100ns drain × 12 runtime sub-phases × 1000 tests = significant wasted sim time.

  • Masks real objection bugs — sim 'works' because drain covers the gap.

systemverilog
// Tuning approach: start with protocol-appropriate minimum
// AXI: max outstanding burst completion time
// APB: 2-3 clock cycles
// Custom: measure monitor-to-scoreboard latency in waves

function void start_of_simulation_phase(uvm_phase phase);
  uvm_phase run_ph;
  run_ph = uvm_domain::get_common_domain().find(uvm_run_phase::get());
  // Protocol-specific: 3 clock periods at fastest interface clock
  run_ph.get_objection().set_drain_time(3 * cfg.fastest_clk_period);
endfunction
diagram
[PHASE][RUN] drain time sizing guide

  APB/simple bus:     2-5 clock cycles
  AXI/ACE:            max burst latency + pipeline depth
  Multi-clock SoC:    slowest relevant clock domain period × N
  Scoreboard queue:   measure max compare latency, add margin

  verify: check_phase pending count = 0 with chosen drain time
  • Measure actual monitor-to-scoreboard latency on a known-good test.

  • Set drain to minimum that clears pending queue in check_phase.

  • Different runtime sub-phases may need different drain times.


Drain time and runtime sub-phases

Each runtime sub-phase has its own objection and drain time. A short drain on reset_phase is fine; main_phase may need more:

systemverilog
class my_test extends uvm_test;
  function void start_of_simulation_phase(uvm_phase phase);
    uvm_domain dom = uvm_domain::get_common_domain();
    // Short drain for structural phases
    dom.find_by_name("reset_phase").get_objection().set_drain_time(50ns);
    dom.find_by_name("configure_phase").get_objection().set_drain_time(50ns);
    // Longer drain for traffic phases
    dom.find_by_name("main_phase").get_objection().set_drain_time(200ns);
    dom.find_by_name("shutdown_phase").get_objection().set_drain_time(500ns);
  endfunction
endclass

Key takeaways

  • Drain time provides a grace period after count=0 for trailing activity.

  • set_drain_time(this, t) per component; global set on phase objection for floor.

  • Tune to minimum that clears pending work — excessive drain wastes regression time.

  • Each runtime sub-phase can have independent drain time settings.

Common pitfalls

  • Using drain time to mask a missing objection drop — sim 'passes' but hides bug.

  • Same large drain on all 12 sub-phases — cumulative waste.

  • Zero drain on main_phase with pipelined scoreboard — last transactions lost.

  • Setting drain in run_phase instead of start_of_simulation — too late for some phases.