On 10/13/26 6:52 PM, Akihiko Odaki wrote:
On 2026/10/14 10:13, Pierrick Bouvier wrote:
On 10/13/2026 6:02 PM, Akihiko Odaki wrote:
On 2026/10/13 11:12, Pierrick Bouvier wrote:
On 10/12/26 7:00 AM, Akihiko Odaki wrote:
On 2026/10/09 15:37, Pierrick Bouvier wrote:
On 10/8/26 8:33 PM, Akihiko Odaki wrote:
On 2026/10/09 11:30, Pierrick Bouvier wrote:
On 10/8/2026 5:57 PM, Akihiko Odaki wrote:
On 2026/10/09 1:40, Pierrick Bouvier wrote:
On 10/7/2026 11:11 PM, Akihiko Odaki wrote:
On 2026/10/08 10:08, Pierrick Bouvier wrote:
On 10/6/2026 11:01 PM, Akihiko Odaki wrote:
A region of interest (ROI) often excludes the program startup
and
exit.
For example, a benchmark may parse a large input and then run a
computation kernel and the former may take longer than the
latter;
basic block vectors generated for the whole execution will not
properly
represent the latter.

Add options to specify the beginning and the end of the ROI to
bound
basic block vector generation to the ROI.

Signed-off-by: Akihiko Odaki <[email protected]>
---
        docs/about/emulation.rst |  8 +++++
        contrib/plugins/bbv.c    | 80 +++++++++++++++++++++++++
++++
++++++++
+++++++++--
        2 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/docs/about/emulation.rst b/docs/about/emulation.rst
index c58149086cdd..0566171b517b 100644
--- a/docs/about/emulation.rst
+++ b/docs/about/emulation.rst
@@ -284,6 +284,14 @@ The bbv plugin allows you to generate basic
block vectors for use with the
            * - Option
            - Description
+  * - begin=A
+    - The virtual address of the instruction that starts basic
block
vector
+      generation. Generation starts from the beginning of the
execution if
+      not specified.
+  * - end=A
+    - The virtual address of the instruction that ends basic
block
vector
+      generation. Generation continues until the endo of the
execution if

small typo here: s/endo/end

Thanks for catching the typo.


+      not specified.
          * - interval=N
            - The interval to generate a basic block vector
specified by
the number of
              instructions (Default: N = 100000000)
diff --git a/contrib/plugins/bbv.c b/contrib/plugins/bbv.c
index e5caf5d9543b..a2a779f7639e 100644
--- a/contrib/plugins/bbv.c
+++ b/contrib/plugins/bbv.c
@@ -5,6 +5,8 @@
         * SPDX-License-Identifier: GPL-2.0-or-later
         */
        +#include <stdbool.h>
+#include <stdint.h>
        #include <stdio.h>
        #include <glib.h>
        @@ -26,6 +28,10 @@ static GHashTable *bbs;
        static GRWLock bbs_lock;
        static char *filename;
        static struct qemu_plugin_scoreboard *vcpus;
+static bool begin_present;
+static uint64_t begin;
+static bool end_present;
+static uint64_t end;
        static int64_t interval = 100000000;
          static void plugin_exit(void *p)
@@ -66,6 +72,7 @@ static void vcpu_init(unsigned int vcpu_index,
void
*userdata)
            Vcpu *vcpu = qemu_plugin_scoreboard_find(vcpus,
vcpu_index);
              vcpu_filename = g_strdup_printf("%s.%u.bb",
filename,
vcpu_index);
+    vcpu->count = begin_present ? INT64_MIN : -interval;

Not sure which semantic we are trying to achieve with -interval
here.

The original logic was to increment this count, until reaching
interval,
after what we would reset it by using vcpu_count -= interval, to
keep
the extra insn that went in after the threshold.

The counter now starts at -interval and counts up to zero.
Subtracting
interval after emitting a vector still preserves the extra
instructions.
For example, with interval=100:

          initial count:       -100
          after 130 insns:       30  -> emit a vector
          subtract interval:    -70  -> 70 insns until the next
vector


            vcpu->file = fopen(vcpu_filename, "w");
        }
        @@ -102,6 +109,22 @@ static void
