[PATCH 2/2] migration: expose RAM stats and timing on destination via query-migrate

Trieu Huynh posted 2 patches 6 days, 3 hours ago
Maintainers: Peter Xu <peterx@redhat.com>, Fabiano Rosas <farosas@suse.de>, Eric Blake <eblake@redhat.com>, Markus Armbruster <armbru@redhat.com>
[PATCH 2/2] migration: expose RAM stats and timing on destination via query-migrate
Posted by Trieu Huynh 6 days, 3 hours ago
From: Trieu Huynh <vikingtc4@gmail.com>

MigrationStats had all-required QAPI fields, so partially populating
info->ram on the destination would force display of source-only metrics
(dirty-sync-count, precopy-bytes, etc.) as misleading zeros.

Make those source-only fields optional with '*' in the QAPI schema.
Update populate_ram_info() to set them via has_* setters so source
output is unchanged.

As per schema's changes, fill_destination_migration_info() can now
populate info->ram for the COMPLETED case with the fields that are
meaningful on the receive side:

  * total, remaining, page-size         (RAM layout)
  * transferred, normal, normal-bytes   (bytes/pages received)
  * duplicate                           (zero pages received)
  * mbps                                (throughput: transferred*8
                                         /time_ms/1000)
  * pages-per-second                    (total pages / duration in
                                         seconds)

Others optional fields (dirty-sync-count, precopy-bytes,
downtime-bytes, postcopy-bytes, multifd-bytes, postcopy-requests,
dirty-sync-missed-zero-copy, dirty-pages-rate) are left unset and
are absent from the destination output.

