//===-- tsd_shared.h --------------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef SCUDO_TSD_SHARED_H_
#define SCUDO_TSD_SHARED_H_

#include "tsd.h"

#include "string_utils.h"

#if SCUDO_HAS_PLATFORM_TLS_SLOT
// This is a platform-provided header that needs to be on the include path when
// Scudo is compiled. It must declare a function with the prototype:
//   uintptr_t *getPlatformAllocatorTlsSlot()
// that returns the address of a thread-local word of storage reserved for
// Scudo, that must be zero-initialized in newly created threads.
#include "scudo_platform_tls_slot.h"
#endif

namespace scudo {

template <class Allocator, u32 TSDsArraySize, u32 DefaultTSDCount>
struct TSDRegistrySharedT {
  using ThisT = TSDRegistrySharedT<Allocator, TSDsArraySize, DefaultTSDCount>;

  struct ScopedTSD {
    ALWAYS_INLINE ScopedTSD(ThisT &TSDRegistry) {
      CurrentTSD = TSDRegistry.getTSDAndLock();
      DCHECK_NE(CurrentTSD, nullptr);
    }

    ~ScopedTSD() { CurrentTSD->unlock(); }

    TSD<Allocator> &operator*() { return *CurrentTSD; }

    TSD<Allocator> *operator->() {
      CurrentTSD->assertLocked(/*BypassCheck=*/false);
      return CurrentTSD;
    }

  private:
    TSD<Allocator> *CurrentTSD;
  };

  void init(Allocator *Instance) EXCLUDES(Mutex) {
    ScopedLock L(Mutex);
    // If more than one thread is initializing at the exact same moment, the
    // threads that lose don't need to do anything.
    if (UNLIKELY(atomic_load_relaxed(&Initialized) != 0))
      return;

    Instance->init();
    for (u32 I = 0; I < TSDsArraySize; I++)
      TSDs[I].init(Instance);
    const u32 NumberOfCPUs = getNumberOfCPUs();
    setNumberOfTSDs((NumberOfCPUs == 0) ? DefaultTSDCount
                                        : Min(NumberOfCPUs, DefaultTSDCount));
    atomic_store_relaxed(&Initialized, 1);
  }

  void initOnceMaybe(Allocator *Instance) {
    if (LIKELY(atomic_load_relaxed(&Initialized) != 0))
      return;
    init(Instance); // Sets Initialized.
  }

  void unmapTestOnly(Allocator *Instance) EXCLUDES(Mutex) {
    for (u32 I = 0; I < TSDsArraySize; I++) {
      TSDs[I].commitBack(Instance);
      TSDs[I] = {};
    }
    setCurrentTSD(nullptr);
    ScopedLock L(Mutex);
    atomic_store_relaxed(&Initialized, 0);
  }

  void drainCaches(Allocator *Instance) {
    ScopedLock L(Mutex);
    for (uptr I = 0; I < NumberOfTSDs; ++I) {
      TSDs[I].lock();
      Instance->drainCache(&TSDs[I]);
      TSDs[I].unlock();
    }
  }

  ALWAYS_INLINE void initThreadMaybe(Allocator *Instance,
                                     UNUSED bool MinimalInit) {
    if (LIKELY(getCurrentTSD()))
      return;
    initThread(Instance);
  }

  void disable() NO_THREAD_SAFETY_ANALYSIS {
    Mutex.lock();
    for (u32 I = 0; I < TSDsArraySize; I++)
      TSDs[I].lock();
  }

  void enable() NO_THREAD_SAFETY_ANALYSIS {
    for (s32 I = static_cast<s32>(TSDsArraySize - 1); I >= 0; I--)
      TSDs[I].unlock();
    Mutex.unlock();
  }

  bool setOption(Option O, sptr Value) {
    if (O == Option::MaxTSDsCount) {
      ScopedLock L(Mutex);
      return setNumberOfTSDs(static_cast<u32>(Value));
    }
    if (O == Option::ThreadDisableMemInit)
      setDisableMemInit(Value);
    // Not supported by the TSD Registry, but not an error either.
    return true;
  }

  bool getDisableMemInit() const { return *getTlsPtr() & 1; }

  void getStats(ScopedString *Str) EXCLUDES(Mutex) {
    ScopedLock L(Mutex);

    Str->append("Stats: SharedTSDs: %u available; total %u\n", NumberOfTSDs,
                TSDsArraySize);
    for (uptr I = 0; I < NumberOfTSDs; ++I) {
      TSDs[I].lock();
      // Theoretically, we want to mark TSD::lock()/TSD::unlock() with proper
      // thread annotations. However, given the TSD is only locked on shared
      // path, do the assertion in a separate path to avoid confusing the
      // analyzer.
      TSDs[I].assertLocked(/*BypassCheck=*/true);
      Str->append("  Shared TSD[%zu]:\n", I);
      TSDs[I].getSizeClassAllocator().getStats(Str);
      TSDs[I].unlock();
    }
  }

private:
  ALWAYS_INLINE TSD<Allocator> *getTSDAndLock() NO_THREAD_SAFETY_ANALYSIS {
    TSD<Allocator> *TSD = getCurrentTSD();
    DCHECK(TSD);
    // Try to lock the currently associated context.
    if (TSD->tryLock())
      return TSD;
    // If that fails, go down the slow path.
    if (TSDsArraySize == 1U) {
      // Only 1 TSD, not need to go any further.
      // The compiler will optimize this one way or the other.
      TSD->lock();
      return TSD;
    }
    return getTSDAndLockSlow(TSD);
  }

