/* -*- 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/BlockingResourceBase.h"

#ifdef DEBUG
#include "prthread.h"

#include "nsAutoPtr.h"

#ifndef MOZ_CALLSTACK_DISABLED
#include "CodeAddressService.h"
#include "nsHashKeys.h"
#include "mozilla/StackWalk.h"
#include "nsTHashtable.h"
#endif

#include "mozilla/CondVar.h"
#include "mozilla/DeadlockDetector.h"
#include "mozilla/ReentrantMonitor.h"
#include "mozilla/Mutex.h"

#if defined(MOZILLA_INTERNAL_API)
#include "GeckoProfiler.h"
#endif //MOZILLA_INTERNAL_API

#endif // ifdef DEBUG

namespace mozilla {
//
// BlockingResourceBase implementation
//

// static members
const char* const BlockingResourceBase::kResourceTypeName[] = {
  // needs to be kept in sync with BlockingResourceType
  "Mutex", "ReentrantMonitor", "CondVar"
};

#ifdef DEBUG

PRCallOnceType BlockingResourceBase::sCallOnce;
unsigned BlockingResourceBase::sResourceAcqnChainFrontTPI = (unsigned)-1;
BlockingResourceBase::DDT* BlockingResourceBase::sDeadlockDetector;


void
BlockingResourceBase::StackWalkCallback(uint32_t aFrameNumber, void* aPc,
                                        void* aSp, void* aClosure)
{
#ifndef MOZ_CALLSTACK_DISABLED
  AcquisitionState* state = (AcquisitionState*)aClosure;
  state->AppendElement(aPc);
#endif
}

void
BlockingResourceBase::GetStackTrace(AcquisitionState& aState)
{
#ifndef MOZ_CALLSTACK_DISABLED
  // Skip this function and the calling function.
  const uint32_t kSkipFrames = 2;

  aState.Clear();

  // NB: Ignore the return value, there's nothing useful we can do if this
  //     this fails.
  MozStackWalk(StackWalkCallback, kSkipFrames, 24, &aState, 0, nullptr);
#endif
}

/**
 * PrintCycle
 * Append to |aOut| detailed information about the circular
 * dependency in |aCycle|.  Returns true if it *appears* that this
 * cycle may represent an imminent deadlock, but this is merely a
 * heuristic; the value returned may be a false positive or false
 * negative.
 *
 * *NOT* thread safe.  Calls |Print()|.
 *
 * FIXME bug 456272 hack alert: because we can't write call
 * contexts into strings, all info is written to stderr, but only
 * some info is written into |aOut|
 */
bool
PrintCycle(const BlockingResourceBase::DDT::ResourceAcquisitionArray* aCycle,
           nsACString& aOut)
{
  NS_ASSERTION(aCycle->Length() > 1, "need > 1 element for cycle!");

  bool maybeImminent = true;

  fputs("=== Cyclical dependency starts at\n", stderr);
  aOut += "Cyclical dependency starts at\n";

  const BlockingResourceBase::DDT::ResourceAcquisitionArray::elem_type res =
    aCycle->ElementAt(0);
  maybeImminent &= res->Print(aOut);

  BlockingResourceBase::DDT::ResourceAcquisitionArray::index_type i;
  BlockingResourceBase::DDT::ResourceAcquisitionArray::size_type len =
    aCycle->Length();
  const BlockingResourceBase::DDT::ResourceAcquisitionArray::elem_type* it =
    1 + aCycle->Elements();
  for (i = 1; i < len - 1; ++i, ++it) {
    fputs("\n--- Next dependency:\n", stderr);
    aOut += "\nNext dependency:\n";

    maybeImminent &= (*it)->Print(aOut);
  }

  fputs("\n=== Cycle completed at\n", stderr);
  aOut += "Cycle completed at\n";
  (*it)->Print(aOut);

  return maybeImminent;
}

#ifndef MOZ_CALLSTACK_DISABLED
struct CodeAddressServiceLock final
{
  static void Unlock() { }
  static void Lock() { }
  static bool IsLocked() { return true; }
};

struct CodeAddressServiceStringAlloc final
{
  static char* copy(const char* aString) { return ::strdup(aString); }
  static void free(char* aString) { ::free(aString); }
};

class CodeAddressServiceStringTable final
{
public:
  CodeAddressServiceStringTable() : mSet(32) {}

  const char* Intern(const char* aString)
  {
    nsCharPtrHashKey* e = mSet.PutEntry(aString);
    return e->GetKey();
  }

  size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
  {
    return mSet.SizeOfExcludingThis(aMallocSizeOf);
  }

private:
  typedef nsTHashtable<nsCharPtrHashKey> StringSet;
  StringSet mSet;
};

typedef CodeAddressService<CodeAddressServiceStringTable,
                           CodeAddressServiceStringAlloc,
                           CodeAddressServiceLock> WalkTheStackCodeAddressService;
#endif

