/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=8 et ft=cpp : */
/* 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/dom/ipc/ProcessPriorityManager.h"
#include "mozilla/Hal.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/HalTypes.h"
#include "mozilla/TimeStamp.h"
#include "prlog.h"
#include "nsWeakPtr.h"
#include "nsXULAppAPI.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsITimer.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIDocument.h"
#include "nsIDOMEventListener.h"
#include "nsIDOMWindow.h"
#include "nsIDOMEvent.h"
#include "nsIDOMEventTarget.h"
#include "nsIDOMDocument.h"
#include "nsPIDOMWindow.h"

#ifdef XP_WIN
#include <process.h>
#define getpid _getpid
#else
#include <unistd.h>
#endif

using namespace mozilla::hal;

namespace mozilla {
namespace dom {
namespace ipc {

namespace {
static bool sInitialized = false;

// Some header defines a LOG macro, but we don't want it here.
#ifdef LOG
#undef LOG
#endif

// Enable logging by setting
//
//   NSPR_LOG_MODULES=ProcessPriorityManager:5
//
// in your environment.

#ifdef PR_LOGGING
static PRLogModuleInfo* logModule = PR_NewLogModule("ProcessPriorityManager");
#define LOG(fmt, ...) \
  PR_LOG(logModule, PR_LOG_DEBUG, \
         ("[%d] ProcessPriorityManager - " fmt, getpid(), ##__VA_ARGS__))
#else
#define LOG(fmt, ...)
#endif

/**
 * This class listens to window creation and visibilitychange events and
 * informs the hal back-end when this process transitions between having no
 * visible top-level windows, and when it has at least one visible top-level
 * window.
 *
 *
 * An important heuristic here is that we don't mark a process as background
 * until it's had no visible top-level windows for some amount of time.
 *
 * We do this because the notion of visibility is tied to inner windows
 * (actually, to documents).  When we navigate a page with outer window W, we
 * first destroy W's inner window and document, then insert a new inner window
 * and document into W.  If not for our grace period, this transition could
 * cause us to inform hal that this process quickly transitioned from
 * foreground to background to foreground again.
 *
 */
class ProcessPriorityManager MOZ_FINAL
  : public nsIObserver
  , public nsIDOMEventListener
{
public:
  ProcessPriorityManager();
  void Init();

  NS_DECL_ISUPPORTS
  NS_DECL_NSIOBSERVER
  NS_DECL_NSIDOMEVENTLISTENER

private:
  void SetPriority(ProcessPriority aPriority);
  void OnContentDocumentGlobalCreated(nsISupports* aOuterWindow);
  void OnInnerWindowDestroyed();
  void OnGracePeriodTimerFired();
  void RecomputeNumVisibleWindows();

  // mProcessPriority tracks the priority we've given this process in hal,
  // except that, when the grace period timer is active,
  // mProcessPriority == BACKGROUND even though hal still thinks we're a
  // foreground process.
  ProcessPriority mProcessPriority;

  nsTArray<nsWeakPtr> mWindows;
  nsCOMPtr<nsITimer> mGracePeriodTimer;
  TimeStamp mStartupTime;
};

NS_IMPL_ISUPPORTS2(ProcessPriorityManager, nsIObserver, nsIDOMEventListener);

ProcessPriorityManager::ProcessPriorityManager()
  : mProcessPriority(PROCESS_PRIORITY_FOREGROUND)
  , mStartupTime(TimeStamp::Now())
{
}

void
ProcessPriorityManager::Init()
{
  LOG("Starting up.");

  // We can't do this in the constructor because we need to hold a strong ref
  // to |this| before calling these methods.
  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
  os->AddObserver(this, "content-document-global-created", /* ownsWeak = */ false);
  os->AddObserver(this, "inner-window-destroyed", /* ownsWeak = */ false);

  SetPriority(PROCESS_PRIORITY_FOREGROUND);
}

NS_IMETHODIMP
ProcessPriorityManager::Observe(
  nsISupports* aSubject,
  const char* aTopic,
  const PRUnichar* aData)
{
  if (!strcmp(aTopic, "content-document-global-created")) {
    OnContentDocumentGlobalCreated(aSubject);
  } else if (!strcmp(aTopic, "inner-window-destroyed")) {
    OnInnerWindowDestroyed();
  } else if (!strcmp(aTopic, "timer-callback")) {
    OnGracePeriodTimerFired();
  } else {
    MOZ_ASSERT(false);
  }
  return NS_OK;
}

NS_IMETHODIMP
ProcessPriorityManager::HandleEvent(
  nsIDOMEvent* aEvent)
{
  LOG("Got visibilitychange.");
  RecomputeNumVisibleWindows();
  return NS_OK;
}

void
ProcessPriorityManager::OnContentDocumentGlobalCreated(
  nsISupports* aOuterWindow)
{
  // Get the inner window (the topic of content-document-global-created is
  // the /outer/ window!).
  nsCOMPtr<nsPIDOMWindow> outerWindow = do_QueryInterface(aOuterWindow);
  NS_ENSURE_TRUE_VOID(outerWindow);
  nsCOMPtr<nsPIDOMWindow> innerWindow = outerWindow->GetCurrentInnerWindow();
  NS_ENSURE_TRUE_VOID(innerWindow);

  // We're only interested in top-level windows.
  nsCOMPtr<nsIDOMWindow> parentOuterWindow;
  innerWindow->GetScriptableParent(getter_AddRefs(parentOuterWindow));
  NS_ENSURE_TRUE_VOID(parentOuterWindow);
  if (parentOuterWindow != outerWindow) {
    return;
  }

  nsCOMPtr<nsIDOMEventTarget> target = do_QueryInterface(innerWindow);
  NS_ENSURE_TRUE_VOID(target);

  nsWeakPtr weakWin = do_GetWeakReference(innerWindow);
  NS_ENSURE_TRUE_VOID(weakWin);

  if (mWindows.Contains(weakWin)) {
    return;
  }

  target->AddSystemEventListener(NS_LITERAL_STRING("mozvisibilitychange"),
                                 this,
                                 /* useCapture = */ false,
                                 /* wantsUntrusted = */ false);

  mWindows.AppendElement(weakWin);
  RecomputeNumVisibleWindows();
}

