Who owns an analysis transaction

Decide who copies an analysis transaction, when and at what cost: the publisher that creates it, and the listener that keeps it.

Lesson
U7.2
Time
25 min
Runs on
Verilator with UVM
Check
5 questions

The pins carried 16, 32 and 48, but a slow checker reports 32, 48 and 48. The monitor refilled one object while the checker was still reading it. A listener that cloned each transaction to protect itself kept 0, 0 and 0.

One object, refilled while the checker works
012345time (ns)15456575115165transfer1632-48-object A holds163248checker reads x16-32-48checker actiongetworkingdone, getworkingdone, getdonemonitor refills A

The checker finishes its first item at 65 ns and reads 32, because the monitor refilled object A at 45 ns.

Two sides of one rule

U3.5 showed that one write reaches every listener, that write cannot wait, and that a listener must not change what it receives. This lesson takes the two sides that rule leaves open: the publisher that creates the object, and the listener that keeps it.

Three references can still mean one objectWith reuse enabled, this is the state at 45 ns: refilling object A changes what every holder can read.
Three references can still mean one objectWith reuse enabled, this is the state at 45 ns: refilling object A changes what every holder can read. reference Recorder entry Saved during thefirst transfer reference FIFO entry Queued by the secondtransfer reference Checker variable Still processing thefirst transfer shared object Object A x = 32; previously x =16 handlehandlehandle
  1. First transfer15 ns (illustrative)

    The monitor fills object A with x = 16 and writes it. The recorder saves the handle; the checker’s get returns the same handle and reads x = 16 to start its 50 ns of work.

    More context

    Neither listener receives a copy. Both handles name the one object the monitor created.

    Follow the calls, values and waits
    1. data handle to A: Object A → Recorder entry
    2. data handle to A: Object A → Checker variable
    3. data x = 16: Object A → Checker variable
    4. Waiting: Checker variable — working for 50 ns
  2. Refill and write again45 ns (illustrative)

    With reuse enabled, the monitor refills the same object A with x = 32 and writes it again. The FIFO queues a handle to A, the object the recorder and the busy checker still hold.

    More context

    The refill is one assignment to A. No holder is told, and UVM does not prevent it.

    Follow the calls, values and waits
    1. data handle to A: Object A → FIFO entry
  3. The checker reads x again65 ns (illustrative)

    The checker finishes its first item and reads x through its handle: 32, not the 16 the pins carried at 15 ns.

    More context

    The example logs done x=32 at 65 ns. A fresh object per transfer, or a copy made inside write, keeps 16.

    Follow the calls, values and waits
    1. data x = 32: Object A → Checker variable
    2. Released: Checker variable
Read the diagram as text
  • Recorder entry: Saved during the first transfer
  • FIFO entry: Queued by the second transfer
  • Checker variable: Still processing the first transfer
  • Object A: x = 32; previously x = 16
  • Recorder entry → Object A: handle
  • FIFO entry → Object A: handle
  • Checker variable → Object A: handle

These arrows are references, not copies or calls. When the checker reads x at 65 ns, the same object now holds 32.

A handle is a reference to an object. Saving it in a FIFO or a queue does not create another object, so every holder sees later changes to its fields. A transaction that nobody changes after write is an immutable snapshot; UVM does not enforce that for you.

Refills one object

shared.x = vif.x;
in_ap.write(shared);

Every saved handle names the one object, shared. The next assignment changes earlier entries too.

Creates one per transfer

incr_item t = incr_item::type_id::create("t");
t.x = vif.x;
in_ap.write(t);

Each observation has its own object. Read-only listeners can keep it safely.

Who copies, when, at what cost

WhoCopies whenWhyCost
Publisher (your monitor)Always: a fresh object per transfer, never changed after writeIt is the only writer; each observation needs its own snapshotOne allocation per transfer, the cheap default
Listener that reads only inside writeNeverIt has finished before the publisher can change anythingNone
Listener that keeps handles for laterInside write, unless the publisher never changes a published objectA refill or later change would alter what it keptOne allocation per kept transfer; the copy must cover its fields
Listener that transforms dataA local variable for a derived value, or a private clone to change a whole transactionThe received object belongs to every listenerA clone is one allocation per transfer

With a publisher that never changes a published object, only a listener that transforms whole transactions needs a copy. Each clone is one more allocation; how much stays live depends on what is kept (U2.1 S5).

Copy inside write, at the moment you receive the handle. A listener that copies later, after a FIFO get or in report_phase, copies whatever the publisher has written since.

A clone is only as good as do_copy

clone() creates an object of the same type and fills it through copy, which calls do_copy (U2.1 S2). The example's incr_item has no do_copy and no field macros, so the copy covers no user field: the clone keeps x at its initial value, 0. Nothing reports it.

Clone without do_copy

class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [7:0] x;
  ...
endclass
if (!$cast(c, t.clone())) `uvm_fatal("CLONE", "not an incr_item")
// c.x == 0

The example's clone keeper keeps 0, 0 and 0 while the pins carried 16, 32 and 48.

Clone with do_copy

function void do_copy(uvm_object rhs);
  incr_item r;
  super.do_copy(rhs);
  if (!$cast(r, rhs)) `uvm_fatal("COPY", "not an incr_item")
  x = r.x;
endfunction

Now clone keeps 16, 32 and 48, the same as copying the field by hand.

A copy is also only as deep as do_copy makes it. Assigning a queue member copies the queue; assigning a class-handle member copies the handle, so both objects share one header unless do_copy creates a separate one (U2.1 S4).

An empty FIFO with a busy checker

The slow checker receives through an analysis FIFO and works on each item for 50 ns, as U3.5 S2 described. Its FIFO empties when get returns, which is before the work is done.