vcpu_interval_exec(unsigned int
vcpu_index, void *udata)
            fputc('\n', vcpu->file);
        }
        +static void vcpu_begin_insn_exec(unsigned int vcpu_index,
void
*userdata)
+{
+    GHashTableIter iter;
+    void *value;
+
+    g_rw_lock_reader_lock(&bbs_lock);
+    g_hash_table_iter_init(&iter, bbs);
+
+    while (g_hash_table_iter_next(&iter, NULL, &value)) {
+        qemu_plugin_u64_set(bb_count_u64(value), vcpu_index,
0);
+    }
+
+    g_rw_lock_reader_unlock(&bbs_lock);
+    qemu_plugin_u64_set(count_u64(), vcpu_index, -interval);
+}
+
        static void vcpu_tb_trans(struct qemu_plugin_tb *tb, void
*userdata)
        {
            uint64_t n_insns = qemu_plugin_tb_n_insns(tb);
@@ -119,6 +142,19 @@ static void vcpu_tb_trans(struct
qemu_plugin_tb
*tb, void *userdata)
            }
            g_rw_lock_writer_unlock(&bbs_lock);
        +    for (size_t idx = 0; idx < n_insns; idx++) {
+        struct qemu_plugin_insn *insn =
qemu_plugin_tb_get_insn(tb,
idx);
+        uint64_t insn_vaddr = qemu_plugin_insn_vaddr(insn);
+
+        if (begin_present && insn_vaddr == begin) {
+            qemu_plugin_register_vcpu_insn_exec_cb(
+                insn, vcpu_begin_insn_exec,
QEMU_PLUGIN_CB_NO_REGS,
NULL);
+        } else if (end_present && insn_vaddr == end) {
+
qemu_plugin_register_vcpu_insn_exec_inline_per_vcpu(
+                insn, QEMU_PLUGIN_INLINE_STORE_U64,
count_u64(),
INT64_MIN);
+        }
+    }
+
            qemu_plugin_register_vcpu_tb_exec_inline_per_vcpu(
                tb, QEMU_PLUGIN_INLINE_ADD_U64, count_u64(),
n_insns);
        @@ -127,7 +163,32 @@ static void vcpu_tb_trans(struct
qemu_plugin_tb *tb, void *userdata)
              qemu_plugin_register_vcpu_tb_exec_cond_cb(
                tb, vcpu_interval_exec, QEMU_PLUGIN_CB_NO_REGS,
-        QEMU_PLUGIN_COND_GE, count_u64(), interval, NULL);
+        QEMU_PLUGIN_COND_LT, count_u64(), INT64_MIN, NULL);
+}

is INT64_MIN used as an arbitrary large value when interpreted as
unsigned immediate? If yes, that's confusing to the reader.

The unsigned comparison count < (uint64_t)INT64_MIN is equivalent
with
the signed comparison (int64_t)count >= 0. That is why this
particular
value is used.

Setting count to INT64_MIN suppresses output for the next 2^63
counted
instructions, which is effectively disabled for this use. The
beginEmulation
callback clears the BB counters and sets count to -interval; end
sets it
back to INT64_MIN. The requested interval stays unchanged.


Ok I see better.
Even though it works, it's pretty confusing to be honest. Inline
operatons works with unsigned values, so with conversion and wrap
around, it's safe, but really not intuitive.

I would prefer if you could refactor this to keep a zero
initialized
count value, and add a specific instrumentation path for begin, end
logic (COND_LT, UINT64_MAX - N) to keep the same behavior of
"infinite
interval".

We cannot choose a separate instrumentation path based on whether
execution is inside the ROI. Instrumentation is installed when a
TB is
translated, but the same TB may execute both inside and outside the
ROI.

I will preserve this logic and add code comments for clarification.


I took some time to read again how bbv works. Indeed, my idea to
modify
interval was a non sense, and not what is expected.

The current instrumentation per tb is always done unconditionally,
and
the only need thing to do is to snapshot bbs hashtable to keep
track of
the correct data only when being inside the ROI. The rest of existing
logic stays the same.

- On enter, set vcpu->count to 0 (fresh start) and restore bb
table from
previous exit (empty table if there was no exit yet).
- On exit, duplicate the bb table to allow to restore it next time we
enter in ROI.
Key idea is that we keep on adding things in bb table, but save/
restore
when leaving/entering ROI.