void
ProcessPriorityManager::OnInnerWindowDestroyed()
{
  RecomputeNumVisibleWindows();
}

void
ProcessPriorityManager::RecomputeNumVisibleWindows()
{
  // We could try to be clever and count the number of visible windows, instead
  // of iterating over mWindows every time one window's visibility state changes.
  // But experience suggests that iterating over the windows is prone to fewer
  // errors (and one mistake doesn't mess you up for the entire session).
  // Moreover, mWindows should be a very short list, since it contains only
  // top-level content windows.

  bool allHidden = true;
  for (uint32_t i = 0; i < mWindows.Length(); i++) {
    nsCOMPtr<nsIDOMWindow> window = do_QueryReferent(mWindows[i]);
    if (!window) {
      mWindows.RemoveElementAt(i);
      i--;
      continue;
    }

    nsCOMPtr<nsIDOMDocument> doc;
    window->GetDocument(getter_AddRefs(doc));
    if (!doc) {
      continue;
    }

    bool hidden = false;
    doc->GetMozHidden(&hidden);
#ifdef DEBUG
    nsAutoString spec;
    doc->GetDocumentURI(spec);
    LOG("Document at %s has visibility %d.", NS_ConvertUTF16toUTF8(spec).get(), !hidden);
#endif

    allHidden = allHidden && hidden;

    // We could break out early from this loop if
    //   !hidden && mProcessPriority == BACKGROUND,
    // but then we might not clean up all the weak refs.
  }

  SetPriority(allHidden ?
              PROCESS_PRIORITY_BACKGROUND :
              PROCESS_PRIORITY_FOREGROUND);
}

void
ProcessPriorityManager::SetPriority(ProcessPriority aPriority)
{
  if (aPriority == mProcessPriority) {
    return;
  }

  if (aPriority == PROCESS_PRIORITY_BACKGROUND) {
    // If this is a foreground --> background transition, give ourselves a
    // grace period before informing hal.
    uint32_t gracePeriodMS = Preferences::GetUint("dom.ipc.processPriorityManager.gracePeriodMS", 1000);
    if (mGracePeriodTimer) {
      LOG("Grace period timer already active.");
      return;
    }

    LOG("Initializing grace period timer.");
    mProcessPriority = aPriority;
    mGracePeriodTimer = do_CreateInstance("@mozilla.org/timer;1");
    mGracePeriodTimer->Init(this, gracePeriodMS, nsITimer::TYPE_ONE_SHOT);

  } else if (aPriority == PROCESS_PRIORITY_FOREGROUND) {
    // If this is a background --> foreground transition, do it immediately, and
    // cancel the outstanding grace period timer, if there is one.
    if (mGracePeriodTimer) {
      mGracePeriodTimer->Cancel();
      mGracePeriodTimer = nullptr;
    }

    LOG("Setting priority to %d.", aPriority);
    mProcessPriority = aPriority;
    hal::SetProcessPriority(getpid(), aPriority);

  } else {
    MOZ_ASSERT(false);
  }
}

void
ProcessPriorityManager::OnGracePeriodTimerFired()
{
  LOG("Grace period timer fired; setting priority to %d.",
      PROCESS_PRIORITY_BACKGROUND);

  // mProcessPriority should already be BACKGROUND: We set it in
  // SetPriority(BACKGROUND), and we canceled this timer if there was an
  // intervening SetPriority(FOREGROUND) call.
  MOZ_ASSERT(mProcessPriority == PROCESS_PRIORITY_BACKGROUND);

  mGracePeriodTimer = nullptr;
  hal::SetProcessPriority(getpid(), PROCESS_PRIORITY_BACKGROUND);

  // We're in the background; dump as much memory as we can.
  nsCOMPtr<nsIMemoryReporterManager> mgr =
    do_GetService("@mozilla.org/memory-reporter-manager;1");
  if (mgr) {
    mgr->MinimizeMemoryUsage(/* callback = */ nullptr);
  }
}

} // anonymous namespace

void
InitProcessPriorityManager()
{
  if (sInitialized) {
    return;
  }

  // If IPC tabs aren't enabled at startup, don't bother with any of this.
  if (!Preferences::GetBool("dom.ipc.processPriorityManager.enabled") ||
      Preferences::GetBool("dom.ipc.tabs.disabled")) {
    return;
  }

  sInitialized = true;

  // If we're the master process, mark ourselves as such and don't create a
  // ProcessPriorityManager (we never want to mark the master process as
  // backgrounded).
  if (XRE_GetProcessType() == GeckoProcessType_Default) {
    LOG("This is the master process.");
    hal::SetProcessPriority(getpid(), PROCESS_PRIORITY_MASTER);
    return;
  }

  // This object is held alive by the observer service.
  nsRefPtr<ProcessPriorityManager> mgr = new ProcessPriorityManager();
  mgr->Init();
}

} // namespace ipc
} // namespace dom
} // namespace mozilla
