When --format json is passed to ovs-appctl, pmd-perf-show returns a
JSON object keyed by thread identifier ("pmd-cNN" for PMD threads,
"main" for the main thread).  Each entry contains:

  - "core" (only for PMD threads) and "numa"
  - "measurement-duration-s" (seconds as real)
  - "cycles" (used, idle and busy cycles, only for PMD threads)
  - "iterations" (total, idle, busy and sleep iterations, only for PMD
    threads)
  - "sleep" (sleep time, only for PMD threads)
  - "packets" (received, recirculated, transmitted and tx batches)
  - "flow-cache-hits" and "upcalls" (only if packets were received)
  - "averages" (per packet, per batch and per upcall ratios)

Percentages name what they are calculated from, the same way the text
output does: "used-percentage-of-total" is relative to all cycles in
the measurement interval, "idle-percentage-of-used" and
"busy-percentage-of-used" to the used cycles, and
"sleep-percentage-of-total" to all iterations.

Keys are omitted when the value is not available for a thread, rather
than being reported as null.  Derived values that cannot be calculated
because their denominator is zero are omitted as well, instead of being
reported as 0.  The rates in kpps and the cycles per packet are integers,
rounded the same way as in the text output.

When the detailed metrics are enabled, the command options select the
same additional sections as in text mode: "histograms" unless -nh is
given, "iteration-history" for -it and "millisecond-history" for -ms.
Each histogram is an object keyed by the upper bound of each bin, with
the sample count as value.  The last bin, which collects everything
above the last bound, is keyed "more".  The two histories are arrays
with the most recent sample first.

The counters are read and the arithmetic is done in a single place:
pmd_perf_format_overall_stats_json() builds the JSON object and
pmd_perf_format_overall_stats() renders the text output from it, so
the two formats cannot drift apart.

Example output:
  {"pmd-c03": {"core": 3, "numa": 0, "measurement-duration-s": 1.0,
               "cycles": {"used": 9600, "used-percentage-of-total": 0.4,
                          "idle": 9000, "idle-percentage-of-used": 93.75,
                          "busy": 600, "busy-percentage-of-used": 6.25},
               "iterations": {"total": 100, "idle": 80, "busy": 20,
                              "sleep": 0, "sleep-percentage-of-total": 0,
                              "us-per-iteration": 0.04},
               "sleep": {"time-us": 0},
               "packets": {"received": 20, "recirculated": 0,
                           "transmitted": 20, "tx-batches": 20},
               "flow-cache-hits": {"partial-hardware-offload": 0,
                                   "simple": 0, "exact": 19,
                                   "signature": 0, "megaflow": 0},
               "upcalls": {"success": 1, "failure": 0},
               "averages": {"datapath-passes-per-packet": 1,
                            "rx-kpps": 0, "cycles-per-packet": 30,
                            "us-per-upcall": 2.5, "tx-kpps": 0,
                            "packets-per-tx-batch": 1}}}

Signed-off-by: Timothy Redaelli <[email protected]>
---
 NEWS                   |   3 +-
 lib/dpif-netdev-perf.c | 505 ++++++++++++++++++++++++++++++++++++-----
 lib/dpif-netdev-perf.h |  11 +
 lib/dpif-netdev.c      | 107 +++++++--
 tests/pmd.at           | 172 ++++++++++++++
 5 files changed, 724 insertions(+), 74 deletions(-)

diff --git a/NEWS b/NEWS
index 97b4e2680..b6f95303e 100644
--- a/NEWS
+++ b/NEWS
@@ -2,7 +2,8 @@ Post-v4.0.0
 --------------------
    - ovs-appctl:
      * Added JSON output support (--format json) for 'fdb/stats-show',
-       'upcall/show', 'dpif-netdev/pmd-sleep-show'.
+       'upcall/show', 'dpif-netdev/pmd-sleep-show',
+       'dpif-netdev/pmd-perf-show'.
 
 
 v4.0.0 - 17 Aug 2026
diff --git a/lib/dpif-netdev-perf.c b/lib/dpif-netdev-perf.c
index 4b3b35852..caca0ca0f 100644
--- a/lib/dpif-netdev-perf.c
+++ b/lib/dpif-netdev-perf.c
@@ -206,34 +206,67 @@ pmd_perf_stats_init(struct pmd_perf_stats *s)
     s->log_reason = NULL;
 }
 
