From patchwork Wed Aug 12 13:07:49 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Timothy Redaelli X-Patchwork-Id: 2284407 X-Patchwork-Delegate: echaudro@redhat.com Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=WVqaNIcJ; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=2605:bc80:3010::138; helo=smtp1.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp1.osuosl.org (smtp1.osuosl.org [IPv6:2605:bc80:3010::138]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4hKphH10b9z1xsQ for ; Wed, 12 Aug 2026 23:08:15 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp1.osuosl.org (Postfix) with ESMTP id 0B38781238; Wed, 12 Aug 2026 13:08:13 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp1.osuosl.org ([127.0.0.1]) by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id TjR3cNfy7HL5; Wed, 12 Aug 2026 13:08:12 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=140.211.9.56; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp1.osuosl.org D88CF81239 Authentication-Results: smtp1.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=WVqaNIcJ Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56]) by smtp1.osuosl.org (Postfix) with ESMTPS id D88CF81239; Wed, 12 Aug 2026 13:08:11 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 70BF5C03FF; Wed, 12 Aug 2026 13:08:11 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp1.osuosl.org (smtp1.osuosl.org [140.211.166.138]) by lists.linuxfoundation.org (Postfix) with ESMTP id 0430CC0013 for ; Wed, 12 Aug 2026 13:08:10 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp1.osuosl.org (Postfix) with ESMTP id DF24F8120A for ; Wed, 12 Aug 2026 13:08:09 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp1.osuosl.org ([127.0.0.1]) by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id 8mB2yDztykjw for ; Wed, 12 Aug 2026 13:08:08 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=170.10.133.124; helo=us-smtp-delivery-124.mimecast.com; envelope-from=tredaelli@redhat.com; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp1.osuosl.org 287DD8122B Authentication-Results: smtp1.osuosl.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 smtp1.osuosl.org 287DD8122B Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by smtp1.osuosl.org (Postfix) with ESMTP id 287DD8122B for ; Wed, 12 Aug 2026 13:08:07 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1786540086; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=1w7JAiI2QOy0b/L7cjPbPe0lax8YEP5XHQW+v9UKHmg=; b=WVqaNIcJ/flzhWpn4sACKFkUUs3pgqSFLvpx2zo5XtyeCsFCqe4mj7+F+LD5LCLqwxF1sj YyQ47Y/GmpHu6Z5Uj2aItnX0UxYqGSYA/M0+2p6xSZgXQHqSWnClNcP97+TdkqocPq9tZp siQIMkwfh7JZqBY9Da+aeXVQZVNfGAg= Received: from mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-634-eRAKP7QSN5qGACKW_wS_Eg-1; Wed, 12 Aug 2026 09:08:05 -0400 X-MC-Unique: eRAKP7QSN5qGACKW_wS_Eg-1 X-Mimecast-MFC-AGG-ID: eRAKP7QSN5qGACKW_wS_Eg_1786540084 Received: from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.93]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 0BCFA1956042; Wed, 12 Aug 2026 13:08:04 +0000 (UTC) Received: from aldebaran.drizzt.lan (unknown [10.44.49.243]) by mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id AAF9C1800348; Wed, 12 Aug 2026 13:08:02 +0000 (UTC) To: dev@openvswitch.org Date: Wed, 12 Aug 2026 15:07:49 +0200 Message-ID: <93610094ffdecae413c7b09de35b2a98a4818b9e.1786481318.git.tredaelli@redhat.com> In-Reply-To: References: MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.93 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: 90syDRGUjhy0Myc5YYzuQXHXgY3m4eRJ5O7LLNtyyqM_1786540084 X-Mimecast-Originator: redhat.com Subject: [ovs-dev] [PATCH v5 1/5] ofproto-dpif: Add JSON output to fdb/stats-show. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Timothy Redaelli via dev From: Timothy Redaelli Reply-To: Timothy Redaelli Cc: Ilya Maximets Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" When --format json is passed to ovs-appctl, fdb/stats-show returns a JSON object keyed by bridge name. Each bridge contains two sub-objects: "entries" (with "current", "maximum", and "static" counts) and "events" (with "evicted", "expired", "learned", and "moved" counters). The implementation is split into separate text and JSON helper functions to keep the main handler simple. Example output: {"br0": {"entries": {"current": 17, "maximum": 8192, "static": 17}, "events": {"evicted": 0, "expired": 0, "learned": 17, "moved": 0}}} Reported-at: https://issues.redhat.com/browse/FDP-2444 Signed-off-by: Timothy Redaelli Acked-by: Eelco Chaudron --- NEWS | 2 + ofproto/ofproto-dpif.c | 94 ++++++++++++++++++++++++++++---------- tests/ofproto-dpif.at | 15 ++++++ vswitchd/ovs-vswitchd.8.in | 3 ++ 4 files changed, 90 insertions(+), 24 deletions(-) diff --git a/NEWS b/NEWS index 58bf25767..eb5e84d6f 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ Post-v4.0.0 -------------------- + - ovs-appctl: + * Added JSON output support (--format json) for 'fdb/stats-show'. v4.0.0 - xx xxx xxxx diff --git a/ofproto/ofproto-dpif.c b/ofproto/ofproto-dpif.c index 0f0f71d14..97af249ba 100644 --- a/ofproto/ofproto-dpif.c +++ b/ofproto/ofproto-dpif.c @@ -6437,43 +6437,89 @@ ofproto_unixctl_fdb_stats_clear(struct unixctl_conn *conn, int argc, unixctl_command_reply(conn, "statistics successfully cleared"); } +static struct json * +ofproto_unixctl_fdb_stats_show_json(const struct ofproto_dpif *ofproto) +{ + struct json *entries = json_object_create(); + struct json *events = json_object_create(); + struct json *bridge = json_object_create(); + struct json *json = json_object_create(); + + ovs_rwlock_rdlock(&ofproto->ml->rwlock); + json_object_put(entries, "current", + json_integer_create(hmap_count(&ofproto->ml->table))); + json_object_put(entries, "maximum", + json_integer_create(ofproto->ml->max_entries)); + json_object_put(entries, "static", + json_integer_create(ofproto->ml->static_entries)); + json_object_put(bridge, "entries", entries); + + json_object_put(events, "evicted", + json_integer_create(ofproto->ml->total_evicted)); + json_object_put(events, "expired", + json_integer_create(ofproto->ml->total_expired)); + json_object_put(events, "learned", + json_integer_create(ofproto->ml->total_learned)); + json_object_put(events, "moved", + json_integer_create(ofproto->ml->total_moved)); + json_object_put(bridge, "events", events); + ovs_rwlock_unlock(&ofproto->ml->rwlock); + + json_object_put(json, ofproto->up.name, bridge); + return json; +} + +static void +ofproto_unixctl_fdb_stats_show_text(const struct ofproto_dpif *ofproto, + struct ds *ds) +{ + ovs_rwlock_rdlock(&ofproto->ml->rwlock); + ds_put_format(ds, "Statistics for bridge \"%s\":\n", ofproto->up.name); + ds_put_format(ds, " Current/maximum MAC entries in the table: %" + PRIuSIZE"/%"PRIuSIZE"\n", + hmap_count(&ofproto->ml->table), + ofproto->ml->max_entries); + ds_put_format(ds, + " Current static MAC entries in the table : %" + PRIuSIZE"\n", ofproto->ml->static_entries); + ds_put_format(ds, + " Total number of learned MAC entries : %" + PRIu64"\n", ofproto->ml->total_learned); + ds_put_format(ds, + " Total number of expired MAC entries : %" + PRIu64"\n", ofproto->ml->total_expired); + ds_put_format(ds, + " Total number of evicted MAC entries : %" + PRIu64"\n", ofproto->ml->total_evicted); + ds_put_format(ds, + " Total number of port moved MAC entries : %" + PRIu64"\n", ofproto->ml->total_moved); + ovs_rwlock_unlock(&ofproto->ml->rwlock); +} + static void ofproto_unixctl_fdb_stats_show(struct unixctl_conn *conn, int argc OVS_UNUSED, const char *argv[], void *aux OVS_UNUSED) { - struct ds ds = DS_EMPTY_INITIALIZER; const struct ofproto_dpif *ofproto; + ofproto = ofproto_dpif_lookup_by_name(argv[1]); if (!ofproto) { unixctl_command_reply_error(conn, "no such bridge"); return; } - ds_put_format(&ds, "Statistics for bridge \"%s\":\n", argv[1]); - ovs_rwlock_rdlock(&ofproto->ml->rwlock); + if (unixctl_command_get_output_format(conn) == UNIXCTL_OUTPUT_FMT_JSON) { + struct json *json = ofproto_unixctl_fdb_stats_show_json(ofproto); - ds_put_format(&ds, " Current/maximum MAC entries in the table: %" - PRIuSIZE"/%"PRIuSIZE"\n", - hmap_count(&ofproto->ml->table), ofproto->ml->max_entries); - ds_put_format(&ds, - " Current static MAC entries in the table : %"PRIuSIZE"\n", - ofproto->ml->static_entries); - ds_put_format(&ds, - " Total number of learned MAC entries : %"PRIu64"\n", - ofproto->ml->total_learned); - ds_put_format(&ds, - " Total number of expired MAC entries : %"PRIu64"\n", - ofproto->ml->total_expired); - ds_put_format(&ds, - " Total number of evicted MAC entries : %"PRIu64"\n", - ofproto->ml->total_evicted); - ds_put_format(&ds, - " Total number of port moved MAC entries : %"PRIu64"\n", - ofproto->ml->total_moved); + unixctl_command_reply_json(conn, json); + } else { + struct ds ds = DS_EMPTY_INITIALIZER; - ovs_rwlock_unlock(&ofproto->ml->rwlock); - unixctl_command_reply(conn, ds_cstr(&ds)); - ds_destroy(&ds); + ofproto_unixctl_fdb_stats_show_text(ofproto, &ds); + unixctl_command_reply(conn, ds_cstr(&ds)); + ds_destroy(&ds); + } } static void diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at index ee6ac873d..207c61ad5 100644 --- a/tests/ofproto-dpif.at +++ b/tests/ofproto-dpif.at @@ -8303,6 +8303,21 @@ AT_CHECK_UNQUOTED([ovs-appctl fdb/stats-show br0 | grep static], [0], [dnl Current static MAC entries in the table : 17 ]) +dnl Check JSON output. +AT_CHECK([ovs-appctl --format json --pretty fdb/stats-show br0], [0], [dnl +{ + "br0": { + "entries": { + "current": 17, + "maximum": 8192, + "static": 17}, + "events": { + "evicted": 0, + "expired": 0, + "learned": 18, + "moved": 0}}} +]) + OVS_VSWITCHD_STOP AT_CLEANUP diff --git a/vswitchd/ovs-vswitchd.8.in b/vswitchd/ovs-vswitchd.8.in index 3d80bceb6..72517341b 100644 --- a/vswitchd/ovs-vswitchd.8.in +++ b/vswitchd/ovs-vswitchd.8.in @@ -191,6 +191,9 @@ Clear \fIbridge\fR MAC address learning table statistics, or all statistics if no \fIbridge\fR is given. .IP "\fBfdb/stats-show\fR \fIbridge\fR" Show MAC address learning table statistics for the specified \fIbridge\fR. +JSON output is an object keyed by bridge name, whose value holds an +"entries" object with the current, maximum and static entry counts and an +"events" object with the evicted, expired, learned and moved counters. .IP "\fBmdb/flush\fR [\fIbridge\fR]" Flushes \fIbridge\fR multicast snooping table, or all snooping tables if no \fIbridge\fR is given. From patchwork Wed Aug 12 13:07:50 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Timothy Redaelli X-Patchwork-Id: 2284411 X-Patchwork-Delegate: echaudro@redhat.com Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=KGuo3ld1; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=2605:bc80:3010::138; helo=smtp1.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp1.osuosl.org (smtp1.osuosl.org [IPv6:2605:bc80:3010::138]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4hKphR5Ypcz1xx2 for ; Wed, 12 Aug 2026 23:08:23 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp1.osuosl.org (Postfix) with ESMTP id 668DA8127C; Wed, 12 Aug 2026 13:08:22 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp1.osuosl.org ([127.0.0.1]) by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id ay16g-RN0ZbU; Wed, 12 Aug 2026 13:08:20 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=2605:bc80:3010:104::8cd3:938; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp1.osuosl.org 3EF978125F Authentication-Results: smtp1.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=KGuo3ld1 Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [IPv6:2605:bc80:3010:104::8cd3:938]) by smtp1.osuosl.org (Postfix) with ESMTPS id 3EF978125F; Wed, 12 Aug 2026 13:08:20 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 0F1DDC040E; Wed, 12 Aug 2026 13:08:20 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp3.osuosl.org (smtp3.osuosl.org [IPv6:2605:bc80:3010::136]) by lists.linuxfoundation.org (Postfix) with ESMTP id 11117C059F for ; Wed, 12 Aug 2026 13:08:19 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp3.osuosl.org (Postfix) with ESMTP id E313460A92 for ; Wed, 12 Aug 2026 13:08:18 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp3.osuosl.org ([127.0.0.1]) by localhost (smtp3.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id RQuTFHDNKVcG for ; Wed, 12 Aug 2026 13:08:16 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=170.10.133.124; helo=us-smtp-delivery-124.mimecast.com; envelope-from=tredaelli@redhat.com; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp3.osuosl.org 77EAD60A68 Authentication-Results: smtp3.osuosl.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 smtp3.osuosl.org 77EAD60A68 Authentication-Results: smtp3.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=KGuo3ld1 Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by smtp3.osuosl.org (Postfix) with ESMTP id 77EAD60A68 for ; Wed, 12 Aug 2026 13:08:16 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1786540095; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=3fn4j2BlQNHHx6CscIdkpR+o8Qdy5D3wobSWXgOh5gI=; b=KGuo3ld1bF11sKXt3Q4XgrH3GoXfhxKcm7nWtpyfcLtZbqUFrifl9N2qxhb0aB0zAzb2T4 H8of8Cv6ZSNhP8YNow87VCwhrsHItTiEfcvKtP/prl01ZrTeQYHxe6b3GjGjGbz23SsQnd XCopz7BTzX4PueUuUyPaaeRq5fYnnls= Received: from mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-146-X_AphajbNA-o7U3Ln8ExDw-1; Wed, 12 Aug 2026 09:08:06 -0400 X-MC-Unique: X_AphajbNA-o7U3Ln8ExDw-1 X-Mimecast-MFC-AGG-ID: X_AphajbNA-o7U3Ln8ExDw_1786540086 Received: from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.93]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id E445C18000A3; Wed, 12 Aug 2026 13:08:05 +0000 (UTC) Received: from aldebaran.drizzt.lan (unknown [10.44.49.243]) by mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 8CD0F180049F; Wed, 12 Aug 2026 13:08:04 +0000 (UTC) To: dev@openvswitch.org Date: Wed, 12 Aug 2026 15:07:50 +0200 Message-ID: In-Reply-To: References: MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.93 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: 48xidkmDvvOM4LsjvFTmrXa-lGnZ2294LKrHsk5-pU0_1786540086 X-Mimecast-Originator: redhat.com Subject: [ovs-dev] [PATCH v5 2/5] ofproto-dpif-upcall: Add JSON output to upcall/show. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Timothy Redaelli via dev From: Timothy Redaelli Reply-To: Timothy Redaelli Cc: Ilya Maximets Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" When --format json is passed to ovs-appctl, upcall/show returns a JSON object keyed by datapath name. Each entry contains a "flows" sub-object (with "average", "current", "limit", "maximum", and "offloaded" counts), "dump-duration-ms", "ufid-enabled", and a "revalidators" object indexed by revalidator ID. The implementation is split into separate text and JSON helper functions. Example output: {"ovs-system": {"dump-duration-ms": 1, "flows": {"average": 0, "current": 0, "limit": 10000, "maximum": 0, "offloaded": 0}, "revalidators": {"4": {"keys": 0}}, "ufid-enabled": true}} Signed-off-by: Timothy Redaelli Acked-by: Eelco Chaudron --- NEWS | 1 + ofproto/ofproto-dpif-upcall.c | 95 +++++++++++++++++++++++++++++------ tests/ofproto-dpif.at | 49 +++++++++++++++++- 3 files changed, 130 insertions(+), 15 deletions(-) diff --git a/NEWS b/NEWS index eb5e84d6f..045cc0d6f 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,7 @@ Post-v4.0.0 -------------------- - ovs-appctl: * Added JSON output support (--format json) for 'fdb/stats-show'. + * Added JSON output support (--format json) for 'upcall/show'. v4.0.0 - xx xxx xxxx diff --git a/ofproto/ofproto-dpif-upcall.c b/ofproto/ofproto-dpif-upcall.c index 8e4897202..b3b80c8f4 100644 --- a/ofproto/ofproto-dpif-upcall.c +++ b/ofproto/ofproto-dpif-upcall.c @@ -28,6 +28,7 @@ #include "openvswitch/dynamic-string.h" #include "fail-open.h" #include "guarded-list.h" +#include "openvswitch/json.h" #include "latch.h" #include "openvswitch/list.h" #include "netlink.h" @@ -3160,11 +3161,65 @@ dp_purge_cb(void *aux, unsigned pmd_id) udpif_resume_revalidators(udpif); } +static struct json * +upcall_unixctl_show_json(void) +{ + struct json *json_udpifs = json_object_create(); + struct udpif *udpif; + + LIST_FOR_EACH (udpif, list_node, &all_udpifs) { + struct json *json_revalidators = json_object_create(); + struct json *json_flows = json_object_create(); + struct json *json_udpif = json_object_create(); + uint64_t n_offloaded_flows; + unsigned int flow_limit; + bool ufid_enabled; + size_t i; + + atomic_read_relaxed(&udpif->flow_limit, &flow_limit); + ufid_enabled = udpif_use_ufid(udpif); + n_offloaded_flows = dpif_offload_flow_count(udpif->dpif); + + json_object_put(json_flows, "average", + json_integer_create(udpif->avg_n_flows)); + json_object_put(json_flows, "current", + json_integer_create(udpif_get_n_flows(udpif))); + json_object_put(json_flows, "limit", + json_integer_create(flow_limit)); + json_object_put(json_flows, "maximum", + json_integer_create(udpif->max_n_flows)); + json_object_put(json_flows, "offloaded", + json_integer_create(n_offloaded_flows)); + json_object_put(json_udpif, "dump-duration-ms", + json_integer_create(udpif->dump_duration)); + json_object_put(json_udpif, "flows", json_flows); + + for (i = 0; i < udpif->n_revalidators; i++) { + struct revalidator *revalidator = &udpif->revalidators[i]; + struct json *json_rv; + int j, elements = 0; + char *key; + + json_rv = json_object_create(); + key = xasprintf("%u", revalidator->id); + for (j = i; j < N_UMAPS; j += udpif->n_revalidators) { + elements += cmap_count(&udpif->ukeys[j].cmap); + } + json_object_put(json_rv, "keys", json_integer_create(elements)); + json_object_put_nocopy(json_revalidators, key, json_rv); + } + json_object_put(json_udpif, "revalidators", json_revalidators); + json_object_put(json_udpif, "ufid-enabled", + json_boolean_create(ufid_enabled)); + + json_object_put(json_udpifs, dpif_name(udpif->dpif), json_udpif); + } + return json_udpifs; +} + static void -upcall_unixctl_show(struct unixctl_conn *conn, int argc OVS_UNUSED, - const char *argv[] OVS_UNUSED, void *aux OVS_UNUSED) +upcall_unixctl_show_text(struct ds *ds) { - struct ds ds = DS_EMPTY_INITIALIZER; uint64_t n_offloaded_flows; struct udpif *udpif; @@ -3176,23 +3231,23 @@ upcall_unixctl_show(struct unixctl_conn *conn, int argc OVS_UNUSED, atomic_read_relaxed(&udpif->flow_limit, &flow_limit); ufid_enabled = udpif_use_ufid(udpif); - ds_put_format(&ds, "%s:\n", dpif_name(udpif->dpif)); - ds_put_format(&ds, " flows : (current %lu)" + ds_put_format(ds, "%s:\n", dpif_name(udpif->dpif)); + ds_put_format(ds, " flows : (current %lu)" " (avg %u) (max %u) (limit %u)\n", udpif_get_n_flows(udpif), udpif->avg_n_flows, udpif->max_n_flows, flow_limit); n_offloaded_flows = dpif_offload_flow_count(udpif->dpif); if (n_offloaded_flows) { - ds_put_format(&ds, " offloaded flows : %"PRIu64"\n", + ds_put_format(ds, " offloaded flows : %"PRIu64"\n", n_offloaded_flows); } - ds_put_format(&ds, " dump duration : %lldms\n", udpif->dump_duration); - ds_put_format(&ds, " ufid enabled : "); + ds_put_format(ds, " dump duration : %lldms\n", udpif->dump_duration); + ds_put_format(ds, " ufid enabled : "); if (ufid_enabled) { - ds_put_format(&ds, "true\n"); + ds_put_format(ds, "true\n"); } else { - ds_put_format(&ds, "false\n"); + ds_put_format(ds, "false\n"); } - ds_put_char(&ds, '\n'); + ds_put_char(ds, '\n'); for (i = 0; i < udpif->n_revalidators; i++) { struct revalidator *revalidator = &udpif->revalidators[i]; @@ -3201,12 +3256,24 @@ upcall_unixctl_show(struct unixctl_conn *conn, int argc OVS_UNUSED, for (j = i; j < N_UMAPS; j += udpif->n_revalidators) { elements += cmap_count(&udpif->ukeys[j].cmap); } - ds_put_format(&ds, " %u: (keys %d)\n", revalidator->id, elements); + ds_put_format(ds, " %u: (keys %d)\n", revalidator->id, elements); } } +} - unixctl_command_reply(conn, ds_cstr(&ds)); - ds_destroy(&ds); +static void +upcall_unixctl_show(struct unixctl_conn *conn, int argc OVS_UNUSED, + const char *argv[] OVS_UNUSED, void *aux OVS_UNUSED) +{ + if (unixctl_command_get_output_format(conn) == UNIXCTL_OUTPUT_FMT_JSON) { + unixctl_command_reply_json(conn, upcall_unixctl_show_json()); + } else { + struct ds ds = DS_EMPTY_INITIALIZER; + + upcall_unixctl_show_text(&ds); + unixctl_command_reply(conn, ds_cstr(&ds)); + ds_destroy(&ds); + } } /* Disable using the megaflows. diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at index 207c61ad5..b7469bee2 100644 --- a/tests/ofproto-dpif.at +++ b/tests/ofproto-dpif.at @@ -13922,9 +13922,13 @@ AT_CLEANUP AT_SETUP([ofproto-dpif - Cleanup missing datapath flows]) -OVS_VSWITCHD_START +OVS_VSWITCHD_START([], [], [=override]) add_of_ports br0 1 2 +dnl Set to 1 revalidator thread early for deterministic output. +AT_CHECK([ovs-vsctl set open_vswitch . other_config:n-revalidator-threads=1]) +OVS_WAIT_UNTIL([ovs-appctl upcall/show | grep -c '(keys' | grep -q '^1$']) + m4_define([ICMP_PKT], [m4_join([,], [eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800)], [ipv4(src=10.10.10.2,dst=10.10.10.1,proto=1,tos=1,ttl=128,frag=no)], @@ -13945,6 +13949,49 @@ dnl Make sure the ukey exists. AT_CHECK([ovs-appctl upcall/show | grep '(keys' | awk '{print $3}' | \ grep -q '1)'], [0]) +dnl Add a second datapath to check the JSON output with multiple datapaths. +AT_CHECK([ovs-vsctl add-br br1 -- set bridge br1 datapath-type=netdev]) +OVS_WAIT_UNTIL([test $(ovs-appctl upcall/show | grep -c '^[[a-z]]*@') = 2]) + +dnl Check upcall/show JSON output has the expected structure. +AT_CHECK([ovs-appctl --format json --pretty upcall/show | dnl + sed 's/"dump-duration-ms": [[0-9]][[0-9]]*/"dump-duration-ms": /g' | dnl + sed 's/"average": [[0-9]][[0-9]]*/"average": /g' | dnl + sed 's/"current": [[0-9]][[0-9]]*/"current": /g' | dnl + sed 's/"limit": [[0-9]][[0-9]]*/"limit": /g' | dnl + sed 's/"maximum": [[0-9]][[0-9]]*/"maximum": /g' | dnl + sed 's/"keys": [[0-9]][[0-9]]*/"keys": /g' | dnl + sed 's/"[[0-9]][[0-9]]*": {/"": {/g'], [0], [dnl +{ + "dummy@ovs-dummy": { + "dump-duration-ms": , + "flows": { + "average": , + "current": , + "limit": , + "maximum": , + "offloaded": 0}, + "revalidators": { + "": { + "keys": }}, + "ufid-enabled": true}, + "netdev@ovs-netdev": { + "dump-duration-ms": , + "flows": { + "average": , + "current": , + "limit": , + "maximum": , + "offloaded": 0}, + "revalidators": { + "": { + "keys": }}, + "ufid-enabled": true}} +]) + +AT_CHECK([ovs-vsctl del-br br1]) +OVS_WAIT_UNTIL([test $(ovs-appctl upcall/show | grep -c '^[[a-z]]*@') = 1]) + dnl Delete all datapath flows, and make sure they are gone. AT_CHECK([ovs-appctl dpctl/del-flows]) AT_CHECK([ovs-appctl dpctl/dump-flows --names ], [0], []) From patchwork Wed Aug 12 13:07:51 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Timothy Redaelli X-Patchwork-Id: 2284413 X-Patchwork-Delegate: echaudro@redhat.com Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=N8ux09Oc; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=2605:bc80:3010::138; helo=smtp1.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp1.osuosl.org (smtp1.osuosl.org [IPv6:2605:bc80:3010::138]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4hKphV0PRZz1xwH for ; Wed, 12 Aug 2026 23:08:26 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp1.osuosl.org (Postfix) with ESMTP id 8D5508128D; Wed, 12 Aug 2026 13:08:24 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp1.osuosl.org ([127.0.0.1]) by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id 5tBbHn-rR7LZ; Wed, 12 Aug 2026 13:08:23 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=140.211.9.56; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp1.osuosl.org 70E8881241 Authentication-Results: smtp1.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=N8ux09Oc Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56]) by smtp1.osuosl.org (Postfix) with ESMTPS id 70E8881241; Wed, 12 Aug 2026 13:08:23 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 33FA5C0560; Wed, 12 Aug 2026 13:08:23 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp2.osuosl.org (smtp2.osuosl.org [IPv6:2605:bc80:3010::133]) by lists.linuxfoundation.org (Postfix) with ESMTP id B5D71C040E for ; Wed, 12 Aug 2026 13:08:19 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp2.osuosl.org (Postfix) with ESMTP id A58B6403F4 for ; Wed, 12 Aug 2026 13:08:19 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp2.osuosl.org ([127.0.0.1]) by localhost (smtp2.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id Cu1JofiaU64m for ; Wed, 12 Aug 2026 13:08:18 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=170.10.133.124; helo=us-smtp-delivery-124.mimecast.com; envelope-from=tredaelli@redhat.com; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp2.osuosl.org 51E19403F7 Authentication-Results: smtp2.osuosl.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 smtp2.osuosl.org 51E19403F7 Authentication-Results: smtp2.osuosl.org; dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=N8ux09Oc Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by smtp2.osuosl.org (Postfix) with ESMTP id 51E19403F7 for ; Wed, 12 Aug 2026 13:08:18 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1786540097; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=iZGuT0MPiKh4/mbS8Hya8FXe1RKJGbE0SH15hW2Ra7Q=; b=N8ux09OcpwpKWxyZPeQkz814MlhHObBkmKL7EL+IMBOvLvucd6zezDLcDMeapV/YNVufxI oPJU9MqU8k0uwmIGDxCzuwHVxXRhngwmio2/IRMAC/o6DSHikTAP9IN+lvqX2owQxo9qCv yD9yVU9ny4u9F5d3t8jF5PaQWZr/xNE= Received: from mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-28-sUArC9pPMAGj8LHXtLyNrA-1; Wed, 12 Aug 2026 09:08:08 -0400 X-MC-Unique: sUArC9pPMAGj8LHXtLyNrA-1 X-Mimecast-MFC-AGG-ID: sUArC9pPMAGj8LHXtLyNrA_1786540088 Received: from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.93]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id B9384195605B; Wed, 12 Aug 2026 13:08:07 +0000 (UTC) Received: from aldebaran.drizzt.lan (unknown [10.44.49.243]) by mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 705FC1800348; Wed, 12 Aug 2026 13:08:06 +0000 (UTC) To: dev@openvswitch.org Date: Wed, 12 Aug 2026 15:07:51 +0200 Message-ID: <96e9768307d02f22dea1b6f554b11862b8ef7f51.1786481318.git.tredaelli@redhat.com> In-Reply-To: References: MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.93 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: TpCYIMUV4cc7H0n_wGQE6VTxvQsslorIckQABeGBWDQ_1786540088 X-Mimecast-Originator: redhat.com Subject: [ovs-dev] [PATCH v5 3/5] dpif-netdev: Add JSON output to pmd-sleep-show. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Timothy Redaelli via dev From: Timothy Redaelli Reply-To: Timothy Redaelli Cc: Ilya Maximets Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" When --format json is passed to ovs-appctl, pmd-sleep-show returns a JSON object with "default-max-sleep-us" and a "threads" object. Each thread is keyed by a stable identifier ("pmd-cNN" derived from the core ID) and contains "core", "max-sleep-us", and "numa" fields. The "pmd-cNN" key format is chosen over the full thread name ("pmd-cNN/id:XXX") because the thread ID suffix is dynamic and would make the key unstable across restarts. The implementation is split into separate text and JSON helper functions. Example output: {"default-max-sleep-us": 100, "threads": {"pmd-c03": {"core": 3, "max-sleep-us": 100, "numa": 1}}} Signed-off-by: Timothy Redaelli --- NEWS | 2 ++ lib/dpif-netdev.c | 49 +++++++++++++++++++++++++++++++++++++++++++++++ tests/pmd.at | 19 ++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/NEWS b/NEWS index 045cc0d6f..82b3d9500 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ Post-v4.0.0 - ovs-appctl: * Added JSON output support (--format json) for 'fdb/stats-show'. * Added JSON output support (--format json) for 'upcall/show'. + * Added JSON output support (--format json) for + 'dpif-netdev/pmd-sleep-show'. v4.0.0 - xx xxx xxxx diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c index 4151ea056..79b5d1b7a 100644 --- a/lib/dpif-netdev.c +++ b/lib/dpif-netdev.c @@ -65,6 +65,7 @@ #include "odp-execute.h" #include "odp-util.h" #include "openvswitch/dynamic-string.h" +#include "openvswitch/json.h" #include "openvswitch/list.h" #include "openvswitch/match.h" #include "openvswitch/ofp-parse.h" @@ -880,6 +881,43 @@ dpif_netdev_pmd_rebalance(struct unixctl_conn *conn, int argc, ds_destroy(&reply); } +static struct json * +pmd_info_sleep_show_json(struct dp_netdev *dp, + struct dp_netdev_pmd_thread **pmd_list, size_t n) +{ + struct json *json_threads = json_object_create(); + struct json *json_result = json_object_create(); + + json_object_put(json_result, "default-max-sleep-us", + json_integer_create(dp->pmd_max_sleep_default)); + + for (size_t i = 0; i < n; i++) { + struct dp_netdev_pmd_thread *pmd = pmd_list[i]; + struct json *json_pmd; + uint64_t max_sleep; + char *key; + + if (!pmd || pmd->core_id == NON_PMD_CORE_ID) { + continue; + } + + json_pmd = json_object_create(); + key = xasprintf("pmd-c%02u", pmd->core_id); + + atomic_read_relaxed(&pmd->max_sleep, &max_sleep); + json_object_put(json_pmd, "core", + json_integer_create(pmd->core_id)); + json_object_put(json_pmd, "max-sleep-us", + json_integer_create(max_sleep)); + json_object_put(json_pmd, "numa", + json_integer_create(pmd->numa_id)); + json_object_put_nocopy(json_threads, key, json_pmd); + } + + json_object_put(json_result, "threads", json_threads); + return json_result; +} + static void pmd_info_show_sleep(struct ds *reply, unsigned core_id, int numa_id, uint64_t pmd_max_sleep) @@ -947,6 +985,17 @@ 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); + free(pmd_list); + ovs_mutex_unlock(&dp_netdev_mutex); + unixctl_command_reply_json(conn, json_result); + return; + } + for (size_t i = 0; i < n; i++) { struct dp_netdev_pmd_thread *pmd = pmd_list[i]; if (!pmd) { diff --git a/tests/pmd.at b/tests/pmd.at index 66fccc1d5..79ee2d470 100644 --- a/tests/pmd.at +++ b/tests/pmd.at @@ -1213,6 +1213,25 @@ pmd thread numa_id 8 core_id 5: max sleep: 1 us ]) +dnl Check JSON output. +AT_CHECK([ovs-appctl --format json --pretty dpif-netdev/pmd-sleep-show], [0], [dnl +{ + "default-max-sleep-us": 1, + "threads": { + "pmd-c00": { + "core": 0, + "max-sleep-us": 1, + "numa": 0}, + "pmd-c03": { + "core": 3, + "max-sleep-us": 1, + "numa": 1}, + "pmd-c05": { + "core": 5, + "max-sleep-us": 1, + "numa": 8}}} +]) + dnl Check high value max sleep get_log_next_line_num AT_CHECK([ovs-vsctl set open_vswitch . other_config:pmd-sleep-max="10000"]) From patchwork Wed Aug 12 13:07:52 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Timothy Redaelli X-Patchwork-Id: 2284414 X-Patchwork-Delegate: echaudro@redhat.com Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=S4tRzq6N; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=2605:bc80:3010::137; helo=smtp4.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp4.osuosl.org (smtp4.osuosl.org [IPv6:2605:bc80:3010::137]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4hKphV5s5Yz1xwH for ; Wed, 12 Aug 2026 23:08:26 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp4.osuosl.org (Postfix) with ESMTP id 0C805408F2; Wed, 12 Aug 2026 13:08:25 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp4.osuosl.org ([127.0.0.1]) by localhost (smtp4.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id mHR6OCJ4nBYD; Wed, 12 Aug 2026 13:08:19 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=140.211.9.56; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp4.osuosl.org D812B408E9 Authentication-Results: smtp4.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=S4tRzq6N Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56]) by smtp4.osuosl.org (Postfix) with ESMTPS id D812B408E9; Wed, 12 Aug 2026 13:08:16 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 96EB6C0013; Wed, 12 Aug 2026 13:08:16 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp2.osuosl.org (smtp2.osuosl.org [IPv6:2605:bc80:3010::133]) by lists.linuxfoundation.org (Postfix) with ESMTP id DDCDEC0013 for ; Wed, 12 Aug 2026 13:08:15 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp2.osuosl.org (Postfix) with ESMTP id B70CB403FE for ; Wed, 12 Aug 2026 13:08:15 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp2.osuosl.org ([127.0.0.1]) by localhost (smtp2.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id DaU0GUxGcTUH for ; Wed, 12 Aug 2026 13:08:13 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=170.10.129.124; helo=us-smtp-delivery-124.mimecast.com; envelope-from=tredaelli@redhat.com; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp2.osuosl.org 977AB403F4 Authentication-Results: smtp2.osuosl.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 smtp2.osuosl.org 977AB403F4 Authentication-Results: smtp2.osuosl.org; dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=S4tRzq6N Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.129.124]) by smtp2.osuosl.org (Postfix) with ESMTP id 977AB403F4 for ; Wed, 12 Aug 2026 13:08:13 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1786540092; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=eT+lzuyajkVoMn9aNi1Bno3MbrT406rWKXdW+79ZZ2o=; b=S4tRzq6Ni1azeUhwPKzfLGQpZorNP3IvFiiCDcePaj0pbMCp7NEPGiFdjEaYzNMgDBHblu 7Sz+uTcQWP8iTr0aKnMymDade5mbnndp0bDeBuhzNBtwcdfGALdJ+VRyvoJ90bTT3AjaEp XOVWsuCtBsQlIL5s3yQc1MCdBFlwRTI= Received: from mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-111--f_CE2NeMiSMI9Kes5XCdQ-1; Wed, 12 Aug 2026 09:08:10 -0400 X-MC-Unique: -f_CE2NeMiSMI9Kes5XCdQ-1 X-Mimecast-MFC-AGG-ID: -f_CE2NeMiSMI9Kes5XCdQ_1786540090 Received: from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.93]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id E2B961800877; Wed, 12 Aug 2026 13:08:09 +0000 (UTC) Received: from aldebaran.drizzt.lan (unknown [10.44.49.243]) by mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 54A24180049F; Wed, 12 Aug 2026 13:08:08 +0000 (UTC) To: dev@openvswitch.org Date: Wed, 12 Aug 2026 15:07:52 +0200 Message-ID: In-Reply-To: References: MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.93 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: SSRHMMAN0EyxW908AOXFYqZtnK6CYs8jFxU4dbwRSp0_1786540090 X-Mimecast-Originator: redhat.com Subject: [ovs-dev] [PATCH v5 4/5] dpif-netdev: Add JSON output to pmd-perf-show. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Timothy Redaelli via dev From: Timothy Redaelli Reply-To: Timothy Redaelli Cc: Ilya Maximets Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" 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) - "iterations" (iteration counts and times, only for PMD threads) - "packets" (received, recirculated, transmitted and tx batches) - "cycles" (idle and processing cycles, only for PMD threads) - "flow-cache-hits" and "upcalls" (only if packets were received) - "averages" (per packet, per batch and per upcall ratios) Keys are omitted when the value is not available for a thread, rather than being reported as null. 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 with a "walls" array of upper bin bounds and a "bins" array of sample counts, one element longer than "walls" because the last bin collects everything above the last wall. 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, "packets": {"received": 20, "recirculated": 0, "transmitted": 20, "tx-batches": 20}, "cycles": {"idle": {"count": 0, "percentage": 0}, "processing": {"count": 8, "percentage": 100}}, "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, "packets-per-tx-batch": 1}}} Signed-off-by: Timothy Redaelli --- NEWS | 2 + lib/dpif-netdev-perf.c | 450 +++++++++++++++++++++++++++++++----- lib/dpif-netdev-perf.h | 11 + lib/dpif-netdev-unixctl.man | 9 + lib/dpif-netdev.c | 92 +++++++- tests/pmd.at | 92 ++++++++ 6 files changed, 593 insertions(+), 63 deletions(-) diff --git a/NEWS b/NEWS index 82b3d9500..845b0798b 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,8 @@ Post-v4.0.0 * Added JSON output support (--format json) for 'upcall/show'. * Added JSON output support (--format json) for 'dpif-netdev/pmd-sleep-show'. + * Added JSON output support (--format json) for + 'dpif-netdev/pmd-perf-show'. v4.0.0 - xx xxx xxxx diff --git a/lib/dpif-netdev-perf.c b/lib/dpif-netdev-perf.c index 4b3b35852..766bc57e6 100644 --- a/lib/dpif-netdev-perf.c +++ b/lib/dpif-netdev-perf.c @@ -206,34 +206,64 @@ 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; + uint64_t exact, lost, megaflow, passes, phwol, simple, smc, upcalls; + const struct json *averages, *hits, *iter, *packets, *upcalls_json; + uint64_t rx_packets, tx_batches, tx_packets; + struct json *json = json_object_create(); - if (duration == 0) { + 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"); + + 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 +271,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(iter, "used-tsc-cycles"), + pmd_perf_json_get_real(iter, "used-tsc-percentage"), + pmd_perf_json_get_int(iter, "idle"), + pmd_perf_json_get_real(iter, "idle-percentage"), + pmd_perf_json_get_int(iter, "busy"), + pmd_perf_json_get_real(iter, "busy-percentage"), + pmd_perf_json_get_int(iter, "sleep"), + pmd_perf_json_get_real(iter, "sleep-percentage"), + pmd_perf_json_get_real(iter, "sleep-time-us"), + pmd_perf_json_get_real(iter, "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) { + rx_packets, pmd_perf_json_get_real(averages, "rx-kpps")); + if (pmd_perf_json_get(averages, "cycles-per-packet")) { ds_put_format(str, ", %.0f cycles/pkt", - 1.0 * stats[PMD_CYCLES_ITER_BUSY] / rx_packets); + pmd_perf_json_get_real(averages, "cycles-per-packet")); } ds_put_cstr(str, ")\n"); @@ -272,33 +302,28 @@ 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) { + if (pmd_perf_json_get(averages, "us-per-upcall")) { 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); @@ -307,12 +332,171 @@ pmd_perf_format_overall_stats(struct ds *str, struct pmd_perf_stats *s, ds_put_format(str, " Tx packets: %12"PRIu64" (%.0f 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_real(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) +{ + uint64_t stats[PMD_N_STATS]; + double us_per_cycle = 1000000.0 / tsc_hz; + + 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) { + struct json *processing = json_object_create(); + struct json *cycles = json_object_create(); + struct json *idle = json_object_create(); + struct json *iter = json_object_create(); + + json_object_put(iter, "total", json_integer_create(tot_iter)); + json_object_put(iter, "us-per-iteration", + json_real_create( + tot_iter + ? (tot_cycles + tot_sleep_cycles) + * us_per_cycle / tot_iter + : 0)); + json_object_put(iter, "used-tsc-cycles", + json_integer_create(tot_cycles)); + json_object_put(iter, "used-tsc-percentage", + json_real_create( + 100.0 * (tot_cycles / duration) / tsc_hz)); + json_object_put(iter, "idle", json_integer_create(idle_iter)); + json_object_put(iter, "idle-percentage", + json_real_create( + tot_cycles + ? 100.0 * stats[PMD_CYCLES_ITER_IDLE] / tot_cycles + : 0)); + json_object_put(iter, "busy", json_integer_create(busy_iter)); + json_object_put(iter, "busy-percentage", + json_real_create( + tot_cycles + ? 100.0 * stats[PMD_CYCLES_ITER_BUSY] / tot_cycles + : 0)); + json_object_put(iter, "sleep", json_integer_create(sleep_iter)); + json_object_put(iter, "sleep-percentage", + json_real_create( + tot_iter ? 100.0 * sleep_iter / tot_iter : 0)); + json_object_put(iter, "sleep-time-us", + json_real_create(tot_sleep_cycles * us_per_cycle)); + json_object_put(iter, "sleep-us-per-iteration", + json_real_create( + sleep_iter + ? (tot_sleep_cycles * us_per_cycle) / sleep_iter + : 0)); + json_object_put(json, "iterations", iter); + + json_object_put(idle, "count", + json_integer_create(stats[PMD_CYCLES_ITER_IDLE])); + json_object_put(idle, "percentage", + json_real_create( + tot_cycles + ? 100.0 * stats[PMD_CYCLES_ITER_IDLE] / tot_cycles + : 0)); + json_object_put(cycles, "idle", idle); + json_object_put(processing, "count", + json_integer_create(stats[PMD_CYCLES_ITER_BUSY])); + json_object_put(processing, "percentage", + json_real_create( + tot_cycles + ? 100.0 * stats[PMD_CYCLES_ITER_BUSY] / tot_cycles + : 0)); + json_object_put(cycles, "processing", processing); + json_object_put(json, "cycles", cycles); + } + + 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) { + json_object_put(averages, "datapath-passes-per-packet", + json_real_create(1.0 * passes / rx_packets)); + json_object_put(averages, "subtable-lookups-per-megaflow-hit", + json_real_create( + stats[PMD_STAT_MASKED_HIT] + ? 1.0 * stats[PMD_STAT_MASKED_LOOKUP] + / stats[PMD_STAT_MASKED_HIT] + : 0)); + json_object_put(averages, "rx-kpps", + json_real_create((rx_packets / duration) / 1000)); + if (tot_iter) { + json_object_put(averages, "cycles-per-packet", + json_real_create( + 1.0 * stats[PMD_CYCLES_ITER_BUSY] + / rx_packets)); + json_object_put(averages, "us-per-upcall", + json_real_create( + upcalls + ? (upcall_cycles * us_per_cycle) / upcalls + : 0)); + } + } + if (tx_packets) { + json_object_put(averages, "packets-per-tx-batch", + json_real_create(1.0 * tx_packets / tx_batches)); + json_object_put(averages, "tx-kpps", + json_real_create((tx_packets / duration) / 1000)); + } + json_object_put(json, "averages", averages); + } } void @@ -377,6 +561,164 @@ 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 *walls = json_array_create_empty(); + struct json *bins = json_array_create_empty(); + struct json *json = json_object_create(); + int i; + + /* The last bin has no wall, it collects everything above the last + * wall. */ + for (i = 0; i < NUM_BINS - 1; i++) { + json_array_add(walls, json_integer_create(hist->wall[i])); + } + for (i = 0; i < NUM_BINS; i++) { + json_array_add(bins, json_integer_create(hist->bin[i])); + } + json_object_put(json, "walls", walls); + json_object_put(json, "bins", bins); + + 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)); + + json_object_put(averages, "cycles-per-iteration", + json_integer_create(s->totals.iterations + ? s->totals.cycles / s->totals.iterations : 0)); + json_object_put(averages, "packets-per-iteration", + json_real_create(s->totals.iterations + ? 1.0 * s->totals.pkts / s->totals.iterations : 0)); + json_object_put(averages, "cycles-per-packet", + json_integer_create(s->totals.pkts + ? s->totals.busy_cycles / s->totals.pkts : 0)); + json_object_put(averages, "packets-per-batch", + json_real_create(s->totals.batches + ? 1.0 * s->totals.pkts / s->totals.batches : 0)); + json_object_put(averages, "max-vhost-qlen", + json_real_create(s->totals.iterations + ? 1.0 * s->totals.max_vhost_qfill + / s->totals.iterations + : 0)); + json_object_put(averages, "upcalls-per-iteration", + json_real_create(s->totals.iterations + ? 1.0 * s->totals.upcalls / s->totals.iterations : 0)); + json_object_put(averages, "cycles-per-upcall", + json_integer_create(s->totals.upcalls + ? s->totals.upcall_cycles / s->totals.upcalls : 0)); + + 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)); + json_object_put(sample, "cycles-per-packet", + json_integer_create(is->pkts + ? is->cycles / is->pkts : 0)); + json_object_put(sample, "packets-per-batch", + json_integer_create(is->batches + ? is->pkts / is->batches : 0)); + json_object_put(sample, "max-vhost-qlen", + json_integer_create(is->max_vhost_qfill)); + json_object_put(sample, "upcalls", json_integer_create(is->upcalls)); + json_object_put(sample, "cycles-per-upcall", + json_integer_create(is->upcalls + ? is->upcall_cycles / is->upcalls : 0)); + 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)); + json_object_put(sample, "cycles-per-iteration", + json_integer_create(is->iterations + ? is->cycles / is->iterations : 0)); + json_object_put(sample, "kpps", json_integer_create(is->pkts)); + json_object_put(sample, "cycles-per-packet", + json_integer_create(is->pkts + ? is->busy_cycles / is->pkts : 0)); + json_object_put(sample, "packets-per-batch", + json_integer_create(is->batches + ? is->pkts / is->batches : 0)); + json_object_put(sample, "max-vhost-qlen", + json_integer_create(is->iterations + ? is->max_vhost_qfill / is->iterations : 0)); + json_object_put(sample, "upcalls", json_integer_create(is->upcalls)); + json_object_put(sample, "cycles-per-upcall", + json_integer_create(is->upcalls + ? is->upcall_cycles / is->upcalls : 0)); + 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 #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-unixctl.man b/lib/dpif-netdev-unixctl.man index 2fb2c5442..9b01e2689 100644 --- a/lib/dpif-netdev-unixctl.man +++ b/lib/dpif-netdev-unixctl.man @@ -151,6 +151,15 @@ any packets. "busy iterations" refers to PMD iterations that included processing of at least one packet. The reported used TSC cycles include the cost for polling, processing and transmitting said packets. +JSON output is an object keyed by thread name ("main" or "pmd-c\fIXX\fR"), +whose values hold the same statistics. The histograms, selected by +\fB-nh\fR, are reported under "histograms", each metric as an object with +a "walls" array of upper bin bounds and a "bins" array of sample counts, +one longer than "walls" because the last bin collects everything above the +last wall. The iteration and millisecond histories, selected by \fB-it\fR +and \fB-ms\fR, are reported as arrays under "iteration-history" and +"millisecond-history", most recent sample first. + To reset the counters and start a new measurement use \fBdpif-netdev/pmd-stats-clear\fR. . diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c index 79b5d1b7a..384eaa695 100644 --- a/lib/dpif-netdev.c +++ b/lib/dpif-netdev.c @@ -644,6 +644,71 @@ 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,9 +1002,10 @@ dpif_netdev_pmd_info(struct unixctl_conn *conn, int argc, const char *argv[], { struct ds reply = DS_EMPTY_INITIALIZER; struct dp_netdev_pmd_thread **pmd_list; + struct json *json_result = NULL; struct dp_netdev *dp = NULL; enum pmd_info_type type = *(enum pmd_info_type *) aux; - unsigned int core_id; + unsigned int core_id = 0; bool filter_on_pmd = false; size_t n; unsigned int secs = 0; @@ -986,14 +1052,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); - free(pmd_list); - ovs_mutex_unlock(&dp_netdev_mutex); - unixctl_command_reply_json(conn, json_result); - return; + 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); + } + + if (json_result) { + free(pmd_list); + ovs_mutex_unlock(&dp_netdev_mutex); + 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 79ee2d470..3a806c071 100644 --- a/tests/pmd.at +++ b/tests/pmd.at @@ -484,6 +484,98 @@ pmd thread numa_id core_id : Tx batches: 20 (1.00 pkts/batch) ]) +dnl Check JSON output. +dnl Mask dynamic values: core id, measurement duration, all iteration and +dnl cycle fields, kpps, cycles-per-packet, and us-per-upcall. +AT_CHECK([ovs-appctl --format json --pretty dpif-netdev/pmd-perf-show | dnl + sed 's/"pmd-c[[0-9]][[0-9]]"/"pmd-c"/g' | dnl + sed 's/"core": [[0-9]][[0-9]]*/"core": /g' | dnl + sed 's/"measurement-duration-s": [[0-9.]][[0-9.]]*/"measurement-duration-s": /g' | dnl + sed 's/"rx-kpps": [[0-9.]][[0-9.]]*/"rx-kpps": /g' | dnl + sed 's/"tx-kpps": [[0-9.]][[0-9.]]*/"tx-kpps": /g' | dnl + sed 's/"cycles-per-packet": [[0-9.]][[0-9.]]*/"cycles-per-packet": /g' | dnl + sed 's/"us-per-upcall": [[0-9.]][[0-9.]]*/"us-per-upcall": /g' | dnl + sed '/"cycles": {/,/}}/{ s/: [[0-9.e+-]][[0-9.e+-]]*/: /g; }' | dnl + sed '/"iterations": {/,/}/{ s/: [[0-9.e+-]][[0-9.e+-]]*/: /g; }'], [0], [dnl +{ + "main": { + "measurement-duration-s": , + "packets": { + "received": 0, + "recirculated": 0, + "transmitted": 0, + "tx-batches": 0}}, + "pmd-c": { + "averages": { + "cycles-per-packet": , + "datapath-passes-per-packet": 1, + "packets-per-tx-batch": 1, + "rx-kpps": , + "subtable-lookups-per-megaflow-hit": 0, + "tx-kpps": , + "us-per-upcall": }, + "core": , + "cycles": { + "idle": { + "count": , + "percentage": }, + "processing": { + "count": , + "percentage": }}, + "flow-cache-hits": { + "exact": 19, + "megaflow": 0, + "partial-hardware-offload": 0, + "signature": 0, + "simple": 0}, + "iterations": { + "busy": , + "busy-percentage": , + "idle": , + "idle-percentage": , + "sleep": , + "sleep-percentage": , + "sleep-time-us": , + "sleep-us-per-iteration": , + "total": , + "us-per-iteration": , + "used-tsc-cycles": , + "used-tsc-percentage": }, + "measurement-duration-s": , + "numa": 0, + "packets": { + "received": 20, + "recirculated": 0, + "transmitted": 20, + "tx-batches": 20}, + "upcalls": { + "failure": 0, + "success": 1}}} +]) + +dnl Check that the JSON output honours -nh, -it and -ms. The values depend +dnl on the recorded iterations, so only the structure is checked here. +AT_CHECK([ovs-vsctl set Open_vSwitch . other_config:pmd-perf-metrics=true]) +ovs-appctl time/warp 100 + +AT_CHECK([ovs-appctl --format json --pretty dpif-netdev/pmd-perf-show dnl + -it 2 -ms 3 > perf.json]) +AT_CHECK([grep -c '"histograms":' perf.json], [0], [1 +]) +AT_CHECK([grep -c '"iteration":' perf.json], [0], [2 +]) +AT_CHECK([grep -c '"millisecond":' perf.json], [0], [3 +]) + +AT_CHECK([ovs-appctl --format json --pretty dpif-netdev/pmd-perf-show -nh dnl + > perf-nh.json]) +AT_CHECK([grep -c '"histograms":' perf-nh.json], [1], [0 +]) +AT_CHECK([grep -c '"iteration-history":' perf-nh.json], [1], [0 +]) +AT_CHECK([grep -c '"millisecond-history":' perf-nh.json], [1], [0 +]) + OVS_VSWITCHD_STOP AT_CLEANUP From patchwork Wed Aug 12 13:07:53 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Timothy Redaelli X-Patchwork-Id: 2284416 X-Patchwork-Delegate: echaudro@redhat.com Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=WZhmZK2i; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=openvswitch.org (client-ip=2605:bc80:3010::138; helo=smtp1.osuosl.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver=patchwork.ozlabs.org) Received: from smtp1.osuosl.org (smtp1.osuosl.org [IPv6:2605:bc80:3010::138]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4hKphc6v2Zz1xsQ for ; Wed, 12 Aug 2026 23:08:32 +1000 (AEST) Received: from localhost (localhost [127.0.0.1]) by smtp1.osuosl.org (Postfix) with ESMTP id 65D6D81277; Wed, 12 Aug 2026 13:08:31 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp1.osuosl.org ([127.0.0.1]) by localhost (smtp1.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id JnfrKcdvrIFb; Wed, 12 Aug 2026 13:08:29 +0000 (UTC) X-Comment: SPF check N/A for local connections - client-ip=140.211.9.56; helo=lists.linuxfoundation.org; envelope-from=ovs-dev-bounces@openvswitch.org; receiver= DKIM-Filter: OpenDKIM Filter v2.11.0 smtp1.osuosl.org ADF628126C Authentication-Results: smtp1.osuosl.org; dkim=fail reason="signature verification failed" (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=WZhmZK2i Received: from lists.linuxfoundation.org (lf-lists.osuosl.org [140.211.9.56]) by smtp1.osuosl.org (Postfix) with ESMTPS id ADF628126C; Wed, 12 Aug 2026 13:08:29 +0000 (UTC) Received: from lf-lists.osuosl.org (localhost [127.0.0.1]) by lists.linuxfoundation.org (Postfix) with ESMTP id 85F44C03FF; Wed, 12 Aug 2026 13:08:29 +0000 (UTC) X-Original-To: dev@openvswitch.org Delivered-To: ovs-dev@lists.linuxfoundation.org Received: from smtp4.osuosl.org (smtp4.osuosl.org [140.211.166.137]) by lists.linuxfoundation.org (Postfix) with ESMTP id 37E21C0013 for ; Wed, 12 Aug 2026 13:08:29 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by smtp4.osuosl.org (Postfix) with ESMTP id 225DB40919 for ; Wed, 12 Aug 2026 13:08:29 +0000 (UTC) X-Virus-Scanned: amavis at osuosl.org Received: from smtp4.osuosl.org ([127.0.0.1]) by localhost (smtp4.osuosl.org [127.0.0.1]) (amavis, port 10024) with ESMTP id kyNdT-4V30Yx for ; Wed, 12 Aug 2026 13:08:24 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=170.10.133.124; helo=us-smtp-delivery-124.mimecast.com; envelope-from=tredaelli@redhat.com; receiver= DMARC-Filter: OpenDMARC Filter v1.4.2 smtp4.osuosl.org B07D0408BA Authentication-Results: smtp4.osuosl.org; dmarc=pass (p=quarantine dis=none) header.from=redhat.com DKIM-Filter: OpenDKIM Filter v2.11.0 smtp4.osuosl.org B07D0408BA Authentication-Results: smtp4.osuosl.org; dkim=pass (1024-bit key) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=WZhmZK2i Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by smtp4.osuosl.org (Postfix) with ESMTP id B07D0408BA for ; Wed, 12 Aug 2026 13:08:22 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1786540101; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=DJgZm3KAbZ3yOCkHDTyq+RAc/i1Lpu/MXG39VicaD/I=; b=WZhmZK2iV6pLk8WETSpe7f0/MQZLQc5FGBFbquTW532Fu1Xsl4a0Muz4m4U0QZlJNaqmz4 oV0aoLKKHZ4+zFdqUaKF1C9EEYqv016FqT1EsnZ6kABB4pqrSB57K31TRzUgJM/mP6hmlX fGILRfgYvbnlNJPiQAKUUp7i7EuEFyc= Received: from mx-prod-mc-05.mail-002.prod.us-west-2.aws.redhat.com (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-16-IH2AlPx4PoyVmTeHmvLAmQ-1; Wed, 12 Aug 2026 09:08:12 -0400 X-MC-Unique: IH2AlPx4PoyVmTeHmvLAmQ-1 X-Mimecast-MFC-AGG-ID: IH2AlPx4PoyVmTeHmvLAmQ_1786540091 Received: from mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.93]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-05.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id B26CA195606D; Wed, 12 Aug 2026 13:08:11 +0000 (UTC) Received: from aldebaran.drizzt.lan (unknown [10.44.49.243]) by mx-prod-int-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 67C22180049F; Wed, 12 Aug 2026 13:08:10 +0000 (UTC) To: dev@openvswitch.org Date: Wed, 12 Aug 2026 15:07:53 +0200 Message-ID: <25e9d06060c0421f1a29b8de9d3149f618f1fb74.1786481318.git.tredaelli@redhat.com> In-Reply-To: References: MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.30.177.93 X-Mimecast-Spam-Score: 0 X-Mimecast-MFC-PROC-ID: ZBZR9dpdKf3T4RRRAldnzPVc3m2wr-ZBCVH020Bf6ik_1786540091 X-Mimecast-Originator: redhat.com Subject: [ovs-dev] [PATCH v5 5/5] dpctl: Add JSON output to dpctl/show. X-BeenThere: ovs-dev@openvswitch.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Timothy Redaelli via dev From: Timothy Redaelli Reply-To: Timothy Redaelli Cc: Ilya Maximets Errors-To: ovs-dev-bounces@openvswitch.org Sender: "dev" When --format json is passed to ovs-appctl, dpctl/show returns a JSON object keyed by datapath name. Each entry contains "flows", "lookups" (hit/lost/missed), and "ports" (keyed by port name, each with "port-number" and "type"). The optional sections "masks" and "cache" are only reported when the datapath supports them, and so are the port "config" and "statistics" sub-objects. "cache" contains "statistics" (hits and hit rate) and "config", an array with the name and the size of every cache level. The output_format field is added to struct dpctl_params so the handler can select the appropriate callback (show_dpif or show_dpif_json). The JSON accumulator is stored in dpctl_params.json, and the reply is sent by dpctl_unixctl_handler based on whether json is set. On error, the JSON object is discarded and a text error reply is sent. Since the JSON error path reuses the text error string, a failure of dps_for_each() to enumerate the datapaths is now reported instead of replying with an empty message. Example output: {"ovs-system": {"flows": 0, "lookups": {"hit": 0, "lost": 0, "missed": 0}, "ports": {"br0": {"port-number": 0, "type": "internal"}}}} Signed-off-by: Timothy Redaelli --- NEWS | 1 + lib/dpctl.c | 216 +++++++++++++++++++++++++++++++++++++++- lib/dpctl.h | 11 ++ lib/dpctl.man | 8 ++ tests/dpctl.at | 88 ++++++++++++++++ tests/system-traffic.at | 31 ++++++ 6 files changed, 350 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index 845b0798b..6c9fc6492 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,7 @@ Post-v4.0.0 'dpif-netdev/pmd-sleep-show'. * Added JSON output support (--format json) for 'dpif-netdev/pmd-perf-show'. + * Added JSON output support (--format json) for 'dpctl/show'. v4.0.0 - xx xxx xxxx diff --git a/lib/dpctl.c b/lib/dpctl.c index d6ff348e5..bd96519ed 100644 --- a/lib/dpctl.c +++ b/lib/dpctl.c @@ -37,6 +37,7 @@ #include "dpif-provider.h" #include "openvswitch/dynamic-string.h" #include "flow.h" +#include "openvswitch/json.h" #include "openvswitch/match.h" #include "netdev.h" #include "netlink.h" @@ -635,6 +636,189 @@ show_dpif_cache(struct dpif *dpif, struct dpctl_params *dpctl_p) show_dpif_cache__(dpif, dpctl_p); } +/* Adds 'value' to 'json' under 'name', unless the statistic is not + * supported by the netdev, which is indicated by UINT64_MAX. */ +static void +json_put_stat(struct json *json, const char *name, uint64_t value) +{ + if (value != UINT64_MAX) { + json_object_put(json, name, json_integer_create(value)); + } +} + +static struct json * +netdev_stats_to_json(const struct netdev_stats *s) +{ + struct json *json = json_object_create(); + + json_put_stat(json, "collisions", s->collisions); + json_put_stat(json, "rx-bytes", s->rx_bytes); + json_put_stat(json, "rx-dropped", s->rx_dropped); + json_put_stat(json, "rx-errors", s->rx_errors); + json_put_stat(json, "rx-frame-errors", s->rx_frame_errors); + json_put_stat(json, "rx-over-errors", s->rx_over_errors); + json_put_stat(json, "rx-packets", s->rx_packets); + json_put_stat(json, "tx-aborted-errors", s->tx_aborted_errors); + json_put_stat(json, "tx-bytes", s->tx_bytes); + json_put_stat(json, "tx-carrier-errors", s->tx_carrier_errors); + json_put_stat(json, "tx-dropped", s->tx_dropped); + json_put_stat(json, "tx-errors", s->tx_errors); + json_put_stat(json, "tx-packets", s->tx_packets); + json_put_stat(json, "upcall-errors", s->upcall_errors); + json_put_stat(json, "upcall-packets", s->upcall_packets); + + return json; +} + +static void +show_dpif_json(struct dpif *dpif, struct dpctl_params *dpctl_p) +{ + struct json *json_ports = json_object_create(); + size_t allocated_port_nos = 0, n_port_nos = 0; + struct json *json_dp = json_object_create(); + struct json *json_dps = dpctl_p->json; + struct json *json_cache = NULL; + odp_port_t *port_nos = NULL; + struct dpif_port_dump dump; + struct dpif_dp_stats stats; + struct dpif_port dpif_port; + uint32_t nr_caches; + + if (!dpif_get_dp_stats(dpif, &stats)) { + struct json *json_lookups = json_object_create(); + uint64_t n_pkts = stats.n_hit + stats.n_missed; + + json_object_put(json_lookups, "hit", + json_integer_create(stats.n_hit)); + json_object_put(json_lookups, "lost", + json_integer_create(stats.n_lost)); + json_object_put(json_lookups, "missed", + json_integer_create(stats.n_missed)); + json_object_put(json_dp, "flows", json_integer_create(stats.n_flows)); + json_object_put(json_dp, "lookups", json_lookups); + + if (stats.n_masks != UINT32_MAX) { + double avg = n_pkts ? (double) stats.n_mask_hit / n_pkts : 0.0; + struct json *json_masks = json_object_create(); + + json_object_put(json_masks, "hit", + json_integer_create(stats.n_mask_hit)); + json_object_put(json_masks, "hit-per-packet", + json_real_create(avg)); + json_object_put(json_masks, "total", + json_integer_create(stats.n_masks)); + json_object_put(json_dp, "masks", json_masks); + } + + if (stats.n_cache_hit != UINT64_MAX) { + double avg_hits = n_pkts + ? (double) stats.n_cache_hit / n_pkts * 100 : 0.0; + struct json *json_stats = json_object_create(); + + json_object_put(json_stats, "hits", + json_integer_create(stats.n_cache_hit)); + json_object_put(json_stats, "hit-rate", + json_real_create(avg_hits)); + json_cache = json_object_create(); + json_object_put(json_cache, "statistics", json_stats); + } + } + + if (!dpif_cache_get_supported_levels(dpif, &nr_caches) && nr_caches > 0) { + struct json *json_config = json_array_create_empty(); + + for (uint32_t i = 0; i < nr_caches; i++) { + struct json *json_c; + const char *name; + uint32_t size; + + if (dpif_cache_get_name(dpif, i, &name) || + dpif_cache_get_size(dpif, i, &size)) { + continue; + } + + json_c = json_object_create(); + json_object_put_string(json_c, "name", name); + json_object_put(json_c, "size", json_integer_create(size)); + json_array_add(json_config, json_c); + } + + if (!json_cache) { + json_cache = json_object_create(); + } + json_object_put(json_cache, "config", json_config); + } + + if (json_cache) { + json_object_put(json_dp, "cache", json_cache); + } + + DPIF_PORT_FOR_EACH (&dpif_port, &dump, dpif) { + if (n_port_nos >= allocated_port_nos) { + port_nos = x2nrealloc(port_nos, &allocated_port_nos, + sizeof *port_nos); + } + port_nos[n_port_nos++] = dpif_port.port_no; + } + + if (port_nos) { + qsort(port_nos, n_port_nos, sizeof *port_nos, compare_port_nos); + } + + for (int i = 0; i < n_port_nos; i++) { + struct json *json_port; + struct netdev *netdev; + + if (dpif_port_query_by_number(dpif, port_nos[i], &dpif_port, true)) { + continue; + } + + json_port = json_object_create(); + json_object_put(json_port, "port-number", + json_integer_create(odp_to_u32(dpif_port.port_no))); + json_object_put_string(json_port, "type", dpif_port.type); + + if (strcmp(dpif_port.type, "system")) { + int error = netdev_open(dpif_port.name, dpif_port.type, &netdev); + + if (!error) { + struct smap config; + + smap_init(&config); + error = netdev_get_config(netdev, &config); + if (!error && smap_count(&config) > 0) { + json_object_put(json_port, "config", + smap_to_json(&config)); + } + smap_destroy(&config); + netdev_close(netdev); + } + } + + if (dpctl_p->print_statistics) { + int error = netdev_open(dpif_port.name, dpif_port.type, &netdev); + struct netdev_stats s; + + if (!error) { + error = netdev_get_stats(netdev, &s); + + netdev_close(netdev); + if (!error) { + json_object_put(json_port, "statistics", + netdev_stats_to_json(&s)); + } + } + } + + json_object_put(json_ports, dpif_port.name, json_port); + dpif_port_destroy(&dpif_port); + } + + free(port_nos); + json_object_put(json_dp, "ports", json_ports); + json_object_put(json_dps, dpif_name(dpif), json_dp); +} + static void show_dpif(struct dpif *dpif, struct dpctl_params *dpctl_p) { @@ -824,15 +1008,29 @@ dps_for_each(struct dpctl_params *dpctl_p, dps_for_each_cb cb) * is not loaded. */ if (openerror) { return openerror; - } else { - return at_least_one ? 0 : enumerror; } + if (at_least_one) { + return 0; + } + if (enumerror) { + dpctl_error(dpctl_p, enumerror, "enumerating datapaths failed"); + } + return enumerror; } static int dpctl_show(int argc, const char *argv[], struct dpctl_params *dpctl_p) { int error, lasterror = 0; + dps_for_each_cb cb; + + if (dpctl_p->output_format == UNIXCTL_OUTPUT_FMT_JSON) { + dpctl_p->json = json_object_create(); + cb = show_dpif_json; + } else { + cb = show_dpif; + } + if (argc > 1) { int i; for (i = 1; i < argc; i++) { @@ -841,7 +1039,7 @@ dpctl_show(int argc, const char *argv[], struct dpctl_params *dpctl_p) error = parsed_dpif_open(name, false, &dpif); if (!error) { - show_dpif(dpif, dpctl_p); + cb(dpif, dpctl_p); dpif_close(dpif); } else { dpctl_error(dpctl_p, error, "opening datapath %s failed", @@ -850,7 +1048,7 @@ dpctl_show(int argc, const char *argv[], struct dpctl_params *dpctl_p) } } } else { - lasterror = dps_for_each(dpctl_p, show_dpif); + lasterror = dps_for_each(dpctl_p, cb); } return lasterror; @@ -3159,6 +3357,7 @@ dpctl_unixctl_handler(struct unixctl_conn *conn, int argc, const char *argv[], .is_appctl = true, .output = dpctl_unixctl_print, .aux = &ds, + .output_format = unixctl_command_get_output_format(conn), }; /* Parse options (like getopt). Unfortunately it does @@ -3226,7 +3425,14 @@ dpctl_unixctl_handler(struct unixctl_conn *conn, int argc, const char *argv[], error = handler(argc, argv, &dpctl_p) != 0; } - if (error) { + if (dpctl_p.json) { + if (error) { + json_destroy(dpctl_p.json); + unixctl_command_reply_error(conn, ds_cstr(&ds)); + } else { + unixctl_command_reply_json(conn, dpctl_p.json); + } + } else if (error) { unixctl_command_reply_error(conn, ds_cstr(&ds)); } else { unixctl_command_reply(conn, ds_cstr(&ds)); diff --git a/lib/dpctl.h b/lib/dpctl.h index 9d0052152..fe475471e 100644 --- a/lib/dpctl.h +++ b/lib/dpctl.h @@ -19,6 +19,9 @@ #include #include "compiler.h" +#include "unixctl.h" + +struct json; struct dpctl_params { /* True if it is called by ovs-appctl command. */ @@ -51,6 +54,14 @@ struct dpctl_params { /* 'usage' (if != NULL) gets called for the "help" command. */ void (*usage)(void *aux); + + /* Output format requested by the caller. */ + enum unixctl_output_fmt output_format; + + /* JSON object for accumulating structured output. Command handlers + * set this to a non-NULL value when producing JSON output; the caller + * uses it to decide whether to send a JSON or text reply. */ + struct json *json; }; int dpctl_run_command(int argc, const char *argv[], diff --git a/lib/dpctl.man b/lib/dpctl.man index 66fc50903..eb12af2a6 100644 --- a/lib/dpctl.man +++ b/lib/dpctl.man @@ -87,6 +87,14 @@ packets processed by the datapath. If one or more datapaths are specified, information on only those datapaths are displayed. Otherwise, \fB\*(PN\fR displays information about all configured datapaths. +.IP +JSON output is an object keyed by datapath name. Each datapath holds the +"flows" count, a "lookups" object with the "hit", "missed" and "lost" +counters, a "masks" object with the "hit", "total" and "hit-per-packet" +values, a "cache" object with the cache "statistics" and "config", and a +"ports" object keyed by port name. Each port holds its "port-number" and, +where available, its "config" and its "statistics". A row that the text +output omits is left out of the JSON output as well. .SS "DATAPATH FLOW TABLE DEBUGGING COMMANDS" The following commands are primarily useful for debugging Open vSwitch. The flow table entries (both matches and actions) that they diff --git a/tests/dpctl.at b/tests/dpctl.at index a87f67f98..8e590bfdd 100644 --- a/tests/dpctl.at +++ b/tests/dpctl.at @@ -25,6 +25,22 @@ dummy@br0: flows: 0 port 0: br0 (dummy-internal) ]) + +dnl Check dpctl/show JSON output. +AT_CHECK([ovs-appctl --format json --pretty dpctl/show dummy@br0], [0], [dnl +{ + "dummy@br0": { + "flows": 0, + "lookups": { + "hit": 0, + "lost": 0, + "missed": 0}, + "ports": { + "br0": { + "port-number": 0, + "type": "dummy-internal"}}}} +]) + AT_CHECK([ovs-appctl dpctl/add-if dummy@br0 vif1.0,type=dummy,port_no=5]) AT_CHECK([ovs-appctl dpctl/show dummy@br0], [0], [dnl dummy@br0: @@ -33,6 +49,78 @@ dummy@br0: port 0: br0 (dummy-internal) port 5: vif1.0 (dummy) ]) + +dnl Check that netdev configuration shows up in the JSON output. +AT_CHECK([ovs-appctl dpctl/add-if dummy@br0 vif2.0,type=dummy,ifindex=42,port_no=6]) +AT_CHECK([ovs-appctl --format json --pretty dpctl/show dummy@br0], [0], [dnl +{ + "dummy@br0": { + "flows": 0, + "lookups": { + "hit": 0, + "lost": 0, + "missed": 0}, + "ports": { + "br0": { + "port-number": 0, + "type": "dummy-internal"}, + "vif1.0": { + "port-number": 5, + "type": "dummy"}, + "vif2.0": { + "config": { + "ifindex": "42"}, + "port-number": 6, + "type": "dummy"}}}} +]) + +dnl Check port statistics in the JSON output. Dummy netdevs only collect +dnl rx/tx packet and byte counters; unavailable counters must be omitted. +AT_CHECK([ovs-appctl --format json --pretty dpctl/show -s dummy@br0], [0], [dnl +{ + "dummy@br0": { + "flows": 0, + "lookups": { + "hit": 0, + "lost": 0, + "missed": 0}, + "ports": { + "br0": { + "port-number": 0, + "statistics": { + "rx-bytes": 0, + "rx-packets": 0, + "tx-bytes": 0, + "tx-packets": 0}, + "type": "dummy-internal"}, + "vif1.0": { + "port-number": 5, + "statistics": { + "rx-bytes": 0, + "rx-packets": 0, + "tx-bytes": 0, + "tx-packets": 0}, + "type": "dummy"}, + "vif2.0": { + "config": { + "ifindex": "42"}, + "port-number": 6, + "statistics": { + "rx-bytes": 0, + "rx-packets": 0, + "tx-bytes": 0, + "tx-packets": 0}, + "type": "dummy"}}}} +]) + +dnl Check that errors are reported as text even if JSON was requested. +AT_CHECK([ovs-appctl --format json dpctl/show dummy@nodp], [2], [], [stderr]) +AT_CHECK([sed 's/(.*)/(...)/' stderr], [0], [dnl +ovs-vswitchd: opening datapath dummy@nodp failed (...) +ovs-appctl: ovs-vswitchd: server returned an error +]) + +AT_CHECK([ovs-appctl dpctl/del-if dummy@br0 vif2.0]) AT_CHECK([ovs-appctl dpctl/add-if dummy@br0 vif1.0,type=dummy], [2], [], [stderr]) AT_CHECK([sed 's/(.*)/(...)/' stderr], [0], diff --git a/tests/system-traffic.at b/tests/system-traffic.at index 6550f90d9..067a9edb9 100644 --- a/tests/system-traffic.at +++ b/tests/system-traffic.at @@ -2524,6 +2524,37 @@ AT_CHECK([ovs-dpctl cache-get-size | grep masks-cache | tr -d [[:blank:]]], [0], masks-cache:size:256 ]) +dnl Check that the mask and cache details are present in the JSON output. +dnl The values depend on the traffic, so only the structure is checked. The +dnl cache statistics are not included, as the datapath reports no cache hits +dnl at this point. +AT_CHECK([ovs-appctl --format json --pretty dpctl/show | dnl + sed 's/: [[0-9]][[0-9.e+-]]*/: /g'], [0], [dnl +{ + "system@ovs-system": { + "cache": { + "config": [[ + { + "name": "masks-cache", + "size": }]]}, + "flows": , + "lookups": { + "hit": , + "lost": , + "missed": }, + "masks": { + "hit": , + "hit-per-packet": , + "total": }, + "ports": { + "br0": { + "port-number": , + "type": "internal"}, + "ovs-system": { + "port-number": , + "type": "internal"}}}} +]) + OVS_TRAFFIC_VSWITCHD_STOP AT_CLEANUP