I think for robustness you should also add detection of those
expected
transitions: Out of ROI -> Enter ROI -> Exit ROI -> Enter ROI -> ...

Nothing is currently done to detect if if we meet enter/exit address
multiple times, without having the correct sequence. I don't know
if we
want to report an error or handle reentrance correctly in this case.

Also, should count be reset on each entrance, or do we want to keep
track of it also, and save/restore that on exit/enter? I tend to
think
the idea is create a "virtual" interval than only is alive when being
inside the ROI. In this case, we should keep track of this value
also.

The current design that resets the bb table is intentional. A
"virtual"
interval spanning multiple ROIs would be misleading because it mixes
different sections of execution.


Ok.

I don't think we need to add a special handling to enforce a
particular
order of transitions. This patch performs the same action for the
begin
and end addresses and that makes the behavior predictable.

Perhaps we can extend the documentation to clarify the "begin"
marker to
tell that it resets the bb table, making the behavior clear regardless
of transition order.


I'm fine to keep a simple model, but I'll ask you to remove the logic
involving negative numbers, it's just too confusing. Please remove:
vcpu->count = begin_present ? INT64_MIN : -interval;
and
QEMU_PLUGIN_COND_LT, count_u64(), INT64_MIN, NULL);

Instead, let's add a new boolean "tracking_region" in struct Vcpu, that
is set to true when entering ROI (or true by default if no begin
parameter was given).

The flag may look simpler in isolation, but streamlining the
interactions with the counter requires more careful thought. Please see
below.


Then, modify instrumentation to replace:
```
else if (end_present && insn_vaddr == end) {
                qemu_plugin_register_vcpu_insn_exec_inline_per_vcpu(
                    insn, QEMU_PLUGIN_INLINE_STORE_U64, count_u64(),
INT64_MIN);
```
with:
```
else if (end_present && insn_vaddr == end) {
       qemu_plugin_register_vcpu_insn_exec_cb(insn, vcpu_exit_region,
QEMU_PLUGIN_CB_NO_REGS, NULL);
}

static void vcpu_exit_region(unsigned int vcpu_index, void *userdata)
{
       Vcpu *vcpu = qemu_plugin_scoreboard_find(vcpus, vcpu_index);
       vcpu_interval_exec(vcpu_index, NULL);
       vcpu->tracking_region = false;
}
```

vcpu_exit_region should only disable tracking. Calling
vcpu_interval_exec directly bypasses its count >= interval precondition:
it subtracts interval even when count is smaller, wrapping the unsigned
counter, and emits a partial vector or potentially an empty one.


I'm fine with it. I was unsure if you wanted to dump the partial data,
aggregate it when reentering the section, or simply discard it.
With a discard, it's even simpler, just need to set tracking_region to
false.


In addition, let's rename s/vcpu_begin_insn_exec/vcpu_enter_region, to
be consistent and explicit on what it's doing.