+static const struct json *
+pmd_perf_json_get(const struct json *json, const char *name)
+{
+    return json ? shash_find_data(json_object(json), name) : NULL;
+}
+
+static uint64_t
+pmd_perf_json_get_int(const struct json *json, const char *name)
+{
+    const struct json *value = pmd_perf_json_get(json, name);
+
+    return value ? json_integer(value) : 0;
+}
+
+static double
+pmd_perf_json_get_real(const struct json *json, const char *name)
+{
+    const struct json *value = pmd_perf_json_get(json, name);
+
+    return value ? json_real(value) : 0;
+}
+
 void
 pmd_perf_format_overall_stats(struct ds *str, struct pmd_perf_stats *s,
                               double duration, bool format_iterations)
 {
-    uint64_t stats[PMD_N_STATS];
-    double us_per_cycle = 1000000.0 / tsc_hz;
-
-    if (duration == 0) {
+    const struct json *averages, *cycles, *hits, *iter, *packets, *sleep;
+    uint64_t exact, lost, megaflow, passes, phwol, simple, smc, upcalls;
+    uint64_t rx_packets, tx_batches, tx_packets;
+    struct json *json = json_object_create();
+    const struct json *upcalls_json;
+
+    pmd_perf_format_overall_stats_json(json, s, duration, format_iterations);
+
+    packets = pmd_perf_json_get(json, "packets");
+    if (!packets) {
+        /* No statistics have been collected. */
+        json_destroy(json);
         return;
     }
 
-    pmd_perf_read_counters(s, stats);
-    uint64_t tot_cycles = stats[PMD_CYCLES_ITER_IDLE] +
-                          stats[PMD_CYCLES_ITER_BUSY];
-    uint64_t rx_packets = stats[PMD_STAT_RECV];
-    uint64_t tx_packets = stats[PMD_STAT_SENT_PKTS];
-    uint64_t tx_batches = stats[PMD_STAT_SENT_BATCHES];
-    uint64_t passes = stats[PMD_STAT_RECV] +
-                      stats[PMD_STAT_RECIRC];
-    uint64_t upcalls = stats[PMD_STAT_MISS];
-    uint64_t upcall_cycles = stats[PMD_CYCLES_UPCALL];
-    uint64_t tot_iter = histogram_samples(&s->pkts);
-    uint64_t idle_iter = s->pkts.bin[0];
-    uint64_t busy_iter = tot_iter >= idle_iter ? tot_iter - idle_iter : 0;
-    uint64_t sleep_iter = stats[PMD_SLEEP_ITER];
-    uint64_t tot_sleep_cycles = stats[PMD_CYCLES_SLEEP];
-
-    if (format_iterations) {
+    averages = pmd_perf_json_get(json, "averages");
+    hits = pmd_perf_json_get(json, "flow-cache-hits");
+    upcalls_json = pmd_perf_json_get(json, "upcalls");
+    iter = pmd_perf_json_get(json, "iterations");
+    cycles = pmd_perf_json_get(json, "cycles");
+    sleep = pmd_perf_json_get(json, "sleep");
+
+    rx_packets = pmd_perf_json_get_int(packets, "received");
+    tx_packets = pmd_perf_json_get_int(packets, "transmitted");
+    tx_batches = pmd_perf_json_get_int(packets, "tx-batches");
+    passes = rx_packets + pmd_perf_json_get_int(packets, "recirculated");
+    phwol = pmd_perf_json_get_int(hits, "partial-hardware-offload");
+    simple = pmd_perf_json_get_int(hits, "simple");
+    exact = pmd_perf_json_get_int(hits, "exact");
+    smc = pmd_perf_json_get_int(hits, "signature");
+    megaflow = pmd_perf_json_get_int(hits, "megaflow");
+    upcalls = pmd_perf_json_get_int(upcalls_json, "success");
+    lost = pmd_perf_json_get_int(upcalls_json, "failure");
+
+    if (iter) {
         ds_put_format(str,
             "  Iterations:         %12"PRIu64"  (%.2f us/it)\n"
             "  - Used TSC cycles:  %12"PRIu64"  (%5.1f %% of total cycles)\n"
@@ -241,26 +274,26 @@ pmd_perf_format_overall_stats(struct ds *str, struct 
pmd_perf_stats *s,
             "  - busy iterations:  %12"PRIu64"  (%5.1f %% of used cycles)\n"
             "  - sleep iterations: %12"PRIu64"  (%5.1f %% of iterations)\n"
             "  Sleep time (us):    %12.0f  (%3.0f us/iteration avg.)\n",
-            tot_iter,
-            tot_iter
-                ? (tot_cycles + tot_sleep_cycles) * us_per_cycle / tot_iter
-                : 0,
-            tot_cycles, 100.0 * (tot_cycles / duration) / tsc_hz,
-            idle_iter,
-            tot_cycles ? 100.0 * stats[PMD_CYCLES_ITER_IDLE] / tot_cycles : 0,
-            busy_iter,
-            tot_cycles ? 100.0 * stats[PMD_CYCLES_ITER_BUSY] / tot_cycles : 0,
-            sleep_iter, tot_iter ? 100.0 * sleep_iter / tot_iter : 0,
-            tot_sleep_cycles * us_per_cycle,
-            sleep_iter ? (tot_sleep_cycles * us_per_cycle) / sleep_iter : 0);
+            pmd_perf_json_get_int(iter, "total"),
+            pmd_perf_json_get_real(iter, "us-per-iteration"),
+            pmd_perf_json_get_int(cycles, "used"),
+            pmd_perf_json_get_real(cycles, "used-percentage-of-total"),
+            pmd_perf_json_get_int(iter, "idle"),
+            pmd_perf_json_get_real(cycles, "idle-percentage-of-used"),
+            pmd_perf_json_get_int(iter, "busy"),
+            pmd_perf_json_get_real(cycles, "busy-percentage-of-used"),
+            pmd_perf_json_get_int(iter, "sleep"),
+            pmd_perf_json_get_real(iter, "sleep-percentage-of-total"),
+            pmd_perf_json_get_real(sleep, "time-us"),
+            pmd_perf_json_get_real(sleep, "us-per-iteration"));
     }
     if (rx_packets > 0) {
         ds_put_format(str,
-            "  Rx packets:         %12"PRIu64"  (%.0f Kpps",
-            rx_packets, (rx_packets / duration) / 1000);
-        if (tot_iter) {
-            ds_put_format(str, ", %.0f cycles/pkt",
-                1.0 * stats[PMD_CYCLES_ITER_BUSY] / rx_packets);
+            "  Rx packets:         %12"PRIu64"  (%"PRIu64" Kpps",
+            rx_packets, pmd_perf_json_get_int(averages, "rx-kpps"));
+        if (pmd_perf_json_get(averages, "cycles-per-packet")) {
+            ds_put_format(str, ", %"PRIu64" cycles/pkt",
+                pmd_perf_json_get_int(averages, "cycles-per-packet"));
         }
         ds_put_cstr(str, ")\n");
 
@@ -272,47 +305,209 @@ pmd_perf_format_overall_stats(struct ds *str, struct 
pmd_perf_stats *s,
             "  - SMC hits:         %12"PRIu64"  (%5.1f %%)\n"
             "  - Megaflow hits:    %12"PRIu64"  (%5.1f %%, %.2f "
                                                  "subtbl lookups/hit)\n",
-            passes, 1.0 * passes / rx_packets,
-            stats[PMD_STAT_PHWOL_HIT],
-            100.0 * stats[PMD_STAT_PHWOL_HIT] / passes,
-            stats[PMD_STAT_SIMPLE_HIT],
-            100.0 * stats[PMD_STAT_SIMPLE_HIT] / passes,
-            stats[PMD_STAT_EXACT_HIT],
-            100.0 * stats[PMD_STAT_EXACT_HIT] / passes,
-            stats[PMD_STAT_SMC_HIT],
-            100.0 * stats[PMD_STAT_SMC_HIT] / passes,
-            stats[PMD_STAT_MASKED_HIT],
-            100.0 * stats[PMD_STAT_MASKED_HIT] / passes,
-            stats[PMD_STAT_MASKED_HIT]
-            ? 1.0 * stats[PMD_STAT_MASKED_LOOKUP] / stats[PMD_STAT_MASKED_HIT]
-            : 0);
+            passes,
+            pmd_perf_json_get_real(averages, "datapath-passes-per-packet"),
+            phwol, 100.0 * phwol / passes,
+            simple, 100.0 * simple / passes,
+            exact, 100.0 * exact / passes,
+            smc, 100.0 * smc / passes,
+            megaflow, 100.0 * megaflow / passes,
+            pmd_perf_json_get_real(averages,
+                                   "subtable-lookups-per-megaflow-hit"));
 
         ds_put_format(str,
             "  - Upcalls:          %12"PRIu64"  (%5.1f %%",
             upcalls, 100.0 * upcalls / passes);
-        if (tot_iter) {
+        /* Same condition as for cycles/pkt above.  "us-per-upcall" itself
+         * is omitted when there were no upcalls and is printed as 0. */
+        if (pmd_perf_json_get(averages, "cycles-per-packet")) {
             ds_put_format(str, ", %.1f us/upcall",
-                upcalls ? (upcall_cycles * us_per_cycle) / upcalls : 0);
+                pmd_perf_json_get_real(averages, "us-per-upcall"));
         }
         ds_put_cstr(str, ")\n");
 
         ds_put_format(str,
             "  - Lost upcalls:     %12"PRIu64"  (%5.1f %%)\n",
-            stats[PMD_STAT_LOST], 100.0 * stats[PMD_STAT_LOST] / passes);
+            lost, 100.0 * lost / passes);
     } else {
         ds_put_format(str,
             "  Rx packets:         %12d\n", 0);
     }
     if (tx_packets > 0) {
         ds_put_format(str,
-            "  Tx packets:         %12"PRIu64"  (%.0f Kpps)\n"
+            "  Tx packets:         %12"PRIu64"  (%"PRIu64" Kpps)\n"
             "  Tx batches:         %12"PRIu64"  (%.2f pkts/batch)\n",
-            tx_packets, (tx_packets / duration) / 1000,
-            tx_batches, 1.0 * tx_packets / tx_batches);
+            tx_packets, pmd_perf_json_get_int(averages, "tx-kpps"),
+            tx_batches,
+            pmd_perf_json_get_real(averages, "packets-per-tx-batch"));
     } else {
         ds_put_format(str,
             "  Tx packets:         %12d\n\n", 0);
     }
