Bug 1755823 - BaseProfilerSharedMutex and exclusive&shared RAII locks - r=canaltinova

This is a profiler-specific shared lock (aka readers-writer lock) implemented on top of RWLockImpl.
Similar to BaseProfilerMutex, it records which thread is currently holding the exclusive lock.

Differential Revision: https://phabricator.services.mozilla.com/D139916
This commit is contained in:
Gerald Squelart 2022-03-03 23:03:05 +00:00
parent 2e2899b994
commit b204f08834
2 changed files with 277 additions and 0 deletions

View File

@ -12,6 +12,7 @@
#include "mozilla/Atomics.h"
#include "mozilla/Maybe.h"
#include "mozilla/PlatformMutex.h"
#include "mozilla/PlatformRWLock.h"
#include "mozilla/BaseProfilerUtils.h"
namespace mozilla {
@ -183,6 +184,96 @@ class MOZ_RAII BaseProfilerMaybeAutoLock {
BaseProfilerMaybeMutex& mMaybeMutex;
};
class BaseProfilerSharedMutex : public ::mozilla::detail::RWLockImpl {
public:
#ifdef DEBUG
~BaseProfilerSharedMutex() {
MOZ_ASSERT(!BaseProfilerThreadId::FromNumber(mOwningThreadId).IsSpecified(),
"BaseProfilerMutex should have been unlocked when destroyed");
}
#endif // DEBUG
[[nodiscard]] bool IsLockedExclusiveOnCurrentThread() const {
return BaseProfilerThreadId::FromNumber(mOwningThreadId) ==
baseprofiler::profiler_current_thread_id();
}
void LockExclusive() {
const BaseProfilerThreadId tid = baseprofiler::profiler_current_thread_id();
MOZ_ASSERT(tid.IsSpecified());
MOZ_ASSERT(!IsLockedExclusiveOnCurrentThread(), "Recursive locking");
::mozilla::detail::RWLockImpl::writeLock();
MOZ_ASSERT(!BaseProfilerThreadId::FromNumber(mOwningThreadId).IsSpecified(),
"Not unlocked properly");
mOwningThreadId = tid.ToNumber();
}
void UnlockExclusive() {
MOZ_ASSERT(IsLockedExclusiveOnCurrentThread(),
"Unlocking when not locked here");
// We're still holding the mutex here, so it's safe to just reset
// `mOwningThreadId`.
mOwningThreadId = BaseProfilerThreadId{}.ToNumber();
writeUnlock();
}
void LockShared() { readLock(); }
void UnlockShared() { readUnlock(); }
private:
// Thread currently owning the exclusive lock, or 0.
// Atomic because it may be read at any time independent of the mutex.
// Relaxed because threads only need to know if they own it already, so:
// - If it's their id, only *they* wrote that value with a locked mutex.
// - If it's different from their thread id it doesn't matter what other
// number it is (0 or another id) and that it can change again at any time.
Atomic<typename BaseProfilerThreadId::NumberType, MemoryOrdering::Relaxed>
mOwningThreadId;
};
// RAII class to lock a shared mutex exclusively.
class MOZ_RAII BaseProfilerAutoLockExclusive {
public:
explicit BaseProfilerAutoLockExclusive(BaseProfilerSharedMutex& aSharedMutex)
: mSharedMutex(aSharedMutex) {
mSharedMutex.LockExclusive();
}
BaseProfilerAutoLockExclusive(const BaseProfilerAutoLockExclusive&) = delete;
BaseProfilerAutoLockExclusive& operator=(
const BaseProfilerAutoLockExclusive&) = delete;
BaseProfilerAutoLockExclusive(BaseProfilerAutoLockExclusive&&) = delete;
BaseProfilerAutoLockExclusive& operator=(BaseProfilerAutoLockExclusive&&) =
delete;
~BaseProfilerAutoLockExclusive() { mSharedMutex.UnlockExclusive(); }
private:
BaseProfilerSharedMutex& mSharedMutex;
};
// RAII class to lock a shared mutex non-exclusively, other
// BaseProfilerAutoLockShared's may happen in other threads.
class MOZ_RAII BaseProfilerAutoLockShared {
public:
explicit BaseProfilerAutoLockShared(BaseProfilerSharedMutex& aSharedMutex)
: mSharedMutex(aSharedMutex) {
mSharedMutex.LockShared();
}
BaseProfilerAutoLockShared(const BaseProfilerAutoLockShared&) = delete;
BaseProfilerAutoLockShared& operator=(const BaseProfilerAutoLockShared&) =
delete;
BaseProfilerAutoLockShared(BaseProfilerAutoLockShared&&) = delete;
BaseProfilerAutoLockShared& operator=(BaseProfilerAutoLockShared&&) = delete;
~BaseProfilerAutoLockShared() { mSharedMutex.UnlockShared(); }
private:
BaseProfilerSharedMutex& mSharedMutex;
};
} // namespace detail
} // namespace baseprofiler
} // namespace mozilla

View File