And finally, the last piece:
static void vcpu_interval_exec(unsigned int vcpu_index, void *udata)
{
        Vcpu *vcpu = qemu_plugin_scoreboard_find(vcpus, vcpu_index);
        if (!vcpu->tracking) {
          return;
        }
        ...

The early return must come after vcpu->count -= interval. Otherwise,
once count reaches interval outside the ROI, it remains above the
threshold and invokes the callback on every subsequent TB, rather than
once per interval.


Right, thanks for catching it.


The impact performance of calling vcpu_interval_exec + one if every N
instructions is invisible, and at least, it's a piece of code we can
read in 6 months, and understand in less than 2 min without any
additional doc.

I agree the performance impact is negligible, but at least you need to
consider about that to be sure. Adding another flag requires those
additional considerations.

More broadly, I don't think it's ideal to introduce application-specific
logic for this. Looking at the bigger picture, a counting timer with
variable expiration is a common pattern that should ideally be supported
generally.

An alternative option is to have signed integer variants of
qemu_plugin_register_vcpu_insn_exec_inline_per_vcpu() and similar
functions to support for that. However, I don't think those are
necessary either. Taking at an even broader perspective, C allows
avoiding such specialization by letting unsigned variables hold signed
values in a well-defined manner; even the underlying implementation of
the inline op uses signed and unsigned types interchangeably.


If the argument is "C allows that with a defined behaviour", I don't
think it makes a point for me. There is a lot of well defined and
confusing code you can write.

Something like:
    qemu_plugin_register_vcpu_tb_exec_cond_cb(
           QEMU_PLUGIN_COND_LT, count_u64(), INT64_MIN, NULL);
with limit and counter being unsigned is not readable for me.

Although it might not be immediately obvious that an unsigned variable
is storing a signed value, adding a brief clarifying comment is still
simpler than introducing another variable overall.


   From there I'll let you decide between introducing the flag, or
reimplement that with positive values (sticking to init count=0) only
if you really prefer this approach.

I agree that "C allows this with defined behaviour" is insufficient on
its own. Ideally, we could express the signed interpretation directly
with int64_t without adding complexity. That would improve clarity,
even though the existing arithmetic is already well-defined.

However, we need to consider the whole design. Both approaches require
readers to understand more than the individual expressions.

The counter-only approach requires knowing that:

- count is actually treated as signed.
- Any non-negative is smaller than (uint64_t)INT64_MIN when compared as
    unsigned.

The flag approach requires considering:
- How the count and flag together determine when a vector is emitted.
- The required ordering of count and flag accesses.
- Whether the number of callback invocations will significantly
    increase.

The count-only approach requires fewer considerations, and the ones it
does require needs no deduction. The first point is a common practice,
as seen in the underlying implementation. The second point is simply a
matter of the int64_t definition. In both cases, knowing the definition
is sufficient, and both can be documented clearly in the comments.

In contrast, the flag approach requires real reasoning. Comments won't
help here; explaining the ordering and interactions via comments would
be as complicated as the code itself, thereby imposing a greater
cognitive burden.

Furthermore, supporting a variable interval (as done in the count-only
approach) is a pattern we should embrace, rather than working around it
by introducing a separate flag.

We have also already discussed why retaining count=0 is not a drop-in
alternative under the same counter-only instrumentation. The initial
state must distinguish execution that starts inside the ROI from
execution waiting for the begin marker.

Putting all together, I still believe utilizing the existing signed
representation is cleaner than inventing an alternative approach ourselves.


I respect your opinion, but I would prefer to not introduce such usage
of the existing API in contrib/, since it might (wrongly) inspire other
people reading this plugin code.

I agree that contrib plugins should set a good example. What pattern
would you like them to demonstrate for a counting timer whose expiration
can change at runtime, including effectively disabling the timer?


You can still emulate this behavior by having a fixed limit, and set scoreboard value as you wish. This emulates a counting timer with variable expiration, by growing/shrinking your distance to the bound as you wish. You're well aware that's also exactly what the current patch is doing, except that it's using signed values converted to unsigned, and that's my only problem with it. If it was only unsigned values with an arbitrary limit (UINT64_MAX >> 1) for instance, I would be ok with it.

When we designed those inline operations, we considered various things, including if a semantic of "add only with unsigned values" was enough. So far, there was nothing that couldn't be expressed easily with that.

The counter-only design expresses this by updating the counter while
keeping the instrumentation unchanged. I think that is a useful pattern
to demonstrate, with its interpretation and assumptions documented.

I would like to understand which general pattern you would prefer to
encourage, so we can compare the approaches on that basis.


For current use case, I don't see any new pattern. It's just an instrumentation that is always active, with a callback that does something or not based on a predicate (in_region in this case).

So I'll repeat one of my previous answer, you have the choice between:
- A simple bool tracking_region or in_region representing the predicate, with {enter,exit}_region callbacks. The code is self documenting and explicit. - Choose an arbitrary strictly positive bound, keep vcpu->count init to 0, and implement the rest accordingly. I'll accept that too if the introduction of a bool is really something not acceptable for you.

I would finally say that if you're really focused on performance, and having done my homework on this part, I can tell you that conditional callbacks are not as "free" as what you might think, since they are still composed of multiple tcg_op, which translates to a lot of host instructions. They can even be slower than a callback that does not access registers. So, personally, I have no problem with a simple callback checking a if and returning early.

Regards,
Akihiko Odaki

Regards,
Pierrick

Reply via email to