+
+    json_destroy(json);
+}
+
+void
+pmd_perf_format_overall_stats_json(struct json *json, struct pmd_perf_stats *s,
+                                   double duration, bool show_iterations)
+{
+    double us_per_cycle = 1000000.0 / tsc_hz;
+    uint64_t stats[PMD_N_STATS];
+
+    if (duration == 0) {
+        return;
+    }
+
+    pmd_perf_read_counters(s, stats);
+    uint64_t tot_cycles = stats[PMD_CYCLES_ITER_IDLE] +
+                          stats[PMD_CYCLES_ITER_BUSY];
+    uint64_t rx_packets = stats[PMD_STAT_RECV];
+    uint64_t tx_packets = stats[PMD_STAT_SENT_PKTS];
+    uint64_t tx_batches = stats[PMD_STAT_SENT_BATCHES];
+    uint64_t passes = stats[PMD_STAT_RECV] +
+                      stats[PMD_STAT_RECIRC];
+    uint64_t upcalls = stats[PMD_STAT_MISS];
+    uint64_t upcall_cycles = stats[PMD_CYCLES_UPCALL];
+    uint64_t tot_iter = histogram_samples(&s->pkts);
+    uint64_t idle_iter = s->pkts.bin[0];
+    uint64_t busy_iter = tot_iter >= idle_iter ? tot_iter - idle_iter : 0;
+    uint64_t sleep_iter = stats[PMD_SLEEP_ITER];
+    uint64_t tot_sleep_cycles = stats[PMD_CYCLES_SLEEP];
+    struct json *packets = json_object_create();
+
+    if (show_iterations) {
+        double used_pct = 100.0 * (tot_cycles / duration) / tsc_hz;
+        double sleep_us = tot_sleep_cycles * us_per_cycle;
+        struct json *cycles = json_object_create();
+        struct json *sleep = json_object_create();
+        struct json *iter = json_object_create();
+
+        json_object_put(cycles, "used", json_integer_create(tot_cycles));
+        json_object_put(cycles, "used-percentage-of-total",
+                        json_real_create(used_pct));
+        json_object_put(cycles, "idle",
+                        json_integer_create(stats[PMD_CYCLES_ITER_IDLE]));
+        json_object_put(cycles, "busy",
+                        json_integer_create(stats[PMD_CYCLES_ITER_BUSY]));
+        if (tot_cycles) {
+            double idle_pct = 100.0 * stats[PMD_CYCLES_ITER_IDLE] / tot_cycles;
+            double busy_pct = 100.0 * stats[PMD_CYCLES_ITER_BUSY] / tot_cycles;
+
+            json_object_put(cycles, "idle-percentage-of-used",
+                            json_real_create(idle_pct));
+            json_object_put(cycles, "busy-percentage-of-used",
+                            json_real_create(busy_pct));
+        }
+
+        json_object_put(json, "cycles", cycles);
+
+        json_object_put(iter, "total", json_integer_create(tot_iter));
+        json_object_put(iter, "idle", json_integer_create(idle_iter));
+        json_object_put(iter, "busy", json_integer_create(busy_iter));
+        json_object_put(iter, "sleep", json_integer_create(sleep_iter));
+        if (tot_iter) {
+            double us_per_iter = (tot_cycles + tot_sleep_cycles)
+                                 * us_per_cycle / tot_iter;
+            double sleep_pct = 100.0 * sleep_iter / tot_iter;
+
+            json_object_put(iter, "sleep-percentage-of-total",
+                            json_real_create(sleep_pct));
+            json_object_put(iter, "us-per-iteration",
+                            json_real_create(us_per_iter));
+        }
+
+        json_object_put(json, "iterations", iter);
+
+        json_object_put(sleep, "time-us", json_real_create(sleep_us));
+        if (sleep_iter) {
+            json_object_put(sleep, "us-per-iteration",
+                            json_real_create(sleep_us / sleep_iter));
+        }
+
+        json_object_put(json, "sleep", sleep);
+    }
+
+    json_object_put(packets, "received", json_integer_create(rx_packets));
+    json_object_put(packets, "recirculated",
+                    json_integer_create(stats[PMD_STAT_RECIRC]));
+    json_object_put(packets, "transmitted", json_integer_create(tx_packets));
+    json_object_put(packets, "tx-batches", json_integer_create(tx_batches));
+    json_object_put(json, "packets", packets);
+
+    if (rx_packets) {
+        struct json *upcalls_json = json_object_create();
+        struct json *hits = json_object_create();
+
+        json_object_put(hits, "partial-hardware-offload",
+                        json_integer_create(stats[PMD_STAT_PHWOL_HIT]));
+        json_object_put(hits, "simple",
+                        json_integer_create(stats[PMD_STAT_SIMPLE_HIT]));
+        json_object_put(hits, "exact",
+                        json_integer_create(stats[PMD_STAT_EXACT_HIT]));
+        json_object_put(hits, "signature",
+                        json_integer_create(stats[PMD_STAT_SMC_HIT]));
+        json_object_put(hits, "megaflow",
+                        json_integer_create(stats[PMD_STAT_MASKED_HIT]));
+        json_object_put(json, "flow-cache-hits", hits);
+
+        json_object_put(upcalls_json, "success",
+                        json_integer_create(upcalls));
+        json_object_put(upcalls_json, "failure",
+                        json_integer_create(stats[PMD_STAT_LOST]));
+        json_object_put(json, "upcalls", upcalls_json);
+    }
+
+    if (rx_packets || tx_packets) {
+        struct json *averages = json_object_create();
+
+        if (rx_packets) {
+            /* Rounded the same way as "%.0f" in the text output. */
+            long long int rx_kpps = llrint((rx_packets / duration) / 1000);
+            double passes_per_pkt = 1.0 * passes / rx_packets;
+
+            json_object_put(averages, "datapath-passes-per-packet",
+                            json_real_create(passes_per_pkt));
+            json_object_put(averages, "rx-kpps",
+                            json_integer_create(rx_kpps));
+            if (stats[PMD_STAT_MASKED_HIT]) {
+                double lookups_per_hit = 1.0 * stats[PMD_STAT_MASKED_LOOKUP]
+                                         / stats[PMD_STAT_MASKED_HIT];
+
+                json_object_put(averages, "subtable-lookups-per-megaflow-hit",
+                                json_real_create(lookups_per_hit));
+            }
+
+            if (tot_iter) {
+                long long int cycles_per_pkt =
+                    llrint(1.0 * stats[PMD_CYCLES_ITER_BUSY] / rx_packets);
+
+                json_object_put(averages, "cycles-per-packet",
+                                json_integer_create(cycles_per_pkt));
+                if (upcalls) {
+                    double us_per_upcall = (upcall_cycles * us_per_cycle)
+                                           / upcalls;
+
+                    json_object_put(averages, "us-per-upcall",
+                                    json_real_create(us_per_upcall));
+                }
+            }
+        }
+
+        if (tx_packets) {
+            long long int tx_kpps = llrint((tx_packets / duration) / 1000);
+
+            json_object_put(averages, "tx-kpps", json_integer_create(tx_kpps));
+            if (tx_batches) {
+                double pkts_per_batch = 1.0 * tx_packets / tx_batches;
+
+                json_object_put(averages, "packets-per-tx-batch",
+                                json_real_create(pkts_per_batch));
+            }
+        }
+
+        json_object_put(json, "averages", averages);
+    }
 }
 
 void
