// SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note /* * * (C) COPYRIGHT 2018-2023 ARM Limited. All rights reserved. * * This program is free software and is provided to you under the terms of the * GNU General Public License version 2 as published by the Free Software * Foundation, and any use by you of this program is subject to the terms * of such GNU license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you can access it online at * http://www.gnu.org/licenses/gpl-2.0.html. * */ /* * Implementation of hardware counter context and accumulator APIs. */ #include "hwcnt/mali_kbase_hwcnt_context.h" #include "hwcnt/mali_kbase_hwcnt_accumulator.h" #include "hwcnt/backend/mali_kbase_hwcnt_backend.h" #include "hwcnt/mali_kbase_hwcnt_types.h" #include #include #include /** * enum kbase_hwcnt_accum_state - Hardware counter accumulator states. * @ACCUM_STATE_ERROR: Error state, where all accumulator operations fail. * @ACCUM_STATE_DISABLED: Disabled state, where dumping is always disabled. * @ACCUM_STATE_ENABLED: Enabled state, where dumping is enabled if there are * any enabled counters. */ enum kbase_hwcnt_accum_state { ACCUM_STATE_ERROR, ACCUM_STATE_DISABLED, ACCUM_STATE_ENABLED }; /** * struct kbase_hwcnt_accumulator - Hardware counter accumulator structure. * @metadata: Pointer to immutable hwcnt metadata. * @backend: Pointer to created counter backend. * @state: The current state of the accumulator. * - State transition from disabled->enabled or * disabled->error requires state_lock. * - State transition from enabled->disabled or * enabled->error requires both accum_lock and * state_lock. * - Error state persists until next disable. * @enable_map: The current set of enabled counters. * - Must only be modified while holding both * accum_lock and state_lock. * - Can be read while holding either lock. * - Must stay in sync with enable_map_any_enabled. * @enable_map_any_enabled: True if any counters in the map are enabled, else * false. If true, and state is ACCUM_STATE_ENABLED, * then the counter backend will be enabled. * - Must only be modified while holding both * accum_lock and state_lock. * - Can be read while holding either lock. * - Must stay in sync with enable_map. * @scratch_map: Scratch enable map, used as temporary enable map * storage during dumps. * - Must only be read or modified while holding * accum_lock. * @accum_buf: Accumulation buffer, where dumps will be accumulated * into on transition to a disable state. * - Must only be read or modified while holding * accum_lock. * @accumulated: True if the accumulation buffer has been accumulated * into and not subsequently read from yet, else false. * - Must only be read or modified while holding * accum_lock. * @ts_last_dump_ns: Timestamp (ns) of the end time of the most recent * dump that was requested by the user. * - Must only be read or modified while holding * accum_lock. */ struct kbase_hwcnt_accumulator { const struct kbase_hwcnt_metadata *metadata; struct kbase_hwcnt_backend *backend; enum kbase_hwcnt_accum_state state; struct kbase_hwcnt_enable_map enable_map; bool enable_map_any_enabled; struct kbase_hwcnt_enable_map scratch_map; struct kbase_hwcnt_dump_buffer accum_buf; bool accumulated; u64 ts_last_dump_ns; }; /** * struct kbase_hwcnt_context - Hardware counter context structure. * @iface: Pointer to hardware counter backend interface. * @state_lock: Spinlock protecting state. * @disable_count: Disable count of the context. Initialised to 1. * Decremented when the accumulator is acquired, and incremented * on release. Incremented on calls to * kbase_hwcnt_context_disable[_atomic], and decremented on * calls to kbase_hwcnt_context_enable. * - Must only be read or modified while holding state_lock. * @accum_lock: Mutex protecting accumulator. * @accum_inited: Flag to prevent concurrent accumulator initialisation and/or * termination. Set to true before accumulator initialisation, * and false after accumulator termination. * - Must only be modified while holding both accum_lock and * state_lock. * - Can be read while holding either lock. * @accum: Hardware counter accumulator structure. * @wq: Centralized workqueue for users of hardware counters to * submit async hardware counter related work. Never directly * called, but it's expected that a lot of the functions in this * API will end up called from the enqueued async work. */ struct kbase_hwcnt_context { const struct kbase_hwcnt_backend_interface *iface; spinlock_t state_lock; size_t disable_count; struct mutex accum_lock; bool accum_inited; struct kbase_hwcnt_accumulator accum; struct workqueue_struct *wq; }; int kbase_hwcnt_context_init(const struct kbase_hwcnt_backend_interface *iface, struct kbase_hwcnt_context **out_hctx) { struct kbase_hwcnt_context *hctx = NULL; if (!iface || !out_hctx) return -EINVAL; hctx = kzalloc(sizeof(*hctx), GFP_KERNEL); if (!hctx) goto err_alloc_hctx; hctx->iface = iface; spin_lock_init(&hctx->state_lock); hctx->disable_count = 1; mutex_init(&hctx->accum_lock); hctx->accum_inited = false; hctx->wq = alloc_workqueue("mali_kbase_hwcnt", WQ_HIGHPRI | WQ_UNBOUND, 0); if (!hctx->wq) goto err_alloc_workqueue; *out_hctx = hctx; return 0; err_alloc_workqueue: kfree(hctx); err_alloc_hctx: return -ENOMEM; } void kbase_hwcnt_context_term(struct kbase_hwcnt_context *hctx) { if (!hctx) return; /* Make sure we didn't leak the accumulator */ WARN_ON(hctx->accum_inited); /* We don't expect any work to be pending on this workqueue. * Regardless, this will safely drain and complete the work. */ destroy_workqueue(hctx->wq); kfree(hctx); } /** * kbasep_hwcnt_accumulator_term() - Terminate the accumulator for the context. * @hctx: Non-NULL pointer to hardware counter context. */ static void kbasep_hwcnt_accumulator_term(struct kbase_hwcnt_context *hctx) { WARN_ON(!hctx); WARN_ON(!hctx->accum_inited); kbase_hwcnt_enable_map_free(&hctx->accum.scratch_map); kbase_hwcnt_dump_buffer_free(&hctx->accum.accum_buf); kbase_hwcnt_enable_map_free(&hctx->accum.enable_map); hctx->iface->term(hctx->accum.backend); memset(&hctx->accum, 0, sizeof(hctx->accum)); } /** * kbasep_hwcnt_accumulator_init() - Initialise the accumulator for the context. * @hctx: Non-NULL pointer to hardware counter context. * * Return: 0 on success, else error code. */ static int kbasep_hwcnt_accumulator_init(struct kbase_hwcnt_context *hctx) { int errcode; WARN_ON(!hctx); WARN_ON(!hctx->accum_inited); errcode = hctx->iface->init(hctx->iface->info, &hctx->accum.backend); if (errcode) goto error; hctx->accum.metadata = hctx->iface->metadata(hctx->iface->info); hctx->accum.state = ACCUM_STATE_ERROR; errcode = kbase_hwcnt_enable_map_alloc(hctx->accum.metadata, &hctx->accum.enable_map); if (errcode) goto error; hctx->accum.enable_map_any_enabled = false; errcode = kbase_hwcnt_dump_buffer_alloc(hctx->accum.metadata, &hctx->accum.accum_buf); if (errcode) goto error; errcode = kbase_hwcnt_enable_map_alloc(hctx->accum.metadata, &hctx->accum.scratch_map); if (errcode) goto error; hctx->accum.accumulated = false; hctx->accum.ts_last_dump_ns = hctx->iface->timestamp_ns(hctx->accum.backend); return 0; error: kbasep_hwcnt_accumulator_term(hctx); return errcode; } /** * kbasep_hwcnt_accumulator_disable() - Transition the accumulator into the * disabled state, from the enabled or * error states. * @hctx: Non-NULL pointer to hardware counter context. * @accumulate: True if we should accumulate before disabling, else false. */ static void kbasep_hwcnt_accumulator_disable(struct kbase_hwcnt_context *hctx, bool accumulate) { int errcode = 0; bool backend_enabled = false; struct kbase_hwcnt_accumulator *accum; unsigned long flags; u64 dump_time_ns; WARN_ON(!hctx); lockdep_assert_held(&hctx->accum_lock); WARN_ON(!hctx->accum_inited); accum = &hctx->accum; spin_lock_irqsave(&hctx->state_lock, flags); WARN_ON(hctx->disable_count != 0); WARN_ON(hctx->accum.state == ACCUM_STATE_DISABLED); if ((hctx->accum.state == ACCUM_STATE_ENABLED) && (accum->enable_map_any_enabled)) backend_enabled = true; if (!backend_enabled) hctx->accum.state = ACCUM_STATE_DISABLED; spin_unlock_irqrestore(&hctx->state_lock, flags); /* Early out if the backend is not already enabled */ if (!backend_enabled) return; if (!accumulate) goto disable; /* Try and accumulate before disabling */ errcode = hctx->iface->dump_request(accum->backend, &dump_time_ns); if (errcode) goto disable; errcode = hctx->iface->dump_wait(accum->backend); if (errcode) goto disable; errcode = hctx->iface->dump_get(accum->backend, &accum->accum_buf, &accum->enable_map, accum->accumulated); if (errcode) goto disable; accum->accumulated = true; disable: hctx->iface->dump_disable(accum->backend, (accum->accumulated) ? &accum->accum_buf : NULL, &accum->enable_map); /* Regardless of any errors during the accumulate, put the accumulator * in the disabled state. */ spin_lock_irqsave(&hctx->state_lock, flags); hctx->accum.state = ACCUM_STATE_DISABLED; spin_unlock_irqrestore(&hctx->state_lock, flags); } /** * kbasep_hwcnt_accumulator_enable() - Transition the accumulator into the * enabled state, from the disabled state. * @hctx: Non-NULL pointer to hardware counter context. */ static void kbasep_hwcnt_accumulator_enable(struct kbase_hwcnt_context *hctx) { int errcode = 0; struct kbase_hwcnt_accumulator *accum; WARN_ON(!hctx); lockdep_assert_held(&hctx->state_lock); WARN_ON(!hctx->accum_inited); WARN_ON(hctx->accum.state != ACCUM_STATE_DISABLED); accum = &hctx->accum; /* The backend only needs enabling if any counters are enabled */ if (accum->enable_map_any_enabled) errcode = hctx->iface->dump_enable_nolock(accum->backend, &accum->enable_map); if (!errcode) accum->state = ACCUM_STATE_ENABLED; else accum->state = ACCUM_STATE_ERROR; } /** * kbasep_hwcnt_accumulator_dump() - Perform a dump with the most up-to-date * values of enabled counters possible, and * optionally update the set of enabled * counters. * @hctx: Non-NULL pointer to the hardware counter context * @ts_start_ns: Non-NULL pointer where the start timestamp of the dump will * be written out to on success * @ts_end_ns: Non-NULL pointer where the end timestamp of the dump will * be written out to on success * @dump_buf: Pointer to the buffer where the dump will be written out to on * success. If non-NULL, must have the same metadata as the * accumulator. If NULL, the dump will be discarded. * @new_map: Pointer to the new counter enable map. If non-NULL, must have * the same metadata as the accumulator. If NULL, the set of * enabled counters will be unchanged. * * Return: 0 on success, else error code. */ static int kbasep_hwcnt_accumulator_dump(struct kbase_hwcnt_context *hctx, u64 *ts_start_ns, u64 *ts_end_ns, struct kbase_hwcnt_dump_buffer *dump_buf, const struct kbase_hwcnt_enable_map *new_map) { int errcode = 0; unsigned long flags; enum kbase_hwcnt_accum_state state; bool dump_requested = false; bool dump_written = false; bool cur_map_any_enabled; struct kbase_hwcnt_enable_map *cur_map; bool new_map_any_enabled = false; u64 dump_time_ns = 0; struct kbase_hwcnt_accumulator *accum; WARN_ON(!hctx); WARN_ON(!ts_start_ns); WARN_ON(!ts_end_ns); WARN_ON(dump_buf && (dump_buf->metadata != hctx->accum.metadata)); WARN_ON(new_map && (new_map->metadata != hctx->accum.metadata)); WARN_ON(!hctx->accum_inited); lockdep_assert_held(&hctx->accum_lock); accum = &hctx->accum; cur_map = &accum->scratch_map; /* Save out info about the current enable map */ cur_map_any_enabled = accum->enable_map_any_enabled; kbase_hwcnt_enable_map_copy(cur_map, &accum->enable_map); if (new_map) new_map_any_enabled = kbase_hwcnt_enable_map_any_enabled(new_map); /* * We're holding accum_lock, so the accumulator state might transition * from disabled to enabled during this function (as enabling is lock * free), but it will never disable (as disabling needs to hold the * accum_lock), nor will it ever transition from enabled to error (as * an enable while we're already enabled is impossible). * * If we're already disabled, we'll only look at the accumulation buffer * rather than do a real dump, so a concurrent enable does not affect * us. * * If a concurrent enable fails, we might transition to the error * state, but again, as we're only looking at the accumulation buffer, * it's not an issue. */ spin_lock_irqsave(&hctx->state_lock, flags); state = accum->state; /* * Update the new map now, such that if an enable occurs during this * dump then that enable will set the new map. If we're already enabled, * then we'll do it ourselves after the dump. */ if (new_map) { kbase_hwcnt_enable_map_copy(&accum->enable_map, new_map); accum->enable_map_any_enabled = new_map_any_enabled; } spin_unlock_irqrestore(&hctx->state_lock, flags); /* Error state, so early out. No need to roll back any map updates */ if (state == ACCUM_STATE_ERROR) return -EIO; /* Initiate the dump if the backend is enabled. */ if ((state == ACCUM_STATE_ENABLED) && cur_map_any_enabled) { if (dump_buf) { errcode = hctx->iface->dump_request(accum->backend, &dump_time_ns); dump_requested = true; } else { dump_time_ns = hctx->iface->timestamp_ns(accum->backend); errcode = hctx->iface->dump_clear(accum->backend); } if (errcode) goto error; } else { dump_time_ns = hctx->iface->timestamp_ns(accum->backend); } /* Copy any accumulation into the dest buffer */ if (accum->accumulated && dump_buf) { kbase_hwcnt_dump_buffer_copy(dump_buf, &accum->accum_buf, cur_map); dump_written = true; } /* Wait for any requested dumps to complete */ if (dump_requested) { WARN_ON(state != ACCUM_STATE_ENABLED); errcode = hctx->iface->dump_wait(accum->backend); if (errcode) goto error; } /* If we're enabled and there's a new enable map, change the enabled set * as soon after the dump has completed as possible. */ if ((state == ACCUM_STATE_ENABLED) && new_map) { /* Backend is only enabled if there were any enabled counters */ if (cur_map_any_enabled) { /* In this case we do *not* want to have the buffer updated with extra * block state, it should instead remain in the backend until the next dump * happens, hence supplying NULL as the dump_buffer parameter here. * * Attempting to take ownership of backend-accumulated block state at this * point will instead give inaccurate information. For example the dump * buffer for 'set_counters' operation might be dumping a period that * should've been entirely in the 'ON' state, but would report it as * partially in the 'OFF' state. Instead, that 'OFF' state should be * reported in the _next_ dump. */ hctx->iface->dump_disable(accum->backend, NULL, NULL); } /* (Re-)enable the backend if the new map has enabled counters. * No need to acquire the spinlock, as concurrent enable while * we're already enabled and holding accum_lock is impossible. */ if (new_map_any_enabled) { errcode = hctx->iface->dump_enable(accum->backend, new_map); if (errcode) goto error; } } /* Copy, accumulate, or zero into the dest buffer to finish */ if (dump_buf) { /* If we dumped, copy or accumulate it into the destination */ if (dump_requested) { WARN_ON(state != ACCUM_STATE_ENABLED); errcode = hctx->iface->dump_get(accum->backend, dump_buf, cur_map, dump_written); if (errcode) goto error; dump_written = true; } /* If we've not written anything into the dump buffer so far, it * means there was nothing to write. Zero any enabled counters. * * In this state, the blocks are likely to be off (and at the very least, not * counting), so write in the 'off' block state */ if (!dump_written) { kbase_hwcnt_dump_buffer_zero(dump_buf, cur_map); kbase_hwcnt_dump_buffer_block_state_update(dump_buf, cur_map, KBASE_HWCNT_STATE_OFF); } } /* Write out timestamps */ *ts_start_ns = accum->ts_last_dump_ns; *ts_end_ns = dump_time_ns; accum->accumulated = false; accum->ts_last_dump_ns = dump_time_ns; return 0; error: /* An error was only physically possible if the backend was enabled */ WARN_ON(state != ACCUM_STATE_ENABLED); /* Disable the backend, and transition to the error state. In this case, we can try to save * the block state into the accumulated buffer, but there's no guarantee we'll have one, so * this is more of a 'best effort' for error cases. There would be an suitable block * state recorded on the next dump_enable() anyway. */ hctx->iface->dump_disable(accum->backend, (accum->accumulated) ? &accum->accum_buf : NULL, cur_map); spin_lock_irqsave(&hctx->state_lock, flags); accum->state = ACCUM_STATE_ERROR; spin_unlock_irqrestore(&hctx->state_lock, flags); return errcode; } /** * kbasep_hwcnt_context_disable() - Increment the disable count of the context. * @hctx: Non-NULL pointer to hardware counter context. * @accumulate: True if we should accumulate before disabling, else false. */ static void kbasep_hwcnt_context_disable(struct kbase_hwcnt_context *hctx, bool accumulate) { unsigned long flags; WARN_ON(!hctx); lockdep_assert_held(&hctx->accum_lock); if (!kbase_hwcnt_context_disable_atomic(hctx)) { kbasep_hwcnt_accumulator_disable(hctx, accumulate); spin_lock_irqsave(&hctx->state_lock, flags); /* Atomic disable failed and we're holding the mutex, so current * disable count must be 0. */ WARN_ON(hctx->disable_count != 0); hctx->disable_count++; spin_unlock_irqrestore(&hctx->state_lock, flags); } } int kbase_hwcnt_accumulator_acquire(struct kbase_hwcnt_context *hctx, struct kbase_hwcnt_accumulator **accum) { int errcode = 0; unsigned long flags; if (!hctx || !accum) return -EINVAL; mutex_lock(&hctx->accum_lock); spin_lock_irqsave(&hctx->state_lock, flags); if (!hctx->accum_inited) /* Set accum initing now to prevent concurrent init */ hctx->accum_inited = true; else /* Already have an accum, or already being inited */ errcode = -EBUSY; spin_unlock_irqrestore(&hctx->state_lock, flags); mutex_unlock(&hctx->accum_lock); if (errcode) return errcode; errcode = kbasep_hwcnt_accumulator_init(hctx); if (errcode) { mutex_lock(&hctx->accum_lock); spin_lock_irqsave(&hctx->state_lock, flags); hctx->accum_inited = false; spin_unlock_irqrestore(&hctx->state_lock, flags); mutex_unlock(&hctx->accum_lock); return errcode; } spin_lock_irqsave(&hctx->state_lock, flags); WARN_ON(hctx->disable_count == 0); WARN_ON(hctx->accum.enable_map_any_enabled); /* Decrement the disable count to allow the accumulator to be accessible * now that it's fully constructed. */ hctx->disable_count--; /* * Make sure the accumulator is initialised to the correct state. * Regardless of initial state, counters don't need to be enabled via * the backend, as the initial enable map has no enabled counters. */ hctx->accum.state = (hctx->disable_count == 0) ? ACCUM_STATE_ENABLED : ACCUM_STATE_DISABLED; spin_unlock_irqrestore(&hctx->state_lock, flags); *accum = &hctx->accum; return 0; } void kbase_hwcnt_accumulator_release(struct kbase_hwcnt_accumulator *accum) { unsigned long flags; struct kbase_hwcnt_context *hctx; if (!accum) return; hctx = container_of(accum, struct kbase_hwcnt_context, accum); mutex_lock(&hctx->accum_lock); /* Double release is a programming error */ WARN_ON(!hctx->accum_inited); /* Disable the context to ensure the accumulator is inaccesible while * we're destroying it. This performs the corresponding disable count * increment to the decrement done during acquisition. */ kbasep_hwcnt_context_disable(hctx, false); mutex_unlock(&hctx->accum_lock); kbasep_hwcnt_accumulator_term(hctx); mutex_lock(&hctx->accum_lock); spin_lock_irqsave(&hctx->state_lock, flags); hctx->accum_inited = false; spin_unlock_irqrestore(&hctx->state_lock, flags); mutex_unlock(&hctx->accum_lock); } void kbase_hwcnt_context_disable(struct kbase_hwcnt_context *hctx) { if (WARN_ON(!hctx)) return; /* Try and atomically disable first, so we can avoid locking the mutex * if we don't need to. */ if (kbase_hwcnt_context_disable_atomic(hctx)) return; mutex_lock(&hctx->accum_lock); kbasep_hwcnt_context_disable(hctx, true); mutex_unlock(&hctx->accum_lock); } bool kbase_hwcnt_context_disable_atomic(struct kbase_hwcnt_context *hctx) { unsigned long flags; bool atomic_disabled = false; if (WARN_ON(!hctx)) return false; spin_lock_irqsave(&hctx->state_lock, flags); if (!WARN_ON(hctx->disable_count == SIZE_MAX)) { /* * If disable count is non-zero, we can just bump the disable * count. * * Otherwise, we can't disable in an atomic context. */ if (hctx->disable_count != 0) { hctx->disable_count++; atomic_disabled = true; } } spin_unlock_irqrestore(&hctx->state_lock, flags); return atomic_disabled; } void kbase_hwcnt_context_enable(struct kbase_hwcnt_context *hctx) { unsigned long flags; if (WARN_ON(!hctx)) return; spin_lock_irqsave(&hctx->state_lock, flags); if (!WARN_ON(hctx->disable_count == 0)) { if (hctx->disable_count == 1) kbasep_hwcnt_accumulator_enable(hctx); hctx->disable_count--; } spin_unlock_irqrestore(&hctx->state_lock, flags); } const struct kbase_hwcnt_metadata *kbase_hwcnt_context_metadata(struct kbase_hwcnt_context *hctx) { if (!hctx) return NULL; return hctx->iface->metadata(hctx->iface->info); } bool kbase_hwcnt_context_queue_work(struct kbase_hwcnt_context *hctx, struct work_struct *work) { if (WARN_ON(!hctx) || WARN_ON(!work)) return false; return queue_work(hctx->wq, work); } int kbase_hwcnt_accumulator_set_counters(struct kbase_hwcnt_accumulator *accum, const struct kbase_hwcnt_enable_map *new_map, u64 *ts_start_ns, u64 *ts_end_ns, struct kbase_hwcnt_dump_buffer *dump_buf) { int errcode; struct kbase_hwcnt_context *hctx; if (!accum || !new_map || !ts_start_ns || !ts_end_ns) return -EINVAL; hctx = container_of(accum, struct kbase_hwcnt_context, accum); if ((new_map->metadata != hctx->accum.metadata) || (dump_buf && (dump_buf->metadata != hctx->accum.metadata))) return -EINVAL; mutex_lock(&hctx->accum_lock); errcode = kbasep_hwcnt_accumulator_dump(hctx, ts_start_ns, ts_end_ns, dump_buf, new_map); mutex_unlock(&hctx->accum_lock); return errcode; } int kbase_hwcnt_accumulator_dump(struct kbase_hwcnt_accumulator *accum, u64 *ts_start_ns, u64 *ts_end_ns, struct kbase_hwcnt_dump_buffer *dump_buf) { int errcode; struct kbase_hwcnt_context *hctx; if (!accum || !ts_start_ns || !ts_end_ns) return -EINVAL; hctx = container_of(accum, struct kbase_hwcnt_context, accum); if (dump_buf && (dump_buf->metadata != hctx->accum.metadata)) return -EINVAL; mutex_lock(&hctx->accum_lock); errcode = kbasep_hwcnt_accumulator_dump(hctx, ts_start_ns, ts_end_ns, dump_buf, NULL); mutex_unlock(&hctx->accum_lock); return errcode; } u64 kbase_hwcnt_accumulator_timestamp_ns(struct kbase_hwcnt_accumulator *accum) { struct kbase_hwcnt_context *hctx; if (WARN_ON(!accum)) return 0; hctx = container_of(accum, struct kbase_hwcnt_context, accum); return hctx->iface->timestamp_ns(accum->backend); }