On dst, {"execute":"query-migrate"}
* As-is:
{
    "return": {
        "status": "completed"
}
* To-be:
{
    "return": {
        "status": "completed",
        "total-time": 94,
        "ram": {
            "total": 554508288,
            "pages-per-second": 1440234,
            "page-size": 4096,
            "remaining": 0,
            "mbps": 97.955404255319152,
            "transferred": 1150976,
            "duplicate": 135101,
            "normal-bytes": 1150976,
            "normal": 281
        }
    }
}

Signed-off-by: Trieu Huynh <vikingtc4@gmail.com>
---
 migration/migration.c | 35 +++++++++++++++++++++++++++++++++++
 qapi/migration.json   | 14 +++++++-------
 2 files changed, 42 insertions(+), 7 deletions(-)

diff --git a/migration/migration.c b/migration/migration.c
index 17c9a8b344..925d29890d 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -1063,17 +1063,24 @@ static void populate_ram_info(MigrationInfo *info, MigrationState *s)
     info->ram->normal = qatomic_read(&mig_stats.normal_pages);
     info->ram->normal_bytes = info->ram->normal * page_size;
     info->ram->mbps = s->mbps;
+    info->ram->has_dirty_sync_count = true;
     info->ram->dirty_sync_count =
         qatomic_read(&mig_stats.dirty_sync_count);
+    info->ram->has_dirty_sync_missed_zero_copy = true;
     info->ram->dirty_sync_missed_zero_copy =
         qatomic_read(&mig_stats.dirty_sync_missed_zero_copy);
+    info->ram->has_postcopy_requests = true;
     info->ram->postcopy_requests =
         qatomic_read(&mig_stats.postcopy_requests);
     info->ram->page_size = page_size;
+    info->ram->has_multifd_bytes = true;
     info->ram->multifd_bytes = qatomic_read(&mig_stats.multifd_bytes);
     info->ram->pages_per_second = s->pages_per_second;
+    info->ram->has_precopy_bytes = true;
     info->ram->precopy_bytes = qatomic_read(&mig_stats.precopy_bytes);
+    info->ram->has_downtime_bytes = true;
     info->ram->downtime_bytes = qatomic_read(&mig_stats.downtime_bytes);
+    info->ram->has_postcopy_bytes = true;
     info->ram->postcopy_bytes = qatomic_read(&mig_stats.postcopy_bytes);
 
     if (migrate_xbzrle()) {
@@ -1094,6 +1101,7 @@ static void populate_ram_info(MigrationInfo *info, MigrationState *s)
 
     if (s->state != MIGRATION_STATUS_COMPLETED) {
         info->ram->remaining = ram_bytes_remaining();
+        info->ram->has_dirty_pages_rate = true;
         info->ram->dirty_pages_rate =
            qatomic_read(&mig_stats.dirty_pages_rate);
     }
@@ -1209,6 +1217,33 @@ static void fill_destination_migration_info(MigrationInfo *info)
     case MIGRATION_STATUS_COMPLETED:
         info->has_status = true;
         fill_destination_postcopy_migration_info(info);
+        if (mis->total_time > 0) {
+            info->has_total_time = true;
+            info->total_time = mis->total_time;
+        }
+        {
+            size_t page_size = qemu_target_page_size();
+            uint64_t normal  = mis->received_normal_pages;
+            uint64_t zero    = mis->received_zero_pages;
+            uint64_t xbzrle  = mis->received_xbzrle_pages;
+
+            info->ram = g_malloc0(sizeof(*info->ram));
+            info->ram->total        = ram_bytes_total();
+            info->ram->remaining    = 0;
+            info->ram->normal       = normal + xbzrle;
+            info->ram->normal_bytes = (normal + xbzrle) * page_size;
+            info->ram->duplicate    = zero;
+            info->ram->transferred  = (normal + xbzrle) * page_size;
+            info->ram->page_size    = page_size;
+
+            if (info->has_total_time) {
+                info->ram->mbps = info->ram->transferred * 8.0
+                                  / info->total_time / 1000.0;
+                info->ram->pages_per_second = (normal + xbzrle + zero)
+                                              * 1000.0 / info->total_time;
+            }
+            /* source-only optional omitted from output */
+        }
         break;
     default:
         return;
diff --git a/qapi/migration.json b/qapi/migration.json
index 7134d4ce47..a695d04a22 100644
--- a/qapi/migration.json
+++ b/qapi/migration.json
@@ -68,13 +68,13 @@
   'data': {'transferred': 'int', 'remaining': 'int', 'total': 'int' ,
            'duplicate': 'int',
            'normal': 'int',
-           'normal-bytes': 'int', 'dirty-pages-rate': 'int',
-           'mbps': 'number', 'dirty-sync-count': 'int',
-           'postcopy-requests': 'int', 'page-size': 'int',
-           'multifd-bytes': 'uint64', 'pages-per-second': 'uint64',
-           'precopy-bytes': 'uint64', 'downtime-bytes': 'uint64',
-           'postcopy-bytes': 'uint64',
-           'dirty-sync-missed-zero-copy': 'uint64' } }
+           'normal-bytes': 'int', '*dirty-pages-rate': 'int',
+           'mbps': 'number', '*dirty-sync-count': 'int',
+           '*postcopy-requests': 'int', 'page-size': 'int',
+           '*multifd-bytes': 'uint64', 'pages-per-second': 'uint64',
+           '*precopy-bytes': 'uint64', '*downtime-bytes': 'uint64',
+           '*postcopy-bytes': 'uint64',
+           '*dirty-sync-missed-zero-copy': 'uint64' } }
 
 ##
 # @XBZRLECacheStats:
-- 
2.43.0
Re: [PATCH 2/2] migration: expose RAM stats and timing on destination via query-migrate
Posted by Claudio Fontana 3 days, 2 hours ago
Hello Trieu,

makes sense to me in general, but maintainers can give you a more proper review on this,
I can only point out a few sources of confusion from my side, which likely stem from lack of knowledge from my side:

On 4/5/26 17:26, Trieu Huynh wrote:
> From: Trieu Huynh <vikingtc4@gmail.com>
> 
> MigrationStats had all-required QAPI fields, so partially populating
> info->ram on the destination would force display of source-only metrics
> (dirty-sync-count, precopy-bytes, etc.) as misleading zeros.
> 
> Make those source-only fields optional with '*' in the QAPI schema.
> Update populate_ram_info() to set them via has_* setters so source
> output is unchanged.
> 
> As per schema's changes, fill_destination_migration_info() can now
> populate info->ram for the COMPLETED case with the fields that are
> meaningful on the receive side:
> 
>   * total, remaining, page-size         (RAM layout)

