mirror of
https://github.com/wolfpld/tracy.git
synced 2024-11-10 10:41:50 +00:00
Remove BlockingReaderWriterQueue and semephores.
This commit is contained in:
parent
3d21c4e7ea
commit
ff3382391e
@ -1,8 +1,3 @@
|
||||
// This license applies to all the code in this repository except that written by third
|
||||
// parties, namely the files in benchmarks/ext, which have their own licenses, and Jeff
|
||||
// Preshing's semaphore implementation (used in the blocking queues) which has a zlib
|
||||
// license (embedded in atomicops.h).
|
||||
//
|
||||
// Simplified BSD License:
|
||||
//
|
||||
// Copyright (c) 2013-2021, Cameron Desrochers
|
||||
@ -37,8 +32,6 @@
|
||||
// ©2013-2016 Cameron Desrochers.
|
||||
// Distributed under the simplified BSD license (see the license file that
|
||||
// should have come with this header).
|
||||
// Uses Jeff Preshing's semaphore implementation (under the terms of its
|
||||
// separate zlib license, embedded below).
|
||||
|
||||
#pragma once
|
||||
|
||||
@ -368,345 +361,6 @@ private:
|
||||
} // end namespace moodycamel
|
||||
|
||||
|
||||
|
||||
// Portable single-producer, single-consumer semaphore below:
|
||||
|
||||
#if defined(_WIN32)
|
||||
// Avoid including windows.h in a header; we only need a handful of
|
||||
// items, so we'll redeclare them here (this is relatively safe since
|
||||
// the API generally has to remain stable between Windows versions).
|
||||
// I know this is an ugly hack but it still beats polluting the global
|
||||
// namespace with thousands of generic names or adding a .cpp for nothing.
|
||||
extern "C" {
|
||||
struct _SECURITY_ATTRIBUTES;
|
||||
__declspec(dllimport) void* __stdcall CreateSemaphoreW(_SECURITY_ATTRIBUTES* lpSemaphoreAttributes, long lInitialCount, long lMaximumCount, const wchar_t* lpName);
|
||||
__declspec(dllimport) int __stdcall CloseHandle(void* hObject);
|
||||
__declspec(dllimport) unsigned long __stdcall WaitForSingleObject(void* hHandle, unsigned long dwMilliseconds);
|
||||
__declspec(dllimport) int __stdcall ReleaseSemaphore(void* hSemaphore, long lReleaseCount, long* lpPreviousCount);
|
||||
}
|
||||
#elif defined(__MACH__)
|
||||
#include <mach/mach.h>
|
||||
#elif defined(__unix__)
|
||||
#include <semaphore.h>
|
||||
#endif
|
||||
|
||||
namespace tracy
|
||||
{
|
||||
// Code in the spsc_sema namespace below is an adaptation of Jeff Preshing's
|
||||
// portable + lightweight semaphore implementations, originally from
|
||||
// https://github.com/preshing/cpp11-on-multicore/blob/master/common/sema.h
|
||||
// LICENSE:
|
||||
// Copyright (c) 2015 Jeff Preshing
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied
|
||||
// warranty. In no event will the authors be held liable for any damages
|
||||
// arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it
|
||||
// freely, subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented; you must not
|
||||
// claim that you wrote the original software. If you use this software
|
||||
// in a product, an acknowledgement in the product documentation would be
|
||||
// appreciated but is not required.
|
||||
// 2. Altered source versions must be plainly marked as such, and must not be
|
||||
// misrepresented as being the original software.
|
||||
// 3. This notice may not be removed or altered from any source distribution.
|
||||
namespace spsc_sema
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
class Semaphore
|
||||
{
|
||||
private:
|
||||
void* m_hSema;
|
||||
|
||||
Semaphore(const Semaphore& other);
|
||||
Semaphore& operator=(const Semaphore& other);
|
||||
|
||||
public:
|
||||
AE_NO_TSAN Semaphore(int initialCount = 0) : m_hSema()
|
||||
{
|
||||
assert(initialCount >= 0);
|
||||
const long maxLong = 0x7fffffff;
|
||||
m_hSema = CreateSemaphoreW(nullptr, initialCount, maxLong, nullptr);
|
||||
assert(m_hSema);
|
||||
}
|
||||
|
||||
AE_NO_TSAN ~Semaphore()
|
||||
{
|
||||
CloseHandle(m_hSema);
|
||||
}
|
||||
|
||||
bool wait() AE_NO_TSAN
|
||||
{
|
||||
const unsigned long infinite = 0xffffffff;
|
||||
return WaitForSingleObject(m_hSema, infinite) == 0;
|
||||
}
|
||||
|
||||
bool try_wait() AE_NO_TSAN
|
||||
{
|
||||
return WaitForSingleObject(m_hSema, 0) == 0;
|
||||
}
|
||||
|
||||
bool timed_wait(std::uint64_t usecs) AE_NO_TSAN
|
||||
{
|
||||
return WaitForSingleObject(m_hSema, (unsigned long)(usecs / 1000)) == 0;
|
||||
}
|
||||
|
||||
void signal(int count = 1) AE_NO_TSAN
|
||||
{
|
||||
while (!ReleaseSemaphore(m_hSema, count, nullptr));
|
||||
}
|
||||
};
|
||||
#elif defined(__MACH__)
|
||||
//---------------------------------------------------------
|
||||
// Semaphore (Apple iOS and OSX)
|
||||
// Can't use POSIX semaphores due to http://lists.apple.com/archives/darwin-kernel/2009/Apr/msg00010.html
|
||||
//---------------------------------------------------------
|
||||
class Semaphore
|
||||
{
|
||||
private:
|
||||
semaphore_t m_sema;
|
||||
|
||||
Semaphore(const Semaphore& other);
|
||||
Semaphore& operator=(const Semaphore& other);
|
||||
|
||||
public:
|
||||
AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema()
|
||||
{
|
||||
assert(initialCount >= 0);
|
||||
kern_return_t rc = semaphore_create(mach_task_self(), &m_sema, SYNC_POLICY_FIFO, initialCount);
|
||||
assert(rc == KERN_SUCCESS);
|
||||
AE_UNUSED(rc);
|
||||
}
|
||||
|
||||
AE_NO_TSAN ~Semaphore()
|
||||
{
|
||||
semaphore_destroy(mach_task_self(), m_sema);
|
||||
}
|
||||
|
||||
bool wait() AE_NO_TSAN
|
||||
{
|
||||
return semaphore_wait(m_sema) == KERN_SUCCESS;
|
||||
}
|
||||
|
||||
bool try_wait() AE_NO_TSAN
|
||||
{
|
||||
return timed_wait(0);
|
||||
}
|
||||
|
||||
bool timed_wait(std::uint64_t timeout_usecs) AE_NO_TSAN
|
||||
{
|
||||
mach_timespec_t ts;
|
||||
ts.tv_sec = static_cast<unsigned int>(timeout_usecs / 1000000);
|
||||
ts.tv_nsec = static_cast<int>((timeout_usecs % 1000000) * 1000);
|
||||
|
||||
// added in OSX 10.10: https://developer.apple.com/library/prerelease/mac/documentation/General/Reference/APIDiffsMacOSX10_10SeedDiff/modules/Darwin.html
|
||||
kern_return_t rc = semaphore_timedwait(m_sema, ts);
|
||||
return rc == KERN_SUCCESS;
|
||||
}
|
||||
|
||||
void signal() AE_NO_TSAN
|
||||
{
|
||||
while (semaphore_signal(m_sema) != KERN_SUCCESS);
|
||||
}
|
||||
|
||||
void signal(int count) AE_NO_TSAN
|
||||
{
|
||||
while (count-- > 0)
|
||||
{
|
||||
while (semaphore_signal(m_sema) != KERN_SUCCESS);
|
||||
}
|
||||
}
|
||||
};
|
||||
#elif defined(__unix__)
|
||||
//---------------------------------------------------------
|
||||
// Semaphore (POSIX, Linux)
|
||||
//---------------------------------------------------------
|
||||
class Semaphore
|
||||
{
|
||||
private:
|
||||
sem_t m_sema;
|
||||
|
||||
Semaphore(const Semaphore& other);
|
||||
Semaphore& operator=(const Semaphore& other);
|
||||
|
||||
public:
|
||||
AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema()
|
||||
{
|
||||
assert(initialCount >= 0);
|
||||
int rc = sem_init(&m_sema, 0, static_cast<unsigned int>(initialCount));
|
||||
assert(rc == 0);
|
||||
AE_UNUSED(rc);
|
||||
}
|
||||
|
||||
AE_NO_TSAN ~Semaphore()
|
||||
{
|
||||
sem_destroy(&m_sema);
|
||||
}
|
||||
|
||||
bool wait() AE_NO_TSAN
|
||||
{
|
||||
// http://stackoverflow.com/questions/2013181/gdb-causes-sem-wait-to-fail-with-eintr-error
|
||||
int rc;
|
||||
do
|
||||
{
|
||||
rc = sem_wait(&m_sema);
|
||||
}
|
||||
while (rc == -1 && errno == EINTR);
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
bool try_wait() AE_NO_TSAN
|
||||
{
|
||||
int rc;
|
||||
do {
|
||||
rc = sem_trywait(&m_sema);
|
||||
} while (rc == -1 && errno == EINTR);
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
bool timed_wait(std::uint64_t usecs) AE_NO_TSAN
|
||||
{
|
||||
struct timespec ts;
|
||||
const int usecs_in_1_sec = 1000000;
|
||||
const int nsecs_in_1_sec = 1000000000;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
ts.tv_sec += static_cast<time_t>(usecs / usecs_in_1_sec);
|
||||
ts.tv_nsec += static_cast<long>(usecs % usecs_in_1_sec) * 1000;
|
||||
// sem_timedwait bombs if you have more than 1e9 in tv_nsec
|
||||
// so we have to clean things up before passing it in
|
||||
if (ts.tv_nsec >= nsecs_in_1_sec) {
|
||||
ts.tv_nsec -= nsecs_in_1_sec;
|
||||
++ts.tv_sec;
|
||||
}
|
||||
|
||||
int rc;
|
||||
do {
|
||||
rc = sem_timedwait(&m_sema, &ts);
|
||||
} while (rc == -1 && errno == EINTR);
|
||||
return rc == 0;
|
||||
}
|
||||
|
||||
void signal() AE_NO_TSAN
|
||||
{
|
||||
while (sem_post(&m_sema) == -1);
|
||||
}
|
||||
|
||||
void signal(int count) AE_NO_TSAN
|
||||
{
|
||||
while (count-- > 0)
|
||||
{
|
||||
while (sem_post(&m_sema) == -1);
|
||||
}
|
||||
}
|
||||
};
|
||||
#else
|
||||
#error Unsupported platform! (No semaphore wrapper available)
|
||||
#endif
|
||||
|
||||
//---------------------------------------------------------
|
||||
// LightweightSemaphore
|
||||
//---------------------------------------------------------
|
||||
class LightweightSemaphore
|
||||
{
|
||||
public:
|
||||
typedef std::make_signed<std::size_t>::type ssize_t;
|
||||
|
||||
private:
|
||||
weak_atomic<ssize_t> m_count;
|
||||
Semaphore m_sema;
|
||||
|
||||
bool waitWithPartialSpinning(std::int64_t timeout_usecs = -1) AE_NO_TSAN
|
||||
{
|
||||
ssize_t oldCount;
|
||||
// Is there a better way to set the initial spin count?
|
||||
// If we lower it to 1000, testBenaphore becomes 15x slower on my Core i7-5930K Windows PC,
|
||||
// as threads start hitting the kernel semaphore.
|
||||
int spin = 1024;
|
||||
while (--spin >= 0)
|
||||
{
|
||||
if (m_count.load() > 0)
|
||||
{
|
||||
m_count.fetch_add_acquire(-1);
|
||||
return true;
|
||||
}
|
||||
compiler_fence(memory_order_acquire); // Prevent the compiler from collapsing the loop.
|
||||
}
|
||||
oldCount = m_count.fetch_add_acquire(-1);
|
||||
if (oldCount > 0)
|
||||
return true;
|
||||
if (timeout_usecs < 0)
|
||||
{
|
||||
if (m_sema.wait())
|
||||
return true;
|
||||
}
|
||||
if (timeout_usecs > 0 && m_sema.timed_wait(static_cast<uint64_t>(timeout_usecs)))
|
||||
return true;
|
||||
// At this point, we've timed out waiting for the semaphore, but the
|
||||
// count is still decremented indicating we may still be waiting on
|
||||
// it. So we have to re-adjust the count, but only if the semaphore
|
||||
// wasn't signaled enough times for us too since then. If it was, we
|
||||
// need to release the semaphore too.
|
||||
while (true)
|
||||
{
|
||||
oldCount = m_count.fetch_add_release(1);
|
||||
if (oldCount < 0)
|
||||
return false; // successfully restored things to the way they were
|
||||
// Oh, the producer thread just signaled the semaphore after all. Try again:
|
||||
oldCount = m_count.fetch_add_acquire(-1);
|
||||
if (oldCount > 0 && m_sema.try_wait())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
AE_NO_TSAN LightweightSemaphore(ssize_t initialCount = 0) : m_count(initialCount), m_sema()
|
||||
{
|
||||
assert(initialCount >= 0);
|
||||
}
|
||||
|
||||
bool tryWait() AE_NO_TSAN
|
||||
{
|
||||
if (m_count.load() > 0)
|
||||
{
|
||||
m_count.fetch_add_acquire(-1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool wait() AE_NO_TSAN
|
||||
{
|
||||
return tryWait() || waitWithPartialSpinning();
|
||||
}
|
||||
|
||||
bool wait(std::int64_t timeout_usecs) AE_NO_TSAN
|
||||
{
|
||||
return tryWait() || waitWithPartialSpinning(timeout_usecs);
|
||||
}
|
||||
|
||||
void signal(ssize_t count = 1) AE_NO_TSAN
|
||||
{
|
||||
assert(count >= 0);
|
||||
ssize_t oldCount = m_count.fetch_add_release(count);
|
||||
assert(oldCount >= -1);
|
||||
if (oldCount < 0)
|
||||
{
|
||||
m_sema.signal(1);
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t availableApprox() const AE_NO_TSAN
|
||||
{
|
||||
ssize_t count = m_count.load();
|
||||
return count > 0 ? static_cast<std::size_t>(count) : 0;
|
||||
}
|
||||
};
|
||||
} // end namespace spsc_sema
|
||||
} // end namespace moodycamel
|
||||
|
||||
#if defined(AE_VCPP) && (_MSC_VER < 1700 || defined(__cplusplus_cli))
|
||||
#pragma warning(pop)
|
||||
#ifdef __cplusplus_cli
|
||||
@ -1465,224 +1119,6 @@ private:
|
||||
#endif
|
||||
};
|
||||
|
||||
// Like ReaderWriterQueue, but also providees blocking operations
|
||||
template<typename T, size_t MAX_BLOCK_SIZE = 512>
|
||||
class BlockingReaderWriterQueue
|
||||
{
|
||||
private:
|
||||
typedef ::tracy::ReaderWriterQueue<T, MAX_BLOCK_SIZE> ReaderWriterQueue;
|
||||
|
||||
public:
|
||||
explicit BlockingReaderWriterQueue(size_t size = 15) AE_NO_TSAN
|
||||
: inner(size), sema(new spsc_sema::LightweightSemaphore())
|
||||
{ }
|
||||
|
||||
BlockingReaderWriterQueue(BlockingReaderWriterQueue&& other) AE_NO_TSAN
|
||||
: inner(std::move(other.inner)), sema(std::move(other.sema))
|
||||
{ }
|
||||
|
||||
BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue&& other) AE_NO_TSAN
|
||||
{
|
||||
std::swap(sema, other.sema);
|
||||
std::swap(inner, other.inner);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
// Enqueues a copy of element if there is room in the queue.
|
||||
// Returns true if the element was enqueued, false otherwise.
|
||||
// Does not allocate memory.
|
||||
AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN
|
||||
{
|
||||
if (inner.try_enqueue(element)) {
|
||||
sema->signal();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enqueues a moved copy of element if there is room in the queue.
|
||||
// Returns true if the element was enqueued, false otherwise.
|
||||
// Does not allocate memory.
|
||||
AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN
|
||||
{
|
||||
if (inner.try_enqueue(std::forward<T>(element))) {
|
||||
sema->signal();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#if MOODYCAMEL_HAS_EMPLACE
|
||||
// Like try_enqueue() but with emplace semantics (i.e. construct-in-place).
|
||||
template<typename... Args>
|
||||
AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN
|
||||
{
|
||||
if (inner.try_emplace(std::forward<Args>(args)...)) {
|
||||
sema->signal();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Enqueues a copy of element on the queue.
|
||||
// Allocates an additional block of memory if needed.
|
||||
// Only fails (returns false) if memory allocation fails.
|
||||
AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN
|
||||
{
|
||||
if (inner.enqueue(element)) {
|
||||
sema->signal();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enqueues a moved copy of element on the queue.
|
||||
// Allocates an additional block of memory if needed.
|
||||
// Only fails (returns false) if memory allocation fails.
|
||||
AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN
|
||||
{
|
||||
if (inner.enqueue(std::forward<T>(element))) {
|
||||
sema->signal();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#if MOODYCAMEL_HAS_EMPLACE
|
||||
// Like enqueue() but with emplace semantics (i.e. construct-in-place).
|
||||
template<typename... Args>
|
||||
AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN
|
||||
{
|
||||
if (inner.emplace(std::forward<Args>(args)...)) {
|
||||
sema->signal();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Attempts to dequeue an element; if the queue is empty,
|
||||
// returns false instead. If the queue has at least one element,
|
||||
// moves front to result using operator=, then returns true.
|
||||
template<typename U>
|
||||
bool try_dequeue(U& result) AE_NO_TSAN
|
||||
{
|
||||
if (sema->tryWait()) {
|
||||
bool success = inner.try_dequeue(result);
|
||||
assert(success);
|
||||
AE_UNUSED(success);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Attempts to dequeue an element; if the queue is empty,
|
||||
// waits until an element is available, then dequeues it.
|
||||
template<typename U>
|
||||
void wait_dequeue(U& result) AE_NO_TSAN
|
||||
{
|
||||
while (!sema->wait());
|
||||
bool success = inner.try_dequeue(result);
|
||||
AE_UNUSED(result);
|
||||
assert(success);
|
||||
AE_UNUSED(success);
|
||||
}
|
||||
|
||||
|
||||
// Attempts to dequeue an element; if the queue is empty,
|
||||
// waits until an element is available up to the specified timeout,
|
||||
// then dequeues it and returns true, or returns false if the timeout
|
||||
// expires before an element can be dequeued.
|
||||
// Using a negative timeout indicates an indefinite timeout,
|
||||
// and is thus functionally equivalent to calling wait_dequeue.
|
||||
template<typename U>
|
||||
bool wait_dequeue_timed(U& result, std::int64_t timeout_usecs) AE_NO_TSAN
|
||||
{
|
||||
if (!sema->wait(timeout_usecs)) {
|
||||
return false;
|
||||
}
|
||||
bool success = inner.try_dequeue(result);
|
||||
AE_UNUSED(result);
|
||||
assert(success);
|
||||
AE_UNUSED(success);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
#if __cplusplus > 199711L || _MSC_VER >= 1700
|
||||
// Attempts to dequeue an element; if the queue is empty,
|
||||
// waits until an element is available up to the specified timeout,
|
||||
// then dequeues it and returns true, or returns false if the timeout
|
||||
// expires before an element can be dequeued.
|
||||
// Using a negative timeout indicates an indefinite timeout,
|
||||
// and is thus functionally equivalent to calling wait_dequeue.
|
||||
template<typename U, typename Rep, typename Period>
|
||||
inline bool wait_dequeue_timed(U& result, std::chrono::duration<Rep, Period> const& timeout) AE_NO_TSAN
|
||||
{
|
||||
return wait_dequeue_timed(result, std::chrono::duration_cast<std::chrono::microseconds>(timeout).count());
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Returns a pointer to the front element in the queue (the one that
|
||||
// would be removed next by a call to `try_dequeue` or `pop`). If the
|
||||
// queue appears empty at the time the method is called, nullptr is
|
||||
// returned instead.
|
||||
// Must be called only from the consumer thread.
|
||||
AE_FORCEINLINE T* peek() const AE_NO_TSAN
|
||||
{
|
||||
return inner.peek();
|
||||
}
|
||||
|
||||
// Removes the front element from the queue, if any, without returning it.
|
||||
// Returns true on success, or false if the queue appeared empty at the time
|
||||
// `pop` was called.
|
||||
AE_FORCEINLINE bool pop() AE_NO_TSAN
|
||||
{
|
||||
if (sema->tryWait()) {
|
||||
bool result = inner.pop();
|
||||
assert(result);
|
||||
AE_UNUSED(result);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns the approximate number of items currently in the queue.
|
||||
// Safe to call from both the producer and consumer threads.
|
||||
AE_FORCEINLINE size_t size_approx() const AE_NO_TSAN
|
||||
{
|
||||
return sema->availableApprox();
|
||||
}
|
||||
|
||||
// Returns the total number of items that could be enqueued without incurring
|
||||
// an allocation when this queue is empty.
|
||||
// Safe to call from both the producer and consumer threads.
|
||||
//
|
||||
// NOTE: The actual capacity during usage may be different depending on the consumer.
|
||||
// If the consumer is removing elements concurrently, the producer cannot add to
|
||||
// the block the consumer is removing from until it's completely empty, except in
|
||||
// the case where the producer was writing to the same block the consumer was
|
||||
// reading from the whole time.
|
||||
AE_FORCEINLINE size_t max_capacity() const {
|
||||
return inner.max_capacity();
|
||||
}
|
||||
|
||||
private:
|
||||
// Disable copying & assignment
|
||||
BlockingReaderWriterQueue(BlockingReaderWriterQueue const&) { }
|
||||
BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue const&) { }
|
||||
|
||||
private:
|
||||
ReaderWriterQueue inner;
|
||||
std::unique_ptr<spsc_sema::LightweightSemaphore> sema;
|
||||
};
|
||||
|
||||
} // end namespace moodycamel
|
||||
|
||||
#ifdef AE_VCPP
|
||||
|
Loading…
Reference in New Issue
Block a user