bool
BlockingResourceBase::Print(nsACString& aOut) const
{
  fprintf(stderr, "--- %s : %s",
          kResourceTypeName[mType], mName);
  aOut += BlockingResourceBase::kResourceTypeName[mType];
  aOut += " : ";
  aOut += mName;

  bool acquired = IsAcquired();

  if (acquired) {
    fputs(" (currently acquired)\n", stderr);
    aOut += " (currently acquired)\n";
  }

  fputs(" calling context\n", stderr);
#ifdef MOZ_CALLSTACK_DISABLED
  fputs("  [stack trace unavailable]\n", stderr);
#else
  const AcquisitionState& state = acquired ? mAcquired : mFirstSeen;

  WalkTheStackCodeAddressService addressService;

  for (uint32_t i = 0; i < state.Length(); i++) {
    const size_t kMaxLength = 1024;
    char buffer[kMaxLength];
    addressService.GetLocation(i + 1, state[i], buffer, kMaxLength);
    const char* fmt = "    %s\n";
    aOut.AppendLiteral("    ");
    aOut.Append(buffer);
    aOut.AppendLiteral("\n");
    fprintf(stderr, fmt, buffer);
  }

#endif

  return acquired;
}


BlockingResourceBase::BlockingResourceBase(
    const char* aName,
    BlockingResourceBase::BlockingResourceType aType)
  : mName(aName)
  , mType(aType)
#ifdef MOZ_CALLSTACK_DISABLED
  , mAcquired(false)
#else
  , mAcquired()
#endif
{
  MOZ_ASSERT(mName, "Name must be nonnull");
  // PR_CallOnce guaranatees that InitStatics is called in a
  // thread-safe way
  if (PR_SUCCESS != PR_CallOnce(&sCallOnce, InitStatics)) {
    NS_RUNTIMEABORT("can't initialize blocking resource static members");
  }

  mChainPrev = 0;
  sDeadlockDetector->Add(this);
}


BlockingResourceBase::~BlockingResourceBase()
{
  // we don't check for really obviously bad things like freeing
  // Mutexes while they're still locked.  it is assumed that the
  // base class, or its underlying primitive, will check for such
  // stupid mistakes.
  mChainPrev = 0;             // racy only for stupidly buggy client code
  if (sDeadlockDetector) {
    sDeadlockDetector->Remove(this);
  }
}


size_t
BlockingResourceBase::SizeOfDeadlockDetector(MallocSizeOf aMallocSizeOf)
{
  return sDeadlockDetector ?
      sDeadlockDetector->SizeOfIncludingThis(aMallocSizeOf) : 0;
}


PRStatus
BlockingResourceBase::InitStatics()
{
  PR_NewThreadPrivateIndex(&sResourceAcqnChainFrontTPI, 0);
  sDeadlockDetector = new DDT();
  if (!sDeadlockDetector) {
    NS_RUNTIMEABORT("can't allocate deadlock detector");
  }
  return PR_SUCCESS;
}


void
BlockingResourceBase::Shutdown()
{
  delete sDeadlockDetector;
  sDeadlockDetector = 0;
}


void
BlockingResourceBase::CheckAcquire()
{
  if (mType == eCondVar) {
    NS_NOTYETIMPLEMENTED(
      "FIXME bug 456272: annots. to allow CheckAcquire()ing condvars");
    return;
  }

  BlockingResourceBase* chainFront = ResourceChainFront();
  nsAutoPtr<DDT::ResourceAcquisitionArray> cycle(
    sDeadlockDetector->CheckAcquisition(
      chainFront ? chainFront : 0, this));
  if (!cycle) {
    return;
  }

#ifndef MOZ_CALLSTACK_DISABLED
  // Update the current stack before printing.
  GetStackTrace(mAcquired);
#endif

  fputs("###!!! ERROR: Potential deadlock detected:\n", stderr);
  nsAutoCString out("Potential deadlock detected:\n");
  bool maybeImminent = PrintCycle(cycle, out);

  if (maybeImminent) {
    fputs("\n###!!! Deadlock may happen NOW!\n\n", stderr);
    out.AppendLiteral("\n###!!! Deadlock may happen NOW!\n\n");
  } else {
    fputs("\nDeadlock may happen for some other execution\n\n",
          stderr);
    out.AppendLiteral("\nDeadlock may happen for some other execution\n\n");
  }

  // XXX can customize behavior on whether we /think/ deadlock is
  // XXX about to happen.  for example:
  // XXX   if (maybeImminent)
  //           NS_RUNTIMEABORT(out.get());
  NS_ERROR(out.get());
}


void
BlockingResourceBase::Acquire()
{
  if (mType == eCondVar) {
    NS_NOTYETIMPLEMENTED(
      "FIXME bug 456272: annots. to allow Acquire()ing condvars");
    return;
  }
  NS_ASSERTION(!IsAcquired(),
               "reacquiring already acquired resource");

  ResourceChainAppend(ResourceChainFront());

#ifdef MOZ_CALLSTACK_DISABLED
  mAcquired = true;
#else
  // Take a stack snapshot.
  GetStackTrace(mAcquired);
  if (mFirstSeen.IsEmpty()) {
    mFirstSeen = mAcquired;
  }
#endif
}