  ALWAYS_INLINE uptr *getTlsPtr() const {
#if SCUDO_HAS_PLATFORM_TLS_SLOT
    return reinterpret_cast<uptr *>(getPlatformAllocatorTlsSlot());
#else
    static thread_local uptr ThreadTSD;
    return &ThreadTSD;
#endif
  }

  static_assert(alignof(TSD<Allocator>) >= 2, "");

  ALWAYS_INLINE void setCurrentTSD(TSD<Allocator> *CurrentTSD) {
    *getTlsPtr() &= 1;
    *getTlsPtr() |= reinterpret_cast<uptr>(CurrentTSD);
  }

  ALWAYS_INLINE TSD<Allocator> *getCurrentTSD() {
    return reinterpret_cast<TSD<Allocator> *>(*getTlsPtr() & ~1ULL);
  }

  bool setNumberOfTSDs(u32 N) REQUIRES(Mutex) {
    if (N < NumberOfTSDs)
      return false;
    if (N > TSDsArraySize)
      N = TSDsArraySize;
    NumberOfTSDs = N;
    NumberOfCoPrimes = 0;
    // Compute all the coprimes of NumberOfTSDs. This will be used to walk the
    // array of TSDs in a random order. For details, see:
    // https://lemire.me/blog/2017/09/18/visiting-all-values-in-an-array-exactly-once-in-random-order/
    for (u32 I = 0; I < N; I++) {
      u32 A = I + 1;
      u32 B = N;
      // Find the GCD between I + 1 and N. If 1, they are coprimes.
      while (B != 0) {
        const u32 T = A;
        A = B;
        B = T % B;
      }
      if (A == 1)
        CoPrimes[NumberOfCoPrimes++] = I + 1;
    }
    return true;
  }

  void setDisableMemInit(bool B) {
    *getTlsPtr() &= ~1ULL;
    *getTlsPtr() |= B;
  }

  NOINLINE void initThread(Allocator *Instance) NO_THREAD_SAFETY_ANALYSIS {
    initOnceMaybe(Instance);
    // Initial context assignment is done in a plain round-robin fashion.
    const u32 Index = atomic_fetch_add(&CurrentIndex, 1U, memory_order_relaxed);
    setCurrentTSD(&TSDs[Index % NumberOfTSDs]);
    Instance->callPostInitCallback();
  }

  // TSDs is an array of locks which is not supported for marking thread-safety
  // capability.
  NOINLINE TSD<Allocator> *getTSDAndLockSlow(TSD<Allocator> *CurrentTSD)
      EXCLUDES(Mutex) {
    // Use the Precedence of the current TSD as our random seed. Since we are
    // in the slow path, it means that tryLock failed, and as a result it's
    // very likely that said Precedence is non-zero.
    const u32 R = static_cast<u32>(CurrentTSD->getPrecedence());
    u32 N, Inc;
    {
      ScopedLock L(Mutex);
      N = NumberOfTSDs;
      DCHECK_NE(NumberOfCoPrimes, 0U);
      Inc = CoPrimes[R % NumberOfCoPrimes];
    }
    if (N > 1U) {
      u32 Index = R % N;
      uptr LowestPrecedence = UINTPTR_MAX;
      TSD<Allocator> *CandidateTSD = nullptr;
      // Go randomly through at most 4 contexts and find a candidate.
      for (u32 I = 0; I < Min(4U, N); I++) {
        if (TSDs[Index].tryLock()) {
          setCurrentTSD(&TSDs[Index]);
          return &TSDs[Index];
        }
        const uptr Precedence = TSDs[Index].getPrecedence();
        // A 0 precedence here means another thread just locked this TSD.
        if (Precedence && Precedence < LowestPrecedence) {
          CandidateTSD = &TSDs[Index];
          LowestPrecedence = Precedence;
        }
        Index += Inc;
        if (Index >= N)
          Index -= N;
      }
      if (CandidateTSD) {
        CandidateTSD->lock();
        setCurrentTSD(CandidateTSD);
        return CandidateTSD;
      }
    }
    // Last resort, stick with the current one.
    CurrentTSD->lock();
    return CurrentTSD;
  }

  atomic_u32 CurrentIndex = {};
  u32 NumberOfTSDs GUARDED_BY(Mutex) = 0;
  u32 NumberOfCoPrimes GUARDED_BY(Mutex) = 0;
  u32 CoPrimes[TSDsArraySize] GUARDED_BY(Mutex) = {};
  atomic_u8 Initialized = {};
  // Used for global initialization and TSDs access.
  // Acquiring the global initialization should only lock once in normal
  // operation, which is why using it for TSDs access should not cause
  // any interference.
  HybridMutex Mutex;
  TSD<Allocator> TSDs[TSDsArraySize];
};

} // namespace scudo

#endif // SCUDO_TSD_SHARED_H_