@ -9,6 +9,7 @@
#include "mozilla/Attributes.h"
#include "mozilla/BaseAndGeckoProfilerDetail.h"
#include "mozilla/BaseProfileJSONWriter.h"
#include "mozilla/BaseProfilerDetail.h"
#include "mozilla/FloatingPoint.h"
#include "mozilla/ProgressLogger.h"
#include "mozilla/ProportionValue.h"
@ -316,6 +317,190 @@ void TestBaseAndProfilerDetail() {
printf("TestBaseAndProfilerDetail done\n");
}
void TestSharedMutex() {
printf("TestSharedMutex...\n");
mozilla::baseprofiler::detail::BaseProfilerSharedMutex sm;
// First round of minimal tests in this thread.
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
sm.LockExclusive();
MOZ_RELEASE_ASSERT(sm.IsLockedExclusiveOnCurrentThread());
sm.UnlockExclusive();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
sm.LockShared();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
sm.UnlockShared();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
{
mozilla::baseprofiler::detail::BaseProfilerAutoLockExclusive exclusiveLock{
sm};
MOZ_RELEASE_ASSERT(sm.IsLockedExclusiveOnCurrentThread());
}
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
{
mozilla::baseprofiler::detail::BaseProfilerAutoLockShared sharedLock{sm};
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
}
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
// The following will run actions between two threads, to verify that
// exclusive and shared locks work as expected.
// These actions will happen from top to bottom.
// This will test all possible lock interactions.
enum NextAction { // State of the lock:
t1Starting, // (x=exclusive, s=shared, ?=blocked)
t2Starting, // t1 t2
t1LockExclusive, // x
t2LockExclusiveAndBlock, // x x? - Can't have two exclusives.
t1UnlockExclusive, // x
t2UnblockedAfterT1Unlock, // x
t1LockSharedAndBlock, // s? x - Can't have shared during excl
t2UnlockExclusive, // s
t1UnblockedAfterT2Unlock, // s
t2LockShared, // s s - Can have multiple shared locks
t1UnlockShared, // s
t2StillLockedShared, // s
t1LockExclusiveAndBlock, // x? s - Can't have excl during shared
t2UnlockShared, // x
t1UnblockedAfterT2UnlockShared, // x
t2CheckAfterT1Lock, // x
t1LastUnlockExclusive, // (unlocked)
done
};
// Each thread will repeatedly read this `nextAction`, and run actions that
// target it...
std::atomic<NextAction> nextAction{static_cast<NextAction>(0)};
// ... and advance to the next available action (which should usually be for
// the other thread).
auto AdvanceAction = [&nextAction]() {
MOZ_RELEASE_ASSERT(nextAction <= done);
nextAction = static_cast<NextAction>(static_cast<int>(nextAction) + 1);
};
std::thread t1{[&]() {
for (;;) {
switch (nextAction) {
case t1Starting:
AdvanceAction();
break;
case t1LockExclusive:
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
sm.LockExclusive();
MOZ_RELEASE_ASSERT(sm.IsLockedExclusiveOnCurrentThread());
AdvanceAction();
break;
case t1UnlockExclusive:
MOZ_RELEASE_ASSERT(sm.IsLockedExclusiveOnCurrentThread());
// Advance first, before unlocking, so that t2 sees the new state.
AdvanceAction();
sm.UnlockExclusive();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
break;
case t1LockSharedAndBlock:
// Advance action before attempting to lock after t2's exclusive lock.
AdvanceAction();
sm.LockShared();
// We will only acquire the lock after t1 unlocks.
MOZ_RELEASE_ASSERT(nextAction == t1UnblockedAfterT2Unlock);
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
AdvanceAction();
break;
case t1UnlockShared:
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
sm.UnlockShared();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
AdvanceAction();
break;
case t1LockExclusiveAndBlock:
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
// Advance action before attempting to lock after t2's shared lock.
AdvanceAction();
sm.LockExclusive();
// We will only acquire the lock after t2 unlocks.
MOZ_RELEASE_ASSERT(nextAction == t1UnblockedAfterT2UnlockShared);
MOZ_RELEASE_ASSERT(sm.IsLockedExclusiveOnCurrentThread());
AdvanceAction();
break;
case t1LastUnlockExclusive:
MOZ_RELEASE_ASSERT(sm.IsLockedExclusiveOnCurrentThread());
// Advance first, before unlocking, so that t2 sees the new state.
AdvanceAction();
sm.UnlockExclusive();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
break;
case done:
return;
default:
// Ignore other actions intended for t2.
break;
}
}
}};
std::thread t2{[&]() {
for (;;) {
switch (nextAction) {
case t2Starting:
AdvanceAction();
break;
case t2LockExclusiveAndBlock:
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
// Advance action before attempting to lock after t1's exclusive lock.
AdvanceAction();
sm.LockExclusive();
// We will only acquire the lock after t1 unlocks.
MOZ_RELEASE_ASSERT(nextAction == t2UnblockedAfterT1Unlock);
MOZ_RELEASE_ASSERT(sm.IsLockedExclusiveOnCurrentThread());
AdvanceAction();
break;
case t2UnlockExclusive:
MOZ_RELEASE_ASSERT(sm.IsLockedExclusiveOnCurrentThread());
// Advance first, before unlocking, so that t1 sees the new state.
AdvanceAction();
sm.UnlockExclusive();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
break;
case t2LockShared:
sm.LockShared();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
AdvanceAction();
break;
case t2StillLockedShared:
AdvanceAction();
break;
case t2UnlockShared:
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
sm.UnlockShared();
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
AdvanceAction();
break;
case t2CheckAfterT1Lock:
MOZ_RELEASE_ASSERT(!sm.IsLockedExclusiveOnCurrentThread());
AdvanceAction();
break;
case done:
return;
default:
// Ignore other actions intended for t1.
break;
}
}
}};
t1.join();
t2.join();
printf("TestSharedMutex done\n");
}
void TestProportionValue() {
printf("TestProportionValue...\n");
@ -5217,6 +5402,7 @@ int main()
TestProfilerUtils();
TestBaseAndProfilerDetail();
TestSharedMutex();
TestProportionValue();
TestProgressLogger();
// Note that there are two `TestProfiler{,Markers}` functions above, depending