Does 'remaining' make sense if the migration is completed? Don't we just assume 0?
But maybe it makes sense to keep it for simplicity/consistency.

>   * transferred, normal, normal-bytes   (bytes/pages received)
>   * duplicate                           (zero pages received)

it is interesting that zero pages are called "duplicate".

>   * mbps                                (throughput: transferred*8
>                                          /time_ms/1000)
>   * pages-per-second                    (total pages / duration in
>                                          seconds)
> 
> Others optional fields (dirty-sync-count, precopy-bytes,
> downtime-bytes, postcopy-bytes, multifd-bytes, postcopy-requests,
> dirty-sync-missed-zero-copy, dirty-pages-rate) are left unset and
> are absent from the destination output.
> 
> On dst, {"execute":"query-migrate"}
> * As-is:
> {
>     "return": {
>         "status": "completed"
> }
> * To-be:
> {
>     "return": {
>         "status": "completed",
>         "total-time": 94,
>         "ram": {
>             "total": 554508288,
>             "pages-per-second": 1440234,
>             "page-size": 4096,
>             "remaining": 0,
>             "mbps": 97.955404255319152,
>             "transferred": 1150976,
>             "duplicate": 135101,
>             "normal-bytes": 1150976,
>             "normal": 281
>         }
>     }
> }
> 
> Signed-off-by: Trieu Huynh <vikingtc4@gmail.com>
> ---
>  migration/migration.c | 35 +++++++++++++++++++++++++++++++++++
>  qapi/migration.json   | 14 +++++++-------
>  2 files changed, 42 insertions(+), 7 deletions(-)
> 
> diff --git a/migration/migration.c b/migration/migration.c
> index 17c9a8b344..925d29890d 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
> @@ -1063,17 +1063,24 @@ static void populate_ram_info(MigrationInfo *info, MigrationState *s)
>      info->ram->normal = qatomic_read(&mig_stats.normal_pages);
>      info->ram->normal_bytes = info->ram->normal * page_size;
>      info->ram->mbps = s->mbps;
> +    info->ram->has_dirty_sync_count = true;
>      info->ram->dirty_sync_count =
>          qatomic_read(&mig_stats.dirty_sync_count);
> +    info->ram->has_dirty_sync_missed_zero_copy = true;
>      info->ram->dirty_sync_missed_zero_copy =
>          qatomic_read(&mig_stats.dirty_sync_missed_zero_copy);
> +    info->ram->has_postcopy_requests = true;
>      info->ram->postcopy_requests =
>          qatomic_read(&mig_stats.postcopy_requests);
>      info->ram->page_size = page_size;
> +    info->ram->has_multifd_bytes = true;
>      info->ram->multifd_bytes = qatomic_read(&mig_stats.multifd_bytes);
>      info->ram->pages_per_second = s->pages_per_second;
> +    info->ram->has_precopy_bytes = true;
>      info->ram->precopy_bytes = qatomic_read(&mig_stats.precopy_bytes);
> +    info->ram->has_downtime_bytes = true;
>      info->ram->downtime_bytes = qatomic_read(&mig_stats.downtime_bytes);
> +    info->ram->has_postcopy_bytes = true;
>      info->ram->postcopy_bytes = qatomic_read(&mig_stats.postcopy_bytes);
>  
>      if (migrate_xbzrle()) {
> @@ -1094,6 +1101,7 @@ static void populate_ram_info(MigrationInfo *info, MigrationState *s)
>  
>      if (s->state != MIGRATION_STATUS_COMPLETED) {
>          info->ram->remaining = ram_bytes_remaining();
> +        info->ram->has_dirty_pages_rate = true;
>          info->ram->dirty_pages_rate =
>             qatomic_read(&mig_stats.dirty_pages_rate);
>      }
> @@ -1209,6 +1217,33 @@ static void fill_destination_migration_info(MigrationInfo *info)
>      case MIGRATION_STATUS_COMPLETED:
>          info->has_status = true;
>          fill_destination_postcopy_migration_info(info);
> +        if (mis->total_time > 0) {
> +            info->has_total_time = true;
> +            info->total_time = mis->total_time;
> +        }
> +        {
> +            size_t page_size = qemu_target_page_size();
> +            uint64_t normal  = mis->received_normal_pages;
> +            uint64_t zero    = mis->received_zero_pages;
> +            uint64_t xbzrle  = mis->received_xbzrle_pages;
> +
> +            info->ram = g_malloc0(sizeof(*info->ram));
> +            info->ram->total        = ram_bytes_total();
> +            info->ram->remaining    = 0;
> +            info->ram->normal       = normal + xbzrle;
> +            info->ram->normal_bytes = (normal + xbzrle) * page_size;
> +            info->ram->duplicate    = zero;
> +            info->ram->transferred  = (normal + xbzrle) * page_size;
> +            info->ram->page_size    = page_size;
> +
> +            if (info->has_total_time) {
> +                info->ram->mbps = info->ram->transferred * 8.0
> +                                  / info->total_time / 1000.0;
> +                info->ram->pages_per_second = (normal + xbzrle + zero)
> +                                              * 1000.0 / info->total_time;
> +            }
> +            /* source-only optional omitted from output */
> +        }
>          break;
>      default:
>          return;
> diff --git a/qapi/migration.json b/qapi/migration.json
> index 7134d4ce47..a695d04a22 100644
> --- a/qapi/migration.json
> +++ b/qapi/migration.json
> @@ -68,13 +68,13 @@
>    'data': {'transferred': 'int', 'remaining': 'int', 'total': 'int' ,
>             'duplicate': 'int',
>             'normal': 'int',
> -           'normal-bytes': 'int', 'dirty-pages-rate': 'int',
> -           'mbps': 'number', 'dirty-sync-count': 'int',
> -           'postcopy-requests': 'int', 'page-size': 'int',
> -           'multifd-bytes': 'uint64', 'pages-per-second': 'uint64',
> -           'precopy-bytes': 'uint64', 'downtime-bytes': 'uint64',
> -           'postcopy-bytes': 'uint64',
> -           'dirty-sync-missed-zero-copy': 'uint64' } }
> +           'normal-bytes': 'int', '*dirty-pages-rate': 'int',
> +           'mbps': 'number', '*dirty-sync-count': 'int',
> +           '*postcopy-requests': 'int', 'page-size': 'int',
> +           '*multifd-bytes': 'uint64', 'pages-per-second': 'uint64',
> +           '*precopy-bytes': 'uint64', '*downtime-bytes': 'uint64',
> +           '*postcopy-bytes': 'uint64',
> +           '*dirty-sync-missed-zero-copy': 'uint64' } }
>  
>  ##
>  # @XBZRLECacheStats:

