// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/performance_manager/mechanisms/userspace_swap_chromeos.h"
#include <memory>
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/page_size.h"
#include "base/posix/safe_strerror.h"
#include "base/threading/scoped_blocking_call.h"
#include "chromeos/ash/components/memory/userspace_swap/region.h"
#include "chromeos/ash/components/memory/userspace_swap/swap_storage.h"
#include "chromeos/ash/components/memory/userspace_swap/userfaultfd.h"
#include "chromeos/ash/components/memory/userspace_swap/userspace_swap.h"
#include "chromeos/ash/components/memory/userspace_swap/userspace_swap.mojom.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/graph/node_attached_data.h"
#include "components/performance_manager/public/graph/process_node.h"
#include "components/performance_manager/public/performance_manager.h"
#include "components/performance_manager/public/render_process_host_proxy.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
namespace performance_manager {
namespace mechanism {
namespace userspace_swap {
namespace {
using ::ash::memory::userspace_swap::Region;
using ::ash::memory::userspace_swap::RendererSwapData;
using ::ash::memory::userspace_swap::SwapFile;
using ::ash::memory::userspace_swap::UserfaultFD;
using ::ash::memory::userspace_swap::UserspaceSwapConfig;
// We cache the swap device free space so we don't hammer the FS layer with
// unnecessary syscalls. The initial value of 30s was chosen because it seemed
// like a safe value that would prevent hitting the disk too frequently while
// preventing space from getting too low in times of heavy swap. Feel free to
// change it if you find a better value.
constexpr base::TimeDelta kSwapDeviceAvailableSpaceCheckInterval =
base::Seconds(30);
base::TimeTicks g_last_swap_device_free_space_check;
uint64_t g_swap_device_free_swap_bytes;
// We must bind our mojo remote on the UI thread, this callback does that.
void BindUserspaceSwapReceiverOnUIThread(
RenderProcessHostProxy proxy,
mojo::PendingReceiver<::userspace_swap::mojom::UserspaceSwap> receiver) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
content::RenderProcessHost* render_process_host = proxy.Get();
if (!render_process_host) {
return;
}
render_process_host->BindReceiver(std::move(receiver));
}
// UserspaceSwapMechanismData contains process node specific details and
// handles.
class UserspaceSwapMechanismData
: public ExternalNodeAttachedDataImpl<UserspaceSwapMechanismData> {
public:
explicit UserspaceSwapMechanismData(const ProcessNode* node) {}
~UserspaceSwapMechanismData() override = default;
std::unique_ptr<RendererSwapData> swap_data;
};
void InitializeProcessNodeOnGraph(int render_process_host_id,
base::ScopedFD uffd,
Region swap_area,
performance_manager::Graph* graph) {
// Now look up the ProcessNode so we can complete initialization.
DCHECK(graph);
DCHECK(uffd.is_valid());
const ProcessNode* process_node = nullptr;
for (const ProcessNode* proc : graph->GetAllProcessNodes()) {
if (proc->GetRenderProcessHostId().GetUnsafeValue() ==
render_process_host_id) {
process_node = proc;
break;
}
}
if (!process_node) {
LOG(ERROR) << "Couldn't find process node for rphid: "
<< render_process_host_id;
return;
}
if (UserspaceSwapMechanismData::Destroy(process_node)) {
LOG(ERROR) << "ProcessNode contained UserspaceSwapMechanismData";
return;
}
auto* data = UserspaceSwapMechanismData::GetOrCreate(process_node);
// If all other setup has completed successfully, we can tell the renderer to
// construct an implementation of userspace_swap::mojom::UserspaceSwap.
// The RenderProcessHostProxy is a WeakPtr that should only be accessed on the
// UI thread.
mojo::PendingRemote<::userspace_swap::mojom::UserspaceSwap> remote;
const RenderProcessHostProxy& proxy =
process_node->GetRenderProcessHostProxy();
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&BindUserspaceSwapReceiverOnUIThread, proxy,
remote.InitWithNewPipeAndPassReceiver()));
// Wrap up the received userfaultfd into a UserfaultFD instance.
std::unique_ptr<UserfaultFD> userfaultfd =
UserfaultFD::WrapFD(std::move(uffd));
// The SwapFile is always encrypted but the compression layer is optional.
SwapFile::Type swap_type = SwapFile::Type::kEncrypted;
if (UserspaceSwapConfig::Get().use_compressed_swap_file) {
swap_type =
static_cast<SwapFile::Type>(swap_type | SwapFile::Type::kCompressed);
}
std::unique_ptr<SwapFile> swap_file = SwapFile::Create(swap_type);
if (!swap_file) {
PLOG(ERROR) << "Unable to complete userspace swap initialization failure "
"creating swap file";
// If we can't create a swap file, then we will bail freeing our resources.
UserspaceSwapMechanismData::Destroy(process_node);
return;
}
data->swap_data = RendererSwapData::Create(
render_process_host_id, process_node->GetProcessId(),
std::move(userfaultfd), std::move(swap_file), swap_area,
std::move(remote));
}
} // namespace
bool IsEligibleToSwap(const ProcessNode* process_node) {
if (!process_node->GetProcess().IsValid()) {
return false;
}
auto* data = UserspaceSwapMechanismData::Get(process_node);
if (!data || !data->swap_data) {
return false;
}
// Now let the implementation decide if swap should actually be allowed.
return data->swap_data->SwapAllowed();
}
uint64_t GetSwapDeviceFreeSpaceBytes() {
auto now_ticks = base::TimeTicks::Now();
if (now_ticks - g_last_swap_device_free_space_check >
kSwapDeviceAvailableSpaceCheckInterval) {
g_last_swap_device_free_space_check = now_ticks;
g_swap_device_free_swap_bytes = SwapFile::GetBackingStoreFreeSpaceKB()
<< 10; // convert to bytes.
}
return g_swap_device_free_swap_bytes;
}
uint64_t GetProcessNodeSwapFileUsageBytes(const ProcessNode* process_node) {
auto* data = UserspaceSwapMechanismData::Get(process_node);
if (!data || !data->swap_data) {
return 0;
}
return data->swap_data->SwapDiskspaceUsedBytes();
}
uint64_t GetProcessNodeReclaimedBytes(const ProcessNode* process_node) {
auto* data = UserspaceSwapMechanismData::Get(process_node);
if (!data || !data->swap_data) {
return 0;
}
return data->swap_data->ReclaimedBytes();
}
uint64_t GetTotalSwapFileUsageBytes() {
return ash::memory::userspace_swap::GetGlobalSwapDiskspaceUsed();
}
uint64_t GetTotalReclaimedBytes() {
return ash::memory::userspace_swap::GetGlobalMemoryReclaimed();
}
void SwapProcessNode(const ProcessNode* process_node) {
base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
base::BlockingType::WILL_BLOCK);
auto* data = UserspaceSwapMechanismData::Get(process_node);
if (!data || !data->swap_data) {
return;
}
auto& swap_data = data->swap_data;
// SwapProcessNode always starts by determining exactly how many regions we
// can swap based on current swapfile usage for this renderer and globally.
static const size_t kPageSize = base::GetPageSize();
static const uint64_t kPagesPerRegion =
UserspaceSwapConfig::Get().number_of_pages_per_region;
static const uint64_t kRegionSize = kPagesPerRegion * kPageSize;
const auto& config = UserspaceSwapConfig::Get();
uint64_t swap_file_disk_space_used_bytes =
swap_data->SwapDiskspaceUsedBytes();
// This renderer can only swap up to what's available in the global swap file
// limit or what's available in it's own swap file limit.
int64_t available_swap_bytes =
std::min(config.maximum_swap_disk_space_bytes -
ash::memory::userspace_swap::GetGlobalSwapDiskspaceUsed(),
config.renderer_maximum_disk_swap_file_size_bytes -
swap_file_disk_space_used_bytes);
// We have a configurable limit to the number of regions we will consider per
// iteration and adjust based on how much disk space is actually
// available for us which was calculated before.
// Finally, we know how many regions this renderer is able to swap.
int64_t available_swap_regions = available_swap_bytes / kRegionSize;
int64_t total_regions_swapable =
std::min(static_cast<int64_t>(config.renderer_region_limit_per_swap),
available_swap_regions);
if (total_regions_swapable <= 0) {
// We don't have enough space available to swap a single region.
return;
}
// Now we know how many regions this renderer can theoretically swap after
// enforcing all configurable limits.
ash::memory::userspace_swap::SwapRenderer(
swap_data.get(), total_regions_swapable * kRegionSize);
}
UserspaceSwapInitializationImpl::UserspaceSwapInitializationImpl(
int render_process_host_id)
: render_process_host_id_(render_process_host_id) {
CHECK(UserspaceSwapInitializationImpl::UserspaceSwapSupportedAndEnabled());
}
UserspaceSwapInitializationImpl::~UserspaceSwapInitializationImpl() = default;
// static
bool UserspaceSwapInitializationImpl::UserspaceSwapSupportedAndEnabled() {
return ash::memory::userspace_swap::UserspaceSwapSupportedAndEnabled();
}
// static
void UserspaceSwapInitializationImpl::Create(
int render_process_host_id,
mojo::PendingReceiver<::userspace_swap::mojom::UserspaceSwapInitialization>
receiver) {
auto impl =
std::make_unique<UserspaceSwapInitializationImpl>(render_process_host_id);
mojo::MakeSelfOwnedReceiver(std::move(impl), std::move(receiver));
}
void UserspaceSwapInitializationImpl::TransferUserfaultFD(
uint64_t uffd_error,
mojo::PlatformHandle uffd_handle,
uint64_t mmap_error,
MemoryRegionPtr swap_area,
TransferUserfaultFDCallback cb) {
base::ScopedClosureRunner scr(std::move(cb));
if (received_transfer_cb_) {
return;
}
received_transfer_cb_ = true;
if (uffd_error != 0) {
LOG(ERROR) << "Unable to create userfaultfd for renderer: "
<< base::safe_strerror(uffd_error);
return;
}
if (mmap_error != 0) {
LOG(ERROR) << "Unable to create memory area for renderer: "
<< base::safe_strerror(mmap_error);
return;
}
if (!uffd_handle.is_valid()) {
LOG(ERROR) << "FD received is invalid.";
return;
}
// Make sure we're on the graph and complete the initialization.
PerformanceManager::CallOnGraph(
FROM_HERE, base::BindOnce(&InitializeProcessNodeOnGraph,
render_process_host_id_, uffd_handle.TakeFD(),
Region(swap_area->address, swap_area->length)));
}
} // namespace userspace_swap
} // namespace mechanism
} // namespace performance_manager