@@ -377,6 +572,200 @@ pmd_perf_format_histograms(struct ds *str, struct 
pmd_perf_stats *s)
                       ? s->totals.upcall_cycles / s->totals.upcalls : 0);
 }
 
+static struct json *
+histogram_to_json(const struct histogram *hist)
+{
+    struct json *json = json_object_create();
+    int i;
+
+    for (i = 0; i < NUM_BINS - 1; i++) {
+        json_object_put_nocopy(json, xasprintf("%"PRIu32, hist->wall[i]),
+                               json_integer_create(hist->bin[i]));
+    }
+
+    /* The last bin collects everything above the last wall. */
+    json_object_put(json, "more", json_integer_create(hist->bin[i]));
+
+    return json;
+}
+
+void
+pmd_perf_format_histograms_json(struct json *json, struct pmd_perf_stats *s)
+{
+    struct json *histograms = json_object_create();
+    struct json *averages = json_object_create();
+
+    json_object_put(histograms, "cycles-per-iteration",
+                    histogram_to_json(&s->cycles));
+    json_object_put(histograms, "packets-per-iteration",
+                    histogram_to_json(&s->pkts));
+    json_object_put(histograms, "cycles-per-packet",
+                    histogram_to_json(&s->cycles_per_pkt));
+    json_object_put(histograms, "packets-per-batch",
+                    histogram_to_json(&s->pkts_per_batch));
+    json_object_put(histograms, "max-vhost-qlen",
+                    histogram_to_json(&s->max_vhost_qfill));
+    json_object_put(histograms, "upcalls-per-iteration",
+                    histogram_to_json(&s->upcalls));
+    json_object_put(histograms, "cycles-per-upcall",
+                    histogram_to_json(&s->cycles_per_upcall));
+
+    if (s->totals.iterations) {
+        double qlen = 1.0 * s->totals.max_vhost_qfill / s->totals.iterations;
+        double upcalls = 1.0 * s->totals.upcalls / s->totals.iterations;
+        double pkts = 1.0 * s->totals.pkts / s->totals.iterations;
+        uint64_t cycles = s->totals.cycles / s->totals.iterations;
+
+        json_object_put(averages, "cycles-per-iteration",
+                        json_integer_create(cycles));
+        json_object_put(averages, "packets-per-iteration",
+                        json_real_create(pkts));
+        json_object_put(averages, "max-vhost-qlen", json_real_create(qlen));
+        json_object_put(averages, "upcalls-per-iteration",
+                        json_real_create(upcalls));
+    }
+
+    if (s->totals.pkts) {
+        uint64_t cycles = s->totals.busy_cycles / s->totals.pkts;
+
+        json_object_put(averages, "cycles-per-packet",
+                        json_integer_create(cycles));
+    }
+
+    if (s->totals.batches) {
+        double pkts = 1.0 * s->totals.pkts / s->totals.batches;
+
+        json_object_put(averages, "packets-per-batch", json_real_create(pkts));
+    }
+
+    if (s->totals.upcalls) {
+        uint64_t cycles = s->totals.upcall_cycles / s->totals.upcalls;
+
+        json_object_put(averages, "cycles-per-upcall",
+                        json_integer_create(cycles));
+    }
+
+    json_object_put(histograms, "averages", averages);
+    json_object_put(json, "histograms", histograms);
+}
+
+void
+pmd_perf_format_iteration_history_json(struct json *json,
+                                       struct pmd_perf_stats *s, int n_iter)
+{
+    struct iter_stats *is;
+    struct json *history;
+    size_t index;
+    int i;
+
+    if (n_iter == 0) {
+        return;
+    }
+
+    history = json_array_create_empty();
+    for (i = 1; i <= n_iter; i++) {
+        struct json *sample = json_object_create();
+
+        index = history_sub(s->iterations.idx, i);
+        is = &s->iterations.sample[index];
+
+        json_object_put(sample, "iteration",
+                        json_integer_create(is->timestamp));
+        json_object_put(sample, "cycles", json_integer_create(is->cycles));
+        json_object_put(sample, "packets", json_integer_create(is->pkts));
+        if (is->pkts) {
+            uint64_t cycles_per_pkt = is->cycles / is->pkts;
+
+            json_object_put(sample, "cycles-per-packet",
+                            json_integer_create(cycles_per_pkt));
+        }
+
+        if (is->batches) {
+            uint64_t pkts_per_batch = is->pkts / is->batches;
+
+            json_object_put(sample, "packets-per-batch",
+                            json_integer_create(pkts_per_batch));
+        }
+
+        json_object_put(sample, "max-vhost-qlen",
+                        json_integer_create(is->max_vhost_qfill));
+        json_object_put(sample, "upcalls", json_integer_create(is->upcalls));
+        if (is->upcalls) {
+            uint64_t cycles_per_upcall = is->upcall_cycles / is->upcalls;
+
+            json_object_put(sample, "cycles-per-upcall",
+                            json_integer_create(cycles_per_upcall));
+        }
+
+        json_array_add(history, sample);
+    }
+
+    json_object_put(json, "iteration-history", history);
+}
+
+void
+pmd_perf_format_ms_history_json(struct json *json, struct pmd_perf_stats *s,
+                                int n_ms)
+{
+    struct iter_stats *is;
+    struct json *history;
+    size_t index;
+    int i;
+
+    if (n_ms == 0) {
+        return;
+    }
+
+    history = json_array_create_empty();
+    for (i = 1; i <= n_ms; i++) {
+        struct json *sample = json_object_create();
+
+        index = history_sub(s->milliseconds.idx, i);
+        is = &s->milliseconds.sample[index];
+
+        json_object_put(sample, "millisecond",
+                        json_integer_create(is->timestamp));
+        json_object_put(sample, "iterations",
+                        json_integer_create(is->iterations));
+        if (is->iterations) {
+            uint64_t cycles_per_iter = is->cycles / is->iterations;
+            uint64_t qlen = is->max_vhost_qfill / is->iterations;
+
+            json_object_put(sample, "cycles-per-iteration",
+                            json_integer_create(cycles_per_iter));
+            json_object_put(sample, "max-vhost-qlen",
+                            json_integer_create(qlen));
+        }
+
+        json_object_put(sample, "kpps", json_integer_create(is->pkts));
+        if (is->pkts) {
+            uint64_t cycles_per_pkt = is->busy_cycles / is->pkts;
+
+            json_object_put(sample, "cycles-per-packet",
+                            json_integer_create(cycles_per_pkt));
+        }
+
+        if (is->batches) {
+            uint64_t pkts_per_batch = is->pkts / is->batches;
+
+            json_object_put(sample, "packets-per-batch",
+                            json_integer_create(pkts_per_batch));
+        }
+
+        json_object_put(sample, "upcalls", json_integer_create(is->upcalls));
+        if (is->upcalls) {
+            uint64_t cycles_per_upcall = is->upcall_cycles / is->upcalls;
+
+            json_object_put(sample, "cycles-per-upcall",
+                            json_integer_create(cycles_per_upcall));
+        }
+
+        json_array_add(history, sample);
+    }
+
+    json_object_put(json, "millisecond-history", history);
+}
+
 void
 pmd_perf_format_iteration_history(struct ds *str, struct pmd_perf_stats *s,
                                   int n_iter)
