From aaac49f104e3ccad6c827626cade4477ae41945b Mon Sep 17 00:00:00 2001 From: Aaron Bernardino Date: Thu, 30 Jul 2026 20:04:44 +0000 Subject: [PATCH 1/3] [sonic_ext] Add packet-trimming (DROP_AND_TRIM) dataplane to VPP The KVM/DPDK VPP backend has no native egress QoS/admission substrate, so add a software packet-trimming datapath to the sonic_ext plugin: - trim_admission_node.c: per-{port,queue} software admission shim (token bucket driven by the SONiC scheduler rate and buffer-profile capacity) on the interface-output arc. - trim_node.c: trim action (truncate -> DSCP rewrite -> static trim-queue retry -> counters), including the multi-segment/jumbo fix that clears VLIB_BUFFER_EXT_HDR_VALID on the severed segment so the virtio PMD does not re-append the freed tail. - trim_api.c, sonic_ext_trim.api, trim_cli.c: binary API and debug CLI used to drive trimming from SAI-VPP. - sonic_ext.{c,h}, CMakeLists.txt: register the trim nodes/API. - rules/vpp.mk: bump VPP_VERSION for the plugin content change. Validated end to end on vms-kvm-vpp-t1-lag (single-segment and jumbo). Tracking: sonic-net/sonic-buildimage#25789 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Aaron Bernardino --- rules/vpp.mk | 11 +- vppbld/plugins/sonic_ext/CMakeLists.txt | 12 + vppbld/plugins/sonic_ext/sonic_ext.c | 9 +- vppbld/plugins/sonic_ext/sonic_ext.h | 250 +++++++++++++++++ vppbld/plugins/sonic_ext/sonic_ext_trim.api | 100 +++++++ .../plugins/sonic_ext/trim_admission_node.c | 236 ++++++++++++++++ vppbld/plugins/sonic_ext/trim_api.c | 236 ++++++++++++++++ vppbld/plugins/sonic_ext/trim_cli.c | 238 ++++++++++++++++ vppbld/plugins/sonic_ext/trim_node.c | 254 ++++++++++++++++++ 9 files changed, 1340 insertions(+), 6 deletions(-) create mode 100644 vppbld/plugins/sonic_ext/sonic_ext_trim.api create mode 100644 vppbld/plugins/sonic_ext/trim_admission_node.c create mode 100644 vppbld/plugins/sonic_ext/trim_api.c create mode 100644 vppbld/plugins/sonic_ext/trim_cli.c create mode 100644 vppbld/plugins/sonic_ext/trim_node.c diff --git a/rules/vpp.mk b/rules/vpp.mk index 56ee659f..99217721 100644 --- a/rules/vpp.mk +++ b/rules/vpp.mk @@ -1,13 +1,14 @@ # libvpp package VPP_VERSION_BASE = 2606 -# Bump the minor suffix whenever vppbld/patches/series or any patch file -# under vppbld/patches/*.patch changes content. The VPP_VERSION_SONIC string -# is the cache key used by vppbld/Makefile to fetch pre-built debs from +# Bump the minor suffix whenever vppbld/patches/series, any patch file +# under vppbld/patches/*.patch, or any custom plugin under vppbld/plugins/ +# changes content. The VPP_VERSION_SONIC string is the cache key used by +# vppbld/Makefile to fetch pre-built debs from # https://packages.buildkite.com/sonic-vpp/vpp; if the suffix isn't bumped, # downstream sonic-buildimage builds will silently pull stale debs that -# pre-date the new patch series and end up with VPP/SAI CRC drift. -VPP_VERSION = $(VPP_VERSION_BASE)-0.3 +# pre-date the new patch series / plugins and end up with VPP/SAI CRC drift. +VPP_VERSION = $(VPP_VERSION_BASE)-0.6 VPP_VERSION_SONIC = $(VPP_VERSION)+b1sonic1 VPP_SRC_PATH = platform/vpp/vppbld diff --git a/vppbld/plugins/sonic_ext/CMakeLists.txt b/vppbld/plugins/sonic_ext/CMakeLists.txt index 40562f6e..ff26502d 100644 --- a/vppbld/plugins/sonic_ext/CMakeLists.txt +++ b/vppbld/plugins/sonic_ext/CMakeLists.txt @@ -11,6 +11,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +include_directories(${CMAKE_SOURCE_DIR}) + +# for generated API headers (sonic_ext_trim.api.*): +include_directories(${CMAKE_BINARY_DIR}) + add_vpp_plugin(sonic_ext SOURCES sonic_ext.c @@ -19,6 +24,13 @@ add_vpp_plugin(sonic_ext host_xc_node.c l2_trap_fixup_node.c cli.c + trim_admission_node.c + trim_node.c + trim_api.c + trim_cli.c + + API_FILES + sonic_ext_trim.api LINK_LIBRARIES lcp diff --git a/vppbld/plugins/sonic_ext/sonic_ext.c b/vppbld/plugins/sonic_ext/sonic_ext.c index 53b7e127..730d99f9 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.c +++ b/vppbld/plugins/sonic_ext/sonic_ext.c @@ -27,7 +27,7 @@ sonic_ext_main_t sonic_ext_main; VLIB_PLUGIN_REGISTER () = { .version = SONIC_EXT_PLUGIN_BUILD_VER, - .description = "SONiC VPP extensions: punt-via-member, host-xc", + .description = "SONiC VPP extensions: punt-via-member, host-xc, packet-trim", }; /* @@ -276,6 +276,13 @@ sonic_ext_init (vlib_main_t *vm) sonic_ext_set_punt_via_member (1); sonic_ext_set_host_xc (1); + /* Register the packet-trim binary API (sets trim_msg_id_base). */ + { + clib_error_t *error = sonic_ext_trim_api_hookup (vm); + if (error) + return error; + } + return 0; } diff --git a/vppbld/plugins/sonic_ext/sonic_ext.h b/vppbld/plugins/sonic_ext/sonic_ext.h index b35a02ff..3150b3bc 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.h +++ b/vppbld/plugins/sonic_ext/sonic_ext.h @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -85,6 +87,29 @@ typedef struct u32 magic; u32 orig_rx_sw_if_index; u32 orig_vlan_tag; + /* + * Packet-trim per-buffer state, sharing the same opaque2->unused + * overlay as the capture cookie above. + * + * orig_queue -- the SONiC egress queue the packet resolved to on + * the admission-failure path. The admission node writes it + * immediately before diverting the buffer to sonic-ext-trim, and + * the trim node reads it in the same graph dispatch, so it is + * always freshly written before it is read -- it never depends on + * opaque2 surviving (or being cleared on) a buffer recycle. + * + * NOTE: there is deliberately NO persistent "already trimmed" flag + * here. opaque2 is NOT zeroed on buffer recycle, so a stale flag on + * a reused buffer would make a genuinely new packet read as + * "already trimmed" and wrongly bypass trimming. Instead the trim + * node re-injects the truncated copy straight to + * "interface-output-arc-end" (past the admission feature), so a + * trimmed copy never re-enters admission and no one-shot guard is + * needed. See trim_node.c / trim_admission_node.c. + */ + u8 orig_queue; /* 0 .. SONIC_EXT_TRIM_MAX_QUEUES-1 */ + u8 trim_pad0; + u16 trim_pad1; } sonic_ext_buffer_opaque_t; STATIC_ASSERT (sizeof (sonic_ext_buffer_opaque_t) <= @@ -97,6 +122,63 @@ sonic_ext_buffer (vlib_buffer_t *b) return (sonic_ext_buffer_opaque_t *) vnet_buffer2 (b)->unused; } +/* + * ------------------------------------------------------------------ + * Packet trimming (SAI DROP_AND_TRIM) -- software egress admission + + * trim action. See trim_admission_node.c, trim_node.c, trim_api.c. + * + * VPP has no native egress QoS / per-queue admission substrate, so the + * SONiC scheduler's PIR is a dataplane no-op today. The trim feature + * therefore carries its own software per-(port,queue) token-bucket + * admission shim, sized from the SONiC scheduler rate and buffer + * profile capacity programmed over the binary API. A blocking + * scheduler (PIR=1) drains the bucket and produces a real, recoverable + * admission failure that the trim action node hangs off of. + * ------------------------------------------------------------------ + */ + +/* SONiC standard egress unicast queue count per port (0..7). */ +#define SONIC_EXT_TRIM_MAX_QUEUES 8 + +/* Sentinel returned by sonic_ext_trim_packet_queue() for frames that have + * no resolvable egress queue (non-IP: ARP, LACP, LLDP, ...). Such frames + * must never be policed or trimmed -- indexing port->q[] with this value + * is invalid, so callers must treat it as an unconditional bypass. */ +#define SONIC_EXT_TRIM_QUEUE_NONE 0xff + +/* PACKET_TRIM_DSCP_RESOLUTION_MODE. */ +typedef enum +{ + SONIC_EXT_TRIM_DSCP_MODE_SYMMETRIC = 0, /* use configured dscp_value */ + SONIC_EXT_TRIM_DSCP_MODE_FROM_TC = 1, /* asymmetric egress-map lookup */ +} sonic_ext_trim_dscp_mode_t; + +/* + * One software token-bucket admission slot per egress queue. Bytes are + * the unit throughout: `tokens` and `capacity_bytes` are byte credits, + * `rate_bytes_per_sec` is the SONiC scheduler PIR translated to bytes/s + * (0 => the queue can never admit, i.e. a fully blocking scheduler). + * + * NOTE (first increment): the bucket is a single shared slot per + * (sw_if_index, queue) with no per-worker partitioning, so it assumes + * the vlab single-worker dispatch. Multi-worker correctness (per-thread + * buckets or atomics) is deferred; see the HLD limitations. + */ +typedef struct +{ + u8 eligible; /* queue buffer profile uses DROP_AND_TRIM */ + u8 configured; /* a rate/capacity has been programmed */ + u64 rate_bytes_per_sec; /* SONiC scheduler PIR, bytes/s (0 => block) */ + u64 capacity_bytes; /* token-bucket depth (buffer profile size) */ + f64 tokens; /* current byte credits */ + f64 last_refill; /* vlib_time_now() of the last refill */ +} sonic_ext_trim_queue_t; + +typedef struct +{ + sonic_ext_trim_queue_t q[SONIC_EXT_TRIM_MAX_QUEUES]; +} sonic_ext_trim_port_t; + typedef struct { /* Global feature toggles. */ @@ -114,6 +196,37 @@ typedef struct u64 aggr_tap_redirects; u64 host_xc_direct; u64 l2_trap_fixups; + + /* ---- packet trimming ---- */ + u16 trim_msg_id_base; /* binary API base id (setup_message_id_table) */ + u8 trim_configured; /* any global trim policy pushed yet */ + + /* Global trim policy (SAI SWITCH_ATTR_PACKET_TRIM_*). */ + u16 trim_size; /* bytes retained; 0 => unset */ + u8 trim_dscp_mode; /* sonic_ext_trim_dscp_mode_t */ + u8 trim_dscp_value; /* symmetric DSCP (0..63) */ + u8 trim_tc; /* TC used for FROM_TC egress-map lookup */ + u8 trim_queue; /* static trim queue index (0..7) */ + + /* + * Global DSCP(0..63) -> egress queue(0..7) resolution used by the + * admission shim to classify a packet to its original queue. Pushed + * by SAI-VPP as the composition of the SONiC DSCP_TO_TC and + * TC_TO_QUEUE maps, so the plugin does not replicate both maps. + */ + u8 dscp_to_queue[64]; + + /* + * Per-egress-port admission state, a vec indexed by sw_if_index; each + * element holds one token bucket per queue. NULL/short entries mean + * "no trim admission configured on that port" (feature is a no-op). + */ + sonic_ext_trim_port_t *trim_ports; + + /* Summary counters surfaced by `show sonic-ext trim` and SAI stats. */ + u64 trim_sent; /* truncated copies transmitted */ + u64 trim_drop; /* eligible but trim queue also congested */ + u64 trim_admit_fail; /* original-queue admission failures observed */ } sonic_ext_main_t; extern sonic_ext_main_t sonic_ext_main; @@ -151,4 +264,141 @@ int sonic_ext_phy_is_aggregate (u32 phy_sw_if_index); * the BVI's own ip4-unicast arc; today only BVIs need it). */ int sonic_ext_phy_is_bvi (u32 phy_sw_if_index); +/* ---- packet trimming (trim_admission_node.c / trim_node.c / trim_api.c) ---- */ + +extern vlib_node_registration_t sonic_ext_trim_admission_node; +extern vlib_node_registration_t sonic_ext_trim_node; + +/* Enable/disable the sonic-ext-trim-admission feature on an egress + * interface-output arc. Driven from the binary API when a port gains or + * loses trim-eligible queues. */ +void sonic_ext_trim_enable_disable (u32 sw_if_index, int enable); + +/* Get (optionally allocate) the per-port admission state for sw_if_index. + * Returns NULL when create==0 and the port has no state. */ +sonic_ext_trim_port_t *sonic_ext_trim_port_get (u32 sw_if_index, int create); + +/* Program one egress (port, queue) admission slot and (re)evaluate whether + * the sonic-ext-trim-admission feature should be enabled on the port. + * Shared by the binary API handler and the debug CLI. */ +void sonic_ext_trim_queue_program (u32 sw_if_index, u32 queue, int eligible, + u64 rate_bytes_per_sec, u64 capacity_bytes); + +/* Software token-bucket admission decision for one packet of `len` bytes + * on queue `q`. Refills from rate_bytes_per_sec, then either debits and + * returns 1 (admitted) or leaves the bucket and returns 0 (rejected). A + * queue with rate_bytes_per_sec==0 always rejects once drained. */ +int sonic_ext_trim_bucket_admit (vlib_main_t *vm, sonic_ext_trim_queue_t *q, + u32 len); + +/* Register the trim binary API message table. Called from + * sonic_ext_init(); sets sonic_ext_main.trim_msg_id_base. */ +clib_error_t *sonic_ext_trim_api_hookup (vlib_main_t *vm); + +/* + * ------------------------------------------------------------------ + * Shared L3 classification / DSCP rewrite inlines used by both the + * admission node (classify the packet to its egress queue) and the + * trim action node (rewrite the trimmed copy's DSCP). + * + * Both nodes run on the interface-output arc, where the buffer's + * current data is positioned at the start of the (rewritten) L2 + * ethernet header for normal transit/routed traffic. We parse the + * ethertype, skip up to two VLAN tags, and locate the IPv4/IPv6 + * header. Non-IP frames have no resolvable queue (sentinel + * SONIC_EXT_TRIM_QUEUE_NONE) and are never trimmed. + * ------------------------------------------------------------------ + */ + +/* Locate the L3 header after L2 + up to two VLAN tags. Returns a + * pointer to the L3 header and writes the (host-order) L3 ethertype to + * *etype. Caller must ensure the bytes are present (trim_size and real + * MTU packets always carry the IP header). */ +static_always_inline u8 * +sonic_ext_trim_find_l3 (vlib_buffer_t *b, u16 *etype) +{ + u8 *eth = vlib_buffer_get_current (b); + u16 et = clib_net_to_host_u16 (*(u16 *) (eth + 12)); + u32 o = 14; + int n = 0; + + while ((et == ETHERNET_TYPE_VLAN || et == ETHERNET_TYPE_DOT1AD || + et == ETHERNET_TYPE_VLAN_9100) && + n < 2) + { + et = clib_net_to_host_u16 (*(u16 *) (eth + o + 2)); + o += 4; + n++; + } + + *etype = et; + return eth + o; +} + +/* Extract the 6-bit DSCP from an IPv4/IPv6 packet. Returns 1 and sets + * *dscp for IP packets, 0 for non-IP (caller treats as queue 0). */ +static_always_inline int +sonic_ext_trim_packet_dscp (vlib_buffer_t *b, u8 *dscp) +{ + u16 et; + u8 *l3 = sonic_ext_trim_find_l3 (b, &et); + + if (et == ETHERNET_TYPE_IP4) + { + ip4_header_t *ip = (ip4_header_t *) l3; + *dscp = ip->tos >> 2; + return 1; + } + if (et == ETHERNET_TYPE_IP6) + { + ip6_header_t *ip = (ip6_header_t *) l3; + u32 v = + clib_net_to_host_u32 (ip->ip_version_traffic_class_and_flow_label); + *dscp = (v >> 22) & 0x3f; + return 1; + } + return 0; +} + +/* Resolve the egress queue (0..7) for a packet from its DSCP via the + * SAI-pushed dscp_to_queue table. Non-IP packets (ARP/LACP/LLDP/...) + * have no DSCP and therefore no resolvable egress queue: they return the + * SONIC_EXT_TRIM_QUEUE_NONE sentinel so the admission node bypasses them + * unconditionally and never trims a control frame (which also keeps LAG + * control-plane traffic intact when queue 0 is trim-eligible). */ +static_always_inline u8 +sonic_ext_trim_packet_queue (sonic_ext_main_t *sem, vlib_buffer_t *b) +{ + u8 dscp; + + if (!sonic_ext_trim_packet_dscp (b, &dscp)) + return SONIC_EXT_TRIM_QUEUE_NONE; + return sem->dscp_to_queue[dscp & 0x3f]; +} + +/* Rewrite the DSCP of an (already truncated) trimmed copy to new_dscp, + * fixing the IPv4 header checksum. IPv6 carries no header checksum. */ +static_always_inline void +sonic_ext_trim_rewrite_dscp (vlib_buffer_t *b, u8 new_dscp) +{ + u16 et; + u8 *l3 = sonic_ext_trim_find_l3 (b, &et); + + if (et == ETHERNET_TYPE_IP4) + { + ip4_header_t *ip = (ip4_header_t *) l3; + ip->tos = (u8) ((new_dscp << 2) | (ip->tos & 0x3)); + ip->checksum = 0; + ip->checksum = ip4_header_checksum (ip); + } + else if (et == ETHERNET_TYPE_IP6) + { + ip6_header_t *ip = (ip6_header_t *) l3; + u32 v = + clib_net_to_host_u32 (ip->ip_version_traffic_class_and_flow_label); + v = (v & ~(0x3full << 22)) | (((u32) (new_dscp & 0x3f)) << 22); + ip->ip_version_traffic_class_and_flow_label = clib_host_to_net_u32 (v); + } +} + #endif /* __included_sonic_ext_h__ */ diff --git a/vppbld/plugins/sonic_ext/sonic_ext_trim.api b/vppbld/plugins/sonic_ext/sonic_ext_trim.api new file mode 100644 index 00000000..905a1040 --- /dev/null +++ b/vppbld/plugins/sonic_ext/sonic_ext_trim.api @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** \file + \brief SONiC-VPP packet-trimming (SAI DROP_AND_TRIM) control API. + + SAI-VPP (sonic-sairedis vslib/vpp) programs the trim policy, the + DSCP->queue resolution, and the per-(port,queue) software admission + parameters through these messages, and reads back the trim counters + for SAI switch/port/queue statistics. +*/ + +option version = "1.0.0"; + +import "vnet/interface_types.api"; + +/** \brief Program or clear the global packet-trim policy. + @param is_enable - false clears/disables trimming globally + @param trim_size - maximum bytes retained in a trimmed copy + @param dscp_mode - 0 = symmetric (dscp_value), 1 = from_tc (tc_value) + @param dscp_value - symmetric DSCP applied to the trimmed copy (0..63) + @param tc_value - traffic class used for the from_tc egress-map lookup + @param trim_queue - static egress queue used for the trimmed copy (0..7) +*/ +autoreply define sonic_ext_trim_global_set +{ + u32 client_index; + u32 context; + bool is_enable; + u16 trim_size; + u8 dscp_mode; + u8 dscp_value; + u8 tc_value; + u8 trim_queue; +}; + +/** \brief Push the composed DSCP(0..63) -> egress queue(0..7) table. + + This is the composition of the SONiC DSCP_TO_TC and TC_TO_QUEUE maps, + supplied by SAI-VPP so the plugin does not replicate both maps. The + admission shim uses it to classify a packet to its original queue. + @param dscp_to_queue - 64-entry table, indexed by DSCP, value 0..7 +*/ +autoreply define sonic_ext_trim_dscp_map_set +{ + u32 client_index; + u32 context; + u8 dscp_to_queue[64]; +}; + +/** \brief Program (or clear) one egress (port, queue) admission slot. + @param sw_if_index - egress interface (physical port / LAG member) + @param queue - egress queue index (0..7) + @param eligible - queue's buffer profile uses DROP_AND_TRIM + @param rate_bytes_per_sec - SONiC scheduler PIR in bytes/s (0 => block) + @param capacity_bytes - software token-bucket depth (buffer capacity) +*/ +autoreply define sonic_ext_trim_queue_set +{ + u32 client_index; + u32 context; + vl_api_interface_index_t sw_if_index; + u8 queue; + bool eligible; + u64 rate_bytes_per_sec; + u64 capacity_bytes; +}; + +/** \brief Read back global trim counters (for SAI switch/port/queue stats). + @param trim_sent - truncated copies transmitted + @param trim_drop - eligible packets dropped because the trim queue was + also congested + @param trim_admit_fail - original-queue admission failures observed +*/ +define sonic_ext_trim_counters_get +{ + u32 client_index; + u32 context; +}; + +define sonic_ext_trim_counters_get_reply +{ + u32 context; + i32 retval; + u64 trim_sent; + u64 trim_drop; + u64 trim_admit_fail; +}; diff --git a/vppbld/plugins/sonic_ext/trim_admission_node.c b/vppbld/plugins/sonic_ext/trim_admission_node.c new file mode 100644 index 00000000..db3fbd1b --- /dev/null +++ b/vppbld/plugins/sonic_ext/trim_admission_node.c @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#include +#include +#include + +/* + * sonic-ext-trim-admission + * + * Software egress admission shim on the `interface-output` arc, enabled + * per egress sw_if_index whenever that port has at least one trim-eligible + * queue (buffer profile == DROP_AND_TRIM). It is the substitute for the + * hardware per-queue admission VPP does not have: + * + * ip4-rewrite / l2-output --> interface-output[VLIB_TX=phy] + * --> sonic-ext-trim-admission <-- HERE + * --> -output / -tx + * + * For each packet we resolve the egress (port, queue) -- queue from the + * packet DSCP via the SAI-pushed dscp_to_queue table -- and run the + * queue's software token bucket. The bucket is sized from the SONiC + * scheduler PIR (rate) and buffer profile (capacity), so a blocking + * scheduler (PIR=1) drains it and yields a real admission failure. + * + * - Non-eligible queues (and unconfigured ports) are never policed: + * the packet passes straight through the feature arc. This keeps + * the shim strictly scoped to trim behavior and never perturbs + * normal forwarding. + * - On an eligible queue that admits, the packet continues the arc. + * - On an eligible queue that fails admission, we stamp the resolved + * queue into the buffer and divert to `sonic-ext-trim`, which + * truncates the copy and transmits it via interface-output-arc-end + * (the arc terminator) on the same port. Because the trimmed copy + * goes to the arc END it never re-enters this feature, so a trimmed + * packet can never be re-trimmed and no per-buffer "already trimmed" + * one-shot flag is needed (which matters: VPP does not clear the + * opaque2 area on buffer recycle, so such a flag could go stale). + */ + +typedef struct +{ + u32 sw_if_index; /* egress port (VLIB_TX) */ + u8 queue; /* resolved egress queue */ + u8 eligible; /* queue is trim-eligible */ + u8 admitted; /* token bucket admitted the packet */ +} sonic_ext_trim_admission_trace_t; + +static u8 * +format_sonic_ext_trim_admission_trace (u8 *s, va_list *args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + sonic_ext_trim_admission_trace_t *t = + va_arg (*args, sonic_ext_trim_admission_trace_t *); + s = format (s, "SONIC-EXT-TRIM-ADMISSION: port %u queue %u %s %s", + t->sw_if_index, t->queue, t->eligible ? "eligible" : "bypass", + t->admitted ? "admitted" : "REJECTED->trim"); + return s; +} + +#define foreach_sonic_ext_trim_admission_error \ + _ (ADMITTED, "packets admitted by the egress token bucket") \ + _ (TO_TRIM, "packets that failed admission and were sent to trim") \ + _ (BYPASS, "packets on non-eligible / unconfigured queues (passthru)") + +typedef enum +{ +#define _(sym, str) SONIC_EXT_TRIM_ADMISSION_ERROR_##sym, + foreach_sonic_ext_trim_admission_error +#undef _ + SONIC_EXT_TRIM_ADMISSION_N_ERROR, +} sonic_ext_trim_admission_error_t; + +static char *sonic_ext_trim_admission_error_strings[] = { +#define _(sym, string) string, + foreach_sonic_ext_trim_admission_error +#undef _ +}; + +typedef enum +{ + SONIC_EXT_TRIM_ADMISSION_NEXT_TRIM, + SONIC_EXT_TRIM_ADMISSION_N_NEXT, +} sonic_ext_trim_admission_next_t; + +VLIB_NODE_FN (sonic_ext_trim_admission_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + u32 n_left_from, *from; + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; + u16 nexts[VLIB_FRAME_SIZE], *next; + u32 n_admitted = 0, n_to_trim = 0, n_bypass = 0; + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + vlib_get_buffers (vm, from, bufs, n_left_from); + b = bufs; + next = nexts; + + while (n_left_from > 0) + { + u32 next0 = 0; + sonic_ext_buffer_opaque_t *seb = sonic_ext_buffer (b[0]); + u32 sw_if_index = vnet_buffer (b[0])->sw_if_index[VLIB_TX]; + sonic_ext_trim_port_t *port; + sonic_ext_trim_queue_t *q; + u8 queue = 0; + u8 eligible = 0; + u8 admitted = 1; + + /* Default: continue the feature arc (normal egress). Overridden + * below only when we divert a congested eligible packet to trim. */ + vnet_feature_next (&next0, b[0]); + + if (PREDICT_FALSE (!sem->trim_configured)) + { + n_bypass++; + goto done0; + } + + port = sonic_ext_trim_port_get (sw_if_index, 0 /* no create */); + if (PREDICT_FALSE (port == 0)) + { + n_bypass++; + goto done0; + } + + queue = sonic_ext_trim_packet_queue (sem, b[0]); + + /* Non-IP frames (ARP/LACP/LLDP/...) have no resolvable egress queue + * and must never be policed or trimmed -- bypass unconditionally. */ + if (PREDICT_FALSE (queue == SONIC_EXT_TRIM_QUEUE_NONE)) + { + n_bypass++; + goto done0; + } + + q = &port->q[queue]; + + /* Only trim-eligible queues are policed; everything else is a + * transparent passthrough so normal traffic is never perturbed. */ + if (!q->eligible) + { + n_bypass++; + goto done0; + } + eligible = 1; + + if (sonic_ext_trim_bucket_admit ( + vm, q, vlib_buffer_length_in_chain (vm, b[0]))) + { + admitted = 1; + n_admitted++; + goto done0; + } + + /* Admission failure on an eligible queue: divert to the trim + * action node, which truncates the copy and re-injects it. */ + admitted = 0; + seb->orig_queue = queue; + sem->trim_admit_fail++; + n_to_trim++; + next0 = SONIC_EXT_TRIM_ADMISSION_NEXT_TRIM; + + done0: + next[0] = (u16) next0; + + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && + (b[0]->flags & VLIB_BUFFER_IS_TRACED))) + { + sonic_ext_trim_admission_trace_t *t = + vlib_add_trace (vm, node, b[0], sizeof (*t)); + t->sw_if_index = sw_if_index; + t->queue = queue; + t->eligible = eligible; + t->admitted = admitted; + } + + b += 1; + next += 1; + n_left_from -= 1; + } + + vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); + + if (n_admitted) + vlib_node_increment_counter (vm, sonic_ext_trim_admission_node.index, + SONIC_EXT_TRIM_ADMISSION_ERROR_ADMITTED, + n_admitted); + if (n_to_trim) + vlib_node_increment_counter (vm, sonic_ext_trim_admission_node.index, + SONIC_EXT_TRIM_ADMISSION_ERROR_TO_TRIM, + n_to_trim); + if (n_bypass) + vlib_node_increment_counter (vm, sonic_ext_trim_admission_node.index, + SONIC_EXT_TRIM_ADMISSION_ERROR_BYPASS, + n_bypass); + + return frame->n_vectors; +} + +VLIB_REGISTER_NODE (sonic_ext_trim_admission_node) = { + .name = "sonic-ext-trim-admission", + .vector_size = sizeof (u32), + .format_trace = format_sonic_ext_trim_admission_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = ARRAY_LEN (sonic_ext_trim_admission_error_strings), + .error_strings = sonic_ext_trim_admission_error_strings, + .n_next_nodes = SONIC_EXT_TRIM_ADMISSION_N_NEXT, + .next_nodes = { + [SONIC_EXT_TRIM_ADMISSION_NEXT_TRIM] = "sonic-ext-trim", + }, +}; + +VNET_FEATURE_INIT (sonic_ext_trim_admission_feat, static) = { + .arc_name = "interface-output", + .node_name = "sonic-ext-trim-admission", + /* No strict ordering vs other interface-output features: we either + * pass the packet through the arc unchanged, or divert an eligible + * congested packet to the trim node which then resumes the arc. */ +}; diff --git a/vppbld/plugins/sonic_ext/trim_api.c b/vppbld/plugins/sonic_ext/trim_api.c new file mode 100644 index 00000000..866a1361 --- /dev/null +++ b/vppbld/plugins/sonic_ext/trim_api.c @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief SONiC-VPP packet-trim control API + shared admission helpers. + * + * Registers the sonic_ext_trim.api messages that SAI-VPP uses to program + * the global trim policy, the DSCP->queue resolution table, and the + * per-(port,queue) software admission parameters, and to read trim + * counters back for SAI statistics. Also hosts the small helpers shared + * by the admission and trim action nodes (port-state lookup and the + * token-bucket admission decision). + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#define REPLY_MSG_ID_BASE sonic_ext_main.trim_msg_id_base +#include + +/* ------------------------------------------------------------------ */ +/* Shared helpers (declared in sonic_ext.h, used by the trim nodes). */ +/* ------------------------------------------------------------------ */ + +sonic_ext_trim_port_t * +sonic_ext_trim_port_get (u32 sw_if_index, int create) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + + if (sw_if_index == ~0) + return 0; + + if (sw_if_index >= vec_len (sem->trim_ports)) + { + if (!create) + return 0; + /* vec_validate zero-fills, so new ports start with all queues + * ineligible / unconfigured (feature is a no-op there). */ + vec_validate (sem->trim_ports, sw_if_index); + } + + return vec_elt_at_index (sem->trim_ports, sw_if_index); +} + +int +sonic_ext_trim_bucket_admit (vlib_main_t *vm, sonic_ext_trim_queue_t *q, + u32 len) +{ + f64 now, elapsed; + + /* No admission programmed on this queue -> always admit (no-op). */ + if (!q->configured) + return 1; + + now = vlib_time_now (vm); + elapsed = now - q->last_refill; + if (elapsed > 0) + { + q->tokens += elapsed * (f64) q->rate_bytes_per_sec; + if (q->tokens > (f64) q->capacity_bytes) + q->tokens = (f64) q->capacity_bytes; + q->last_refill = now; + } + + if (q->tokens >= (f64) len) + { + q->tokens -= (f64) len; + return 1; /* admitted */ + } + + return 0; /* rejected -- bucket drained (e.g. PIR=1 blocking scheduler) */ +} + +void +sonic_ext_trim_enable_disable (u32 sw_if_index, int enable) +{ + vnet_feature_enable_disable ("interface-output", "sonic-ext-trim-admission", + sw_if_index, enable, 0, 0); +} + +void +sonic_ext_trim_queue_program (u32 sw_if_index, u32 queue, int eligible, + u64 rate_bytes_per_sec, u64 capacity_bytes) +{ + sonic_ext_trim_port_t *port; + sonic_ext_trim_queue_t *q; + int any_eligible = 0; + int i; + + if (queue >= SONIC_EXT_TRIM_MAX_QUEUES) + return; + + port = sonic_ext_trim_port_get (sw_if_index, 1 /* create */); + q = &port->q[queue]; + q->eligible = eligible ? 1 : 0; + q->rate_bytes_per_sec = rate_bytes_per_sec; + q->capacity_bytes = capacity_bytes; + q->tokens = (f64) capacity_bytes; /* start full */ + q->last_refill = vlib_time_now (vlib_get_main ()); + q->configured = 1; + + /* Enable the admission feature on this port iff at least one queue is + * trim-eligible; otherwise keep the arc a no-op. */ + for (i = 0; i < SONIC_EXT_TRIM_MAX_QUEUES; i++) + if (port->q[i].eligible) + { + any_eligible = 1; + break; + } + sonic_ext_trim_enable_disable (sw_if_index, any_eligible); +} + +/* ------------------------------------------------------------------ */ +/* API message handlers. */ +/* ------------------------------------------------------------------ */ + +static void +vl_api_sonic_ext_trim_global_set_t_handler ( + vl_api_sonic_ext_trim_global_set_t *mp) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + vl_api_sonic_ext_trim_global_set_reply_t *rmp; + int rv = 0; + + if (mp->is_enable) + { + sem->trim_size = ntohs (mp->trim_size); + sem->trim_dscp_mode = mp->dscp_mode; + sem->trim_dscp_value = mp->dscp_value & 0x3f; + sem->trim_tc = mp->tc_value; + sem->trim_queue = mp->trim_queue & (SONIC_EXT_TRIM_MAX_QUEUES - 1); + sem->trim_configured = 1; + } + else + { + sem->trim_configured = 0; + sem->trim_size = 0; + } + + REPLY_MACRO (VL_API_SONIC_EXT_TRIM_GLOBAL_SET_REPLY); +} + +static void +vl_api_sonic_ext_trim_dscp_map_set_t_handler ( + vl_api_sonic_ext_trim_dscp_map_set_t *mp) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + vl_api_sonic_ext_trim_dscp_map_set_reply_t *rmp; + int rv = 0; + int i; + + for (i = 0; i < 64; i++) + sem->dscp_to_queue[i] = + mp->dscp_to_queue[i] & (SONIC_EXT_TRIM_MAX_QUEUES - 1); + + REPLY_MACRO (VL_API_SONIC_EXT_TRIM_DSCP_MAP_SET_REPLY); +} + +static void +vl_api_sonic_ext_trim_queue_set_t_handler ( + vl_api_sonic_ext_trim_queue_set_t *mp) +{ + vl_api_sonic_ext_trim_queue_set_reply_t *rmp; + vnet_main_t *vnm = vnet_get_main (); + vnet_interface_main_t *im = &vnm->interface_main; + u32 sw_if_index = ntohl (mp->sw_if_index); + u32 queue = mp->queue; + int rv = 0; + + if (queue >= SONIC_EXT_TRIM_MAX_QUEUES) + { + rv = VNET_API_ERROR_INVALID_VALUE; + goto done; + } + if (pool_is_free_index (im->sw_interfaces, sw_if_index)) + { + rv = VNET_API_ERROR_INVALID_SW_IF_INDEX; + goto done; + } + + sonic_ext_trim_queue_program (sw_if_index, queue, mp->eligible ? 1 : 0, + clib_net_to_host_u64 (mp->rate_bytes_per_sec), + clib_net_to_host_u64 (mp->capacity_bytes)); + +done: + REPLY_MACRO (VL_API_SONIC_EXT_TRIM_QUEUE_SET_REPLY); +} + +static void +vl_api_sonic_ext_trim_counters_get_t_handler ( + vl_api_sonic_ext_trim_counters_get_t *mp) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + vl_api_sonic_ext_trim_counters_get_reply_t *rmp; + int rv = 0; + + REPLY_MACRO2 (VL_API_SONIC_EXT_TRIM_COUNTERS_GET_REPLY, ({ + rmp->trim_sent = clib_host_to_net_u64 (sem->trim_sent); + rmp->trim_drop = clib_host_to_net_u64 (sem->trim_drop); + rmp->trim_admit_fail = + clib_host_to_net_u64 (sem->trim_admit_fail); + })); +} + +/* API definitions (generated). */ +#include + +clib_error_t * +sonic_ext_trim_api_hookup (vlib_main_t *vm) +{ + sonic_ext_main.trim_msg_id_base = setup_message_id_table (); + return 0; +} diff --git a/vppbld/plugins/sonic_ext/trim_cli.c b/vppbld/plugins/sonic_ext/trim_cli.c new file mode 100644 index 00000000..2a89ce17 --- /dev/null +++ b/vppbld/plugins/sonic_ext/trim_cli.c @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @file + * @brief Packet-trim debug CLI. + * + * `show sonic-ext trim` dumps the global trim policy, the per-(port,queue) + * software admission buckets, and the trim counters. The `sonic-ext trim + * ...` set commands mirror the binary API so the datapath can be exercised + * from vppctl on the dev VM before the SAI-VPP wiring exists. + */ +#include + +#include +#include +#include + +static clib_error_t * +show_sonic_ext_trim_command_fn (vlib_main_t *vm, unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + vnet_main_t *vnm = vnet_get_main (); + u32 i, qi; + + vlib_cli_output (vm, "packet-trim state:"); + vlib_cli_output (vm, " configured : %s", + sem->trim_configured ? "yes" : "no"); + vlib_cli_output (vm, " trim size : %u bytes", sem->trim_size); + vlib_cli_output (vm, " dscp mode : %s", + sem->trim_dscp_mode == SONIC_EXT_TRIM_DSCP_MODE_FROM_TC + ? "from-tc" + : "symmetric"); + vlib_cli_output (vm, " dscp value : %u", sem->trim_dscp_value); + vlib_cli_output (vm, " trim tc : %u", sem->trim_tc); + vlib_cli_output (vm, " trim queue : %u", sem->trim_queue); + vlib_cli_output (vm, " counters : sent %llu drop %llu admit-fail %llu", + sem->trim_sent, sem->trim_drop, sem->trim_admit_fail); + + for (i = 0; i < vec_len (sem->trim_ports); i++) + { + sonic_ext_trim_port_t *p = &sem->trim_ports[i]; + int hdr = 0; + for (qi = 0; qi < SONIC_EXT_TRIM_MAX_QUEUES; qi++) + { + sonic_ext_trim_queue_t *q = &p->q[qi]; + if (!q->configured && !q->eligible) + continue; + if (!hdr) + { + vlib_cli_output (vm, " port %U:", format_vnet_sw_if_index_name, + vnm, i); + hdr = 1; + } + vlib_cli_output ( + vm, " q%u: %s rate %llu B/s cap %llu B tokens %.0f", qi, + q->eligible ? "eligible" : "bypass", q->rate_bytes_per_sec, + q->capacity_bytes, q->tokens); + } + } + return 0; +} + +VLIB_CLI_COMMAND (show_sonic_ext_trim_command, static) = { + .path = "show sonic-ext trim", + .short_help = "show sonic-ext trim", + .function = show_sonic_ext_trim_command_fn, +}; + +static clib_error_t * +sonic_ext_trim_global_command_fn (vlib_main_t *vm, unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + unformat_input_t _li, *li = &_li; + u32 size = ~0, dscp = ~0, tc = ~0, queue = ~0; + int mode = -1; + int disable = 0; + + if (!unformat_user (input, unformat_line_input, li)) + return 0; + + while (unformat_check_input (li) != UNFORMAT_END_OF_INPUT) + { + if (unformat (li, "size %u", &size)) + ; + else if (unformat (li, "dscp %u", &dscp)) + ; + else if (unformat (li, "tc %u", &tc)) + ; + else if (unformat (li, "queue %u", &queue)) + ; + else if (unformat (li, "mode symmetric")) + mode = SONIC_EXT_TRIM_DSCP_MODE_SYMMETRIC; + else if (unformat (li, "mode from-tc")) + mode = SONIC_EXT_TRIM_DSCP_MODE_FROM_TC; + else if (unformat (li, "disable")) + disable = 1; + else + { + unformat_free (li); + return clib_error_return (0, "unknown input `%U'", + format_unformat_error, li); + } + } + unformat_free (li); + + if (disable) + { + sem->trim_configured = 0; + sem->trim_size = 0; + return 0; + } + + if (size != ~0) + sem->trim_size = size; + if (dscp != ~0) + sem->trim_dscp_value = dscp & 0x3f; + if (tc != ~0) + sem->trim_tc = tc; + if (queue != ~0) + sem->trim_queue = queue & (SONIC_EXT_TRIM_MAX_QUEUES - 1); + if (mode != -1) + sem->trim_dscp_mode = mode; + sem->trim_configured = 1; + return 0; +} + +VLIB_CLI_COMMAND (sonic_ext_trim_global_command, static) = { + .path = "sonic-ext trim global", + .short_help = "sonic-ext trim global [size ] [dscp <0-63>] [tc ] " + "[queue <0-7>] [mode symmetric|from-tc] [disable]", + .function = sonic_ext_trim_global_command_fn, +}; + +static clib_error_t * +sonic_ext_trim_dscp_map_command_fn (vlib_main_t *vm, unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + unformat_input_t _li, *li = &_li; + u32 dscp = ~0, queue = ~0; + + if (!unformat_user (input, unformat_line_input, li)) + return 0; + + while (unformat_check_input (li) != UNFORMAT_END_OF_INPUT) + { + if (unformat (li, "%u %u", &dscp, &queue)) + ; + else + { + unformat_free (li); + return clib_error_return (0, "unknown input `%U'", + format_unformat_error, li); + } + } + unformat_free (li); + + if (dscp > 63 || queue >= SONIC_EXT_TRIM_MAX_QUEUES) + return clib_error_return (0, "usage: sonic-ext trim dscp-map <0-63> <0-7>"); + + sem->dscp_to_queue[dscp] = queue; + return 0; +} + +VLIB_CLI_COMMAND (sonic_ext_trim_dscp_map_command, static) = { + .path = "sonic-ext trim dscp-map", + .short_help = "sonic-ext trim dscp-map ", + .function = sonic_ext_trim_dscp_map_command_fn, +}; + +static clib_error_t * +sonic_ext_trim_queue_command_fn (vlib_main_t *vm, unformat_input_t *input, + vlib_cli_command_t *cmd) +{ + vnet_main_t *vnm = vnet_get_main (); + unformat_input_t _li, *li = &_li; + u32 sw_if_index = ~0, queue = ~0; + u64 rate = 0, cap = 0; + int eligible = 1; + + if (!unformat_user (input, unformat_line_input, li)) + return 0; + + while (unformat_check_input (li) != UNFORMAT_END_OF_INPUT) + { + if (unformat (li, "%U", unformat_vnet_sw_interface, vnm, &sw_if_index)) + ; + else if (unformat (li, "queue %u", &queue)) + ; + else if (unformat (li, "rate %llu", &rate)) + ; + else if (unformat (li, "capacity %llu", &cap)) + ; + else if (unformat (li, "eligible")) + eligible = 1; + else if (unformat (li, "not-eligible")) + eligible = 0; + else + { + unformat_free (li); + return clib_error_return (0, "unknown input `%U'", + format_unformat_error, li); + } + } + unformat_free (li); + + if (sw_if_index == ~0 || queue == ~0) + return clib_error_return ( + 0, "usage: sonic-ext trim queue queue <0-7> " + "[rate ] [capacity ] [eligible|not-eligible]"); + if (queue >= SONIC_EXT_TRIM_MAX_QUEUES) + return clib_error_return (0, "queue must be 0..%u", + SONIC_EXT_TRIM_MAX_QUEUES - 1); + + sonic_ext_trim_queue_program (sw_if_index, queue, eligible, rate, cap); + return 0; +} + +VLIB_CLI_COMMAND (sonic_ext_trim_queue_command, static) = { + .path = "sonic-ext trim queue", + .short_help = "sonic-ext trim queue queue <0-7> [rate ] " + "[capacity ] [eligible|not-eligible]", + .function = sonic_ext_trim_queue_command_fn, +}; diff --git a/vppbld/plugins/sonic_ext/trim_node.c b/vppbld/plugins/sonic_ext/trim_node.c new file mode 100644 index 00000000..63d28ae6 --- /dev/null +++ b/vppbld/plugins/sonic_ext/trim_node.c @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2026 SONiC-VPP contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#include +#include +#include + +/* + * sonic-ext-trim + * + * Trim action node, reached only from sonic-ext-trim-admission when an + * eligible egress queue fails software admission. It realizes the SAI + * DROP_AND_TRIM action: + * + * 1. Rewrite the DSCP of the copy (symmetric: configured dscp_value; + * FROM_TC currently falls back to dscp_value -- see note below). + * 2. Truncate the packet to the configured trim_size bytes. + * 3. Steer the copy to the static trim queue on the SAME egress port + * and run that queue's token bucket: if the trim queue is itself + * congested the copy is dropped (trim_drop), otherwise it is + * transmitted on the same port (trim_sent). + * + * VLIB_TX is unchanged (same egress port). The truncated copy is + * enqueued directly to "interface-output-arc-end" -- the node the + * interface-output feature arc terminates at -- so it goes straight to + * the port TX, PAST the admission feature. This is what makes a + * one-shot "already trimmed" buffer flag unnecessary: a trimmed copy + * never re-enters admission, so it can never be re-trimmed and there is + * no dependence on opaque2 being cleared on buffer recycle. (We cannot + * use vnet_feature_next here because this node is not itself a feature + * on the arc.) + * + * NOTE (FROM_TC): asymmetric DSCP resolution needs a TC->DSCP egress map + * which is not pushed in this increment; FROM_TC therefore reuses the + * symmetric dscp_value. Wiring the egress map is future work (HLD). + */ + +typedef struct +{ + u32 sw_if_index; /* egress port */ + u8 orig_queue; /* queue that failed admission */ + u8 trim_queue; /* static trim queue */ + u16 trim_size; /* bytes retained */ + u8 new_dscp; /* DSCP written to the trimmed copy */ + u8 dropped; /* trim queue congested -> copy dropped */ +} sonic_ext_trim_trace_t; + +static u8 * +format_sonic_ext_trim_trace (u8 *s, va_list *args) +{ + CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); + CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); + sonic_ext_trim_trace_t *t = va_arg (*args, sonic_ext_trim_trace_t *); + s = format (s, + "SONIC-EXT-TRIM: port %u orig-queue %u -> trim-queue %u " + "size %u dscp %u %s", + t->sw_if_index, t->orig_queue, t->trim_queue, t->trim_size, + t->new_dscp, t->dropped ? "DROP (trim queue congested)" : "SENT"); + return s; +} + +#define foreach_sonic_ext_trim_error \ + _ (SENT, "trimmed copies transmitted") \ + _ (DROP, "trimmed copies dropped (trim queue congested)") + +typedef enum +{ +#define _(sym, str) SONIC_EXT_TRIM_ERROR_##sym, + foreach_sonic_ext_trim_error +#undef _ + SONIC_EXT_TRIM_N_ERROR, +} sonic_ext_trim_error_t; + +static char *sonic_ext_trim_error_strings[] = { +#define _(sym, string) string, + foreach_sonic_ext_trim_error +#undef _ +}; + +typedef enum +{ + SONIC_EXT_TRIM_NEXT_TX, + SONIC_EXT_TRIM_NEXT_DROP, + SONIC_EXT_TRIM_N_NEXT, +} sonic_ext_trim_next_t; + +/* Truncate a (possibly chained) buffer to at most trim_size bytes, + * freeing any tail buffers. */ +static_always_inline void +sonic_ext_trim_truncate (vlib_main_t *vm, vlib_buffer_t *b0, u16 trim_size) +{ + if (vlib_buffer_length_in_chain (vm, b0) <= trim_size) + return; + + /* Walk the (possibly multi-segment) chain, accumulating retained bytes + * until we reach trim_size. Jumbo frames are chained in data-size + * segments (e.g. 2048B each), so a trim_size larger than the first segment + * must be honored by keeping whole leading segments plus a partial one -- + * NOT by collapsing the packet to just the first segment. Truncate the + * segment that crosses the boundary and free every segment after it. */ + vlib_buffer_t *b = b0; + u32 kept = 0; + while (kept + b->current_length < trim_size) + { + kept += b->current_length; + b = vlib_get_buffer (vm, b->next_buffer); + } + + /* b is the last retained segment; keep exactly (trim_size - kept) bytes. */ + b->current_length = (u16) (trim_size - kept); + + if (b->flags & VLIB_BUFFER_NEXT_PRESENT) + { + vlib_buffer_free_one (vm, b->next_buffer); + b->flags &= ~VLIB_BUFFER_NEXT_PRESENT; + b->next_buffer = 0; + + /* Severing the VLIB chain is not enough for DPDK egress: this buffer + * arrived from dpdk-input with VLIB_BUFFER_EXT_HDR_VALID set, so its + * backing rte_mbuf still carries the RX-time nb_segs and a ->next + * pointer to the (now freed) tail segment. dpdk_validate_rte_mbuf() + * only calls rte_pktmbuf_reset() -- which clears mb->next and resets + * nb_segs to 1 -- when EXT_HDR_VALID is clear. The virtio (and other) + * PMD tx path walks mb->next until NULL rather than trusting nb_segs, + * so a stale ->next makes the freed tail ride onto the wire (trimmed + * 256B first segment + 952B tail = 1208B observed). Clear the flag on + * the severed segment to force the mbuf header to be rebuilt cleanly. */ + b->flags &= ~VLIB_BUFFER_EXT_HDR_VALID; + } + + /* Fix up the first segment's chain accounting so the total chain length is + * exactly trim_size. */ + b0->total_length_not_including_first_buffer = + (u32) trim_size - b0->current_length; +} + +VLIB_NODE_FN (sonic_ext_trim_node) +(vlib_main_t *vm, vlib_node_runtime_t *node, vlib_frame_t *frame) +{ + sonic_ext_main_t *sem = &sonic_ext_main; + u32 n_left_from, *from; + vlib_buffer_t *bufs[VLIB_FRAME_SIZE], **b; + u16 nexts[VLIB_FRAME_SIZE], *next; + u32 n_sent = 0, n_drop = 0; + u8 new_dscp = sem->trim_dscp_value; + u8 trim_queue = sem->trim_queue; + u16 trim_size = sem->trim_size ? sem->trim_size : 128; + + from = vlib_frame_vector_args (frame); + n_left_from = frame->n_vectors; + vlib_get_buffers (vm, from, bufs, n_left_from); + b = bufs; + next = nexts; + + while (n_left_from > 0) + { + u32 next0 = 0; + sonic_ext_buffer_opaque_t *seb = sonic_ext_buffer (b[0]); + u32 sw_if_index = vnet_buffer (b[0])->sw_if_index[VLIB_TX]; + sonic_ext_trim_port_t *port; + u8 dropped = 0; + + /* Rewrite DSCP on the full packet, then truncate. */ + sonic_ext_trim_rewrite_dscp (b[0], new_dscp); + sonic_ext_trim_truncate (vm, b[0], trim_size); + + /* Steer to the static trim queue on the same egress port and run + * its token bucket. If the trim queue is congested, drop. */ + port = sonic_ext_trim_port_get (sw_if_index, 0); + if (port && + !sonic_ext_trim_bucket_admit ( + vm, &port->q[trim_queue], + vlib_buffer_length_in_chain (vm, b[0]))) + { + dropped = 1; + sem->trim_drop++; + n_drop++; + b[0]->error = + node->errors[SONIC_EXT_TRIM_ERROR_DROP]; + next0 = SONIC_EXT_TRIM_NEXT_DROP; + } + else + { + sem->trim_sent++; + n_sent++; + /* Transmit the trimmed copy on the same egress port (VLIB_TX + * unchanged) by enqueuing directly to interface-output-arc-end, + * the node the interface-output feature arc terminates at. This + * sends the copy straight to the port TX, PAST the admission + * feature, so a trimmed copy never re-enters admission and can + * never be re-trimmed -- no one-shot buffer flag required. We + * cannot use vnet_feature_next here because this node is not a + * feature on the arc. */ + next0 = SONIC_EXT_TRIM_NEXT_TX; + } + + next[0] = (u16) next0; + + if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && + (b[0]->flags & VLIB_BUFFER_IS_TRACED))) + { + sonic_ext_trim_trace_t *t = + vlib_add_trace (vm, node, b[0], sizeof (*t)); + t->sw_if_index = sw_if_index; + t->orig_queue = seb->orig_queue; + t->trim_queue = trim_queue; + t->trim_size = trim_size; + t->new_dscp = new_dscp; + t->dropped = dropped; + } + + b += 1; + next += 1; + n_left_from -= 1; + } + + vlib_buffer_enqueue_to_next (vm, node, from, nexts, frame->n_vectors); + + if (n_sent) + vlib_node_increment_counter (vm, sonic_ext_trim_node.index, + SONIC_EXT_TRIM_ERROR_SENT, n_sent); + if (n_drop) + vlib_node_increment_counter (vm, sonic_ext_trim_node.index, + SONIC_EXT_TRIM_ERROR_DROP, n_drop); + + return frame->n_vectors; +} + +VLIB_REGISTER_NODE (sonic_ext_trim_node) = { + .name = "sonic-ext-trim", + .vector_size = sizeof (u32), + .format_trace = format_sonic_ext_trim_trace, + .type = VLIB_NODE_TYPE_INTERNAL, + .n_errors = ARRAY_LEN (sonic_ext_trim_error_strings), + .error_strings = sonic_ext_trim_error_strings, + .n_next_nodes = SONIC_EXT_TRIM_N_NEXT, + .next_nodes = { + [SONIC_EXT_TRIM_NEXT_TX] = "interface-output-arc-end", + [SONIC_EXT_TRIM_NEXT_DROP] = "error-drop", + }, +}; From ee89796caacdcd2c39051d0fbf4d3e0f0c9f834c Mon Sep 17 00:00:00 2001 From: Aaron Bernardino Date: Thu, 30 Jul 2026 22:41:04 +0000 Subject: [PATCH 2/3] [sonic_ext] Harden trim admission feature-arc enable/disable Address code-review gap H1 in the packet-trimming plugin: - sonic_ext_trim_enable_disable now returns int so callers can observe vnet_feature_enable_disable() failures instead of silently ignoring them. - sonic_ext_trim_queue_program transition-guards the interface-output feature-arc toggle on a new per-port feature_enabled flag: it computes any_eligible across all queues and only enables/disables on an actual eligibility transition. vnet_feature_enable_disable() is not idempotent, so this prevents refcount stacking (arc stuck enabled) and guarantees the arc is removed once no queue is trim-eligible. - rules/vpp.mk: bump VPP_VERSION 0.6 -> 0.7 for the plugin content change so downstream sonic-buildimage does not pull stale cached debs. Validated on the dev-VM t1-lag-vpp testbed: sonic-ext-trim-admission appears exactly once on an eligible egress port and is removed (count 0) once its queues become ineligible. Tracking: sonic-net/sonic-buildimage#25789 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Aaron Bernardino --- rules/vpp.mk | 2 +- vppbld/plugins/sonic_ext/sonic_ext.h | 8 +++++++- vppbld/plugins/sonic_ext/trim_api.c | 24 +++++++++++++++++++----- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/rules/vpp.mk b/rules/vpp.mk index 99217721..f5e2e0e4 100644 --- a/rules/vpp.mk +++ b/rules/vpp.mk @@ -8,7 +8,7 @@ VPP_VERSION_BASE = 2606 # https://packages.buildkite.com/sonic-vpp/vpp; if the suffix isn't bumped, # downstream sonic-buildimage builds will silently pull stale debs that # pre-date the new patch series / plugins and end up with VPP/SAI CRC drift. -VPP_VERSION = $(VPP_VERSION_BASE)-0.6 +VPP_VERSION = $(VPP_VERSION_BASE)-0.7 VPP_VERSION_SONIC = $(VPP_VERSION)+b1sonic1 VPP_SRC_PATH = platform/vpp/vppbld diff --git a/vppbld/plugins/sonic_ext/sonic_ext.h b/vppbld/plugins/sonic_ext/sonic_ext.h index 3150b3bc..73caca69 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.h +++ b/vppbld/plugins/sonic_ext/sonic_ext.h @@ -176,6 +176,12 @@ typedef struct typedef struct { + /* Tracks whether the interface-output "sonic-ext-trim-admission" feature + * arc is currently enabled on this port. vnet_feature_enable_disable() is + * not idempotent (each enable pushes another instance of the arc onto the + * feature config), so the arc is toggled only on an actual transition of + * this state. Zero-initialized by vec_validate -> arc off. */ + u8 feature_enabled; sonic_ext_trim_queue_t q[SONIC_EXT_TRIM_MAX_QUEUES]; } sonic_ext_trim_port_t; @@ -272,7 +278,7 @@ extern vlib_node_registration_t sonic_ext_trim_node; /* Enable/disable the sonic-ext-trim-admission feature on an egress * interface-output arc. Driven from the binary API when a port gains or * loses trim-eligible queues. */ -void sonic_ext_trim_enable_disable (u32 sw_if_index, int enable); +int sonic_ext_trim_enable_disable (u32 sw_if_index, int enable); /* Get (optionally allocate) the per-port admission state for sw_if_index. * Returns NULL when create==0 and the port has no state. */ diff --git a/vppbld/plugins/sonic_ext/trim_api.c b/vppbld/plugins/sonic_ext/trim_api.c index 866a1361..6f0c5937 100644 --- a/vppbld/plugins/sonic_ext/trim_api.c +++ b/vppbld/plugins/sonic_ext/trim_api.c @@ -94,11 +94,12 @@ sonic_ext_trim_bucket_admit (vlib_main_t *vm, sonic_ext_trim_queue_t *q, return 0; /* rejected -- bucket drained (e.g. PIR=1 blocking scheduler) */ } -void +int sonic_ext_trim_enable_disable (u32 sw_if_index, int enable) { - vnet_feature_enable_disable ("interface-output", "sonic-ext-trim-admission", - sw_if_index, enable, 0, 0); + return vnet_feature_enable_disable ("interface-output", + "sonic-ext-trim-admission", sw_if_index, + enable, 0, 0); } void @@ -123,14 +124,27 @@ sonic_ext_trim_queue_program (u32 sw_if_index, u32 queue, int eligible, q->configured = 1; /* Enable the admission feature on this port iff at least one queue is - * trim-eligible; otherwise keep the arc a no-op. */ + * trim-eligible; otherwise keep the arc a no-op. vnet_feature_enable_disable + * is not idempotent -- calling it on every queue_set would stack duplicate + * "sonic-ext-trim-admission" instances on the interface-output arc -- so only + * toggle on an actual transition of the port's eligibility state. */ for (i = 0; i < SONIC_EXT_TRIM_MAX_QUEUES; i++) if (port->q[i].eligible) { any_eligible = 1; break; } - sonic_ext_trim_enable_disable (sw_if_index, any_eligible); + + if ((u8) (any_eligible ? 1 : 0) != port->feature_enabled) + { + int rv = sonic_ext_trim_enable_disable (sw_if_index, any_eligible); + if (rv == 0) + port->feature_enabled = any_eligible ? 1 : 0; + else + clib_warning ( + "sonic-ext trim: feature %s on sw_if_index %u failed (rv=%d)", + any_eligible ? "enable" : "disable", sw_if_index, rv); + } } /* ------------------------------------------------------------------ */ From 84bd5c1828b52596a232e4d14448154003eb1511 Mon Sep 17 00:00:00 2001 From: Aaron Bernardino Date: Mon, 3 Aug 2026 18:46:50 +0000 Subject: [PATCH 3/3] [sonic_ext] Propagate trim admission feature-arc enable/disable status to the API caller Previously sonic_ext_trim_queue_program() returned void and only logged a clib_warning when the interface-output feature-arc enable/disable transition failed. The binary-API handler left rv=0 regardless, so SAI-VPP never saw the failure and could not retry, contradicting HLD REQ-12 (feature-arc enable failures must surface to the caller). Make the helper return int (0 on success, the vnet_feature_enable_disable error otherwise, and VNET_API_ERROR_INVALID_VALUE for an out-of-range queue), capture that in the queue_set handler so REPLY_MACRO forwards it as the reply retval, and surface it from the debug CLI. The queue state itself is still always programmed; only the eligibility feature-arc transition can fail, and port->feature_enabled is left untouched on failure so the next queue_set retries the transition. Bump VPP_VERSION suffix so downstream sonic-buildimage rebuilds the changed plugin instead of pulling a stale cached deb. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Aaron Bernardino --- rules/vpp.mk | 2 +- vppbld/plugins/sonic_ext/sonic_ext.h | 9 ++++++--- vppbld/plugins/sonic_ext/trim_api.c | 11 +++++++---- vppbld/plugins/sonic_ext/trim_cli.c | 4 +++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/rules/vpp.mk b/rules/vpp.mk index f5e2e0e4..0a629c34 100644 --- a/rules/vpp.mk +++ b/rules/vpp.mk @@ -8,7 +8,7 @@ VPP_VERSION_BASE = 2606 # https://packages.buildkite.com/sonic-vpp/vpp; if the suffix isn't bumped, # downstream sonic-buildimage builds will silently pull stale debs that # pre-date the new patch series / plugins and end up with VPP/SAI CRC drift. -VPP_VERSION = $(VPP_VERSION_BASE)-0.7 +VPP_VERSION = $(VPP_VERSION_BASE)-0.8 VPP_VERSION_SONIC = $(VPP_VERSION)+b1sonic1 VPP_SRC_PATH = platform/vpp/vppbld diff --git a/vppbld/plugins/sonic_ext/sonic_ext.h b/vppbld/plugins/sonic_ext/sonic_ext.h index 73caca69..5ef5ae8a 100644 --- a/vppbld/plugins/sonic_ext/sonic_ext.h +++ b/vppbld/plugins/sonic_ext/sonic_ext.h @@ -286,9 +286,12 @@ sonic_ext_trim_port_t *sonic_ext_trim_port_get (u32 sw_if_index, int create); /* Program one egress (port, queue) admission slot and (re)evaluate whether * the sonic-ext-trim-admission feature should be enabled on the port. - * Shared by the binary API handler and the debug CLI. */ -void sonic_ext_trim_queue_program (u32 sw_if_index, u32 queue, int eligible, - u64 rate_bytes_per_sec, u64 capacity_bytes); + * Shared by the binary API handler and the debug CLI. Returns 0 on success or + * a nonzero vnet API error if the feature-arc enable/disable transition failed, + * so the caller can surface the failure (the queue state itself is always + * programmed). */ +int sonic_ext_trim_queue_program (u32 sw_if_index, u32 queue, int eligible, + u64 rate_bytes_per_sec, u64 capacity_bytes); /* Software token-bucket admission decision for one packet of `len` bytes * on queue `q`. Refills from rate_bytes_per_sec, then either debits and diff --git a/vppbld/plugins/sonic_ext/trim_api.c b/vppbld/plugins/sonic_ext/trim_api.c index 6f0c5937..6eff01cc 100644 --- a/vppbld/plugins/sonic_ext/trim_api.c +++ b/vppbld/plugins/sonic_ext/trim_api.c @@ -102,17 +102,18 @@ sonic_ext_trim_enable_disable (u32 sw_if_index, int enable) enable, 0, 0); } -void +int sonic_ext_trim_queue_program (u32 sw_if_index, u32 queue, int eligible, u64 rate_bytes_per_sec, u64 capacity_bytes) { sonic_ext_trim_port_t *port; sonic_ext_trim_queue_t *q; int any_eligible = 0; + int rv = 0; int i; if (queue >= SONIC_EXT_TRIM_MAX_QUEUES) - return; + return VNET_API_ERROR_INVALID_VALUE; port = sonic_ext_trim_port_get (sw_if_index, 1 /* create */); q = &port->q[queue]; @@ -137,7 +138,7 @@ sonic_ext_trim_queue_program (u32 sw_if_index, u32 queue, int eligible, if ((u8) (any_eligible ? 1 : 0) != port->feature_enabled) { - int rv = sonic_ext_trim_enable_disable (sw_if_index, any_eligible); + rv = sonic_ext_trim_enable_disable (sw_if_index, any_eligible); if (rv == 0) port->feature_enabled = any_eligible ? 1 : 0; else @@ -145,6 +146,8 @@ sonic_ext_trim_queue_program (u32 sw_if_index, u32 queue, int eligible, "sonic-ext trim: feature %s on sw_if_index %u failed (rv=%d)", any_eligible ? "enable" : "disable", sw_if_index, rv); } + + return rv; } /* ------------------------------------------------------------------ */ @@ -215,7 +218,7 @@ vl_api_sonic_ext_trim_queue_set_t_handler ( goto done; } - sonic_ext_trim_queue_program (sw_if_index, queue, mp->eligible ? 1 : 0, + rv = sonic_ext_trim_queue_program (sw_if_index, queue, mp->eligible ? 1 : 0, clib_net_to_host_u64 (mp->rate_bytes_per_sec), clib_net_to_host_u64 (mp->capacity_bytes)); diff --git a/vppbld/plugins/sonic_ext/trim_cli.c b/vppbld/plugins/sonic_ext/trim_cli.c index 2a89ce17..eeb77a4e 100644 --- a/vppbld/plugins/sonic_ext/trim_cli.c +++ b/vppbld/plugins/sonic_ext/trim_cli.c @@ -226,7 +226,9 @@ sonic_ext_trim_queue_command_fn (vlib_main_t *vm, unformat_input_t *input, return clib_error_return (0, "queue must be 0..%u", SONIC_EXT_TRIM_MAX_QUEUES - 1); - sonic_ext_trim_queue_program (sw_if_index, queue, eligible, rate, cap); + if (sonic_ext_trim_queue_program (sw_if_index, queue, eligible, rate, cap)) + return clib_error_return ( + 0, "failed to enable/disable the trim admission feature on the interface"); return 0; }