void
BlockingResourceBase::Release()
{
  if (mType == eCondVar) {
    NS_NOTYETIMPLEMENTED(
      "FIXME bug 456272: annots. to allow Release()ing condvars");
    return;
  }

  BlockingResourceBase* chainFront = ResourceChainFront();
  NS_ASSERTION(chainFront && IsAcquired(),
               "Release()ing something that hasn't been Acquire()ed");

  if (chainFront == this) {
    ResourceChainRemove();
  } else {
    // not an error, but makes code hard to reason about.
    NS_WARNING("Resource acquired is being released in non-LIFO order; why?\n");
    nsCString tmp;
    Print(tmp);

    // remove this resource from wherever it lives in the chain
    // we walk backwards in order of acquisition:
    //  (1)  ...node<-prev<-curr...
    //              /     /
    //  (2)  ...prev<-curr...
    BlockingResourceBase* curr = chainFront;
    BlockingResourceBase* prev = nullptr;
    while (curr && (prev = curr->mChainPrev) && (prev != this)) {
      curr = prev;
    }
    if (prev == this) {
      curr->mChainPrev = prev->mChainPrev;
    }
  }

  ClearAcquisitionState();
}


//
// Debug implementation of (OffTheBooks)Mutex
void
OffTheBooksMutex::Lock()
{
  CheckAcquire();
  PR_Lock(mLock);
  Acquire();       // protected by mLock
}

void
OffTheBooksMutex::Unlock()
{
  Release();                  // protected by mLock
  PRStatus status = PR_Unlock(mLock);
  NS_ASSERTION(PR_SUCCESS == status, "bad Mutex::Unlock()");
}


//
// Debug implementation of ReentrantMonitor
void
ReentrantMonitor::Enter()
{
  BlockingResourceBase* chainFront = ResourceChainFront();

  // the code below implements monitor reentrancy semantics

  if (this == chainFront) {
    // immediately re-entered the monitor: acceptable
    PR_EnterMonitor(mReentrantMonitor);
    ++mEntryCount;
    return;
  }

  // this is sort of a hack around not recording the thread that
  // owns this monitor
  if (chainFront) {
    for (BlockingResourceBase* br = ResourceChainPrev(chainFront);
         br;
         br = ResourceChainPrev(br)) {
      if (br == this) {
        NS_WARNING(
          "Re-entering ReentrantMonitor after acquiring other resources.");

        // show the caller why this is potentially bad
        CheckAcquire();

        PR_EnterMonitor(mReentrantMonitor);
        ++mEntryCount;
        return;
      }
    }
  }

  CheckAcquire();
  PR_EnterMonitor(mReentrantMonitor);
  NS_ASSERTION(mEntryCount == 0, "ReentrantMonitor isn't free!");
  Acquire();       // protected by mReentrantMonitor
  mEntryCount = 1;
}

void
ReentrantMonitor::Exit()
{
  if (--mEntryCount == 0) {
    Release();              // protected by mReentrantMonitor
  }
  PRStatus status = PR_ExitMonitor(mReentrantMonitor);
  NS_ASSERTION(PR_SUCCESS == status, "bad ReentrantMonitor::Exit()");
}

nsresult
ReentrantMonitor::Wait(PRIntervalTime aInterval)
{
  AssertCurrentThreadIn();

  // save monitor state and reset it to empty
  int32_t savedEntryCount = mEntryCount;
  AcquisitionState savedAcquisitionState = GetAcquisitionState();
  BlockingResourceBase* savedChainPrev = mChainPrev;
  mEntryCount = 0;
  ClearAcquisitionState();
  mChainPrev = 0;

  nsresult rv;
#if defined(MOZILLA_INTERNAL_API)
  {
    GeckoProfilerSleepRAII profiler_sleep;
#endif //MOZILLA_INTERNAL_API

    // give up the monitor until we're back from Wait()
    rv = PR_Wait(mReentrantMonitor, aInterval) == PR_SUCCESS ? NS_OK :
                                                               NS_ERROR_FAILURE;

#if defined(MOZILLA_INTERNAL_API)
  }
#endif //MOZILLA_INTERNAL_API

  // restore saved state
  mEntryCount = savedEntryCount;
  SetAcquisitionState(savedAcquisitionState);
  mChainPrev = savedChainPrev;

  return rv;
}


//
// Debug implementation of CondVar
nsresult
CondVar::Wait(PRIntervalTime aInterval)
{
  AssertCurrentThreadOwnsMutex();

  // save mutex state and reset to empty
  AcquisitionState savedAcquisitionState = mLock->GetAcquisitionState();
  BlockingResourceBase* savedChainPrev = mLock->mChainPrev;
  mLock->ClearAcquisitionState();
  mLock->mChainPrev = 0;

  // give up mutex until we're back from Wait()
  nsresult rv =
    PR_WaitCondVar(mCvar, aInterval) == PR_SUCCESS ? NS_OK : NS_ERROR_FAILURE;

  // restore saved state
  mLock->SetAcquisitionState(savedAcquisitionState);
  mLock->mChainPrev = savedChainPrev;

  return rv;
}

#endif // ifdef DEBUG


} // namespace mozilla