diff --git a/lib/dpif-netdev-perf.h b/lib/dpif-netdev-perf.h
index cf4b6e103..6f7e99cb9 100644
--- a/lib/dpif-netdev-perf.h
+++ b/lib/dpif-netdev-perf.h
@@ -29,6 +29,7 @@
 #include <rte_cycles.h>
 #endif
 
+#include "openvswitch/json.h"
 #include "openvswitch/vlog.h"
 #include "ovs-atomic.h"
 #include "timeval.h"
@@ -423,12 +424,22 @@ struct pmd_perf_params {
 
 void pmd_perf_format_overall_stats(struct ds *str, struct pmd_perf_stats *s,
                                    double duration, bool format_iterations);
+void pmd_perf_format_overall_stats_json(struct json *,
+                                        struct pmd_perf_stats *,
+                                        double duration,
+                                        bool show_iterations);
 void pmd_perf_format_histograms(struct ds *str, struct pmd_perf_stats *s);
+void pmd_perf_format_histograms_json(struct json *, struct pmd_perf_stats *);
 void pmd_perf_format_iteration_history(struct ds *str,
                                        struct pmd_perf_stats *s,
                                        int n_iter);
+void pmd_perf_format_iteration_history_json(struct json *,
+                                            struct pmd_perf_stats *,
+                                            int n_iter);
 void pmd_perf_format_ms_history(struct ds *str, struct pmd_perf_stats *s,
                                 int n_ms);
+void pmd_perf_format_ms_history_json(struct json *, struct pmd_perf_stats *,
+                                     int n_ms);
 void pmd_perf_log_set_cmd(struct unixctl_conn *conn,
                           int argc, const char *argv[],
                           void *aux OVS_UNUSED);
diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c
index 446482a67..70d5aa588 100644
--- a/lib/dpif-netdev.c
+++ b/lib/dpif-netdev.c
@@ -648,6 +648,75 @@ format_pmd_thread(struct ds *reply, struct 
dp_netdev_pmd_thread *pmd)
     ds_put_cstr(reply, ":\n");
 }
 
