зеркало из https://github.com/mozilla/gecko-dev.git
462 строки
15 KiB
C++
462 строки
15 KiB
C++
/* -*- 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/StaticPrefs_page_load.h"
|
|
#include "mozilla/StaticPrefs_javascript.h"
|
|
#include "mozilla/Unused.h"
|
|
#include "mozilla/ipc/IdleSchedulerParent.h"
|
|
#include "mozilla/Telemetry.h"
|
|
#include "nsSystemInfo.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "nsITimer.h"
|
|
#include "nsIThread.h"
|
|
|
|
namespace mozilla::ipc {
|
|
|
|
base::SharedMemory* IdleSchedulerParent::sActiveChildCounter = nullptr;
|
|
std::bitset<NS_IDLE_SCHEDULER_COUNTER_ARRAY_LENGHT>
|
|
IdleSchedulerParent::sInUseChildCounters;
|
|
LinkedList<IdleSchedulerParent> IdleSchedulerParent::sIdleAndGCRequests;
|
|
int32_t IdleSchedulerParent::sMaxConcurrentIdleTasksInChildProcesses = 1;
|
|
uint32_t IdleSchedulerParent::sMaxConcurrentGCs = 1;
|
|
uint32_t IdleSchedulerParent::sActiveGCs = 0;
|
|
bool IdleSchedulerParent::sRecordGCTelemetry = false;
|
|
uint32_t IdleSchedulerParent::sNumWaitingGC = 0;
|
|
uint32_t IdleSchedulerParent::sChildProcessesRunningPrioritizedOperation = 0;
|
|
uint32_t IdleSchedulerParent::sChildProcessesAlive = 0;
|
|
nsITimer* IdleSchedulerParent::sStarvationPreventer = nullptr;
|
|
|
|
uint32_t IdleSchedulerParent::sNumCPUs = 0;
|
|
uint32_t IdleSchedulerParent::sPrefConcurrentGCsMax = 0;
|
|
uint32_t IdleSchedulerParent::sPrefConcurrentGCsCPUDivisor = 0;
|
|
|
|
IdleSchedulerParent::IdleSchedulerParent() {
|
|
sChildProcessesAlive++;
|
|
|
|
uint32_t max_gcs_pref =
|
|
StaticPrefs::javascript_options_concurrent_multiprocess_gcs_max();
|
|
uint32_t cpu_divisor_pref =
|
|
StaticPrefs::javascript_options_concurrent_multiprocess_gcs_cpu_divisor();
|
|
if (!max_gcs_pref) {
|
|
max_gcs_pref = UINT32_MAX;
|
|
}
|
|
if (!cpu_divisor_pref) {
|
|
cpu_divisor_pref = 4;
|
|
}
|
|
|
|
if (!sNumCPUs) {
|
|
// While waiting for the real logical core count behave as if there was
|
|
// just one core.
|
|
sNumCPUs = 1;
|
|
|
|
// nsISystemInfo can be initialized only on the main thread.
|
|
nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
|
|
nsCOMPtr<nsIRunnable> runnable =
|
|
NS_NewRunnableFunction("cpucount getter", [thread]() {
|
|
ProcessInfo processInfo = {};
|
|
if (NS_SUCCEEDED(CollectProcessInfo(processInfo))) {
|
|
uint32_t num_cpus = processInfo.cpuCount;
|
|
// We have a new cpu count, Update the number of idle tasks.
|
|
nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
|
|
"IdleSchedulerParent::CalculateNumIdleTasks", [num_cpus]() {
|
|
// We're setting this within this lambda because it's run on
|
|
// the correct thread and avoids a race.
|
|
sNumCPUs = num_cpus;
|
|
|
|
// This reads the sPrefConcurrentGCsMax and
|
|
// sPrefConcurrentGCsCPUDivisor values set below, it will run
|
|
// after the code that sets those.
|
|
CalculateNumIdleTasks();
|
|
});
|
|
|
|
thread->Dispatch(runnable, NS_DISPATCH_NORMAL);
|
|
}
|
|
});
|
|
NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
|
|
}
|
|
|
|
if (sPrefConcurrentGCsMax != max_gcs_pref ||
|
|
sPrefConcurrentGCsCPUDivisor != cpu_divisor_pref) {
|
|
// We execute this if these preferences have changed. We also want to make
|
|
// sure it executes for the first IdleSchedulerParent, which it does because
|
|
// sPrefConcurrentGCsMax and sPrefConcurrentGCsCPUDivisor are initially
|
|
// zero.
|
|
sPrefConcurrentGCsMax = max_gcs_pref;
|
|
sPrefConcurrentGCsCPUDivisor = cpu_divisor_pref;
|
|
|
|
CalculateNumIdleTasks();
|
|
}
|
|
}
|
|
|
|
void IdleSchedulerParent::CalculateNumIdleTasks() {
|
|
MOZ_ASSERT(sNumCPUs);
|
|
MOZ_ASSERT(sPrefConcurrentGCsMax);
|
|
MOZ_ASSERT(sPrefConcurrentGCsCPUDivisor);
|
|
|
|
// On one and two processor (or hardware thread) systems this will
|
|
// allow one concurrent idle task.
|
|
sMaxConcurrentIdleTasksInChildProcesses = int32_t(std::max(sNumCPUs, 1u));
|
|
sMaxConcurrentGCs =
|
|
std::min(std::max(sNumCPUs / sPrefConcurrentGCsCPUDivisor, 1u),
|
|
sPrefConcurrentGCsMax);
|
|
|
|
if (sActiveChildCounter && sActiveChildCounter->memory()) {
|
|
static_cast<Atomic<int32_t>*>(
|
|
sActiveChildCounter->memory())[NS_IDLE_SCHEDULER_INDEX_OF_CPU_COUNTER] =
|
|
static_cast<int32_t>(sMaxConcurrentIdleTasksInChildProcesses);
|
|
}
|
|
IdleSchedulerParent::Schedule(nullptr);
|
|
}
|
|
|
|
IdleSchedulerParent::~IdleSchedulerParent() {
|
|
// We can't know if an active process just crashed, so we just always expect
|
|
// that is the case.
|
|
if (mChildId) {
|
|
sInUseChildCounters[mChildId] = false;
|
|
if (sActiveChildCounter && sActiveChildCounter->memory() &&
|
|
static_cast<Atomic<int32_t>*>(
|
|
sActiveChildCounter->memory())[mChildId]) {
|
|
--static_cast<Atomic<int32_t>*>(
|
|
sActiveChildCounter
|
|
->memory())[NS_IDLE_SCHEDULER_INDEX_OF_ACTIVITY_COUNTER];
|
|
static_cast<Atomic<int32_t>*>(sActiveChildCounter->memory())[mChildId] =
|
|
0;
|
|
}
|
|
}
|
|
|
|
if (mRunningPrioritizedOperation) {
|
|
--sChildProcessesRunningPrioritizedOperation;
|
|
}
|
|
|
|
if (mDoingGC) {
|
|
// Give back our GC token.
|
|
sActiveGCs--;
|
|
}
|
|
|
|
if (mRequestingGC) {
|
|
mRequestingGC.value()(false);
|
|
mRequestingGC = Nothing();
|
|
}
|
|
|
|
// Remove from the scheduler's queue.
|
|
if (isInList()) {
|
|
remove();
|
|
}
|
|
|
|
MOZ_ASSERT(sChildProcessesAlive > 0);
|
|
sChildProcessesAlive--;
|
|
if (sChildProcessesAlive == 0) {
|
|
MOZ_ASSERT(sIdleAndGCRequests.isEmpty());
|
|
delete sActiveChildCounter;
|
|
sActiveChildCounter = nullptr;
|
|
|
|
if (sStarvationPreventer) {
|
|
sStarvationPreventer->Cancel();
|
|
NS_RELEASE(sStarvationPreventer);
|
|
}
|
|
}
|
|
|
|
Schedule(nullptr);
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvInitForIdleUse(
|
|
InitForIdleUseResolver&& aResolve) {
|
|
// This must already be non-zero, if it is zero then the cleanup code for the
|
|
// shared memory (initialised below) will never run. The invariant is that if
|
|
// the shared memory is initialsed, then this is non-zero.
|
|
MOZ_ASSERT(sChildProcessesAlive > 0);
|
|
|
|
MOZ_ASSERT(IsNotDoingIdleTask());
|
|
|
|
// Create a shared memory object which is shared across all the relevant
|
|
// processes.
|
|
if (!sActiveChildCounter) {
|
|
sActiveChildCounter = new base::SharedMemory();
|
|
size_t shmemSize = NS_IDLE_SCHEDULER_COUNTER_ARRAY_LENGHT * sizeof(int32_t);
|
|
if (sActiveChildCounter->Create(shmemSize) &&
|
|
sActiveChildCounter->Map(shmemSize)) {
|
|
memset(sActiveChildCounter->memory(), 0, shmemSize);
|
|
sInUseChildCounters[NS_IDLE_SCHEDULER_INDEX_OF_ACTIVITY_COUNTER] = true;
|
|
sInUseChildCounters[NS_IDLE_SCHEDULER_INDEX_OF_CPU_COUNTER] = true;
|
|
static_cast<Atomic<int32_t>*>(
|
|
sActiveChildCounter
|
|
->memory())[NS_IDLE_SCHEDULER_INDEX_OF_CPU_COUNTER] =
|
|
static_cast<int32_t>(sMaxConcurrentIdleTasksInChildProcesses);
|
|
} else {
|
|
delete sActiveChildCounter;
|
|
sActiveChildCounter = nullptr;
|
|
}
|
|
}
|
|
Maybe<SharedMemoryHandle> activeCounter;
|
|
if (SharedMemoryHandle handle =
|
|
sActiveChildCounter ? sActiveChildCounter->CloneHandle() : nullptr) {
|
|
activeCounter.emplace(std::move(handle));
|
|
}
|
|
|
|
uint32_t unusedId = 0;
|
|
for (uint32_t i = 0; i < NS_IDLE_SCHEDULER_COUNTER_ARRAY_LENGHT; ++i) {
|
|
if (!sInUseChildCounters[i]) {
|
|
sInUseChildCounters[i] = true;
|
|
unusedId = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If there wasn't an empty item, we'll fallback to 0.
|
|
mChildId = unusedId;
|
|
|
|
aResolve(Tuple<mozilla::Maybe<SharedMemoryHandle>&&, const uint32_t&>(
|
|
std::move(activeCounter), mChildId));
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvRequestIdleTime(uint64_t aId,
|
|
TimeDuration aBudget) {
|
|
MOZ_ASSERT(aBudget);
|
|
MOZ_ASSERT(IsNotDoingIdleTask());
|
|
|
|
mCurrentRequestId = aId;
|
|
mRequestedIdleBudget = aBudget;
|
|
|
|
if (!isInList()) {
|
|
sIdleAndGCRequests.insertBack(this);
|
|
}
|
|
|
|
Schedule(this);
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvIdleTimeUsed(uint64_t aId) {
|
|
// The client can either signal that they've used the idle time or they're
|
|
// canceling the request. We cannot use a seperate cancel message because it
|
|
// could arrive after the parent has granted the request.
|
|
MOZ_ASSERT(IsWaitingForIdle() || IsDoingIdleTask());
|
|
|
|
// The parent process will always know the ID of the current request (since
|
|
// the IPC channel is reliable). The IDs are provided so that the client can
|
|
// check them (it's possible for the client to race ahead of the server).
|
|
MOZ_ASSERT(mCurrentRequestId == aId);
|
|
|
|
if (IsWaitingForIdle() && !mRequestingGC) {
|
|
remove();
|
|
}
|
|
mRequestedIdleBudget = TimeDuration();
|
|
Schedule(nullptr);
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvSchedule() {
|
|
Schedule(nullptr);
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvRunningPrioritizedOperation() {
|
|
++mRunningPrioritizedOperation;
|
|
if (mRunningPrioritizedOperation == 1) {
|
|
++sChildProcessesRunningPrioritizedOperation;
|
|
}
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvPrioritizedOperationDone() {
|
|
MOZ_ASSERT(mRunningPrioritizedOperation);
|
|
|
|
--mRunningPrioritizedOperation;
|
|
if (mRunningPrioritizedOperation == 0) {
|
|
--sChildProcessesRunningPrioritizedOperation;
|
|
Schedule(nullptr);
|
|
}
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvRequestGC(RequestGCResolver&& aResolver) {
|
|
MOZ_ASSERT(!mDoingGC);
|
|
MOZ_ASSERT(!mRequestingGC);
|
|
|
|
mRequestingGC = Some(aResolver);
|
|
if (!isInList()) {
|
|
sIdleAndGCRequests.insertBack(this);
|
|
}
|
|
|
|
sRecordGCTelemetry = true;
|
|
sNumWaitingGC++;
|
|
Schedule(nullptr);
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvStartedGC() {
|
|
if (mDoingGC) {
|
|
return IPC_OK();
|
|
}
|
|
|
|
mDoingGC = true;
|
|
sActiveGCs++;
|
|
|
|
if (mRequestingGC) {
|
|
sNumWaitingGC--;
|
|
// We have to respond to the request before dropping it, even though the
|
|
// content process is already doing the GC.
|
|
mRequestingGC.value()(true);
|
|
mRequestingGC = Nothing();
|
|
if (!IsWaitingForIdle()) {
|
|
remove();
|
|
}
|
|
sRecordGCTelemetry = true;
|
|
}
|
|
|
|
return IPC_OK();
|
|
}
|
|
|
|
IPCResult IdleSchedulerParent::RecvDoneGC() {
|
|
MOZ_ASSERT(mDoingGC);
|
|
sActiveGCs--;
|
|
mDoingGC = false;
|
|
sRecordGCTelemetry = true;
|
|
Schedule(nullptr);
|
|
return IPC_OK();
|
|
}
|
|
|
|
int32_t IdleSchedulerParent::ActiveCount() {
|
|
if (sActiveChildCounter) {
|
|
return (static_cast<Atomic<int32_t>*>(
|
|
sActiveChildCounter
|
|
->memory())[NS_IDLE_SCHEDULER_INDEX_OF_ACTIVITY_COUNTER]);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool IdleSchedulerParent::HasSpareCycles(int32_t aActiveCount) {
|
|
// We can run a new task if we have a spare core. If we're running a
|
|
// prioritised operation we halve the number of regular spare cores.
|
|
//
|
|
// sMaxConcurrentIdleTasksInChildProcesses will always be >0 so on 1 and 2
|
|
// core systems this will allow 1 idle tasks (0 if running a prioritized
|
|
// operation).
|
|
MOZ_ASSERT(sMaxConcurrentIdleTasksInChildProcesses > 0);
|
|
return sChildProcessesRunningPrioritizedOperation
|
|
? sMaxConcurrentIdleTasksInChildProcesses / 2 > aActiveCount
|
|
: sMaxConcurrentIdleTasksInChildProcesses > aActiveCount;
|
|
}
|
|
|
|
bool IdleSchedulerParent::HasSpareGCCycles() {
|
|
return sMaxConcurrentGCs > sActiveGCs;
|
|
}
|
|
|
|
void IdleSchedulerParent::SendIdleTime() {
|
|
// We would assert that IsWaitingForIdle() except after potentially removing
|
|
// the task from it's list this will return false. Instead check
|
|
// mRequestedIdleBudget.
|
|
MOZ_ASSERT(mRequestedIdleBudget);
|
|
Unused << SendIdleTime(mCurrentRequestId, mRequestedIdleBudget);
|
|
}
|
|
|
|
void IdleSchedulerParent::SendMayGC() {
|
|
MOZ_ASSERT(mRequestingGC);
|
|
mRequestingGC.value()(true);
|
|
mRequestingGC = Nothing();
|
|
mDoingGC = true;
|
|
sActiveGCs++;
|
|
sRecordGCTelemetry = true;
|
|
MOZ_ASSERT(sNumWaitingGC > 0);
|
|
sNumWaitingGC--;
|
|
}
|
|
|
|
void IdleSchedulerParent::Schedule(IdleSchedulerParent* aRequester) {
|
|
// Tasks won't update the active count until after they receive their message
|
|
// and start to run, so make a copy of it here and increment it for every task
|
|
// we schedule. It will become an estimate of how many tasks will be active
|
|
// shortly.
|
|
int32_t activeCount = ActiveCount();
|
|
|
|
if (aRequester && aRequester->mRunningPrioritizedOperation) {
|
|
// Prioritised operations are requested only for idle time requests, so this
|
|
// must be an idle time request.
|
|
MOZ_ASSERT(aRequester->IsWaitingForIdle());
|
|
|
|
// If the requester is prioritized, just let it run itself.
|
|
if (aRequester->isInList() && !aRequester->mRequestingGC) {
|
|
aRequester->remove();
|
|
}
|
|
aRequester->SendIdleTime();
|
|
activeCount++;
|
|
}
|
|
|
|
RefPtr<IdleSchedulerParent> idleRequester = sIdleAndGCRequests.getFirst();
|
|
|
|
bool has_spare_cycles = HasSpareCycles(activeCount);
|
|
bool has_spare_gc_cycles = HasSpareGCCycles();
|
|
|
|
while (idleRequester && (has_spare_cycles || has_spare_gc_cycles)) {
|
|
// Get the next element before potentially removing the current one from the
|
|
// list.
|
|
RefPtr<IdleSchedulerParent> next = idleRequester->getNext();
|
|
|
|
if (has_spare_cycles && idleRequester->IsWaitingForIdle()) {
|
|
// We can run an idle task.
|
|
activeCount++;
|
|
if (!idleRequester->mRequestingGC) {
|
|
idleRequester->remove();
|
|
}
|
|
idleRequester->SendIdleTime();
|
|
has_spare_cycles = HasSpareCycles(activeCount);
|
|
}
|
|
|
|
if (has_spare_gc_cycles && idleRequester->mRequestingGC) {
|
|
if (!idleRequester->IsWaitingForIdle()) {
|
|
idleRequester->remove();
|
|
}
|
|
idleRequester->SendMayGC();
|
|
has_spare_gc_cycles = HasSpareGCCycles();
|
|
}
|
|
|
|
idleRequester = next;
|
|
}
|
|
|
|
if (!sIdleAndGCRequests.isEmpty() && HasSpareCycles(activeCount)) {
|
|
EnsureStarvationTimer();
|
|
}
|
|
|
|
if (sRecordGCTelemetry) {
|
|
sRecordGCTelemetry = false;
|
|
Telemetry::Accumulate(Telemetry::GC_WAIT_FOR_IDLE_COUNT, sNumWaitingGC);
|
|
}
|
|
}
|
|
|
|
void IdleSchedulerParent::EnsureStarvationTimer() {
|
|
// Even though idle runnables aren't really guaranteed to get run ever (which
|
|
// is why most of them have the timer fallback), try to not let any child
|
|
// process' idle handling to starve forever in case other processes are busy
|
|
if (!sStarvationPreventer) {
|
|
// Reuse StaticPrefs::page_load_deprioritization_period(), since that
|
|
// is used on child side when deciding the minimum idle period.
|
|
NS_NewTimerWithFuncCallback(
|
|
&sStarvationPreventer, StarvationCallback, nullptr,
|
|
StaticPrefs::page_load_deprioritization_period(),
|
|
nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, "StarvationCallback");
|
|
}
|
|
}
|
|
|
|
void IdleSchedulerParent::StarvationCallback(nsITimer* aTimer, void* aData) {
|
|
RefPtr<IdleSchedulerParent> idleRequester = sIdleAndGCRequests.getFirst();
|
|
while (idleRequester) {
|
|
if (idleRequester->IsWaitingForIdle()) {
|
|
// Treat the first process waiting for idle time as running prioritized
|
|
// operation so that it gets run.
|
|
++idleRequester->mRunningPrioritizedOperation;
|
|
++sChildProcessesRunningPrioritizedOperation;
|
|
Schedule(idleRequester);
|
|
--idleRequester->mRunningPrioritizedOperation;
|
|
--sChildProcessesRunningPrioritizedOperation;
|
|
break;
|
|
}
|
|
|
|
idleRequester = idleRequester->getNext();
|
|
}
|
|
NS_RELEASE(sStarvationPreventer);
|
|
}
|
|
|
|
} // namespace mozilla::ipc
|