| // SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0 |
| /* Copyright (c) 2021 Marvell International Ltd. All rights reserved */ |
| |
| #include "prestera.h" |
| #include "prestera_hw.h" |
| #include "prestera_acl.h" |
| #include "prestera_counter.h" |
| |
| #define COUNTER_POLL_TIME (msecs_to_jiffies(1000)) |
| #define COUNTER_RESCHED_TIME (msecs_to_jiffies(50)) |
| #define COUNTER_BULK_SIZE (256) |
| |
| struct prestera_counter { |
| struct prestera_switch *sw; |
| struct delayed_work stats_dw; |
| struct mutex mtx; /* protect block_list */ |
| struct prestera_counter_block **block_list; |
| u32 total_read; |
| u32 block_list_len; |
| u32 curr_idx; |
| bool is_fetching; |
| }; |
| |
| struct prestera_counter_block { |
| struct list_head list; |
| u32 id; |
| u32 offset; |
| u32 num_counters; |
| u32 client; |
| struct idr counter_idr; |
| refcount_t refcnt; |
| struct mutex mtx; /* protect stats and counter_idr */ |
| struct prestera_counter_stats *stats; |
| u8 *counter_flag; |
| bool is_updating; |
| bool full; |
| }; |
| |
| enum { |
| COUNTER_FLAG_READY = 0, |
| COUNTER_FLAG_INVALID = 1 |
| }; |
| |
| static bool |
| prestera_counter_is_ready(struct prestera_counter_block *block, u32 id) |
| { |
| return block->counter_flag[id - block->offset] == COUNTER_FLAG_READY; |
| } |
| |
| static void prestera_counter_lock(struct prestera_counter *counter) |
| { |
| mutex_lock(&counter->mtx); |
| } |
| |
| static void prestera_counter_unlock(struct prestera_counter *counter) |
| { |
| mutex_unlock(&counter->mtx); |
| } |
| |
| static void prestera_counter_block_lock(struct prestera_counter_block *block) |
| { |
| mutex_lock(&block->mtx); |
| } |
| |
| static void prestera_counter_block_unlock(struct prestera_counter_block *block) |
| { |
| mutex_unlock(&block->mtx); |
| } |
| |
| static bool prestera_counter_block_incref(struct prestera_counter_block *block) |
| { |
| return refcount_inc_not_zero(&block->refcnt); |
| } |
| |
| static bool prestera_counter_block_decref(struct prestera_counter_block *block) |
| { |
| return refcount_dec_and_test(&block->refcnt); |
| } |
| |
| /* must be called with prestera_counter_block_lock() */ |
| static void prestera_counter_stats_clear(struct prestera_counter_block *block, |
| u32 counter_id) |
| { |
| memset(&block->stats[counter_id - block->offset], 0, |
| sizeof(*block->stats)); |
| } |
| |
| static struct prestera_counter_block * |
| prestera_counter_block_lookup_not_full(struct prestera_counter *counter, |
| u32 client) |
| { |
| u32 i; |
| |
| prestera_counter_lock(counter); |
| for (i = 0; i < counter->block_list_len; i++) { |
| if (counter->block_list[i] && |
| counter->block_list[i]->client == client && |
| !counter->block_list[i]->full && |
| prestera_counter_block_incref(counter->block_list[i])) { |
| prestera_counter_unlock(counter); |
| return counter->block_list[i]; |
| } |
| } |
| prestera_counter_unlock(counter); |
| |
| return NULL; |
| } |
| |
| static int prestera_counter_block_list_add(struct prestera_counter *counter, |
| struct prestera_counter_block *block) |
| { |
| struct prestera_counter_block **arr; |
| u32 i; |
| |
| prestera_counter_lock(counter); |
| |
| for (i = 0; i < counter->block_list_len; i++) { |
| if (counter->block_list[i]) |
| continue; |
| |
| counter->block_list[i] = block; |
| prestera_counter_unlock(counter); |
| return 0; |
| } |
| |
| arr = krealloc(counter->block_list, (counter->block_list_len + 1) * |
| sizeof(*counter->block_list), GFP_KERNEL); |
| if (!arr) { |
| prestera_counter_unlock(counter); |
| return -ENOMEM; |
| } |
| |
| counter->block_list = arr; |
| counter->block_list[counter->block_list_len] = block; |
| counter->block_list_len++; |
| prestera_counter_unlock(counter); |
| return 0; |
| } |
| |
| static struct prestera_counter_block * |
| prestera_counter_block_get(struct prestera_counter *counter, u32 client) |
| { |
| struct prestera_counter_block *block; |
| int err; |
| |
| block = prestera_counter_block_lookup_not_full(counter, client); |
| if (block) |
| return block; |
| |
| block = kzalloc(sizeof(*block), GFP_KERNEL); |
| if (!block) |
| return ERR_PTR(-ENOMEM); |
| |
| err = prestera_hw_counter_block_get(counter->sw, client, |
| &block->id, &block->offset, |
| &block->num_counters); |
| if (err) |
| goto err_block; |
| |
| block->stats = kcalloc(block->num_counters, |
| sizeof(*block->stats), GFP_KERNEL); |
| if (!block->stats) { |
| err = -ENOMEM; |
| goto err_stats; |
| } |
| |
| block->counter_flag = kcalloc(block->num_counters, |
| sizeof(*block->counter_flag), |
| GFP_KERNEL); |
| if (!block->counter_flag) { |
| err = -ENOMEM; |
| goto err_flag; |
| } |
| |
| block->client = client; |
| mutex_init(&block->mtx); |
| refcount_set(&block->refcnt, 1); |
| idr_init_base(&block->counter_idr, block->offset); |
| |
| err = prestera_counter_block_list_add(counter, block); |
| if (err) |
| goto err_list_add; |
| |
| return block; |
| |
| err_list_add: |
| idr_destroy(&block->counter_idr); |
| mutex_destroy(&block->mtx); |
| kfree(block->counter_flag); |
| err_flag: |
| kfree(block->stats); |
| err_stats: |
| prestera_hw_counter_block_release(counter->sw, block->id); |
| err_block: |
| kfree(block); |
| return ERR_PTR(err); |
| } |
| |
| static void prestera_counter_block_put(struct prestera_counter *counter, |
| struct prestera_counter_block *block) |
| { |
| u32 i; |
| |
| if (!prestera_counter_block_decref(block)) |
| return; |
| |
| prestera_counter_lock(counter); |
| for (i = 0; i < counter->block_list_len; i++) { |
| if (counter->block_list[i] && |
| counter->block_list[i]->id == block->id) { |
| counter->block_list[i] = NULL; |
| break; |
| } |
| } |
| prestera_counter_unlock(counter); |
| |
| WARN_ON(!idr_is_empty(&block->counter_idr)); |
| |
| prestera_hw_counter_block_release(counter->sw, block->id); |
| idr_destroy(&block->counter_idr); |
| mutex_destroy(&block->mtx); |
| kfree(block->stats); |
| kfree(block); |
| } |
| |
| static int prestera_counter_get_vacant(struct prestera_counter_block *block, |
| u32 *id) |
| { |
| int free_id; |
| |
| if (block->full) |
| return -ENOSPC; |
| |
| prestera_counter_block_lock(block); |
| free_id = idr_alloc_cyclic(&block->counter_idr, NULL, block->offset, |
| block->offset + block->num_counters, |
| GFP_KERNEL); |
| if (free_id < 0) { |
| if (free_id == -ENOSPC) |
| block->full = true; |
| |
| prestera_counter_block_unlock(block); |
| return free_id; |
| } |
| *id = free_id; |
| prestera_counter_block_unlock(block); |
| |
| return 0; |
| } |
| |
| int prestera_counter_get(struct prestera_counter *counter, u32 client, |
| struct prestera_counter_block **bl, u32 *counter_id) |
| { |
| struct prestera_counter_block *block; |
| int err; |
| u32 id; |
| |
| get_next_block: |
| block = prestera_counter_block_get(counter, client); |
| if (IS_ERR(block)) |
| return PTR_ERR(block); |
| |
| err = prestera_counter_get_vacant(block, &id); |
| if (err) { |
| prestera_counter_block_put(counter, block); |
| |
| if (err == -ENOSPC) |
| goto get_next_block; |
| |
| return err; |
| } |
| |
| prestera_counter_block_lock(block); |
| if (block->is_updating) |
| block->counter_flag[id - block->offset] = COUNTER_FLAG_INVALID; |
| prestera_counter_block_unlock(block); |
| |
| *counter_id = id; |
| *bl = block; |
| |
| return 0; |
| } |
| |
| void prestera_counter_put(struct prestera_counter *counter, |
| struct prestera_counter_block *block, u32 counter_id) |
| { |
| if (!block) |
| return; |
| |
| prestera_counter_block_lock(block); |
| idr_remove(&block->counter_idr, counter_id); |
| block->full = false; |
| prestera_counter_stats_clear(block, counter_id); |
| prestera_counter_block_unlock(block); |
| |
| prestera_hw_counter_clear(counter->sw, block->id, counter_id); |
| prestera_counter_block_put(counter, block); |
| } |
| |
| static u32 prestera_counter_block_idx_next(struct prestera_counter *counter, |
| u32 curr_idx) |
| { |
| u32 idx, i, start = curr_idx + 1; |
| |
| prestera_counter_lock(counter); |
| for (i = 0; i < counter->block_list_len; i++) { |
| idx = (start + i) % counter->block_list_len; |
| if (!counter->block_list[idx]) |
| continue; |
| |
| prestera_counter_unlock(counter); |
| return idx; |
| } |
| prestera_counter_unlock(counter); |
| |
| return 0; |
| } |
| |
| static struct prestera_counter_block * |
| prestera_counter_block_get_by_idx(struct prestera_counter *counter, u32 idx) |
| { |
| if (idx >= counter->block_list_len) |
| return NULL; |
| |
| prestera_counter_lock(counter); |
| |
| if (!counter->block_list[idx] || |
| !prestera_counter_block_incref(counter->block_list[idx])) { |
| prestera_counter_unlock(counter); |
| return NULL; |
| } |
| |
| prestera_counter_unlock(counter); |
| return counter->block_list[idx]; |
| } |
| |
| static void prestera_counter_stats_work(struct work_struct *work) |
| { |
| struct delayed_work *dl_work = |
| container_of(work, struct delayed_work, work); |
| struct prestera_counter *counter = |
| container_of(dl_work, struct prestera_counter, stats_dw); |
| struct prestera_counter_block *block; |
| u32 resched_time = COUNTER_POLL_TIME; |
| u32 count = COUNTER_BULK_SIZE; |
| bool done = false; |
| int err; |
| u32 i; |
| |
| block = prestera_counter_block_get_by_idx(counter, counter->curr_idx); |
| if (!block) { |
| if (counter->is_fetching) |
| goto abort; |
| |
| goto next; |
| } |
| |
| if (!counter->is_fetching) { |
| err = prestera_hw_counter_trigger(counter->sw, block->id); |
| if (err) |
| goto abort; |
| |
| prestera_counter_block_lock(block); |
| block->is_updating = true; |
| prestera_counter_block_unlock(block); |
| |
| counter->is_fetching = true; |
| counter->total_read = 0; |
| resched_time = COUNTER_RESCHED_TIME; |
| goto resched; |
| } |
| |
| prestera_counter_block_lock(block); |
| err = prestera_hw_counters_get(counter->sw, counter->total_read, |
| &count, &done, |
| &block->stats[counter->total_read]); |
| prestera_counter_block_unlock(block); |
| if (err) |
| goto abort; |
| |
| counter->total_read += count; |
| if (!done || counter->total_read < block->num_counters) { |
| resched_time = COUNTER_RESCHED_TIME; |
| goto resched; |
| } |
| |
| for (i = 0; i < block->num_counters; i++) { |
| if (block->counter_flag[i] == COUNTER_FLAG_INVALID) { |
| prestera_counter_block_lock(block); |
| block->counter_flag[i] = COUNTER_FLAG_READY; |
| memset(&block->stats[i], 0, sizeof(*block->stats)); |
| prestera_counter_block_unlock(block); |
| } |
| } |
| |
| prestera_counter_block_lock(block); |
| block->is_updating = false; |
| prestera_counter_block_unlock(block); |
| |
| goto next; |
| abort: |
| prestera_hw_counter_abort(counter->sw); |
| next: |
| counter->is_fetching = false; |
| counter->curr_idx = |
| prestera_counter_block_idx_next(counter, counter->curr_idx); |
| resched: |
| if (block) |
| prestera_counter_block_put(counter, block); |
| |
| schedule_delayed_work(&counter->stats_dw, resched_time); |
| } |
| |
| /* Can be executed without rtnl_lock(). |
| * So pay attention when something changing. |
| */ |
| int prestera_counter_stats_get(struct prestera_counter *counter, |
| struct prestera_counter_block *block, |
| u32 counter_id, u64 *packets, u64 *bytes) |
| { |
| if (!block || !prestera_counter_is_ready(block, counter_id)) { |
| *packets = 0; |
| *bytes = 0; |
| return 0; |
| } |
| |
| prestera_counter_block_lock(block); |
| *packets = block->stats[counter_id - block->offset].packets; |
| *bytes = block->stats[counter_id - block->offset].bytes; |
| |
| prestera_counter_stats_clear(block, counter_id); |
| prestera_counter_block_unlock(block); |
| |
| return 0; |
| } |
| |
| int prestera_counter_init(struct prestera_switch *sw) |
| { |
| struct prestera_counter *counter; |
| |
| counter = kzalloc(sizeof(*counter), GFP_KERNEL); |
| if (!counter) |
| return -ENOMEM; |
| |
| counter->block_list = kzalloc(sizeof(*counter->block_list), GFP_KERNEL); |
| if (!counter->block_list) { |
| kfree(counter); |
| return -ENOMEM; |
| } |
| |
| mutex_init(&counter->mtx); |
| counter->block_list_len = 1; |
| counter->sw = sw; |
| sw->counter = counter; |
| |
| INIT_DELAYED_WORK(&counter->stats_dw, prestera_counter_stats_work); |
| schedule_delayed_work(&counter->stats_dw, COUNTER_POLL_TIME); |
| |
| return 0; |
| } |
| |
| void prestera_counter_fini(struct prestera_switch *sw) |
| { |
| struct prestera_counter *counter = sw->counter; |
| u32 i; |
| |
| cancel_delayed_work_sync(&counter->stats_dw); |
| |
| for (i = 0; i < counter->block_list_len; i++) |
| WARN_ON(counter->block_list[i]); |
| |
| mutex_destroy(&counter->mtx); |
| kfree(counter->block_list); |
| kfree(counter); |
| } |