> > After packet classification, packets are queued in to batches depending > on the matching netdev flow. Thereafter each batch is processed to > execute the related actions. This becomes particularly inefficient if > there are few packets in each batch as rte_eth_tx_burst() incurs expensive > MMIO writes. > > This commit adds back intermediate queue implementation. Packets are > queued and burst when the packet count exceeds threshold. Also drain > logic is refactored to handle packets hanging in the tx queues. Testing > shows significant performance gains with this implementation. > > Fixes: b59cc14e032d("netdev-dpdk: Use instant sending instead of > queueing of packets.") > Signed-off-by: Bhanuprakash Bodireddy > <bhanuprakash.bodire...@intel.com>> > Signed-off-by: Antonio Fischetti <antonio.fische...@intel.com> > Co-authored-by: Antonio Fischetti <antonio.fische...@intel.com> > Signed-off-by: Markus Magnusson <markus.magnus...@ericsson.com> > Co-authored-by: Markus Magnusson <markus.magnus...@ericsson.com> > --- > v2->v3 > * Refactor the code > * Use thread local copy 'send_port_cache' instead of 'tx_port' while > draining > * Invoke dp_netdev_drain_txq_port() to drain the packets from the queue > as > part of pmd reconfiguration that gets triggered due to port > addition/deletion > or change in pmd-cpu-mask. > * Invoke netdev_txq_drain() from xps_get_tx_qid() to drain packets in old > queue. This is possible in XPS case where the tx queue can change after > timeout. > * Fix another bug in netdev_dpdk_eth_tx_burst() w.r.t 'txq->count'. > > Latency stats: > Collected the latency stats with PHY2PHY loopback case using 30 IXIA streams > /UDP packets/uni direction traffic. All the stats are in nanoseconds. Results > below compare latency results between Master vs patch. > > case 1: Matching IP flow rules for each IXIA stream > Eg: For an IXIA stream with src Ip: 2.2.2.1, dst tip: 5.5.5.1 > ovs-ofctl add-flow br0 dl_type=0x0800,nw_src=2.2.2.1,actions=output:2 > > For an IXIA stream with src Ip: 4.4.4.1, dst tip: 15.15.15.1 > ovs-ofctl add-flow br0 dl_type=0x0800,nw_src=4.4.4.1,actions=output:2 > > Packet 64 128 256 512 > Branch Master Patch Master Patch Master Patch Master Patch > case 1 26100 222000 26190 217930 23890 199000 30370 212440 (min > latency ns) > 1239100 906910 1168740 691040 575470 574240 724360 734050 (max > latency ns) > 1189501 763908 913602 662941 486187 440482 470060 479376 > (avg > latency ns) > > > 1024 1280 1518 > Master Patch Master Patch Master Patch > 28320 189610 26520 220580 23950 200480 (min latency ns) > 701040 67584670 670390 19783490 685930 747040 (max latency ns) > 444033 469297 415602 506215 429587 491593 (avg latency ns) > > > case 2: ovs-ofctl add-flow br0 in_port=1,action=output:2 > > Packet 64 128 256 512 > Branch Master Patch Master Patch Master Patch Master Patch > case 2 18800 33970 19980 30350 22610 26800 13500 20220 > 506140 596690 363010 363370 544520 541570 549120 77414700 > 459509 473536 254817 256801 287872 287277 290642 301572 > > > 1024 1280 1518 > Master Patch Master Patch Master Patch > 22530 15850 21350 36020 25970 34300 > 549680 131964240 543390 81549210 552060 98207410 > 292436 294388 285468 305727 295133 300080 > > > case 3 is same as case 1 with INTERIM_QUEUE_BURST_THRESHOLD=16, > instead of 32. > > (w) patch > case 3 64 128 256 512 1024 1280 1518 > 122700 119890 135200 117530 118900 116640 123710(min) > 972830 808960 574180 696820 36717550 720500 726790(max) > 783315 674814 463256 439412 467041 463093 471967(avg) > > case 4 is same as case 2 with INTERIM_QUEUE_BURST_THRESHOLD=16, > instead of 32. > > (w) patch > case 4 64 128 256 512 1024 1280 1518 > 31750 26140 25250 17570 14750 28600 31460(min ns) > 722690 363200 539760 538320 301845040 12556210 132114800(max > ns) > 485710 253497 285589 284095 293189 282834 285829(avg ns) > > v1->v2 > * xps_get_tx_qid() is no more called twice. The last used qid is stored so > the drain function will flush the right queue also when XPS is enabled. > * netdev_txq_drain() is called unconditionally and not just for dpdk ports. > * txq_drain() takes the 'tx_lock' for queue in case of dynamic tx queues. > * Restored counting of dropped packets. > * Changed scheduling of drain function. > * Updated comments in netdev-provider.h > * Fixed a comment in dp-packet.h > > Details: > * In worst case scenario with fewer packets in batch, significant > bottleneck is observed at netdev_dpdk_eth_send() function due to > expensive MMIO writes. > > * Also its observed that CPI(cycles per instruction) Rate for the function > stood between 3.15 and 4.1 which is significantly higher than acceptable > limit of 1.0 for HPC applications and theoretical limit of 0.25 (As Backend > pipeline can retire 4 micro-operations in a cycle). > > * With this patch, CPI for netdev_dpdk_eth_send() is at 0.55 and the overall > throughput improved significantly. > > lib/dp-packet.h | 2 +- > lib/dpif-netdev.c | 66 ++++++++++++++++++++++++++++++++++++-- > lib/netdev-bsd.c | 1 + > lib/netdev-dpdk.c | 87 > ++++++++++++++++++++++++++++++++++++++++++++++----- > lib/netdev-dummy.c | 1 + > lib/netdev-linux.c | 1 + > lib/netdev-provider.h | 8 +++++ > lib/netdev-vport.c | 3 +- > lib/netdev.c | 9 ++++++ > lib/netdev.h | 1 + > 10 files changed, 166 insertions(+), 13 deletions(-) > > diff --git a/lib/dp-packet.h b/lib/dp-packet.h > index 17b7026..9e3912a 100644 > --- a/lib/dp-packet.h > +++ b/lib/dp-packet.h > @@ -39,7 +39,7 @@ enum OVS_PACKED_ENUM dp_packet_source { > DPBUF_STACK, /* Un-movable stack space or static buffer. */ > DPBUF_STUB, /* Starts on stack, may expand into heap. */ > DPBUF_DPDK, /* buffer data is from DPDK allocated memory. > - * ref to build_dp_packet() in netdev-dpdk. */ > + * Ref dp_packet_init_dpdk() in dp-packet.c */ > }; > > #define DP_PACKET_CONTEXT_SIZE 64 > diff --git a/lib/dpif-netdev.c b/lib/dpif-netdev.c > index 719a518..b0d47fa 100644 > --- a/lib/dpif-netdev.c > +++ b/lib/dpif-netdev.c > @@ -289,6 +289,8 @@ struct dp_netdev_rxq { > struct dp_netdev_pmd_thread *pmd; /* pmd thread that will poll this > queue. */ > }; > > +#define LAST_USED_QID_NONE -1 > + > /* A port in a netdev-based datapath. */ > struct dp_netdev_port { > odp_port_t port_no; > @@ -437,8 +439,14 @@ struct rxq_poll { > struct tx_port { > struct dp_netdev_port *port; > int qid; > - long long last_used; > + long long last_used; /* In case XPS is enabled, it contains the > + * timestamp of the last time the port was > + * used by the thread to send data. After > + * XPS_TIMEOUT_MS elapses the qid will be > + * marked as -1. */
Replace tabs with spaces in the comment above. > struct hmap_node node; > + int last_used_qid; /* Last queue id where packets could be > + enqueued. */ > }; > > /* PMD: Poll modes drivers. PMD accesses devices via polling to eliminate > @@ -2900,6 +2908,25 @@ cycles_count_end(struct dp_netdev_pmd_thread > *pmd, > } > > static void > +dp_netdev_drain_txq_ports(struct dp_netdev_pmd_thread *pmd) > +{ > + struct tx_port *cached_tx_port; > + int tx_qid; > + > + HMAP_FOR_EACH (cached_tx_port, node, &pmd->send_port_cache) { > + tx_qid = cached_tx_port->last_used_qid; > + > + if (tx_qid != LAST_USED_QID_NONE) { > + netdev_txq_drain(cached_tx_port->port->netdev, tx_qid, > + cached_tx_port->port->dynamic_txqs); > + > + /* Queue drained and mark it empty. */ > + cached_tx_port->last_used_qid = LAST_USED_QID_NONE; > + } > + } > +} > + > +static void > dp_netdev_process_rxq_port(struct dp_netdev_pmd_thread *pmd, > struct netdev_rxq *rx, > odp_port_t port_no) > @@ -3514,15 +3541,18 @@ pmd_load_queues_and_ports(struct > dp_netdev_pmd_thread *pmd, > return i; > } > > +enum { DRAIN_TSC = 20000ULL }; > + > static void * > pmd_thread_main(void *f_) > { > struct dp_netdev_pmd_thread *pmd = f_; > - unsigned int lc = 0; > + unsigned int lc = 0, lc_drain = 0; > struct polled_queue *poll_list; > bool exiting; > int poll_cnt; > int i; > + uint64_t prev = 0, now = 0; > > poll_list = NULL; > > @@ -3555,6 +3585,17 @@ reload: > poll_list[i].port_no); > } > > +#define MAX_LOOP_TO_DRAIN 128 MAX_LOOP_TO_DRAIN and DRAIN_TSC should be moved to somewhere appropriate, perhaps towards the beginning of the file. And should be annotated to inform what their values mean. Is there a need for both or can the desired behaviour be achieved using a single value? Maybe not, just something to consider to reduce the complexity. > + if (lc_drain++ > MAX_LOOP_TO_DRAIN) { > + lc_drain = 0; > + prev = now; > + now = pmd->last_cycles; > + > + if ((now - prev) > DRAIN_TSC) { > + dp_netdev_drain_txq_ports(pmd); > + } > + } > + > if (lc++ > 1024) { > bool reload; > > @@ -3573,6 +3614,9 @@ reload: > } > } > > + /* Drain the queues as part of reconfiguration */ > + dp_netdev_drain_txq_ports(pmd); > + > poll_cnt = pmd_load_queues_and_ports(pmd, &poll_list); > exiting = latch_is_set(&pmd->exit_latch); > /* Signal here to make sure the pmd finishes > @@ -3890,6 +3934,7 @@ dp_netdev_add_port_tx_to_pmd(struct > dp_netdev_pmd_thread *pmd, > > tx->port = port; > tx->qid = -1; > + tx->last_used_qid = LAST_USED_QID_NONE; > > hmap_insert(&pmd->tx_ports, &tx->node, hash_port_no(tx->port- > >port_no)); > pmd->need_reload = true; > @@ -4454,6 +4499,14 @@ dpif_netdev_xps_get_tx_qid(const struct > dp_netdev_pmd_thread *pmd, > > dpif_netdev_xps_revalidate_pmd(pmd, now, false); > > + /* The tx queue can change in XPS case, make sure packets in previous > + * queue is drained properly. */ > + if (tx->last_used_qid != LAST_USED_QID_NONE && > + tx->qid != tx->last_used_qid) { > + netdev_txq_drain(port->netdev, tx->last_used_qid, port- > >dynamic_txqs); > + tx->last_used_qid = LAST_USED_QID_NONE; > + } > + > VLOG_DBG("Core %d: New TX queue ID %d for port \'%s\'.", > pmd->core_id, tx->qid, netdev_get_name(tx->port->netdev)); > return min_qid; > @@ -4548,6 +4601,13 @@ dp_execute_cb(void *aux_, struct > dp_packet_batch *packets_, > tx_qid = pmd->static_tx_qid; > } > > + /* In case these packets gets buffered into an intermediate > + * queue and XPS is enabled the drain function could find a > + * different Tx qid assigned to its thread. We keep track > + * of the qid we're now using, that will trigger the drain > + * function and will select the right queue to flush. */ > + p->last_used_qid = tx_qid; > + > netdev_send(p->port->netdev, tx_qid, packets_, may_steal, > dynamic_txqs); > return; > @@ -4960,7 +5020,7 @@ dpif_dummy_register(enum dummy_level level) > "dp port new-number", > 3, 3, dpif_dummy_change_port_number, NULL); > } > - Remove the character inserted here. > + > /* Datapath Classifier. */ > > /* A set of rules that all have the same fields wildcarded. */ > diff --git a/lib/netdev-bsd.c b/lib/netdev-bsd.c > index 94c515d..00d5263 100644 > --- a/lib/netdev-bsd.c > +++ b/lib/netdev-bsd.c > @@ -1547,6 +1547,7 @@ netdev_bsd_update_flags(struct netdev > *netdev_, enum netdev_flags off, > netdev_bsd_rxq_recv, \ > netdev_bsd_rxq_wait, \ > netdev_bsd_rxq_drain, \ > + NULL, \ > } > > const struct netdev_class netdev_bsd_class = > diff --git a/lib/netdev-dpdk.c b/lib/netdev-dpdk.c > index 94568a1..3def755 100644 > --- a/lib/netdev-dpdk.c > +++ b/lib/netdev-dpdk.c > @@ -166,7 +166,6 @@ static const struct rte_eth_conf port_conf = { > > enum { DPDK_RING_SIZE = 256 }; > BUILD_ASSERT_DECL(IS_POW2(DPDK_RING_SIZE)); > -enum { DRAIN_TSC = 200000ULL }; > > enum dpdk_dev_type { > DPDK_DEV_ETH = 0, > @@ -286,15 +285,26 @@ struct dpdk_mp { > struct ovs_list list_node OVS_GUARDED_BY(dpdk_mp_mutex); > }; > > +/* Queue 'INTERIM_QUEUE_BURST_THRESHOLD' packets before > tranmitting. > + * Defaults to 'NETDEV_MAX_BURST'(32) now. > + */ > +#define INTERIM_QUEUE_BURST_THRESHOLD NETDEV_MAX_BURST > + > /* There should be one 'struct dpdk_tx_queue' created for > * each cpu core. */ > struct dpdk_tx_queue { > + int count; /* Number of buffered packets waiting to > + be sent. */ > rte_spinlock_t tx_lock; /* Protects the members and the NIC queue > * from concurrent access. It is used > only > * if the queue is shared among different > * pmd threads (see 'concurrent_txq'). */ > int map; /* Mapping of configured vhost-user queues > * to enabled by guest. */ > + struct rte_mbuf *burst_pkts[INTERIM_QUEUE_BURST_THRESHOLD]; > + /* Intermediate queues where packets can > + * be buffered to amortize the cost of > MMIO > + * writes. */ > }; > > /* dpdk has no way to remove dpdk ring ethernet devices > @@ -1381,6 +1391,7 @@ static inline int > netdev_dpdk_eth_tx_burst(struct netdev_dpdk *dev, int qid, > struct rte_mbuf **pkts, int cnt) > { > + struct dpdk_tx_queue *txq = &dev->tx_q[qid]; > uint32_t nb_tx = 0; > > while (nb_tx != cnt) { > @@ -1404,6 +1415,7 @@ netdev_dpdk_eth_tx_burst(struct netdev_dpdk > *dev, int qid, > } > } > > + txq->count = 0; > return cnt - nb_tx; > } > > @@ -1788,12 +1800,42 @@ dpdk_do_tx_copy(struct netdev *netdev, int > qid, struct dp_packet_batch *batch) > } > } > > +/* Enqueue packets in an intermediate queue and call the burst > + * function when the queue is full. This way we can amortize the > + * cost of MMIO writes. */ > +static inline int > +netdev_dpdk_eth_tx_queue(struct netdev_dpdk *dev, int qid, > + struct rte_mbuf **pkts, int cnt) > +{ > + struct dpdk_tx_queue *txq = &dev->tx_q[qid]; > + Remove this whitespace. > + int i = 0; > + int dropped = 0; > + > + while (i < cnt) { > + int freeslots = INTERIM_QUEUE_BURST_THRESHOLD - txq->count; > + int tocopy = MIN(freeslots, cnt-i); > + > + memcpy(&txq->burst_pkts[txq->count], &pkts[i], > + tocopy * sizeof (struct rte_mbuf *)); > + > + txq->count += tocopy; > + i += tocopy; > + > + /* Queue full, burst the packets */ > + if (txq->count >= INTERIM_QUEUE_BURST_THRESHOLD) { > + dropped += netdev_dpdk_eth_tx_burst(dev, qid, txq->burst_pkts, > + txq->count); > + } > + } > + return dropped; > +} > + > static int > netdev_dpdk_vhost_send(struct netdev *netdev, int qid, > struct dp_packet_batch *batch, > bool may_steal, bool concurrent_txq OVS_UNUSED) > { > - > if (OVS_UNLIKELY(!may_steal || batch->packets[0]->source != > DPBUF_DPDK)) { > dpdk_do_tx_copy(netdev, qid, batch); > dp_packet_delete_batch(batch, may_steal); > @@ -1836,7 +1878,7 @@ netdev_dpdk_send__(struct netdev_dpdk *dev, > int qid, > cnt = netdev_dpdk_qos_run(dev, pkts, cnt); > dropped = batch->count - cnt; > > - dropped += netdev_dpdk_eth_tx_burst(dev, qid, pkts, cnt); > + dropped += netdev_dpdk_eth_tx_queue(dev, qid, pkts, cnt); > > if (OVS_UNLIKELY(dropped)) { > rte_spinlock_lock(&dev->stats_lock); > @@ -1850,6 +1892,30 @@ netdev_dpdk_send__(struct netdev_dpdk *dev, > int qid, > } > } > > +/* Drain tx queues, this is called periodically to empty the > + * intermediate queue in case of few packets (< > INTERIM_QUEUE_BURST_THRESHOLD) > + * are buffered into the queue. */ > +static int > +netdev_dpdk_txq_drain(struct netdev *netdev, int qid, bool > concurrent_txq) > +{ > + struct netdev_dpdk *dev = netdev_dpdk_cast(netdev); > + struct dpdk_tx_queue *txq = &dev->tx_q[qid]; > + > + if (OVS_LIKELY(txq->count)) { > + if (OVS_UNLIKELY(concurrent_txq)) { > + qid = qid % dev->up.n_txq; > + rte_spinlock_lock(&dev->tx_q[qid].tx_lock); > + } > + > + netdev_dpdk_eth_tx_burst(dev, qid, txq->burst_pkts, txq->count); > + > + if (OVS_UNLIKELY(concurrent_txq)) { > + rte_spinlock_unlock(&dev->tx_q[qid].tx_lock); > + } > + } > + return 0; > +} > + > static int > netdev_dpdk_eth_send(struct netdev *netdev, int qid, > struct dp_packet_batch *batch, bool may_steal, > @@ -3243,7 +3309,7 @@ unlock: > SET_CONFIG, SET_TX_MULTIQ, SEND, \ > GET_CARRIER, GET_STATS, \ > GET_FEATURES, GET_STATUS, \ > - RECONFIGURE, RXQ_RECV) \ > + RECONFIGURE, RXQ_RECV, TXQ_DRAIN) \ > { \ > NAME, \ > true, /* is_pmd */ \ > @@ -3310,6 +3376,7 @@ unlock: > RXQ_RECV, \ > NULL, /* rx_wait */ \ > NULL, /* rxq_drain */ \ > + TXQ_DRAIN, /* txq_drain */ \ Align this comment with the rest. > } > > static const struct netdev_class dpdk_class = > @@ -3326,7 +3393,8 @@ static const struct netdev_class dpdk_class = > netdev_dpdk_get_features, > netdev_dpdk_get_status, > netdev_dpdk_reconfigure, > - netdev_dpdk_rxq_recv); > + netdev_dpdk_rxq_recv, > + netdev_dpdk_txq_drain); > > static const struct netdev_class dpdk_ring_class = > NETDEV_DPDK_CLASS( > @@ -3342,7 +3410,8 @@ static const struct netdev_class dpdk_ring_class = > netdev_dpdk_get_features, > netdev_dpdk_get_status, > netdev_dpdk_reconfigure, > - netdev_dpdk_rxq_recv); > + netdev_dpdk_rxq_recv, > + NULL); > > static const struct netdev_class dpdk_vhost_class = > NETDEV_DPDK_CLASS( > @@ -3358,7 +3427,8 @@ static const struct netdev_class dpdk_vhost_class = > NULL, > NULL, > netdev_dpdk_vhost_reconfigure, > - netdev_dpdk_vhost_rxq_recv); > + netdev_dpdk_vhost_rxq_recv, > + NULL); > static const struct netdev_class dpdk_vhost_client_class = > NETDEV_DPDK_CLASS( > "dpdkvhostuserclient", > @@ -3373,7 +3443,8 @@ static const struct netdev_class > dpdk_vhost_client_class = > NULL, > NULL, > netdev_dpdk_vhost_client_reconfigure, > - netdev_dpdk_vhost_rxq_recv); > + netdev_dpdk_vhost_rxq_recv, > + NULL); > > void > netdev_dpdk_register(void) > diff --git a/lib/netdev-dummy.c b/lib/netdev-dummy.c > index 0657434..4ef659e 100644 > --- a/lib/netdev-dummy.c > +++ b/lib/netdev-dummy.c > @@ -1409,6 +1409,7 @@ netdev_dummy_update_flags(struct netdev > *netdev_, > netdev_dummy_rxq_recv, \ > netdev_dummy_rxq_wait, \ > netdev_dummy_rxq_drain, \ > + NULL, \ > } > > static const struct netdev_class dummy_class = > diff --git a/lib/netdev-linux.c b/lib/netdev-linux.c > index 9ff1333..79478ee 100644 > --- a/lib/netdev-linux.c > +++ b/lib/netdev-linux.c > @@ -2830,6 +2830,7 @@ netdev_linux_update_flags(struct netdev > *netdev_, enum netdev_flags off, > netdev_linux_rxq_recv, \ > netdev_linux_rxq_wait, \ > netdev_linux_rxq_drain, \ > + NULL, \ > } > > const struct netdev_class netdev_linux_class = > diff --git a/lib/netdev-provider.h b/lib/netdev-provider.h > index 8346fc4..97e72c6 100644 > --- a/lib/netdev-provider.h > +++ b/lib/netdev-provider.h > @@ -335,6 +335,11 @@ struct netdev_class { > * If the function returns a non-zero value, some of the packets might > have > * been sent anyway. > * > + * Some netdev provider - like in case of 'dpdk' - may buffer the batch > + * of packets into an intermediate queue. Buffered packets will be sent > + * out when their number will exceed a threshold or by the periodic call > + * to the drain function. > + * > * If 'may_steal' is false, the caller retains ownership of all the > * packets. If 'may_steal' is true, the caller transfers ownership of > all > * the packets to the network device, regardless of success. > @@ -769,6 +774,9 @@ struct netdev_class { > > /* Discards all packets waiting to be received from 'rx'. */ > int (*rxq_drain)(struct netdev_rxq *rx); > + > + /* Drain all packets waiting to be sent on queue 'qid'. */ > + int (*txq_drain)(struct netdev *netdev, int qid, bool concurrent_txq); > }; > > int netdev_register_provider(const struct netdev_class *); > diff --git a/lib/netdev-vport.c b/lib/netdev-vport.c > index 2d0aa43..64cf617 100644 > --- a/lib/netdev-vport.c > +++ b/lib/netdev-vport.c > @@ -847,7 +847,8 @@ get_stats(const struct netdev *netdev, struct > netdev_stats *stats) > NULL, /* rx_dealloc */ \ > NULL, /* rx_recv */ \ > NULL, /* rx_wait */ \ > - NULL, /* rx_drain */ > + NULL, /* rx_drain */ \ > + NULL, /* tx_drain */ > > > #define TUNNEL_CLASS(NAME, DPIF_PORT, BUILD_HEADER, > PUSH_HEADER, POP_HEADER) \ > diff --git a/lib/netdev.c b/lib/netdev.c > index 1e6bb2b..5e0c53f 100644 > --- a/lib/netdev.c > +++ b/lib/netdev.c > @@ -670,6 +670,15 @@ netdev_rxq_drain(struct netdev_rxq *rx) > : 0); > } > > +/* Flush packets on the queue 'qid'. */ > +int > +netdev_txq_drain(struct netdev *netdev, int qid, bool netdev_txq_drain) > +{ > + return (netdev->netdev_class->txq_drain > + ? netdev->netdev_class->txq_drain(netdev, qid, netdev_txq_drain) > + : EOPNOTSUPP); > +} > + > /* Configures the number of tx queues of 'netdev'. Returns 0 if successful, > * otherwise a positive errno value. > * > diff --git a/lib/netdev.h b/lib/netdev.h > index d6c07c1..7ddd790 100644 > --- a/lib/netdev.h > +++ b/lib/netdev.h > @@ -155,6 +155,7 @@ int netdev_rxq_drain(struct netdev_rxq *); > int netdev_send(struct netdev *, int qid, struct dp_packet_batch *, > bool may_steal, bool concurrent_txq); > void netdev_send_wait(struct netdev *, int qid); > +int netdev_txq_drain(struct netdev *, int qid, bool concurrent_txq); > > /* native tunnel APIs */ > /* Structure to pass parameters required to build a tunnel header. */ > -- > 2.4.11 > > _______________________________________________ > dev mailing list > d...@openvswitch.org > https://mail.openvswitch.org/mailman/listinfo/ovs-dev _______________________________________________ dev mailing list d...@openvswitch.org https://mail.openvswitch.org/mailman/listinfo/ovs-dev