From ed836fa70b8f6930d7a410bf80defdf48de5554d Mon Sep 17 00:00:00 2001 From: liqigan Date: Thu, 20 Aug 2026 16:21:37 +0800 Subject: [PATCH] change(bt/bluedroid): Used dynamic osi event to reduce lock cost --- components/bt/common/osi/include/osi/thread.h | 19 +- components/bt/common/osi/thread.c | 500 +++++++++------- .../bluedroid/btc/profile/std/hid/btc_hh.c | 18 +- .../btc/profile/std/include/btc_hh.h | 2 +- .../basic_unit_test/main/CMakeLists.txt | 1 + .../basic_unit_test/main/test_osi_event.c | 535 ++++++++++++++++++ 6 files changed, 859 insertions(+), 216 deletions(-) create mode 100644 components/bt/test_apps/basic_unit_test/main/test_osi_event.c diff --git a/components/bt/common/osi/include/osi/thread.h b/components/bt/common/osi/include/osi/thread.h index a4038bf230a..c0e90eec02f 100644 --- a/components/bt/common/osi/include/osi/thread.h +++ b/components/bt/common/osi/include/osi/thread.h @@ -19,6 +19,7 @@ struct osi_thread; struct osi_event; +struct osi_dynamic_event; typedef struct osi_thread osi_thread_t; @@ -81,10 +82,12 @@ const char *osi_thread_name(osi_thread_t *thread); int osi_thread_queue_wait_size(osi_thread_t *thread, int wq_idx); /* - * brief: Create an osi_event struct and register the handler function and its argument + * brief: Create a session-stable osi_event and register its handler and argument. * An osi_event is a kind of work that can be posted to the workqueue of osi_thread to process, * but the work can have at most one instance the thread workqueue before it is processed. This * allows the "single post, multiple data processing" jobs. + * Delete is logical: storage remains valid until osi_thread_event_deinit(), allowing stale + * posts during session teardown to be rejected without a global alive-list lock. * param func: the handler to process the job * param context: the argument to be passed to the handler function when the job is being processed * return: NULL if no memory, otherwise a valid struct pointer @@ -103,7 +106,7 @@ struct osi_event *osi_event_create(osi_thread_func_t func, void *context); bool osi_event_bind(struct osi_event* event, osi_thread_t *thread, int queue_idx); /* - * brief: Destroy the osi_event struct created by osi_event_create and free the allocated memory + * brief: Logically delete an osi_event. Its memory is reclaimed by osi_thread_event_deinit(). * param event: the pointer to osi_event */ void osi_event_delete(struct osi_event* event); @@ -118,6 +121,18 @@ void osi_event_delete(struct osi_event* event); */ bool osi_thread_post_event(struct osi_event *event, uint32_t timeout); +/* + * Dynamic events may be created and destroyed repeatedly during one + * osi_thread event-subsystem session. Unlike session-stable osi_event objects, + * their storage can be released by delete, so all operations use a separate + * API whose post/bind entry points validate the opaque pointer without first + * dereferencing it. + */ +struct osi_dynamic_event *osi_dynamic_event_create(osi_thread_func_t func, void *context); +bool osi_dynamic_event_bind(struct osi_dynamic_event *event, osi_thread_t *thread, int queue_idx); +bool osi_dynamic_event_post(struct osi_dynamic_event *event, uint32_t timeout); +void osi_dynamic_event_delete(struct osi_dynamic_event *event); + int osi_thread_event_init(void); void osi_thread_event_deinit(void); diff --git a/components/bt/common/osi/thread.c b/components/bt/common/osi/thread.c index cd8294b7832..6cf85cc60b5 100644 --- a/components/bt/common/osi/thread.c +++ b/components/bt/common/osi/thread.c @@ -56,7 +56,7 @@ struct osi_thread_start_arg { int error; }; -struct osi_event { +struct osi_event_core { struct work_item item; osi_mutex_t lock; osi_thread_t *thread; @@ -65,17 +65,25 @@ struct osi_event { uint8_t queue_idx; }; +struct osi_event { + struct osi_event_core core; +}; + +struct osi_dynamic_event { + struct osi_event_core core; +}; + #define OSI_EVENT_FLAG_QUEUED (1U << 0) -#define OSI_EVENT_FLAG_POSTING (1U << 1) -#define OSI_EVENT_FLAG_DELETING (1U << 2) -#define OSI_EVENT_FLAG_RUNNING (1U << 3) +#define OSI_EVENT_FLAG_DELETING (1U << 1) +#define OSI_EVENT_FLAG_RUNNING (1U << 2) #define OSI_EVENT_HAS_FLAG(event, flag) (((event)->flags & (flag)) != 0) #define OSI_EVENT_SET_FLAG(event, flag) ((event)->flags |= (uint8_t)(flag)) #define OSI_EVENT_CLEAR_FLAG(event, flag) ((event)->flags &= (uint8_t)(~(flag))) static const size_t DEFAULT_WORK_QUEUE_CAPACITY = 100; -static list_t *s_osi_event_list; +static list_t *s_osi_session_event_list; +static list_t *s_osi_dynamic_event_list; static osi_mutex_t s_osi_event_lock; #if OSI_THREAD_DEBUG @@ -494,35 +502,47 @@ int osi_thread_queue_wait_size(osi_thread_t *thread, int wq_idx) } -static bool osi_event_add_alive_locked(struct osi_event *event) +static struct osi_event_core *osi_event_core_new(size_t size, osi_thread_func_t func, void *context) { - assert(s_osi_event_list != NULL); - return list_append(s_osi_event_list, event); + struct osi_event_core *event = osi_calloc(size); + + if (event == NULL) { + return NULL; + } + if (osi_mutex_new(&event->lock) != 0) { + osi_free(event); + return NULL; + } + + event->item.func = func; + event->item.context = context; + /* Session events hold a registry pin; dynamic events hold an owner ref. */ + event->ref_count = 1; + return event; } -static void osi_event_free(struct osi_event *event) +static void osi_event_core_free(struct osi_event_core *event) { if (event != NULL) { osi_mutex_free(&event->lock); - memset(event, 0, sizeof(struct osi_event)); + memset(event, 0, sizeof(*event)); osi_free(event); } } -static bool osi_event_is_idle(const struct osi_event *event) +static bool osi_event_is_idle(const struct osi_event_core *event) { return !OSI_EVENT_HAS_FLAG(event, OSI_EVENT_FLAG_QUEUED) && - !OSI_EVENT_HAS_FLAG(event, OSI_EVENT_FLAG_POSTING) && !OSI_EVENT_HAS_FLAG(event, OSI_EVENT_FLAG_RUNNING); } -static bool osi_event_should_free(const struct osi_event *event) +static bool osi_event_should_free(const struct osi_event_core *event) { return OSI_EVENT_HAS_FLAG(event, OSI_EVENT_FLAG_DELETING) && osi_event_is_idle(event); } -static bool osi_event_can_bind_locked(const struct osi_event *event, osi_thread_t *thread, int queue_idx) +static bool osi_event_can_bind_locked(const struct osi_event_core *event, osi_thread_t *thread, int queue_idx) { return !OSI_EVENT_HAS_FLAG(event, OSI_EVENT_FLAG_DELETING) && event->thread == NULL && @@ -531,12 +551,9 @@ static bool osi_event_can_bind_locked(const struct osi_event *event, osi_thread_ queue_idx < thread->work_queue_num; } -static bool osi_event_can_post_locked(const struct osi_event *event) +static bool osi_event_can_post_locked(const struct osi_event_core *event) { if (event->thread == NULL || event->queue_idx >= event->thread->work_queue_num) { - OSI_TRACE_EVENT("%s deny ev=%p flags=0x%x qidx=%u", - __func__, event, event ? event->flags : 0, - event ? event->queue_idx : 0); return false; } @@ -549,173 +566,188 @@ static bool osi_event_can_post_locked(const struct osi_event *event) return false; } - /* Do NOT gate on OSI_EVENT_FLAG_POSTING here. POSTING marks the window in - * osi_thread_post_event() between osi_thread_post() (enqueue) and the - * poster clearing the flag. During that window the generic event handler - * may already have run and cleared QUEUED. A concurrent post that arrives - * after QUEUED is cleared is a legitimate re-post (new work arrived while - * the handler was draining) and must be accepted; rejecting it causes a - * lost wakeup. QUEUED alone prevents genuine double-queueing. POSTING is - * retained only for osi_event_is_idle()/osi_event_should_free(). */ + /* QUEUED alone prevents double-queueing. Do not gate on RUNNING: the + * generic handler clears QUEUED before invoking the user callback, and a + * concurrent post after that is a legitimate re-post of work that arrived + * while the handler was draining. */ return true; } -static bool osi_event_is_alive_locked(const struct osi_event *event) +/* Caller holds event->lock. Drops one reference. Returns true if the caller + * must free the event AFTER unlocking; never destroy the mutex while held. */ +static bool osi_event_release_locked(struct osi_event_core *event) { - /* Do not dereference event here: callers may pass a stale pointer racing - * with osi_event_delete(). The alive list is the ownership boundary. */ - return s_osi_event_list != NULL && list_contains(s_osi_event_list, event); -} - -static bool osi_event_remove_alive_locked(struct osi_event *event) -{ - bool removed = false; - - if (s_osi_event_list == NULL) { + assert(event->ref_count > 0); + event->ref_count--; + if (event->ref_count != 0) { return false; } + return osi_event_should_free(event); +} - removed = list_delete(s_osi_event_list, event); - return removed; +static void osi_event_unlock_and_maybe_free(struct osi_event_core *event, bool should_free) +{ + osi_mutex_unlock(&event->lock); + if (should_free) { + osi_event_core_free(event); + } +} + +static void osi_event_mark_deleting_locked(struct osi_event_core *event) +{ + OSI_EVENT_SET_FLAG(event, OSI_EVENT_FLAG_DELETING); + event->item.func = NULL; + event->item.context = NULL; } struct osi_event *osi_event_create(osi_thread_func_t func, void *context) { + struct osi_event *event = (struct osi_event *)osi_event_core_new(sizeof(*event), func, context); bool added = false; - struct osi_event *event = osi_calloc(sizeof(struct osi_event)); if (event == NULL) { return NULL; } - if (osi_mutex_new(&event->lock) != 0) { - osi_free(event); + osi_event_lock(); + if (s_osi_session_event_list != NULL) { + added = list_append(s_osi_session_event_list, event); + } + osi_event_unlock(); + if (!added) { + osi_event_core_free(&event->core); return NULL; } - - event->item.func = func; - event->item.context = context; - event->ref_count = 1; - - osi_event_lock(); - added = osi_event_add_alive_locked(event); - osi_event_unlock(); - if (added) { - return event; - } - - osi_mutex_free(&event->lock); - osi_free(event); - return NULL; -} - -/* ref_count is protected by the per-event lock (event->lock), NOT the global - * s_osi_event_lock. This keeps the reference-release path independent of the - * global event subsystem: it must stay valid even after - * osi_thread_event_deinit() has freed s_osi_event_lock (e.g. when a thread is - * freed on a shutdown path that tears the event subsystem down first). The - * global lock is only used to gate the alive-list membership that decides - * whether a new reference may be acquired. */ -static bool osi_event_acquire(struct osi_event *event) -{ - bool acquired = false; - - if (event == NULL) { - return false; - } - - /* Hold the global lock so the event cannot be removed from the alive list - * (and thus cannot be freed) while we take a fresh reference. Nesting is - * always global-lock-outer, event->lock-inner; no path takes them in the - * reverse order, so this cannot deadlock. */ - osi_event_lock(); - if (osi_event_is_alive_locked(event)) { - osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); - assert(event->ref_count > 0); - event->ref_count++; - osi_mutex_unlock(&event->lock); - acquired = true; - } - osi_event_unlock(); - - return acquired; -} - -static void osi_event_retain(struct osi_event *event) -{ - osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); - assert(event->ref_count > 0); - event->ref_count++; - osi_mutex_unlock(&event->lock); -} - -static void osi_event_release(struct osi_event *event) -{ - bool should_free = false; - - osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); - assert(event->ref_count > 0); - event->ref_count--; - if (event->ref_count == 0) { - should_free = osi_event_should_free(event); - } - osi_mutex_unlock(&event->lock); - - if (should_free) { - osi_event_free(event); - } + return event; } void osi_event_delete(struct osi_event *event) { - bool removed = false; - if (event == NULL) { return; } - osi_event_lock(); - removed = osi_event_remove_alive_locked(event); - osi_event_unlock(); - if (!removed) { - return; - } - - osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); - OSI_EVENT_SET_FLAG(event, OSI_EVENT_FLAG_DELETING); - event->item.func = NULL; - event->item.context = NULL; - osi_mutex_unlock(&event->lock); - - osi_event_release(event); + /* The registry pin keeps session-event storage valid until subsystem + * deinit, so delete is only a logical, idempotent operation. */ + osi_mutex_lock(&event->core.lock, OSI_MUTEX_MAX_TIMEOUT); + osi_event_mark_deleting_locked(&event->core); + osi_mutex_unlock(&event->core.lock); } bool osi_event_bind(struct osi_event *event, osi_thread_t *thread, int queue_idx) { bool ret = false; - if (!osi_event_acquire(event)) { + if (event == NULL) { return false; } - osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); - if (osi_event_can_bind_locked(event, thread, queue_idx)) { - event->thread = thread; - event->queue_idx = queue_idx; + osi_mutex_lock(&event->core.lock, OSI_MUTEX_MAX_TIMEOUT); + if (osi_event_can_bind_locked(&event->core, thread, queue_idx)) { + event->core.thread = thread; + event->core.queue_idx = (uint8_t)queue_idx; ret = true; } - osi_mutex_unlock(&event->lock); - - osi_event_release(event); - + osi_mutex_unlock(&event->core.lock); return ret; } +struct osi_dynamic_event *osi_dynamic_event_create(osi_thread_func_t func, void *context) +{ + struct osi_dynamic_event *event = + (struct osi_dynamic_event *)osi_event_core_new(sizeof(*event), func, context); + bool added = false; + + if (event == NULL) { + return NULL; + } + + osi_event_lock(); + if (s_osi_dynamic_event_list != NULL) { + added = list_append(s_osi_dynamic_event_list, event); + } + osi_event_unlock(); + if (!added) { + osi_event_core_free(&event->core); + return NULL; + } + return event; +} + +/* The global alive list is the ownership boundary for dynamic events. Never + * dereference event until list membership is confirmed under the global lock. + * Lock nesting is always global-lock-outer, event-lock-inner. + * On success: drops the global lock and returns holding event->core.lock with + * an extra reference; the caller must unlock (and maybe free) via + * osi_event_unlock_and_maybe_free after osi_event_release_locked. */ +static bool osi_dynamic_event_acquire_locked(struct osi_dynamic_event *event) +{ + if (event == NULL) { + return false; + } + + osi_event_lock(); + if (s_osi_dynamic_event_list == NULL || + !list_contains(s_osi_dynamic_event_list, event)) { + osi_event_unlock(); + return false; + } + + osi_mutex_lock(&event->core.lock, OSI_MUTEX_MAX_TIMEOUT); + assert(event->core.ref_count > 0); + event->core.ref_count++; + osi_event_unlock(); + return true; +} + +bool osi_dynamic_event_bind(struct osi_dynamic_event *event, osi_thread_t *thread, int queue_idx) +{ + bool ret = false; + bool should_free; + + if (!osi_dynamic_event_acquire_locked(event)) { + return false; + } + if (osi_event_can_bind_locked(&event->core, thread, queue_idx)) { + event->core.thread = thread; + event->core.queue_idx = (uint8_t)queue_idx; + ret = true; + } + should_free = osi_event_release_locked(&event->core); + osi_event_unlock_and_maybe_free(&event->core, should_free); + return ret; +} + +void osi_dynamic_event_delete(struct osi_dynamic_event *event) +{ + bool removed = false; + bool should_free; + + if (event == NULL) { + return; + } + + osi_event_lock(); + if (s_osi_dynamic_event_list != NULL) { + removed = list_delete(s_osi_dynamic_event_list, event); + } + osi_event_unlock(); + if (!removed) { + return; + } + + osi_mutex_lock(&event->core.lock, OSI_MUTEX_MAX_TIMEOUT); + osi_event_mark_deleting_locked(&event->core); + should_free = osi_event_release_locked(&event->core); + osi_event_unlock_and_maybe_free(&event->core, should_free); +} + static void osi_thread_generic_event_handler(void *context) { - struct osi_event *event = (struct osi_event *)context; + struct osi_event_core *event = (struct osi_event_core *)context; osi_thread_func_t func = NULL; void *func_context = NULL; + bool should_free = false; if (event == NULL) { return; @@ -724,14 +756,13 @@ static void osi_thread_generic_event_handler(void *context) osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); OSI_EVENT_CLEAR_FLAG(event, OSI_EVENT_FLAG_QUEUED); if (OSI_EVENT_HAS_FLAG(event, OSI_EVENT_FLAG_DELETING)) { - osi_mutex_unlock(&event->lock); - osi_event_release(event); + should_free = osi_event_release_locked(event); + osi_event_unlock_and_maybe_free(event, should_free); return; } OSI_EVENT_SET_FLAG(event, OSI_EVENT_FLAG_RUNNING); func = event->item.func; func_context = event->item.context; - OSI_TRACE_DEBUG("%s enter ev=%p flags=0x%x", __func__, event, event->flags); osi_mutex_unlock(&event->lock); if (func != NULL) { @@ -741,20 +772,17 @@ static void osi_thread_generic_event_handler(void *context) osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); OSI_EVENT_CLEAR_FLAG(event, OSI_EVENT_FLAG_RUNNING); OSI_TRACE_DEBUG("%s exit ev=%p flags=0x%x", __func__, event, event->flags); - osi_mutex_unlock(&event->lock); - - osi_event_release(event); + should_free = osi_event_release_locked(event); + osi_event_unlock_and_maybe_free(event, should_free); } /* Reclaim a queued event work item during thread teardown WITHOUT invoking the - * user callback. It only clears the QUEUED flag and drops the reference the - * queued item owns (osi_thread_post_event() retained it); this frees the event - * if it was already deleted, and leaves a still-live event untouched. Unlike - * osi_thread_generic_event_handler(), it never dispatches into stack state that - * may already have been freed on the shutdown path. */ + * user callback. The release path only uses the per-event lock and remains + * valid after the global event subsystem has been deinitialized. */ static void osi_thread_generic_event_drain(void *context) { - struct osi_event *event = (struct osi_event *)context; + struct osi_event_core *event = (struct osi_event_core *)context; + bool should_free = false; if (event == NULL) { return; @@ -762,86 +790,150 @@ static void osi_thread_generic_event_drain(void *context) osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); OSI_EVENT_CLEAR_FLAG(event, OSI_EVENT_FLAG_QUEUED); - osi_mutex_unlock(&event->lock); - - osi_event_release(event); + should_free = osi_event_release_locked(event); + osi_event_unlock_and_maybe_free(event, should_free); } bool osi_thread_post_event(struct osi_event *event, uint32_t timeout) { - bool ret = false; - osi_thread_t *thread = NULL; - uint8_t queue_idx = 0; + struct osi_event_core *core; + osi_thread_t *thread; + uint8_t queue_idx; + bool ret; + bool should_free = false; - if (!osi_event_acquire(event)) { - OSI_TRACE_EVENT("%s acquire fail ev=%p", __func__, event); + if (event == NULL) { return false; } + core = &event->core; - osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); - if (!osi_event_can_post_locked(event)) { - osi_mutex_unlock(&event->lock); - osi_event_release(event); + /* Session-event storage is pinned until subsystem deinit, so the hot path + * needs only the per-event lock and a queue reference. */ + osi_mutex_lock(&core->lock, OSI_MUTEX_MAX_TIMEOUT); + if (!osi_event_can_post_locked(core)) { + OSI_TRACE_EVENT("%s post fail ev=%p qidx=%u wq_len=%d", __func__, event, core->queue_idx, + core->thread ? osi_thread_queue_wait_size(core->thread, core->queue_idx) : -1); + osi_mutex_unlock(&core->lock); return false; } - OSI_EVENT_SET_FLAG(event, OSI_EVENT_FLAG_QUEUED); - OSI_EVENT_SET_FLAG(event, OSI_EVENT_FLAG_POSTING); - thread = event->thread; - queue_idx = event->queue_idx; - osi_mutex_unlock(&event->lock); + OSI_EVENT_SET_FLAG(core, OSI_EVENT_FLAG_QUEUED); + core->ref_count++; + thread = core->thread; + queue_idx = core->queue_idx; + osi_mutex_unlock(&core->lock); - /* The queued work item owns a reference until the generic handler drains it. */ - osi_event_retain(event); - ret = osi_thread_post(thread, osi_thread_generic_event_handler, event, queue_idx, timeout); - osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); - OSI_EVENT_CLEAR_FLAG(event, OSI_EVENT_FLAG_POSTING); - if (!ret) { - // clear "is_queued" when post failure, to allow for following event posts - OSI_EVENT_CLEAR_FLAG(event, OSI_EVENT_FLAG_QUEUED); - } - osi_mutex_unlock(&event->lock); + ret = osi_thread_post(thread, osi_thread_generic_event_handler, core, queue_idx, timeout); + osi_mutex_lock(&core->lock, OSI_MUTEX_MAX_TIMEOUT); if (!ret) { - OSI_TRACE_EVENT("%s enqueue fail ev=%p qidx=%u wq_len=%d", - __func__, event, queue_idx, + OSI_TRACE_EVENT("%s enqueue fail ev=%p qidx=%u wq_len=%d", __func__, event, queue_idx, osi_thread_queue_wait_size(thread, queue_idx)); - osi_event_release(event); + /* Clear QUEUED on post failure so a later post may enqueue. */ + OSI_EVENT_CLEAR_FLAG(core, OSI_EVENT_FLAG_QUEUED); + should_free = osi_event_release_locked(core); } - osi_event_release(event); + osi_event_unlock_and_maybe_free(core, should_free); + return ret; +} + +bool osi_dynamic_event_post(struct osi_dynamic_event *event, uint32_t timeout) +{ + struct osi_event_core *core; + osi_thread_t *thread; + uint8_t queue_idx; + bool ret; + bool should_free = false; + + if (!osi_dynamic_event_acquire_locked(event)) { + return false; + } + core = &event->core; + + if (!osi_event_can_post_locked(core)) { + OSI_TRACE_EVENT("%s post fail ev=%p qidx=%u wq_len=%d", __func__, event, core->queue_idx, + core->thread ? osi_thread_queue_wait_size(core->thread, core->queue_idx) : -1); + should_free = osi_event_release_locked(core); + osi_event_unlock_and_maybe_free(core, should_free); + return false; + } + + OSI_EVENT_SET_FLAG(core, OSI_EVENT_FLAG_QUEUED); + core->ref_count++; + thread = core->thread; + queue_idx = core->queue_idx; + osi_mutex_unlock(&core->lock); + + ret = osi_thread_post(thread, osi_thread_generic_event_handler, core, queue_idx, timeout); + + osi_mutex_lock(&core->lock, OSI_MUTEX_MAX_TIMEOUT); + if (!ret) { + OSI_TRACE_EVENT("%s enqueue fail ev=%p qidx=%u wq_len=%d", __func__, event, queue_idx, + osi_thread_queue_wait_size(thread, queue_idx)); + /* Drop the queue ownership taken above; handler will never run. */ + OSI_EVENT_CLEAR_FLAG(core, OSI_EVENT_FLAG_QUEUED); + should_free = osi_event_release_locked(core); + } + /* Always drop the acquire reference from osi_dynamic_event_acquire_locked. */ + if (osi_event_release_locked(core)) { + should_free = true; + } + osi_event_unlock_and_maybe_free(core, should_free); return ret; } +static void osi_event_retire(struct osi_event_core *event) +{ + bool should_free; + + osi_mutex_lock(&event->lock, OSI_MUTEX_MAX_TIMEOUT); + osi_event_mark_deleting_locked(event); + should_free = osi_event_release_locked(event); + osi_event_unlock_and_maybe_free(event, should_free); +} + int osi_thread_event_init(void) { - int ret = -1; - - do { - if (osi_mutex_new(&s_osi_event_lock) != 0) { - break; - } - - s_osi_event_list = list_new(NULL); - if (s_osi_event_list == NULL) { - break; - } - - ret = 0; - } while (0); - - if (ret != 0) { - osi_thread_event_deinit(); + if (osi_mutex_new(&s_osi_event_lock) != 0) { + return -1; } - return ret; + s_osi_session_event_list = list_new(NULL); + s_osi_dynamic_event_list = list_new(NULL); + if (s_osi_session_event_list == NULL || s_osi_dynamic_event_list == NULL) { + list_free(s_osi_session_event_list); + list_free(s_osi_dynamic_event_list); + s_osi_session_event_list = NULL; + s_osi_dynamic_event_list = NULL; + osi_mutex_free(&s_osi_event_lock); + return -1; + } + return 0; } void osi_thread_event_deinit(void) { - if (s_osi_event_list != NULL) { - list_free(s_osi_event_list); - s_osi_event_list = NULL; + if (s_osi_event_lock == NULL) { + return; } + + osi_event_lock(); + while (s_osi_session_event_list != NULL && !list_is_empty(s_osi_session_event_list)) { + struct osi_event *event = (struct osi_event *)list_front(s_osi_session_event_list); + list_delete(s_osi_session_event_list, event); + osi_event_retire(&event->core); + } + while (s_osi_dynamic_event_list != NULL && !list_is_empty(s_osi_dynamic_event_list)) { + struct osi_dynamic_event *event = + (struct osi_dynamic_event *)list_front(s_osi_dynamic_event_list); + list_delete(s_osi_dynamic_event_list, event); + osi_event_retire(&event->core); + } + list_free(s_osi_session_event_list); + list_free(s_osi_dynamic_event_list); + s_osi_session_event_list = NULL; + s_osi_dynamic_event_list = NULL; + osi_event_unlock(); osi_mutex_free(&s_osi_event_lock); } diff --git a/components/bt/host/bluedroid/btc/profile/std/hid/btc_hh.c b/components/bt/host/bluedroid/btc/profile/std/hid/btc_hh.c index dc0960f355c..ef66c390c7f 100644 --- a/components/bt/host/bluedroid/btc/profile/std/hid/btc_hh.c +++ b/components/bt/host/bluedroid/btc/profile/std/hid/btc_hh.c @@ -1305,7 +1305,7 @@ static void btc_hh_cb_arg_deep_free(btc_msg_t *msg) static void btc_hh_data_path_deinit_inner(btc_hh_device_t *p_dev) { struct pkt_queue *queue; - struct osi_event *event; + struct osi_dynamic_event *event; if (p_dev == NULL) { return; @@ -1319,7 +1319,7 @@ static void btc_hh_data_path_deinit_inner(btc_hh_device_t *p_dev) osi_mutex_unlock(&p_dev->lock); if (event != NULL) { - osi_event_delete(event); + osi_dynamic_event_delete(event); } if (queue != NULL) { @@ -1332,7 +1332,7 @@ bool btc_hh_data_enqueue_linked_pkt(pkt_linked_item_t *linked_pkt) tBTA_HH_DATA_PKT *pkt = linked_pkt != NULL ? (tBTA_HH_DATA_PKT *)linked_pkt->data : NULL; btc_hh_device_t *p_dev = pkt != NULL ? btc_hh_find_connected_dev_by_handle(pkt->dev_handle) : NULL; struct pkt_queue *data_queue; - struct osi_event *data_ready; + struct osi_dynamic_event *data_ready; pkt_linked_item_t *old = NULL; bool enqueue_ok; @@ -1357,7 +1357,7 @@ bool btc_hh_data_enqueue_linked_pkt(pkt_linked_item_t *linked_pkt) enqueue_ok = pkt_queue_enqueue(data_queue, linked_pkt); if (enqueue_ok && data_ready != NULL) { - osi_thread_post_event(data_ready, 0); + osi_dynamic_event_post(data_ready, 0); } osi_mutex_unlock(&p_dev->lock); @@ -1423,7 +1423,7 @@ static bool btc_hh_data_path_init_inner(btc_hh_device_t *p_dev) { bool result = false; struct pkt_queue *data_queue = NULL; - struct osi_event *data_ready = NULL; + struct osi_dynamic_event *data_ready = NULL; do { if (p_dev == NULL) { @@ -1446,13 +1446,13 @@ static bool btc_hh_data_path_init_inner(btc_hh_device_t *p_dev) break; } - data_ready = osi_event_create(btc_hh_data_pkt_handler, p_dev); + data_ready = osi_dynamic_event_create(btc_hh_data_pkt_handler, p_dev); if (data_ready == NULL) { BTC_TRACE_ERROR("%s: osi_event_create failed", __func__); break; } - if (!osi_event_bind(data_ready, btc_get_current_thread(), BTC_HH_DATA_QUEUE_IDX)) { + if (!osi_dynamic_event_bind(data_ready, btc_get_current_thread(), BTC_HH_DATA_QUEUE_IDX)) { BTC_TRACE_ERROR("%s: osi_event_bind failed", __func__); break; } @@ -1470,7 +1470,7 @@ static bool btc_hh_data_path_init_inner(btc_hh_device_t *p_dev) if (!result) { btc_hh_data_path_deinit_inner(p_dev); if (data_ready != NULL) { - osi_event_delete(data_ready); + osi_dynamic_event_delete(data_ready); } if (data_queue != NULL) { pkt_queue_destroy(data_queue, bta_hh_co_data_linked_pkt_free); @@ -1550,7 +1550,7 @@ static void btc_hh_data_pkt_handler(void *arg) } if (p_dev->data_ready != NULL && !pkt_queue_is_empty(p_dev->data_queue)) { - osi_thread_post_event(p_dev->data_ready, 0); + osi_dynamic_event_post(p_dev->data_ready, 0); } } diff --git a/components/bt/host/bluedroid/btc/profile/std/include/btc_hh.h b/components/bt/host/bluedroid/btc/profile/std/include/btc_hh.h index 3117cc7a30f..6627f1c45b4 100644 --- a/components/bt/host/bluedroid/btc/profile/std/include/btc_hh.h +++ b/components/bt/host/bluedroid/btc/profile/std/include/btc_hh.h @@ -84,7 +84,7 @@ typedef struct { uint32_t drop_pkt_cnt; osi_mutex_t lock; struct pkt_queue *data_queue; - struct osi_event *data_ready; + struct osi_dynamic_event *data_ready; osi_alarm_t *vup_timer; } btc_hh_device_t; diff --git a/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt b/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt index 0c7c86ead9a..cc0a34f497b 100644 --- a/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt +++ b/components/bt/test_apps/basic_unit_test/main/CMakeLists.txt @@ -2,6 +2,7 @@ idf_component_register(SRCS "test_bt_main.c" "test_bt_common.c" "test_smp.c" "test_osal.c" + "test_osi_event.c" "test_prf_task.c" INCLUDE_DIRS "." PRIV_REQUIRES unity bt diff --git a/components/bt/test_apps/basic_unit_test/main/test_osi_event.c b/components/bt/test_apps/basic_unit_test/main/test_osi_event.c new file mode 100644 index 00000000000..ee38e60952e --- /dev/null +++ b/components/bt/test_apps/basic_unit_test/main/test_osi_event.c @@ -0,0 +1,535 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* + * Unit tests for osi_event (components/bt/common/osi/thread.c): coalesce, + * delete-while-queued, drain without user callbacks, stale post after delete, + * and re-post after QUEUED is cleared (lost-wakeup regression). + */ + +#include +#include +#include + +#include "unity.h" +#include "unity_test_runner.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#include "osi/thread.h" + +#define TEST_WORKER_STACK 3072 +#define TEST_HELPER_STACK 2560 +#define TEST_WAIT_MS 1000 +#define TEST_NO_RUN_MS 50 +#define TEST_WORKER_PRIO_HIGH (configMAX_PRIORITIES - 2) +#define TEST_WORKER_PRIO_LOW 2 + +static SemaphoreHandle_t s_done; +static SemaphoreHandle_t s_gate; +static SemaphoreHandle_t s_join; +static volatile uint32_t s_run_count; +static volatile uint32_t s_dummy_count; +static struct osi_event *s_event; +static volatile bool s_stop_posters; + +static void handler_count(void *context) +{ + (void)context; + s_run_count++; + xSemaphoreGive(s_done); +} + +static void handler_gated(void *context) +{ + (void)context; + s_run_count++; + xSemaphoreGive(s_done); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_gate, pdMS_TO_TICKS(TEST_WAIT_MS))); +} + +static void handler_repost(void *context) +{ + (void)context; + uint32_t n = ++s_run_count; + if (n < 3) { + TEST_ASSERT_TRUE(osi_thread_post_event(s_event, 0)); + } + if (n == 3) { + xSemaphoreGive(s_done); + } +} + +static void handler_self_delete(void *context) +{ + (void)context; + s_run_count++; + osi_event_delete(s_event); + s_event = NULL; + xSemaphoreGive(s_done); +} + +static void dummy_work(void *context) +{ + (void)context; + s_dummy_count++; +} + +static osi_thread_t *test_thread_create(int priority, size_t queue_len) +{ + const size_t workqueue_len[] = {queue_len}; + /* Pin to core 0: OSI_THREAD_CORE_AFFINITY (== 2) is not a valid FreeRTOS + * core id / tskNO_AFFINITY and trips xTaskCreatePinnedToCore on ESP32. */ + osi_thread_t *thread = osi_thread_create("osi_ev_test", TEST_WORKER_STACK, priority, + OSI_THREAD_CORE_0, 1, workqueue_len, false); + TEST_ASSERT_NOT_NULL(thread); + return thread; +} + +static struct osi_event *test_event_bind(osi_thread_t *thread, osi_thread_func_t func) +{ + struct osi_event *event = osi_event_create(func, NULL); + TEST_ASSERT_NOT_NULL(event); + TEST_ASSERT_TRUE(osi_event_bind(event, thread, 0)); + return event; +} + +static struct osi_dynamic_event *test_dynamic_event_bind(osi_thread_t *thread, + osi_thread_func_t func) +{ + struct osi_dynamic_event *event = osi_dynamic_event_create(func, NULL); + TEST_ASSERT_NOT_NULL(event); + TEST_ASSERT_TRUE(osi_dynamic_event_bind(event, thread, 0)); + return event; +} + +static void osi_event_test_begin(void) +{ + s_run_count = 0; + s_dummy_count = 0; + s_event = NULL; + s_stop_posters = false; + + s_done = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_done); + s_gate = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_gate); + s_join = xSemaphoreCreateCounting(8, 0); + TEST_ASSERT_NOT_NULL(s_join); + + TEST_ASSERT_EQUAL(0, osi_thread_event_init()); +} + +static void osi_event_test_end(osi_thread_t *thread, struct osi_event *event) +{ + if (event != NULL) { + osi_event_delete(event); + } + if (thread != NULL) { + osi_thread_free(thread); + } + osi_thread_event_deinit(); + + vSemaphoreDelete(s_done); + s_done = NULL; + vSemaphoreDelete(s_gate); + s_gate = NULL; + vSemaphoreDelete(s_join); + s_join = NULL; + + /* Idle task reclaims deleted worker TCBs asynchronously. */ + vTaskDelay(pdMS_TO_TICKS(20)); +} + +TEST_CASE("osi_event duplicate post coalesces until handler runs", "[osi_event]") +{ + UBaseType_t saved_prio = uxTaskPriorityGet(NULL); + osi_thread_t *thread; + struct osi_event *event; + + osi_event_test_begin(); + vTaskPrioritySet(NULL, TEST_WORKER_PRIO_HIGH); + thread = test_thread_create(TEST_WORKER_PRIO_LOW, 8); + event = test_event_bind(thread, handler_count); + + TEST_ASSERT_TRUE(osi_thread_post_event(event, 0)); + TEST_ASSERT_FALSE(osi_thread_post_event(event, 0)); + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + vTaskPrioritySet(NULL, saved_prio); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + TEST_ASSERT_EQUAL(pdFALSE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_NO_RUN_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + + osi_event_test_end(thread, event); +} + +TEST_CASE("osi_event re-post while handler is running is accepted", "[osi_event]") +{ + osi_thread_t *thread; + struct osi_event *event; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 8); + event = test_event_bind(thread, handler_gated); + + TEST_ASSERT_TRUE(osi_thread_post_event(event, OSI_THREAD_MAX_TIMEOUT)); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + + /* QUEUED is already clear; POSTING must not reject this re-post. */ + TEST_ASSERT_TRUE(osi_thread_post_event(event, 0)); + xSemaphoreGive(s_gate); + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(2, s_run_count); + xSemaphoreGive(s_gate); + + osi_event_test_end(thread, event); +} + +TEST_CASE("osi_event callback may self-repost", "[osi_event]") +{ + osi_thread_t *thread; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 8); + s_event = test_event_bind(thread, handler_repost); + + TEST_ASSERT_TRUE(osi_thread_post_event(s_event, 0)); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(3, s_run_count); + + osi_event_test_end(thread, s_event); + s_event = NULL; +} + +TEST_CASE("osi_event callback may self-delete", "[osi_event]") +{ + osi_thread_t *thread; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 8); + s_event = test_event_bind(thread, handler_self_delete); + + TEST_ASSERT_TRUE(osi_thread_post_event(s_event, 0)); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + TEST_ASSERT_NULL(s_event); + + osi_event_test_end(thread, NULL); +} + +TEST_CASE("osi_event delete then stale post is rejected", "[osi_event]") +{ + osi_thread_t *thread; + struct osi_event *event; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 8); + event = test_event_bind(thread, handler_count); + + osi_event_delete(event); + TEST_ASSERT_FALSE(osi_thread_post_event(event, 0)); + TEST_ASSERT_FALSE(osi_thread_post_event(NULL, 0)); + osi_event_delete(event); + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + osi_event_test_end(thread, NULL); +} + +TEST_CASE("osi_event delete while queued skips callback and drains refs", "[osi_event]") +{ + UBaseType_t saved_prio = uxTaskPriorityGet(NULL); + osi_thread_t *thread; + struct osi_event *event; + + osi_event_test_begin(); + vTaskPrioritySet(NULL, TEST_WORKER_PRIO_HIGH); + thread = test_thread_create(TEST_WORKER_PRIO_LOW, 8); + event = test_event_bind(thread, handler_count); + + TEST_ASSERT_TRUE(osi_thread_post_event(event, 0)); + osi_event_delete(event); + vTaskPrioritySet(NULL, saved_prio); + + TEST_ASSERT_EQUAL(pdFALSE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_NO_RUN_MS))); + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + osi_event_test_end(thread, NULL); +} + +TEST_CASE("osi_event thread free drains pending event without callback", "[osi_event]") +{ + UBaseType_t saved_prio = uxTaskPriorityGet(NULL); + osi_thread_t *thread; + struct osi_event *event; + + osi_event_test_begin(); + vTaskPrioritySet(NULL, TEST_WORKER_PRIO_HIGH); + thread = test_thread_create(TEST_WORKER_PRIO_LOW, 8); + event = test_event_bind(thread, handler_count); + + TEST_ASSERT_TRUE(osi_thread_post_event(event, 0)); + osi_thread_free(thread); + vTaskPrioritySet(NULL, saved_prio); + + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + osi_event_delete(event); + osi_thread_event_deinit(); + + vSemaphoreDelete(s_done); + s_done = NULL; + vSemaphoreDelete(s_gate); + s_gate = NULL; + vSemaphoreDelete(s_join); + s_join = NULL; + vTaskDelay(pdMS_TO_TICKS(20)); +} + +TEST_CASE("osi_event drain after event subsystem deinit", "[osi_event]") +{ + UBaseType_t saved_prio = uxTaskPriorityGet(NULL); + osi_thread_t *thread; + struct osi_event *event; + + osi_event_test_begin(); + vTaskPrioritySet(NULL, TEST_WORKER_PRIO_HIGH); + thread = test_thread_create(TEST_WORKER_PRIO_LOW, 8); + event = test_event_bind(thread, handler_count); + + TEST_ASSERT_TRUE(osi_thread_post_event(event, 0)); + osi_event_delete(event); + osi_thread_event_deinit(); + osi_thread_free(thread); + vTaskPrioritySet(NULL, saved_prio); + + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + vSemaphoreDelete(s_done); + s_done = NULL; + vSemaphoreDelete(s_gate); + s_gate = NULL; + vSemaphoreDelete(s_join); + s_join = NULL; + vTaskDelay(pdMS_TO_TICKS(20)); +} + +TEST_CASE("osi_event post timeout 0 fails when work queue is full", "[osi_event]") +{ + UBaseType_t saved_prio = uxTaskPriorityGet(NULL); + osi_thread_t *thread; + struct osi_event *event; + const size_t queue_len = 2; + + osi_event_test_begin(); + vTaskPrioritySet(NULL, TEST_WORKER_PRIO_HIGH); + thread = test_thread_create(TEST_WORKER_PRIO_LOW, queue_len); + event = test_event_bind(thread, handler_count); + + for (size_t i = 0; i < queue_len; i++) { + TEST_ASSERT_TRUE(osi_thread_post(thread, dummy_work, NULL, 0, 0)); + } + TEST_ASSERT_FALSE(osi_thread_post_event(event, 0)); + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + /* Queue reservation was rolled back; a later post can succeed once space exists. */ + vTaskPrioritySet(NULL, saved_prio); + TEST_ASSERT_EQUAL(pdFALSE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_NO_RUN_MS))); + + vTaskPrioritySet(NULL, TEST_WORKER_PRIO_HIGH); + TEST_ASSERT_TRUE(osi_thread_post_event(event, 0)); + vTaskPrioritySet(NULL, saved_prio); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + + osi_event_test_end(thread, event); +} + +static void poster_task(void *arg) +{ + struct osi_event *event = (struct osi_event *)arg; + + while (!s_stop_posters) { + osi_thread_post_event(event, 0); + /* Must block, not only yield: two ready posters can pin both CPUs and + * starve IDLE*, which trips the task WDT. */ + vTaskDelay(1); + } + xSemaphoreGive(s_join); + vTaskDelete(NULL); +} + +TEST_CASE("osi_event concurrent post and delete", "[osi_event]") +{ + osi_thread_t *thread; + struct osi_event *event; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 16); + event = test_event_bind(thread, handler_count); + + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(poster_task, "osi_ev_p1", TEST_HELPER_STACK, + event, TEST_WORKER_PRIO_LOW, NULL)); + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(poster_task, "osi_ev_p2", TEST_HELPER_STACK, + event, TEST_WORKER_PRIO_LOW, NULL)); + + vTaskDelay(pdMS_TO_TICKS(30)); + osi_event_delete(event); + s_stop_posters = true; + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_join, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_join, pdMS_TO_TICKS(TEST_WAIT_MS))); + + TEST_ASSERT_FALSE(osi_thread_post_event(event, 0)); + + osi_event_test_end(thread, NULL); +} + +TEST_CASE("osi_event create bind post delete stop cycles", "[osi_event]") +{ + for (int cycle = 0; cycle < 8; cycle++) { + osi_thread_t *thread; + struct osi_event *event; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 8); + event = test_event_bind(thread, handler_count); + + TEST_ASSERT_TRUE(osi_thread_post_event(event, OSI_THREAD_MAX_TIMEOUT)); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL_UINT32(1, s_run_count); + + osi_event_test_end(thread, event); + } +} + +TEST_CASE("osi_dynamic_event delete then stale post is rejected", "[osi_event]") +{ + osi_thread_t *thread; + struct osi_dynamic_event *event; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 8); + event = test_dynamic_event_bind(thread, handler_count); + + osi_dynamic_event_delete(event); + TEST_ASSERT_FALSE(osi_dynamic_event_post(event, 0)); + TEST_ASSERT_FALSE(osi_dynamic_event_post(NULL, 0)); + osi_dynamic_event_delete(event); + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + osi_event_test_end(thread, NULL); +} + +TEST_CASE("osi_dynamic_event delete while queued skips callback", "[osi_event]") +{ + UBaseType_t saved_prio = uxTaskPriorityGet(NULL); + osi_thread_t *thread; + struct osi_dynamic_event *event; + + osi_event_test_begin(); + vTaskPrioritySet(NULL, TEST_WORKER_PRIO_HIGH); + thread = test_thread_create(TEST_WORKER_PRIO_LOW, 8); + event = test_dynamic_event_bind(thread, handler_count); + + TEST_ASSERT_TRUE(osi_dynamic_event_post(event, 0)); + osi_dynamic_event_delete(event); + vTaskPrioritySet(NULL, saved_prio); + + TEST_ASSERT_EQUAL(pdFALSE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_NO_RUN_MS))); + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + osi_event_test_end(thread, NULL); +} + +static void dynamic_poster_task(void *arg) +{ + struct osi_dynamic_event *event = (struct osi_dynamic_event *)arg; + + while (!s_stop_posters) { + osi_dynamic_event_post(event, 0); + vTaskDelay(1); + } + xSemaphoreGive(s_join); + vTaskDelete(NULL); +} + +TEST_CASE("osi_dynamic_event concurrent post and delete", "[osi_event]") +{ + osi_thread_t *thread; + struct osi_dynamic_event *event; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 16); + event = test_dynamic_event_bind(thread, handler_count); + + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(dynamic_poster_task, "osi_dev_p1", TEST_HELPER_STACK, + event, TEST_WORKER_PRIO_LOW, NULL)); + TEST_ASSERT_EQUAL(pdPASS, xTaskCreate(dynamic_poster_task, "osi_dev_p2", TEST_HELPER_STACK, + event, TEST_WORKER_PRIO_LOW, NULL)); + + vTaskDelay(pdMS_TO_TICKS(30)); + osi_dynamic_event_delete(event); + s_stop_posters = true; + + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_join, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_join, pdMS_TO_TICKS(TEST_WAIT_MS))); + TEST_ASSERT_FALSE(osi_dynamic_event_post(event, 0)); + + osi_event_test_end(thread, NULL); +} + +TEST_CASE("osi_dynamic_event create post delete cycles in one session", "[osi_event]") +{ + osi_thread_t *thread; + + osi_event_test_begin(); + thread = test_thread_create(TEST_WORKER_PRIO_HIGH, 8); + + for (int cycle = 0; cycle < 16; cycle++) { + struct osi_dynamic_event *event = test_dynamic_event_bind(thread, handler_count); + + TEST_ASSERT_TRUE(osi_dynamic_event_post(event, OSI_THREAD_MAX_TIMEOUT)); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(s_done, pdMS_TO_TICKS(TEST_WAIT_MS))); + osi_dynamic_event_delete(event); + } + TEST_ASSERT_EQUAL_UINT32(16, s_run_count); + + osi_event_test_end(thread, NULL); +} + +TEST_CASE("osi_event deinit retires queued session and dynamic events", "[osi_event]") +{ + UBaseType_t saved_prio = uxTaskPriorityGet(NULL); + osi_thread_t *thread; + struct osi_event *session_event; + struct osi_dynamic_event *dynamic_event; + + osi_event_test_begin(); + vTaskPrioritySet(NULL, TEST_WORKER_PRIO_HIGH); + thread = test_thread_create(TEST_WORKER_PRIO_LOW, 8); + session_event = test_event_bind(thread, handler_count); + dynamic_event = test_dynamic_event_bind(thread, handler_count); + + TEST_ASSERT_TRUE(osi_thread_post_event(session_event, 0)); + TEST_ASSERT_TRUE(osi_dynamic_event_post(dynamic_event, 0)); + osi_thread_event_deinit(); + osi_thread_free(thread); + vTaskPrioritySet(NULL, saved_prio); + + TEST_ASSERT_EQUAL_UINT32(0, s_run_count); + + vSemaphoreDelete(s_done); + s_done = NULL; + vSemaphoreDelete(s_gate); + s_gate = NULL; + vSemaphoreDelete(s_join); + s_join = NULL; + vTaskDelay(pdMS_TO_TICKS(20)); +}