+static struct json *
+pmd_info_show_perf_json(struct dp_netdev_pmd_thread *pmd,
+                        struct pmd_perf_params *par)
+{
+    double duration = (time_msec() - pmd->perf_stats.start_ms) / 1000.0;
+    bool show_iterations = pmd->core_id != NON_PMD_CORE_ID;
+    struct json *json = json_object_create();
+
+    if (pmd->core_id != OVS_CORE_UNSPEC
+        && pmd->core_id != NON_PMD_CORE_ID) {
+        json_object_put(json, "core", json_integer_create(pmd->core_id));
+    }
+
+    if (pmd->numa_id != OVS_NUMA_UNSPEC) {
+        json_object_put(json, "numa", json_integer_create(pmd->numa_id));
+    }
+
+    json_object_put(json, "measurement-duration-s",
+                    json_real_create(duration));
+
+    pmd_perf_format_overall_stats_json(json, &pmd->perf_stats,
+                                       duration, show_iterations);
+
+    if (pmd_perf_metrics_enabled(pmd) && pmd->core_id != NON_PMD_CORE_ID) {
+        /* Prevent parallel clearing of perf metrics. */
+        ovs_mutex_lock(&pmd->perf_stats.clear_mutex);
+        if (par->histograms) {
+            pmd_perf_format_histograms_json(json, &pmd->perf_stats);
+        }
+
+        pmd_perf_format_iteration_history_json(json, &pmd->perf_stats,
+                                               par->iter_hist_len);
+        pmd_perf_format_ms_history_json(json, &pmd->perf_stats,
+                                        par->ms_hist_len);
+        ovs_mutex_unlock(&pmd->perf_stats.clear_mutex);
+    }
+
+    return json;
+}
+
+static struct json *
+pmd_info_perf_show_json(struct dp_netdev_pmd_thread **pmd_list, size_t n,
+                        bool filter_on_pmd, unsigned int core_id,
+                        struct pmd_perf_params *par)
+{
+    struct json *json_threads = json_object_create();
+
+    for (size_t i = 0; i < n; i++) {
+        struct dp_netdev_pmd_thread *pmd = pmd_list[i];
+        char *key;
+
+        if (!pmd) {
+            break;
+        }
+
+        if (filter_on_pmd && pmd->core_id != core_id) {
+            continue;
+        }
+
+        key = pmd->core_id == NON_PMD_CORE_ID
+              ? xstrdup("main")
+              : xasprintf("pmd-c%02u", pmd->core_id);
+        json_object_put_nocopy(json_threads, key,
+                               pmd_info_show_perf_json(pmd, par));
+    }
+
+    return json_threads;
+}
+
 static void
 pmd_info_show_perf(struct ds *reply,
                    struct dp_netdev_pmd_thread *pmd,
@@ -937,18 +1006,19 @@ static void
 dpif_netdev_pmd_info(struct unixctl_conn *conn, int argc, const char *argv[],
                      void *aux)
 {
-    struct ds reply = DS_EMPTY_INITIALIZER;
+    unsigned long long max_secs = (PMD_INTERVAL_LEN * PMD_INTERVAL_MAX)
+                                      / INTERVAL_USEC_TO_SEC;
+    enum pmd_info_type type = *(enum pmd_info_type *) aux;
     struct dp_netdev_pmd_thread **pmd_list;
+    struct ds reply = DS_EMPTY_INITIALIZER;
+    struct json *json_result = NULL;
     struct dp_netdev *dp = NULL;
-    enum pmd_info_type type = *(enum pmd_info_type *) aux;
-    unsigned int core_id;
     bool filter_on_pmd = false;
-    size_t n;
-    unsigned int secs = 0;
-    unsigned long long max_secs = (PMD_INTERVAL_LEN * PMD_INTERVAL_MAX)
-                                      / INTERVAL_USEC_TO_SEC;
+    unsigned int core_id = 0;
     bool show_header = true;
+    unsigned int secs = 0;
     uint64_t max_sleep;
+    size_t n;
 
     ovs_mutex_lock(&dp_netdev_mutex);
 
@@ -988,15 +1058,22 @@ dpif_netdev_pmd_info(struct unixctl_conn *conn, int 
argc, const char *argv[],
 
     sorted_poll_thread_list(dp, &pmd_list, &n);
 
-    if (type == PMD_INFO_SLEEP_SHOW
-        && unixctl_command_get_output_format(conn)
-               == UNIXCTL_OUTPUT_FMT_JSON) {
-        struct json *json_result = pmd_info_sleep_show_json(dp, pmd_list, n);
+    if (unixctl_command_get_output_format(conn) == UNIXCTL_OUTPUT_FMT_JSON) {
+        if (type == PMD_INFO_PERF_SHOW) {
+            json_result = pmd_info_perf_show_json(pmd_list, n, filter_on_pmd,
+                                                  core_id,
+                                                  (struct pmd_perf_params *)
+                                                  aux);
+        } else if (type == PMD_INFO_SLEEP_SHOW) {
+            json_result = pmd_info_sleep_show_json(dp, pmd_list, n);
+        }
 
-        ovs_mutex_unlock(&dp_netdev_mutex);
-        free(pmd_list);
-        unixctl_command_reply_json(conn, json_result);
-        return;
+        if (json_result) {
+            ovs_mutex_unlock(&dp_netdev_mutex);
+            free(pmd_list);
+            unixctl_command_reply_json(conn, json_result);
+            return;
+        }
     }
 
     for (size_t i = 0; i < n; i++) {
diff --git a/tests/pmd.at b/tests/pmd.at
index 955be50d1..dfb04ae79 100644
--- a/tests/pmd.at
+++ b/tests/pmd.at
@@ -484,6 +484,178 @@ pmd thread numa_id <cleared> core_id <cleared>:
   Tx batches:                   20  (1.00 pkts/batch)
 ])
 
+dnl Check JSON output.
+dnl Mask dynamic values: core id, measurement duration, all cycle,
+dnl iteration and sleep fields, kpps, cycles-per-packet, and us-per-upcall.
+AT_CHECK([ovs-appctl --format json --pretty dpif-netdev/pmd-perf-show | dnl
+          sed -e 's/"pmd-c[[0-9]][[0-9]]"/"pmd-c<cleared>"/' dnl
+              -e '/"cycles": {/,/}/s/: [[0-9.e+-]][[0-9.e+-]]*/: <cleared>/' 
dnl
+              -e '/"iterations": {/,/}/s/: [[0-9.e+-]][[0-9.e+-]]*/: 
<cleared>/' dnl
+              -e '/"sleep": {/,/}/s/: [[0-9.e+-]][[0-9.e+-]]*/: <cleared>/' dnl
+              -e 's/"core": [[0-9]][[0-9]]*/"core": <cleared>/' dnl
+              -e 's/"measurement-duration-s": 
[[0-9.e+-]]*/"measurement-duration-s": <cleared>/' dnl
+              -e 's/"rx-kpps": [[0-9]][[0-9]]*/"rx-kpps": <cleared>/' dnl
+              -e 's/"tx-kpps": [[0-9]][[0-9]]*/"tx-kpps": <cleared>/' dnl
+              -e 's/"cycles-per-packet": [[0-9]][[0-9]]*/"cycles-per-packet": 
<cleared>/' dnl
+              -e 's/"us-per-upcall": [[0-9.e+-]]*/"us-per-upcall": 
<cleared>/'], [0], [dnl
+{
+  "main": {
+    "measurement-duration-s": <cleared>,
+    "packets": {
+      "received": 0,
+      "recirculated": 0,
+      "transmitted": 0,
+      "tx-batches": 0}},
+  "pmd-c<cleared>": {
+    "averages": {
+      "cycles-per-packet": <cleared>,
+      "datapath-passes-per-packet": 1,
+      "packets-per-tx-batch": 1,
+      "rx-kpps": <cleared>,
+      "tx-kpps": <cleared>,
+      "us-per-upcall": <cleared>},
+    "core": <cleared>,
+    "cycles": {
+      "busy": <cleared>,
+      "busy-percentage-of-used": <cleared>,
+      "idle": <cleared>,
+      "idle-percentage-of-used": <cleared>,
+      "used": <cleared>,
+      "used-percentage-of-total": <cleared>},
+    "flow-cache-hits": {
+      "exact": 19,
+      "megaflow": 0,
+      "partial-hardware-offload": 0,
+      "signature": 0,
+      "simple": 0},
+    "iterations": {
+      "busy": <cleared>,
+      "idle": <cleared>,
+      "sleep": <cleared>,
+      "sleep-percentage-of-total": <cleared>,
+      "total": <cleared>,
+      "us-per-iteration": <cleared>},
+    "measurement-duration-s": <cleared>,
+    "numa": 0,
+    "packets": {
+      "received": 20,
+      "recirculated": 0,
+      "transmitted": 20,
+      "tx-batches": 20},
+    "sleep": {
+      "time-us": <cleared>},
+    "upcalls": {
+      "failure": 0,
+      "success": 1}}}
+])
+
+dnl Check that the JSON output honours -nh, -it and -ms.
+AT_CHECK([ovs-vsctl set Open_vSwitch . other_config:pmd-perf-metrics=true])
+AT_CHECK([ovs-appctl time/warp 100], [0], [ignore])
+
+AT_CHECK([ovs-appctl --format json --pretty dpif-netdev/pmd-perf-show dnl
+            -it 2 -ms 3 > perf.json])
+
+dnl No packets were processed since the detailed metrics were enabled, so
+dnl the per packet, per batch and per upcall averages are omitted.
+AT_CHECK([sed -n '/"histograms": {/,/"upcalls-per-iteration": /p' perf.json | 
dnl
+          sed 's/: [[0-9.e+-]][[0-9.e+-]]*/: <cleared>/'], [0], [dnl
+    "histograms": {
+      "averages": {
+        "cycles-per-iteration": <cleared>,
+        "max-vhost-qlen": <cleared>,
+        "packets-per-iteration": <cleared>,
+        "upcalls-per-iteration": <cleared>},
+])
+
+dnl Every histogram has the same shape, so only one of them is checked.  The
+dnl keys are the walls, the last bin collects everything above the last wall.
+AT_CHECK([sed -n '/"packets-per-batch": {$/,/}/p' perf.json | dnl
+          sed 's/: [[0-9]][[0-9]]*/: <cleared>/'], [0], [dnl
+      "packets-per-batch": {
+        "0": <cleared>,
+        "1": <cleared>,
+        "10": <cleared>,
+        "11": <cleared>,
+        "12": <cleared>,
+        "13": <cleared>,
+        "14": <cleared>,
+        "16": <cleared>,
+        "17": <cleared>,
+        "18": <cleared>,
+        "19": <cleared>,
+        "2": <cleared>,
+        "20": <cleared>,
+        "21": <cleared>,
+        "22": <cleared>,
+        "23": <cleared>,
+        "24": <cleared>,
+        "25": <cleared>,
+        "26": <cleared>,
+        "27": <cleared>,
+        "28": <cleared>,
+        "29": <cleared>,
+        "3": <cleared>,
+        "30": <cleared>,
+        "32": <cleared>,
+        "4": <cleared>,
+        "5": <cleared>,
+        "6": <cleared>,
+        "7": <cleared>,
+        "8": <cleared>,
+        "9": <cleared>,
+        "more": <cleared>},
+])
+
+dnl -it and -ms select how many samples of the two histories are reported.
+dnl Without packets, the per packet, per batch and per upcall values are
+dnl omitted.  Milliseconds without iterations omit the per iteration values,
+dnl so these are filtered out.
+AT_CHECK([sed -n '/"iteration-history": /,/}@:>@/p' perf.json | dnl
+          sed 's/: [[0-9.e+-]][[0-9.e+-]]*/: <cleared>/'], [0], [dnl
+    "iteration-history": [[
+      {
+        "cycles": <cleared>,
+        "iteration": <cleared>,
+        "max-vhost-qlen": <cleared>,
+        "packets": <cleared>,
+        "upcalls": <cleared>},
+      {
+        "cycles": <cleared>,
+        "iteration": <cleared>,
+        "max-vhost-qlen": <cleared>,
+        "packets": <cleared>,
+        "upcalls": <cleared>}]],
+])
+
+AT_CHECK([sed -n '/"millisecond-history": /,/}@:>@/p' perf.json | dnl
+          sed -e '/"cycles-per-iteration": /d' -e '/"max-vhost-qlen": /d' dnl
+              -e 's/: [[0-9.e+-]][[0-9.e+-]]*/: <cleared>/'], [0], [dnl
+    "millisecond-history": [[
+      {
+        "iterations": <cleared>,
+        "kpps": <cleared>,
+        "millisecond": <cleared>,
+        "upcalls": <cleared>},
+      {
+        "iterations": <cleared>,
+        "kpps": <cleared>,
+        "millisecond": <cleared>,
+        "upcalls": <cleared>},
+      {
+        "iterations": <cleared>,
+        "kpps": <cleared>,
+        "millisecond": <cleared>,
+        "upcalls": <cleared>}]],
+])
+
+dnl With -nh no histogram is reported, and without -it and -ms neither
+dnl history is.
+AT_CHECK([ovs-appctl --format json --pretty dpif-netdev/pmd-perf-show -nh dnl
+            > perf-nh.json])
+AT_CHECK([sed -n -e '/"histograms": /p' -e '/"iteration-history": /p' dnl
+              -e '/"millisecond-history": /p' perf-nh.json], [0], [])
+
 OVS_VSWITCHD_STOP
 AT_CLEANUP
 
-- 
2.56.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to