@@ -3690,6 +3690,46 @@ static int ieee80211_del_tx_ts(struct wiphy *wiphy, struct net_device *dev,
return -ENOENT;
}
+static void ieee80211_rate_stats(struct wiphy *wiphy,
+ enum cfg80211_rate_stats_ops op)
+{
+ struct ieee80211_local *local = wiphy_priv(wiphy);
+ struct sta_info *sta;
+ struct ieee80211_sta_rate_stats *stats, *tmp;
+ LIST_HEAD(list);
+
+ mutex_lock(&local->sta_mtx);
+ switch (op) {
+ case CFG80211_RATE_STATS_START:
+ local->rate_stats_active = true;
+ list_for_each_entry(sta, &local->sta_list, list)
+ ieee80211_sta_start_rate_stats(sta);
+ /* list stays empty */
+ break;
+ case CFG80211_RATE_STATS_DUMP:
+ list_for_each_entry(sta, &local->sta_list, list) {
+ stats = ieee80211_sta_reset_rate_stats(sta);
+ if (stats)
+ list_add_tail(&stats->list, &list);
+ }
+ break;
+ case CFG80211_RATE_STATS_STOP:
+ local->rate_stats_active = false;
+ list_for_each_entry(sta, &local->sta_list, list) {
+ stats = ieee80211_sta_stop_rate_stats(sta);
+ if (stats)
+ list_add_tail(&stats->list, &list);
+ }
+ break;
+ }
+ mutex_unlock(&local->sta_mtx);
+
+ if (!list_empty(&list))
+ synchronize_rcu();
+ list_for_each_entry_safe(stats, tmp, &list, list)
+ ieee80211_sta_free_rate_stats(stats, true);
+}
+
const struct cfg80211_ops mac80211_config_ops = {
.add_virtual_intf = ieee80211_add_iface,
.del_virtual_intf = ieee80211_del_iface,
@@ -3774,4 +3814,5 @@ const struct cfg80211_ops mac80211_config_ops = {
.set_ap_chanwidth = ieee80211_set_ap_chanwidth,
.add_tx_ts = ieee80211_add_tx_ts,
.del_tx_ts = ieee80211_del_tx_ts,
+ .rate_stats = ieee80211_rate_stats,
};
@@ -1126,6 +1126,8 @@ struct ieee80211_local {
bool use_chanctx;
+ bool rate_stats_active;
+
/* protects the aggregated multicast list and filter calls */
spinlock_t filter_lock;
@@ -545,6 +545,8 @@ struct ieee80211_hw *ieee80211_alloc_hw_nm(size_t priv_data_len,
NL80211_FEATURE_MAC_ON_CREATE |
NL80211_FEATURE_USERSPACE_MPM;
+ wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_RATE_STATS);
+
if (!ops->hw_scan)
wiphy->features |= NL80211_FEATURE_LOW_PRIORITY_SCAN |
NL80211_FEATURE_AP_SCAN;
@@ -859,6 +861,16 @@ int ieee80211_register_hw(struct ieee80211_hw *hw)
/* TODO: consider VHT for RX chains, hopefully it's the same */
}
+ /* if the HW has more than 12 legacy rates, some assumptions in the
+ * data structures break - in this case don't allow the rate-stats
+ * feature flag.
+ */
+ if (WARN_ON(wiphy_ext_feature_isset(hw->wiphy,
+ NL80211_EXT_FEATURE_RATE_STATS) &&
+ max_bitrates > 12))
+ hw->wiphy->ext_features[NL80211_EXT_FEATURE_RATE_STATS / 8] &=
+ ~BIT(NL80211_EXT_FEATURE_RATE_STATS % 8);
+
/* if low-level driver supports AP, we also support VLAN */
if (local->hw.wiphy->interface_modes & BIT(NL80211_IFTYPE_AP)) {
hw->wiphy->interface_modes |= BIT(NL80211_IFTYPE_AP_VLAN);
@@ -2292,6 +2292,56 @@ ieee80211_rx_h_mesh_fwding(struct ieee80211_rx_data *rx)
}
#endif
+static void noinline
+ieee80211_rx_handle_rate_stats(struct ieee80211_local *local,
+ struct ieee80211_rx_status *status,
+ struct ieee80211_sta_rate_stats *rstats)
+{
+ u16 rate = sta_rate_stats_encode_rate(status);
+ unsigned int start;
+ int i;
+
+ start = status->rate_idx;
+ /* for HT - use only the base index */
+ if (status->flag & RX_FLAG_HT)
+ start &= 0xF;
+ for (i = start; i < start + 3; i++) {
+ if (rstats->entries[i].rate == rate ||
+ rstats->entries[i].rate == STA_RATE_STATS_RATE_INVALID) {
+ if (rstats->entries[i].rate ==
+ STA_RATE_STATS_RATE_INVALID)
+ rstats->entries[i].rate = rate;
+ if (rstats->entries[i].rx++ > 64000)
+ goto schedule_dump;
+ return;
+ }
+ }
+
+ /* The last six entries are only really used by legacy rates - so
+ * not that performance sensitive. For HT and VHT though, only the
+ * first 24 (3*8) or 30 (3*10) entries can be used, so try the last
+ * few in the list as "escape" entries. If any such entry is used
+ * then schedule dumping to userspace unconditionally to avoid going
+ * into this code path frequently.
+ */
+ for (i = ARRAY_SIZE(rstats->entries) - 1;
+ i > ARRAY_SIZE(rstats->entries) - 7; i--) {
+ if (rstats->entries[i].rate == rate ||
+ rstats->entries[i].rate == STA_RATE_STATS_RATE_INVALID) {
+ if (rstats->entries[i].rate ==
+ STA_RATE_STATS_RATE_INVALID)
+ rstats->entries[i].rate = rate;
+ rstats->entries[i].rx++;
+ break;
+ }
+ }
+
+ /* if we still didn't find anything, drop the stats for this packet */
+
+ schedule_dump:
+ ieee80211_queue_work(&local->hw, &rstats->dump_wk);
+}
+
static ieee80211_rx_result debug_noinline
ieee80211_rx_h_data(struct ieee80211_rx_data *rx)
{
@@ -2310,12 +2360,19 @@ ieee80211_rx_h_data(struct ieee80211_rx_data *rx)
return RX_DROP_MONITOR;
if (rx->sta) {
+ struct ieee80211_sta_rate_stats *rstats;
+
/* The security index has the same property as needed
* for the rx_msdu field, i.e. it is IEEE80211_NUM_TIDS
* for non-QoS-data frames. Here we know it's a data
* frame, so count MSDUs.
*/
rx->sta->rx_msdu[rx->security_idx]++;
+
+ rstats = rcu_dereference(rx->sta->rate_stats);
+ if (rstats)
+ ieee80211_rx_handle_rate_stats(local,
+ IEEE80211_SKB_RXCB(rx->skb), rstats);
}
/*
@@ -497,6 +497,9 @@ static int sta_info_insert_finish(struct sta_info *sta) __acquires(RCU)
local->sta_generation++;
smp_mb();
+ if (local->rate_stats_active)
+ ieee80211_sta_start_rate_stats(sta);
+
/* simplify things and don't accept BA sessions yet */
set_sta_flag(sta, WLAN_STA_BLOCK_BA);
@@ -882,6 +885,7 @@ static void __sta_info_destroy_part2(struct sta_info *sta)
struct ieee80211_local *local = sta->local;
struct ieee80211_sub_if_data *sdata = sta->sdata;
struct station_info sinfo = {};
+ struct ieee80211_sta_rate_stats *stats;
int ret;
/*
@@ -919,6 +923,10 @@ static void __sta_info_destroy_part2(struct sta_info *sta)
sta_dbg(sdata, "Removed STA %pM\n", sta->sta.addr);
+ stats = ieee80211_sta_stop_rate_stats(sta);
+ /* station was already unlinked and rcu sync'ed before getting here */
+ ieee80211_sta_free_rate_stats(stats, true);
+
sta_set_sinfo(sta, &sinfo);
cfg80211_del_sta_sinfo(sdata->dev, sta->sta.addr, &sinfo, GFP_KERNEL);
@@ -1950,3 +1958,130 @@ void sta_set_sinfo(struct sta_info *sta, struct station_info *sinfo)
sinfo->expected_throughput = thr;
}
}
+
+static void sta_rate_stats_decode_rate(u16 r, struct ieee80211_local *local,
+ struct rate_info *rinfo)
+{
+ rinfo->bw = (r & STA_RATE_STATS_RATE_BW_MASK) >>
+ STA_RATE_STATS_RATE_BW_SHIFT;
+
+ if (r & STA_RATE_STATS_RATE_VHT) {
+ rinfo->flags = RATE_INFO_FLAGS_VHT_MCS;
+ rinfo->mcs = r & 0xf;
+ rinfo->nss = (r & 0xf0) >> 4;
+ } else if (r & STA_RATE_STATS_RATE_HT) {
+ rinfo->flags = RATE_INFO_FLAGS_MCS;
+ rinfo->mcs = r & 0xff;
+ } else if (r & STA_RATE_STATS_RATE_LEGACY) {
+ struct ieee80211_supported_band *sband;
+ u16 brate;
+ unsigned int shift;
+
+ sband = local->hw.wiphy->bands[(r >> 4) & 0xf];
+ brate = sband->bitrates[r & 0xf].bitrate;
+ if (rinfo->bw == RATE_INFO_BW_5)
+ shift = 2;
+ else if (rinfo->bw == RATE_INFO_BW_10)
+ shift = 1;
+ else
+ shift = 0;
+ rinfo->legacy = DIV_ROUND_UP(brate, 1 << shift);
+ }
+
+ if (r & STA_RATE_STATS_RATE_SGI)
+ rinfo->flags |= RATE_INFO_FLAGS_SHORT_GI;
+}
+
+void ieee80211_sta_start_rate_stats(struct sta_info *sta)
+{
+ struct ieee80211_sta_rate_stats *stats;
+
+ stats = ieee80211_sta_reset_rate_stats(sta);
+
+ if (WARN_ON_ONCE(stats)) {
+ /* error case - be really slow but correct */
+ synchronize_rcu();
+ ieee80211_sta_free_rate_stats(stats, true);
+ }
+}
+
+static void ieee80211_sta_dump_rate_stats_wk(struct work_struct *wk)
+{
+ struct ieee80211_sta_rate_stats *stats, *stats2;
+
+ stats = container_of(wk, struct ieee80211_sta_rate_stats, dump_wk);
+
+ mutex_lock(&stats->sta->local->sta_mtx);
+ stats2 = ieee80211_sta_reset_rate_stats(stats->sta);
+ mutex_unlock(&stats->sta->local->sta_mtx);
+
+ WARN_ON_ONCE(stats != stats2);
+
+ synchronize_rcu();
+ ieee80211_sta_free_rate_stats(stats, false);
+}
+
+struct ieee80211_sta_rate_stats * __must_check
+ieee80211_sta_reset_rate_stats(struct sta_info *sta)
+{
+ struct ieee80211_sta_rate_stats *stats, *new;
+
+ stats = rcu_dereference_protected(sta->rate_stats,
+ lockdep_is_held(&sta->local->sta_mtx));
+
+ new = kzalloc(sizeof(*new), GFP_KERNEL);
+ new->sta = sta;
+ INIT_WORK(&new->dump_wk, ieee80211_sta_dump_rate_stats_wk);
+
+ rcu_assign_pointer(sta->rate_stats, new);
+
+ /* caller must wait for grace period and free data */
+ return stats;
+}
+
+struct ieee80211_sta_rate_stats * __must_check
+ieee80211_sta_stop_rate_stats(struct sta_info *sta)
+{
+ struct ieee80211_sta_rate_stats *stats;
+
+ stats = rcu_dereference_protected(sta->rate_stats,
+ lockdep_is_held(&sta->local->sta_mtx));
+
+ RCU_INIT_POINTER(sta->rate_stats, NULL);
+
+ /* caller must wait for grace period and free data */
+ return stats;
+}
+
+void ieee80211_sta_free_rate_stats(struct ieee80211_sta_rate_stats *stats,
+ bool flush)
+{
+ struct cfg80211_rate_stats report;
+ struct sta_info *sta;
+ int i;
+
+ if (!stats)
+ return;
+
+ if (flush && flush_work(&stats->dump_wk))
+ return;
+
+ sta = stats->sta;
+
+ for (i = 0; i < ARRAY_SIZE(stats->entries); i++) {
+ if (stats->entries[i].rate == STA_RATE_STATS_RATE_INVALID)
+ continue;
+
+ sta_rate_stats_decode_rate(stats->entries[i].rate,
+ stats->sta->local, &report.rate);
+
+ report.stats.filled = BIT(NL80211_TID_STATS_RX_MSDU);
+ report.stats.rx_msdu = stats->entries[i].rx;
+
+ cfg80211_report_rate_stats(sta->local->hw.wiphy,
+ &sta->sdata->wdev, sta->sta.addr,
+ 1, &report, GFP_KERNEL);
+ }
+
+ kfree(stats);
+}
@@ -354,6 +354,7 @@ struct ieee80211_tx_latency_stat {
* using IEEE80211_NUM_TID entry for non-QoS frames
* @rx_msdu: MSDUs received from this station, using IEEE80211_NUM_TID
* entry for non-QoS frames
+ * @rate_stats: rate statistics pointer (if enabled)
*/
struct sta_info {
/* General information, mostly static */
@@ -428,6 +429,7 @@ struct sta_info {
u64 tx_msdu_retries[IEEE80211_NUM_TIDS + 1];
u64 tx_msdu_failed[IEEE80211_NUM_TIDS + 1];
u64 rx_msdu[IEEE80211_NUM_TIDS + 1];
+ struct ieee80211_sta_rate_stats __rcu *rate_stats;
/*
* Aggregation information, locked with lock.
@@ -670,4 +672,61 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta);
void ieee80211_sta_ps_deliver_poll_response(struct sta_info *sta);
void ieee80211_sta_ps_deliver_uapsd(struct sta_info *sta);
+/* rate statistics */
+void ieee80211_sta_start_rate_stats(struct sta_info *sta);
+struct ieee80211_sta_rate_stats * __must_check
+ieee80211_sta_reset_rate_stats(struct sta_info *sta);
+struct ieee80211_sta_rate_stats * __must_check
+ieee80211_sta_stop_rate_stats(struct sta_info *sta);
+void ieee80211_sta_free_rate_stats(struct ieee80211_sta_rate_stats *stats,
+ bool flush);
+
+struct ieee80211_sta_rate_stats {
+ struct list_head list;
+ struct sta_info *sta;
+ struct work_struct dump_wk;
+ struct {
+#define STA_RATE_STATS_RATE_INVALID 0
+#define STA_RATE_STATS_RATE_VHT 0x8000
+#define STA_RATE_STATS_RATE_HT 0x4000
+#define STA_RATE_STATS_RATE_LEGACY 0x2000
+#define STA_RATE_STATS_RATE_SGI 0x1000
+#define STA_RATE_STATS_RATE_BW_SHIFT 9
+#define STA_RATE_STATS_RATE_BW_MASK (0x7 << STA_RATE_STATS_RATE_BW_SHIFT)
+ u16 rate;
+ u16 rx;
+ /* 3 * rate_idx/MCS - using a few from the end as escape */
+ } entries[36];
+};
+
+static inline u16 sta_rate_stats_encode_rate(struct ieee80211_rx_status *s)
+{
+ u16 r = s->rate_idx;
+
+ if (s->vht_flag & RX_VHT_FLAG_80MHZ)
+ r |= RATE_INFO_BW_80 << STA_RATE_STATS_RATE_BW_SHIFT;
+ else if (s->vht_flag & RX_VHT_FLAG_160MHZ)
+ r |= RATE_INFO_BW_160 << STA_RATE_STATS_RATE_BW_SHIFT;
+ else if (s->flag & RX_FLAG_40MHZ)
+ r |= RATE_INFO_BW_40 << STA_RATE_STATS_RATE_BW_SHIFT;
+ else if (s->flag & RX_FLAG_10MHZ)
+ r |= RATE_INFO_BW_10 << STA_RATE_STATS_RATE_BW_SHIFT;
+ else if (s->flag & RX_FLAG_5MHZ)
+ r |= RATE_INFO_BW_5 << STA_RATE_STATS_RATE_BW_SHIFT;
+ else
+ r |= RATE_INFO_BW_20 << STA_RATE_STATS_RATE_BW_SHIFT;
+
+ if (s->flag & RX_FLAG_SHORT_GI)
+ r |= STA_RATE_STATS_RATE_SGI;
+
+ if (s->flag & RX_FLAG_VHT)
+ r |= STA_RATE_STATS_RATE_VHT | (s->vht_nss << 4);
+ else if (s->flag & RX_FLAG_HT)
+ r |= STA_RATE_STATS_RATE_HT;
+ else
+ r |= STA_RATE_STATS_RATE_LEGACY | (s->band << 4);
+
+ return r;
+}
+
#endif /* STA_INFO_H */