locking: contended_release tracepoint instrumentation
Subsystems (4)
v2 cover locking: contended_release tracepoint instrumentation
The existing contention_begin/contention_end tracepoints fire on the waiter side. The lock holder's identity and stack can be captured at contention_begin time (e.g. perf lock contention --lock-owner), but this reflects the holder's state when a waiter arrives, not when the lock is actually released.
This series adds a contended_release tracepoint that fires on the holder side when a lock with waiters is released. This provides:
- Hold time estimation: when the holder's own acquisition was contended, its contention_end (acquisition) and contended_release can be correlated to measure how long the lock was held under contention.
- The holder's stack at release time, which may differ from what perf lock contention --lock-owner captures if the holder does significant work between the waiter's arrival and the unlock.
The tracepoint is placed exclusively in slowpath unlock paths, so there is no performance impact on the uncontended fast path and expected minimal impact on binary size.
Based on the feedback on the RFC, here is v2 with the suggested fixes. Since there were no objections to the approach, I am dropping the RFC tag.
RFC -> v2:
- Add trace_contended_release_enabled() guard before waiter checks that exist only for the tracepoint (Steven Rostedt).
- Rename __percpu_up_read_slowpath() to __percpu_up_read() (Peter Zijlstra).
- Add extern for __percpu_up_read() (Peter Zijlstra).
- Squashed tracepoint introduction and usage commits (Masami Hiramatsu).
RFC: https://lore.kernel.org/all/[email protected]/
Dmitry Ilvokhin (2):
locking/percpu-rwsem: Extract __percpu_up_read()
locking: Add contended_release tracepoint
include/linux/percpu-rwsem.h | 15 +++------------ include/trace/events/lock.h | 17 +++++++++++++++++ kernel/locking/mutex.c | 1 + kernel/locking/percpu-rwsem.c | 21 +++++++++++++++++++++ kernel/locking/rtmutex.c | 1 + kernel/locking/rwbase_rt.c | 8 +++++++- kernel/locking/rwsem.c | 9 +++++++-- kernel/locking/semaphore.c | 4 +++- 8 files changed, 60 insertions(+), 16 deletions(-)
Signature · 1 line
2.52.0
PATCH v2 1/2 locking/percpu-rwsem: Extract __percpu_up_read()
Move the percpu_up_read() slowpath out of the inline function into a new __percpu_up_read() to avoid binary size increase from adding a tracepoint to an inlined function.
--- include/linux/percpu-rwsem.h | 15 +++------------ kernel/locking/percpu-rwsem.c | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 12 deletions(-)
include/linux/percpu-rwsem.h +3 -12
@@ -107,6 +107,8 @@ static inline bool percpu_down_read_trylock(struct percpu_rw_semaphore *sem)return ret; } +extern void __percpu_up_read (struct percpu_rw_semaphore *sem); +static inline void percpu_up_read (struct percpu_rw_semaphore *sem) {rwsem_release (&sem->dep_map, _RET_IP_);
@@ -118,18 +120,7 @@ static inline void percpu_up_read(struct percpu_rw_semaphore *sem)if (likely (rcu_sync_is_idle (&sem->rss))) {this_cpu_dec (*sem->read_count); }else { -/* -* slowpath; reader will only ever wake a single blocked -* writer. -*/ -smp_mb ();/* B matches C */ -/* -* In other words, if they see our decrement (presumably to -* aggregate zero, as that is the only time it matters) they -* will also see our critical section. -*/ -this_cpu_dec (*sem->read_count); -rcuwait_wake_up (&sem->writer); +__percpu_up_read (sem); }preempt_enable (); }
kernel/locking/percpu-rwsem.c +18 -0
@@ -288,3 +288,21 @@ void percpu_up_write(struct percpu_rw_semaphore *sem)rcu_sync_exit (&sem->rss); }EXPORT_SYMBOL_GPL (percpu_up_write); + +void __percpu_up_read (struct percpu_rw_semaphore *sem) +{ +lockdep_assert_preemption_disabled (); +/* +* slowpath; reader will only ever wake a single blocked +* writer. +*/ +smp_mb ();/* B matches C */ +/* +* In other words, if they see our decrement (presumably to +* aggregate zero, as that is the only time it matters) they +* will also see our critical section. +*/ +this_cpu_dec (*sem->read_count); +rcuwait_wake_up (&sem->writer); +} +EXPORT_SYMBOL_GPL (__percpu_up_read);
Signature · 1 line
2.52.0
Re: [PATCH v2 1/2] locking/percpu-rwsem: Extract __percpu_up_read()
Quoted 15 lines · On Tue, 10 Mar 2026 17:49:38 +0000 Dmitry Ilvokhin <[email protected]> wrote:
Move the
percpu_up_read()slowpath out of the inline function into a new__percpu_up_read()to avoid binary size increase from adding a tracepoint to an inlined function.Signed-off-by Dmitry Ilvokhin <[email protected]>--- include/linux/percpu-rwsem.h | 15 +++------------ kernel/locking/percpu-rwsem.c | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 12 deletions(-)include/linux/percpu-rwsem.h +0
-0
include/linux/percpu-rwsem.h static inline bool percpu_down_read_trylock(struct percpu_rw_semaphore *sem) · line 109
Quoted 3 lines
include/linux/percpu-rwsem.h +0
-0@@ -107,6 +107,8 @@ static inline bool percpu_down_read_trylock(struct percpu_rw_semaphore *sem)return ret; }
PATCH v2 2/2 locking: Add contended_release tracepoint
In reply to Dmitry IlvokhinAdd the contended_release trace event. This tracepoint fires on the holder side when a contended lock is released, complementing the existing contention_begin/contention_end tracepoints which fire on the waiter side.
This enables correlating lock hold time under contention with waiter events by lock address.
Add trace_contended_release() calls to the slowpath unlock paths of sleepable locks: mutex, rtmutex, semaphore, rwsem, percpu-rwsem, and RT-specific rwbase locks. Each call site fires only when there are blocked waiters being woken, except percpu_up_write() which always wakes via __wake_up().
--- include/trace/events/lock.h | 17 +++++++++++++++++ kernel/locking/mutex.c | 1 + kernel/locking/percpu-rwsem.c | 3 +++ kernel/locking/rtmutex.c | 1 + kernel/locking/rwbase_rt.c | 8 +++++++- kernel/locking/rwsem.c | 9 +++++++-- kernel/locking/semaphore.c | 4 +++- 7 files changed, 39 insertions(+), 4 deletions(-)
include/trace/events/lock.h +17 -0
@@ -138,6 +138,23 @@ TRACE_EVENT(contention_end,TP_printk ("%p (ret=%d)" , __entry->lock_addr, __entry->ret) ); +TRACE_EVENT (contended_release, + +TP_PROTO (void *lock), + +TP_ARGS (lock), + +TP_STRUCT__entry ( + __field(void *, lock_addr) + ), + +TP_fast_assign ( + __entry->lock_addr = lock; + ), + +TP_printk ("%p" , __entry->lock_addr) +); +#endif /* _TRACE_LOCK_H */ /* This part must be outside protection */
kernel/locking/mutex.c +1 -0
@@ -992,6 +992,7 @@ static noinline void __sched __mutex_unlock_slowpath(struct mutex *lock, unsigneif (waiter) { next = waiter->task; +trace_contended_release (lock);debug_mutex_wake_waiter (lock, waiter);__clear_task_blocked_on (next, lock);wake_q_add (&wake_q, next);
kernel/locking/percpu-rwsem.c +3 -0
@@ -263,6 +263,8 @@ void percpu_up_write(struct percpu_rw_semaphore *sem) {rwsem_release (&sem->dep_map, _RET_IP_); +trace_contended_release (sem); +/* * Signal the writer is done, no fast path yet. *
@@ -297,6 +299,7 @@ void __percpu_up_read(struct percpu_rw_semaphore *sem)* writer. */ smp_mb ();/* B matches C */ +trace_contended_release (sem);/* * In other words, if they see our decrement (presumably to * aggregate zero, as that is the only time it matters) they
kernel/locking/rtmutex.c +1 -0
@@ -1466,6 +1466,7 @@ static void __sched rt_mutex_slowunlock(struct rt_mutex_base *lock)raw_spin_lock_irqsave (&lock->wait_lock, flags); } +trace_contended_release (lock);/* * The wakeup next waiter path does not suffer from the above * race. See the comments there.
kernel/locking/rwbase_rt.c +7 -1
@@ -162,8 +162,10 @@ static void __sched __rwbase_read_unlock(struct rwbase_rt *rwb,* worst case which can happen is a spurious wakeup. */ owner =rt_mutex_owner (rtm); -if (owner) +if (owner) { +trace_contended_release (rwb);rt_mutex_wake_q_add_task (&wqh, owner, state); + }/* Pairs with the preempt_enable in rt_mutex_wake_up_q() */ preempt_disable ();
@@ -205,6 +207,8 @@ static inline void rwbase_write_unlock(struct rwbase_rt *rwb)unsigned long flags;raw_spin_lock_irqsave (&rtm->wait_lock, flags); +if (trace_contended_release_enabled () &&rt_mutex_has_waiters (rtm)) +trace_contended_release (rwb);__rwbase_write_unlock (rwb,WRITER_BIAS , flags); }
@@ -214,6 +218,8 @@ static inline void rwbase_write_downgrade(struct rwbase_rt *rwb)unsigned long flags;raw_spin_lock_irqsave (&rtm->wait_lock, flags); +if (trace_contended_release_enabled () &&rt_mutex_has_waiters (rtm)) +trace_contended_release (rwb);/* Release it and account current as reader */ __rwbase_write_unlock (rwb,WRITER_BIAS -1 , flags); }
kernel/locking/rwsem.c +7 -2
@@ -1390,6 +1390,7 @@ static inline void __up_read(struct rw_semaphore *sem)if (unlikely ((tmp & (RWSEM_LOCK_MASK |RWSEM_FLAG_WAITERS )) ==RWSEM_FLAG_WAITERS )) {clear_nonspinnable (sem); +trace_contended_release (sem);rwsem_wake (sem); }preempt_enable ();
@@ -1413,8 +1414,10 @@ static inline void __up_write(struct rw_semaphore *sem)preempt_disable ();rwsem_clear_owner (sem); tmp =atomic_long_fetch_add_release (-RWSEM_WRITER_LOCKED , &sem->count); -if (unlikely (tmp &RWSEM_FLAG_WAITERS )) +if (unlikely (tmp &RWSEM_FLAG_WAITERS )) { +trace_contended_release (sem);rwsem_wake (sem); + }preempt_enable (); }
@@ -1437,8 +1440,10 @@ static inline void __downgrade_write(struct rw_semaphore *sem) tmp =atomic_long_fetch_add_release ( -RWSEM_WRITER_LOCKED +RWSEM_READER_BIAS , &sem->count);rwsem_set_reader_owned (sem); -if (tmp &RWSEM_FLAG_WAITERS ) +if (tmp &RWSEM_FLAG_WAITERS ) { +trace_contended_release (sem);rwsem_downgrade_wake (sem); + }preempt_enable (); }
kernel/locking/semaphore.c +3 -1
@@ -231,8 +231,10 @@ void __sched up(struct semaphore *sem)else __up (sem, &wake_q);raw_spin_unlock_irqrestore (&sem->lock, flags); -if (!wake_q_empty (&wake_q)) +if (!wake_q_empty (&wake_q)) { +trace_contended_release (sem);wake_up_q (&wake_q); + } }EXPORT_SYMBOL (up);
Signature · 1 line
2.52.0
Re: [PATCH v2 2/2] locking: Add contended_release tracepoint
Quoted 70 lines · On Tue, 10 Mar 2026 17:49:39 +0000 Dmitry Ilvokhin <[email protected]> wrote:
Add the
contended_releasetrace event. This tracepoint fires on the holder side when a contended lock is released, complementing the existing contention_begin/contention_end tracepoints which fire on the waiter side.This enables correlating lock hold time under contention with waiter events by lock address.
Add
trace_contended_release()calls to the slowpath unlock paths of sleepable locks: mutex, rtmutex, semaphore, rwsem, percpu-rwsem, and RT-specific rwbase locks. Each call site fires only when there are blocked waiters being woken, exceptpercpu_up_write()which always wakes via__wake_up().Signed-off-by Dmitry Ilvokhin <[email protected]>--- include/trace/events/lock.h | 17 +++++++++++++++++ kernel/locking/mutex.c | 1 + kernel/locking/percpu-rwsem.c | 3 +++ kernel/locking/rtmutex.c | 1 + kernel/locking/rwbase_rt.c | 8 +++++++- kernel/locking/rwsem.c | 9 +++++++-- kernel/locking/semaphore.c | 4 +++- 7 files changed, 39 insertions(+), 4 deletions(-)include/trace/events/lock.h +17
-0@@ -138,6 +138,23 @@ TRACE_EVENT(contention_end,TP_printk ("%p (ret=%d)" , __entry->lock_addr, __entry->ret) ); +TRACE_EVENT (contended_release, + +TP_PROTO (void *lock), + +TP_ARGS (lock), + +TP_STRUCT__entry ( + __field(void *, lock_addr) + ), + +TP_fast_assign ( + __entry->lock_addr = lock; + ), + +TP_printk ("%p" , __entry->lock_addr) +); +#endif /* _TRACE_LOCK_H */ /* This part must be outside protection */ kernel/locking/mutex.c +1
-0@@ -992,6 +992,7 @@ static noinline void __sched __mutex_unlock_slowpath(struct mutex *lock, unsigneif (waiter) { next = waiter->task; +trace_contended_release (lock);debug_mutex_wake_waiter (lock, waiter);__clear_task_blocked_on (next, lock);wake_q_add (&wake_q, next);kernel/locking/percpu-rwsem.c +0
-0
kernel/locking/percpu-rwsem.c void percpu_up_write(struct percpu_rw_semaphore *sem) · line 267
Quoted 5 lines
kernel/locking/percpu-rwsem.c +2
-0@@ -263,6 +263,8 @@ void percpu_up_write(struct percpu_rw_semaphore *sem) {rwsem_release (&sem->dep_map, _RET_IP_); +trace_contended_release (sem); +
Hello!
I saw that you mentioned in the commmit message that you do this for only blocked waiters except for percpu_up_write(). We can use waitqueue_active(&sem->waiters) to check for this over here so that its consistent with every other call?
kernel/locking/percpu-rwsem.c void __percpu_up_read(struct percpu_rw_semaphore *sem) · line 302
Quoted 8 lines
kernel/locking/percpu-rwsem.c +1
-0/* * Signal the writer is done, no fast path yet. * @@ -297,6 +299,7 @@ void __percpu_up_read(struct percpu_rw_semaphore *sem)* writer. */ smp_mb ();/* B matches C */ +trace_contended_release (sem);
Should we do this after this_cpu_dec(*sem->read_count)?
Quoted 105 lines
kernel/locking/percpu-rwsem.c +0
-0/* * In other words, if they see our decrement (presumably to * aggregate zero, as that is the only time it matters) they kernel/locking/rtmutex.c +1
-0@@ -1466,6 +1466,7 @@ static void __sched rt_mutex_slowunlock(struct rt_mutex_base *lock)raw_spin_lock_irqsave (&lock->wait_lock, flags); } +trace_contended_release (lock);/* * The wakeup next waiter path does not suffer from the above * race. See the comments there. kernel/locking/rwbase_rt.c +7
-1@@ -162,8 +162,10 @@ static void __sched __rwbase_read_unlock(struct rwbase_rt *rwb,* worst case which can happen is a spurious wakeup. */ owner =rt_mutex_owner (rtm); -if (owner) +if (owner) { +trace_contended_release (rwb);rt_mutex_wake_q_add_task (&wqh, owner, state); + }/* Pairs with the preempt_enable in rt_mutex_wake_up_q() */ preempt_disable ();@@ -205,6 +207,8 @@ static inline void rwbase_write_unlock(struct rwbase_rt *rwb)unsigned long flags;raw_spin_lock_irqsave (&rtm->wait_lock, flags); +if (trace_contended_release_enabled () &&rt_mutex_has_waiters (rtm)) +trace_contended_release (rwb);__rwbase_write_unlock (rwb,WRITER_BIAS , flags); }@@ -214,6 +218,8 @@ static inline void rwbase_write_downgrade(struct rwbase_rt *rwb)unsigned long flags;raw_spin_lock_irqsave (&rtm->wait_lock, flags); +if (trace_contended_release_enabled () &&rt_mutex_has_waiters (rtm)) +trace_contended_release (rwb);/* Release it and account current as reader */ __rwbase_write_unlock (rwb,WRITER_BIAS -1 , flags); }kernel/locking/rwsem.c +7
-2@@ -1390,6 +1390,7 @@ static inline void __up_read(struct rw_semaphore *sem)if (unlikely ((tmp & (RWSEM_LOCK_MASK |RWSEM_FLAG_WAITERS )) ==RWSEM_FLAG_WAITERS )) {clear_nonspinnable (sem); +trace_contended_release (sem);rwsem_wake (sem); }preempt_enable ();@@ -1413,8 +1414,10 @@ static inline void __up_write(struct rw_semaphore *sem)preempt_disable ();rwsem_clear_owner (sem); tmp =atomic_long_fetch_add_release (-RWSEM_WRITER_LOCKED , &sem->count); -if (unlikely (tmp &RWSEM_FLAG_WAITERS )) +if (unlikely (tmp &RWSEM_FLAG_WAITERS )) { +trace_contended_release (sem);rwsem_wake (sem); + }preempt_enable (); }@@ -1437,8 +1440,10 @@ static inline void __downgrade_write(struct rw_semaphore *sem) tmp =atomic_long_fetch_add_release ( -RWSEM_WRITER_LOCKED +RWSEM_READER_BIAS , &sem->count);rwsem_set_reader_owned (sem); -if (tmp &RWSEM_FLAG_WAITERS ) +if (tmp &RWSEM_FLAG_WAITERS ) { +trace_contended_release (sem);rwsem_downgrade_wake (sem); + }preempt_enable (); }kernel/locking/semaphore.c +3
-1@@ -231,8 +231,10 @@ void __sched up(struct semaphore *sem)else __up (sem, &wake_q);raw_spin_unlock_irqrestore (&sem->lock, flags); -if (!wake_q_empty (&wake_q)) +if (!wake_q_empty (&wake_q)) { +trace_contended_release (sem);wake_up_q (&wake_q); + } }EXPORT_SYMBOL (up);Signature · 1 line
2.52.0
Re: [PATCH v2 2/2] locking: Add contended_release tracepoint
Quoted 27 lines · Usama Arif
Quoted 25 lines · On Tue, 10 Mar 2026 17:49:39 +0000 Dmitry Ilvokhin <[email protected]> wrote:
Add the
contended_releasetrace event. This tracepoint fires on the holder side when a contended lock is released, complementing the existing contention_begin/contention_end tracepoints which fire on the waiter side.This enables correlating lock hold time under contention with waiter events by lock address.
Add
trace_contended_release()calls to the slowpath unlock paths of sleepable locks: mutex, rtmutex, semaphore, rwsem, percpu-rwsem, and RT-specific rwbase locks. Each call site fires only when there are blocked waiters being woken, exceptpercpu_up_write()which always wakes via__wake_up().Signed-off-by Dmitry Ilvokhin <[email protected]>--- include/trace/events/lock.h | 17 +++++++++++++++++ kernel/locking/mutex.c | 1 + kernel/locking/percpu-rwsem.c | 3 +++ kernel/locking/rtmutex.c | 1 + kernel/locking/rwbase_rt.c | 8 +++++++- kernel/locking/rwsem.c | 9 +++++++-- kernel/locking/semaphore.c | 4 +++- 7 files changed, 39 insertions(+), 4 deletions(-)
[...]
Quoted 13 lines
kernel/locking/percpu-rwsem.c void percpu_up_write(struct percpu_rw_semaphore *sem) · line 267
Quoted 10 lines
kernel/locking/percpu-rwsem.c +2
-0@@ -263,6 +263,8 @@ void percpu_up_write(struct percpu_rw_semaphore *sem) {rwsem_release (&sem->dep_map, _RET_IP_); +trace_contended_release (sem); +Hello!
Quoted 4 lines
I saw that you mentioned in the commmit message that you do this for only blocked waiters except for
percpu_up_write(). We can usewaitqueue_active(&sem->waiters)to check for this over here so that its consistent with every other call?
Thanks for the feedback, Usama.
I thought about it and even mentioned in the comment, but I forgot what was the reason. Now, I think you are correct. I added wq_has_sleeper() locally instead of waitqueue_active() locally, since we are not holding the lock here and waitqueue_active() requires a barrier based on the comment. It might be not very important here, but I'd rather make it correct even for tracepoint.
Note that __percpu_up_read() doesn't need this guard. Maybe I was thinking at __percpu_up_read() part before and just made it symmetric.
Anyway, thanks for suggestion.
Quoted 12 lines
kernel/locking/percpu-rwsem.c void __percpu_up_read(struct percpu_rw_semaphore *sem) · line 302
Quoted 10 lines
kernel/locking/percpu-rwsem.c +1
-0/* * Signal the writer is done, no fast path yet. * @@ -297,6 +299,7 @@ void __percpu_up_read(struct percpu_rw_semaphore *sem)* writer. */ smp_mb ();/* B matches C */ +trace_contended_release (sem);Should we do this after
this_cpu_dec(*sem->read_count)?
Good point. I moved it after this_cpu_dec() so the tracepoint fires after the lock is released but before rcuwait_wake_up(). I also went through all other call sites and made the placement consistent where
possible: after release, before wake. It should be fixed in v3.