Part 7 · Environment & Tests · Intermediate
Drain Time in the Env: Settling Trailing Observability
Set and validate drain windows so trailing monitor/scoreboard activity completes after objections clear, without bloating runtime.
What drain time solves
After the last objection drops, passive pipelines may still hold in-flight work: monitor publish queues, analysis FIFOs, predictor pipelines, and scoreboard compare threads. drain_time adds a bounded settle window so this tail work can complete before phase transition.
[UVM][ENV] closure timeline
t0: test drops final objection
t1: objection count reaches zero
t1..t2: drain_time window (tail activity allowed)
t2: run_phase ends and UVM advances
without drain:
t2 happens at t1, tail work may be cut off[ENV] where tail work comes from
- bus monitor decodes multi-cycle transfer after last drive
- scoreboard waits for matching expected/actual pair
- coverage subscriber flushes sampled queue
- predictor aligns delayed response streamDrain time should model real tail latency, not guesswork.
Too small causes false failures or missing checks.
Too large silently inflates every test runtime.
Sizing and setting drain_time
class base_test extends uvm_test;
`uvm_component_utils(base_test)
int unsigned drain_ns = 200;
task run_phase(uvm_phase phase);
phase.phase_done.set_drain_time(this, drain_ns * 1ns);
phase.raise_objection(this, "main");
run_main_scenario();
phase.drop_objection(this, "main done");
endtask
endclassfunction int unsigned estimate_drain_ns(my_env_cfg cfg);
int unsigned mon_tail = cfg.max_decode_cycles * cfg.clk_period_ns;
int unsigned sb_tail = cfg.max_compare_cycles * cfg.clk_period_ns;
int unsigned fifo_tail = cfg.max_fifo_depth * cfg.clk_period_ns;
int unsigned budget = mon_tail + sb_tail + fifo_tail;
if (budget < 50) budget = 50;
return budget;
endfunction[UVM][ENV] drain tuning workflow
1) run with drain=0 and record tail failures
2) measure worst-case tail latency across stress seeds
3) add margin (10-20 percent)
4) lock default in base_test
5) allow plusarg override for experiments onlyCompute a data-backed default from known pipeline depths.
Keep drain override possible but controlled in regression.
Re-tune drain when monitor/checker architecture changes.
Validation patterns and limits
Validation code hooks
function void check_phase(uvm_phase phase);
super.check_phase(phase);
if (env.sb.pending_actual() != 0)
`uvm_error("DRAIN", "actual queue not empty at check_phase")
if (env.sb.pending_expected() != 0)
`uvm_error("DRAIN", "expected queue not empty at check_phase")
endfunctiontask monitor_tail_probe();
int before = env.mon.publish_count;
#0;
#1ns;
if (env.mon.publish_count > before)
`uvm_info("TAIL", "monitor still publishing after drop", UVM_LOW)
endtask[ENV] signs drain is mis-sized
too small:
scoreboard empty-queue mismatches near test end
occasional missing coverage samples
too large:
every seed spends long idle tail time
farm throughput drops with no quality gainKey takeaways
Drain time protects trailing observability work after objections clear.
Size it from measured tail latency, then keep it tight.
Use check_phase invariants to prove drain adequacy.
Treat drain changes as performance-sensitive architecture changes.
Common pitfalls
Setting a single huge drain value to hide unknown behavior.
Assuming drain can fix structural checker deadlocks.
Never re-validating drain after adding deeper pipelines.
Skipping end-of-phase queue assertions and relying on intuition.