/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/ProfilerThreadRegistration.h"

#include "mozilla/ProfilerMarkers.h"
#include "mozilla/ProfilerThreadRegistry.h"
#include "nsString.h"
#ifdef MOZ_GECKO_PROFILER
#  include "platform.h"
#else
#  define profiler_mark_thread_awake()
#  define profiler_mark_thread_asleep()
#endif

namespace mozilla::profiler {

/* static */
MOZ_THREAD_LOCAL(ThreadRegistration*) ThreadRegistration::tlsThreadRegistration;

ThreadRegistration::ThreadRegistration(const char* aName, const void* aStackTop)
    : mData(aName, aStackTop) {
  auto* tls = GetTLS();
  if (MOZ_UNLIKELY(!tls)) {
    // No TLS, nothing can be done without it.
    return;
  }

  if (ThreadRegistration* rootRegistration = tls->get(); rootRegistration) {
    // This is a nested ThreadRegistration object, so the thread is already
    // registered in the TLS and ThreadRegistry and we don't need to register
    // again.
    MOZ_ASSERT(
        mData.Info().ThreadId() == rootRegistration->mData.Info().ThreadId(),
        "Thread being re-registered has changed its TID");
    // TODO: Use new name. This is currently not possible because the
    // TLS-stored RegisteredThread's ThreadInfo cannot be changed.
    // In the meantime, we record a marker that could be used in the frontend.
    PROFILER_MARKER_TEXT("Nested ThreadRegistration()", OTHER_Profiling,
                         MarkerOptions{},
                         ProfilerString8View::WrapNullTerminatedString(aName));
    return;
  }

  tls->set(this);
  ThreadRegistry::Register(OnThreadRef{*this});
  profiler_mark_thread_awake();
}

ThreadRegistration::~ThreadRegistration() {
  MOZ_ASSERT(profiler_current_thread_id() == mData.mInfo.ThreadId(),
             "ThreadRegistration must be destroyed on its thread");
  MOZ_ASSERT(!mDataMutex.IsLockedOnCurrentThread(),
             "Mutex shouldn't be locked here, as it's about to be destroyed "
             "in ~ThreadRegistration()");
  auto* tls = GetTLS();
  if (MOZ_UNLIKELY(!tls)) {
    // No TLS, nothing can be done without it.
    return;
  }

  if (ThreadRegistration* rootRegistration = tls->get(); rootRegistration) {
    if (rootRegistration != this) {
      // `this` is not in the TLS, so it was a nested registration, there is
      // nothing to unregister yet.
      PROFILER_MARKER_TEXT(
          "Nested ~ThreadRegistration()", OTHER_Profiling, MarkerOptions{},
          ProfilerString8View::WrapNullTerminatedString(mData.Info().Name()));
      return;
    }

    profiler_mark_thread_asleep();
#ifdef NIGHTLY_BUILD
    mData.RecordWakeCount();
#endif
    ThreadRegistry::Unregister(OnThreadRef{*this});
#ifdef DEBUG
    // After ThreadRegistry::Unregister, other threads should not be able to
    // find this ThreadRegistration, and shouldn't have kept any reference to
    // it across the ThreadRegistry mutex.
    MOZ_ASSERT(mDataMutex.TryLock(),
               "Mutex shouldn't be locked in any thread, as it's about to be "
               "destroyed in ~ThreadRegistration()");
    // Undo the above successful TryLock.
    mDataMutex.Unlock();
#endif  // DEBUG

    tls->set(nullptr);
    return;
  }

  // Already removed from the TLS!? This could happen with improperly-nested
  // register/unregister calls, and the first ThreadRegistration has already
  // been unregistered.
  // We cannot record a marker on this thread because it was already
  // unregistered. Send it to the main thread (unless this *is* already the
  // main thread, which has been unregistered); this may be useful to catch
  // mismatched register/unregister pairs in Firefox.
  if (!profiler_is_main_thread()) {
    nsAutoCString threadId("thread id: ");
    threadId.AppendInt(profiler_current_thread_id().ToNumber());
    threadId.AppendLiteral(", name: \"");
    threadId.AppendASCII(mData.Info().Name());
    threadId.AppendLiteral("\"");
    PROFILER_MARKER_TEXT(
        "~ThreadRegistration() but TLS is empty", OTHER_Profiling,
        MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
        threadId);
  }
}

/* static */
ProfilingStack* ThreadRegistration::RegisterThread(const char* aName,
                                                   const void* aStackTop) {
  auto* tls = GetTLS();
  if (MOZ_UNLIKELY(!tls)) {
    // No TLS, nothing can be done without it.
    return nullptr;
  }

  if (ThreadRegistration* rootRegistration = tls->get(); rootRegistration) {
    // Already registered, record the extra depth to ignore the matching
    // UnregisterThread.
    ++rootRegistration->mOtherRegistrations;
    // TODO: Use new name. This is currently not possible because the
    // TLS-stored RegisteredThread's ThreadInfo cannot be changed.
    // In the meantime, we record a marker that could be used in the frontend.
    PROFILER_MARKER_TEXT("Nested ThreadRegistration::RegisterThread()",
                         OTHER_Profiling, MarkerOptions{},
                         ProfilerString8View::WrapNullTerminatedString(aName));
    return &rootRegistration->mData.mProfilingStack;
  }

  // Create on heap, it self-registers with the TLS (its effective owner, so
  // we can forget the pointer after this), and with the Profiler.
  ThreadRegistration* tr = new ThreadRegistration(aName, aStackTop);
  tr->mIsOnHeap = true;
  return &tr->mData.mProfilingStack;
}

/* static */
void ThreadRegistration::UnregisterThread() {
  auto* tls = GetTLS();
  if (MOZ_UNLIKELY(!tls)) {
    // No TLS, nothing can be done without it.
    return;
  }

  if (ThreadRegistration* rootRegistration = tls->get(); rootRegistration) {
    if (rootRegistration->mOtherRegistrations != 0) {
      // This is assumed to be a matching UnregisterThread() for a nested
      // RegisterThread(). Decrease depth and we're done.
      --rootRegistration->mOtherRegistrations;
      // We don't know what name was used in the related RegisterThread().
      PROFILER_MARKER_UNTYPED("Nested ThreadRegistration::UnregisterThread()",
                              OTHER_Profiling);
      return;
    }

    if (!rootRegistration->mIsOnHeap) {
      // The root registration was not added by `RegisterThread()`, so it
      // shouldn't be deleted!
      // This could happen if there are un-paired `UnregisterThread` calls when
      // the initial registration (still alive) was done on the stack. We don't
      // know what name was used in the related RegisterThread().
      PROFILER_MARKER_UNTYPED("Excess ThreadRegistration::UnregisterThread()",
                              OTHER_Profiling, MarkerStack::Capture());
      return;
    }

    // This is the last `UnregisterThread()` that should match the first
    // `RegisterThread()` that created this ThreadRegistration on the heap.
    // Just delete this root registration, it will de-register itself from the
    // TLS (and from the Profiler).
    delete rootRegistration;
    return;
  }

  // There is no known ThreadRegistration for this thread, ignore this
  // request. We cannot record a marker on this thread because it was already
  // unregistered. Send it to the main thread (unless this *is* already the
  // main thread, which has been unregistered); this may be useful to catch
  // mismatched register/unregister pairs in Firefox.
  if (!profiler_is_main_thread()) {
    nsAutoCString threadId("thread id: ");
    threadId.AppendInt(profiler_current_thread_id().ToNumber());
    PROFILER_MARKER_TEXT(
        "ThreadRegistration::UnregisterThread() but TLS is empty",
        OTHER_Profiling,
        MarkerOptions(MarkerThreadId::MainThread(), MarkerStack::Capture()),
        threadId);
  }
}

}  // namespace mozilla::profiler