The final get is not the final check
012345time (ns)15456575115165monitor writes1632-48-checker gets16-32-48-queued aftercompleted checks0123empty FIFO; checker still busy

The last item leaves the FIFO at 115 ns, but its 50 ns of work finishes at 165 ns.

So the example ends on completed work, wait (chk.done == 3), never on an empty FIFO. U7.3 gives the full rule for ending a test.

Try it

The monitor refills one object (reuse = 1), as a third-party monitor might. The handle keeper ends with three references to it, all 48. The clone keeper copies at the right time, but clone cannot copy a field that do_copy does not know, so it keeps zeros. Only the field-copy keeper has the pins' values.

Add the do_copy from the right-hand compare to incr_item and rerun: the clone keeper keeps 16 32 48. Then set reuse = 0. The handle keeper is now correct too, and the checker finishes 16, 32 and 48.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 15 ns monitor write x=16 15 ns checker get x=16 45 ns monitor write x=32 65 ns checker done x=32 65 ns checker get x=32 75 ns monitor write x=48 115 ns checker done x=48 115 ns checker get x=48 165 ns checker done x=48 handle keeper kept 48 48 48 clone keeper kept 0 0 0 field-copy keeper kept 16 32 48 UVM_ERROR : 0

Check yourself

Predict the outputThe publisher creates a fresh packet for each write of 5, 7 and 9, then sets x = 99 on the last packet it wrote. One listener keeps handles; the other keeps clone()s. pkt has no field macros and no do_copy. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class pkt extends uvm_sequence_item;
  `uvm_object_utils(pkt)                  // no field macros and no do_copy
  int x;
  function new(string name = "pkt"); super.new(name); endfunction
endclass

class keep_handle extends uvm_subscriber #(pkt);
  `uvm_component_utils(keep_handle)
  pkt kept[$];
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void write(pkt t); kept.push_back(t); endfunction
endclass

class keep_clone extends uvm_subscriber #(pkt);
  `uvm_component_utils(keep_clone)
  pkt kept[$];
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void write(pkt t);
    pkt c;
    if (!$cast(c, t.clone())) `uvm_fatal("CLONE", "not a pkt")
    kept.push_back(c);
  endfunction
endclass

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  uvm_analysis_port #(pkt) ap;
  keep_handle h;
  keep_clone  c;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    ap = new("ap", this);
    h  = keep_handle::type_id::create("h", this);
    c  = keep_clone::type_id::create("c", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    ap.connect(h.analysis_export);
    ap.connect(c.analysis_export);
  endfunction
  task run_phase(uvm_phase phase);
    pkt p;
    phase.raise_objection(this);
    p = pkt::type_id::create("p"); p.x = 5; ap.write(p); #10;
    p = pkt::type_id::create("p"); p.x = 7; ap.write(p); #10;
    p = pkt::type_id::create("p"); p.x = 9; ap.write(p); #10;
    p.x = 99;                               // the publisher changes the last packet it wrote
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    string s = "", t = "";
    foreach (h.kept[i]) s = {s, $sformatf(" %0d", h.kept[i].x)};
    foreach (c.kept[i]) t = {t, $sformatf(" %0d", c.kept[i].x)};
    $display("kept%s, cloned%s", s, t);
  endfunction
endclass

module tb;
  initial run_test("my_test");
endmodule
Predict the outputThis pkt has a queue q and a class handle hdr, and its do_copy assigns both: q = r.q; hdr = r.hdr;. The test clones a packet holding 3 bytes and hdr.len = 3, then pushes a fourth byte onto the original and sets its hdr.len = 4. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class hdr_c;
  int len;
endclass

class pkt extends uvm_sequence_item;
  `uvm_object_utils(pkt)
  byte  q[$];                               // copied by value in do_copy
  hdr_c hdr;                                // the handle is copied; the header is shared
  function new(string name = "pkt"); super.new(name); hdr = new(); endfunction
  function void do_copy(uvm_object rhs);
    pkt r;
    super.do_copy(rhs);
    if (!$cast(r, rhs)) `uvm_fatal("COPY", "not a pkt")
    q   = r.q;
    hdr = r.hdr;
  endfunction
endclass

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    pkt p = pkt::type_id::create("p"), c;
    p.q = '{1, 2, 3};
    p.hdr.len = 3;
    if (!$cast(c, p.clone())) `uvm_fatal("CLONE", "not a pkt")
    p.q.push_back(4);                       // the publisher keeps changing its own packet
    p.hdr.len = 4;
    $display("clone has %0d bytes, header len %0d", c.q.size(), c.hdr.len);
  endtask
endclass

module tb;
  initial run_test("my_test");
endmodule
Spot the bugThe publisher refills one object for each write: 10, 20, 30, then 99. This incr_item implements do_copy. Each option is the body of the keeper's write. Which one ends up with the wrong values?
ConceptA recorder must keep every observation from a third-party monitor that may reuse its object. The recorder receives through an analysis FIFO and logs in its own run_phase. Where must it copy?
ConceptA monitor already creates a fresh object for every transfer and never changes an object after publishing it. Its three listeners are a scoreboard, a coverage collector and a recorder, all read-only. A reviewer asks each listener to clone what it receives. What does that change?

Remember

  • The publisher creates a fresh object for every transfer and never changes it after write; that is the cheap default.
  • A listener that keeps handles copies inside write, unless the publisher never changes a published object.
  • A listener that transforms data uses a local variable or a private clone, never the received object.
  • clone() copies only the fields that field macros or do_copy cover; a nested handle that do_copy assigns stays shared.
  • An empty analysis FIFO says nothing about work a consumer has already taken out; end on completed work.

Practice