This looks good and makes sense to me,
but looking forward to more authoritative voices.

Does this work build ok on all archs? Any tests that could be extended to cover these new stats,
maybe tests/qtest/migration/ ?

Thank you!

Claudio
Re: [PATCH 2/2] migration: expose RAM stats and timing on destination via query-migrate
Posted by Trieu Huynh 2 days, 23 hours ago
On Wed, Apr 08, 2026 at 06:14:41PM +0200, Claudio Fontana wrote:
> Hello Trieu,
> 
> makes sense to me in general, but maintainers can give you a more proper review on this,
> I can only point out a few sources of confusion from my side, which likely stem from lack of knowledge from my side:
> 
> On 4/5/26 17:26, Trieu Huynh wrote:
> > From: Trieu Huynh <vikingtc4@gmail.com>
> > 
> > MigrationStats had all-required QAPI fields, so partially populating
> > info->ram on the destination would force display of source-only metrics
> > (dirty-sync-count, precopy-bytes, etc.) as misleading zeros.
> > 
> > Make those source-only fields optional with '*' in the QAPI schema.
> > Update populate_ram_info() to set them via has_* setters so source
> > output is unchanged.
> > 
> > As per schema's changes, fill_destination_migration_info() can now
> > populate info->ram for the COMPLETED case with the fields that are
> > meaningful on the receive side:
> > 
> >   * total, remaining, page-size         (RAM layout)
> 
> Does 'remaining' make sense if the migration is completed? Don't we just assume 0?
> But maybe it makes sense to keep it for simplicity/consistency.
> 
Indeed, it set to 0 when migration status is completed, just to be
consistent.
> >   * transferred, normal, normal-bytes   (bytes/pages received)
> >   * duplicate                           (zero pages received)
> 
> it is interesting that zero pages are called "duplicate".
> 
Yes, it is. IIUC, in migration stats, duplicate = "pages we didn't
need to send because dest's memory is zero-initialized on startup,
so any all-zero src page is already "duplicate" of what the dst has.
> >   * mbps                                (throughput: transferred*8
> >                                          /time_ms/1000)
> >   * pages-per-second                    (total pages / duration in
> >                                          seconds)
> > 
> > Others optional fields (dirty-sync-count, precopy-bytes,
> > downtime-bytes, postcopy-bytes, multifd-bytes, postcopy-requests,
> > dirty-sync-missed-zero-copy, dirty-pages-rate) are left unset and
> > are absent from the destination output.
> > 
> > On dst, {"execute":"query-migrate"}
> > * As-is:
> > {
> >     "return": {
> >         "status": "completed"
> > }
> > * To-be:
> > {
> >     "return": {
> >         "status": "completed",
> >         "total-time": 94,
> >         "ram": {
> >             "total": 554508288,
> >             "pages-per-second": 1440234,
> >             "page-size": 4096,
> >             "remaining": 0,
> >             "mbps": 97.955404255319152,
> >             "transferred": 1150976,
> >             "duplicate": 135101,
> >             "normal-bytes": 1150976,
> >             "normal": 281
> >         }
> >     }
> > }
> > 
> > Signed-off-by: Trieu Huynh <vikingtc4@gmail.com>
> > ---
> >  migration/migration.c | 35 +++++++++++++++++++++++++++++++++++
> >  qapi/migration.json   | 14 +++++++-------
> >  2 files changed, 42 insertions(+), 7 deletions(-)
> > 
> > diff --git a/migration/migration.c b/migration/migration.c
> > index 17c9a8b344..925d29890d 100644
> > --- a/migration/migration.c
> > +++ b/migration/migration.c
> > @@ -1063,17 +1063,24 @@ static void populate_ram_info(MigrationInfo *info, MigrationState *s)
> >      info->ram->normal = qatomic_read(&mig_stats.normal_pages);
> >      info->ram->normal_bytes = info->ram->normal * page_size;
> >      info->ram->mbps = s->mbps;
> > +    info->ram->has_dirty_sync_count = true;
> >      info->ram->dirty_sync_count =
> >          qatomic_read(&mig_stats.dirty_sync_count);
> > +    info->ram->has_dirty_sync_missed_zero_copy = true;
> >      info->ram->dirty_sync_missed_zero_copy =
> >          qatomic_read(&mig_stats.dirty_sync_missed_zero_copy);
> > +    info->ram->has_postcopy_requests = true;
> >      info->ram->postcopy_requests =
> >          qatomic_read(&mig_stats.postcopy_requests);
> >      info->ram->page_size = page_size;
> > +    info->ram->has_multifd_bytes = true;
> >      info->ram->multifd_bytes = qatomic_read(&mig_stats.multifd_bytes);
> >      info->ram->pages_per_second = s->pages_per_second;
> > +    info->ram->has_precopy_bytes = true;
> >      info->ram->precopy_bytes = qatomic_read(&mig_stats.precopy_bytes);
> > +    info->ram->has_downtime_bytes = true;
> >      info->ram->downtime_bytes = qatomic_read(&mig_stats.downtime_bytes);
> > +    info->ram->has_postcopy_bytes = true;
> >      info->ram->postcopy_bytes = qatomic_read(&mig_stats.postcopy_bytes);
> >  
> >      if (migrate_xbzrle()) {
> > @@ -1094,6 +1101,7 @@ static void populate_ram_info(MigrationInfo *info, MigrationState *s)
> >  
> >      if (s->state != MIGRATION_STATUS_COMPLETED) {
> >          info->ram->remaining = ram_bytes_remaining();
> > +        info->ram->has_dirty_pages_rate = true;
> >          info->ram->dirty_pages_rate =
> >             qatomic_read(&mig_stats.dirty_pages_rate);
> >      }
> > @@ -1209,6 +1217,33 @@ static void fill_destination_migration_info(MigrationInfo *info)
> >      case MIGRATION_STATUS_COMPLETED:
> >          info->has_status = true;
> >          fill_destination_postcopy_migration_info(info);
> > +        if (mis->total_time > 0) {
> > +            info->has_total_time = true;
> > +            info->total_time = mis->total_time;
> > +        }
> > +        {
> > +            size_t page_size = qemu_target_page_size();
> > +            uint64_t normal  = mis->received_normal_pages;
> > +            uint64_t zero    = mis->received_zero_pages;
> > +            uint64_t xbzrle  = mis->received_xbzrle_pages;
> > +
> > +            info->ram = g_malloc0(sizeof(*info->ram));
> > +            info->ram->total        = ram_bytes_total();
> > +            info->ram->remaining    = 0;
> > +            info->ram->normal       = normal + xbzrle;
> > +            info->ram->normal_bytes = (normal + xbzrle) * page_size;
> > +            info->ram->duplicate    = zero;
> > +            info->ram->transferred  = (normal + xbzrle) * page_size;
> > +            info->ram->page_size    = page_size;
> > +
> > +            if (info->has_total_time) {
> > +                info->ram->mbps = info->ram->transferred * 8.0
> > +                                  / info->total_time / 1000.0;
> > +                info->ram->pages_per_second = (normal + xbzrle + zero)
> > +                                              * 1000.0 / info->total_time;
> > +            }
> > +            /* source-only optional omitted from output */
> > +        }
> >          break;
> >      default:
> >          return;
> > diff --git a/qapi/migration.json b/qapi/migration.json
> > index 7134d4ce47..a695d04a22 100644
> > --- a/qapi/migration.json
> > +++ b/qapi/migration.json
> > @@ -68,13 +68,13 @@
> >    'data': {'transferred': 'int', 'remaining': 'int', 'total': 'int' ,
> >             'duplicate': 'int',
> >             'normal': 'int',
> > -           'normal-bytes': 'int', 'dirty-pages-rate': 'int',
> > -           'mbps': 'number', 'dirty-sync-count': 'int',
> > -           'postcopy-requests': 'int', 'page-size': 'int',
> > -           'multifd-bytes': 'uint64', 'pages-per-second': 'uint64',
> > -           'precopy-bytes': 'uint64', 'downtime-bytes': 'uint64',
> > -           'postcopy-bytes': 'uint64',
> > -           'dirty-sync-missed-zero-copy': 'uint64' } }
> > +           'normal-bytes': 'int', '*dirty-pages-rate': 'int',
> > +           'mbps': 'number', '*dirty-sync-count': 'int',
> > +           '*postcopy-requests': 'int', 'page-size': 'int',
> > +           '*multifd-bytes': 'uint64', 'pages-per-second': 'uint64',
> > +           '*precopy-bytes': 'uint64', '*downtime-bytes': 'uint64',
> > +           '*postcopy-bytes': 'uint64',
> > +           '*dirty-sync-missed-zero-copy': 'uint64' } }
> >  
> >  ##
> >  # @XBZRLECacheStats:
> 
> This looks good and makes sense to me,
> but looking forward to more authoritative voices.
> 
Thanks, let's waiting.
> Does this work build ok on all archs? Any tests that could be extended to cover these new stats,
Actually, I have tested x86_64 but not other archs. The patch are
arch-independent IIUC, so I expected it will work without impact any
existing callers on other archs. Anw, I will check it. 
> maybe tests/qtest/migration/ ?
> 
Thanks for the point. I think can reuse existing test_precopy_tcp_plain.
Will try to add it in v2. 
> Thank you!
> 
> Claudio
> 
BRs,
Trieu Huynh