blob: df545ece1ea77535f1c3363ba0d190ac639dc820 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include "vm/compression.h"
#include <lib/boot-options/boot-options.h>
#include <lib/counters.h>
#include <ktl/algorithm.h>
#include <vm/lz4_compressor.h>
#include <vm/physmap.h>
#include <vm/pmm.h>
#include <vm/tri_page_storage.h>
namespace {
// We always add zx_ticks_t to any data that we store, so ensure that the maximum size of the
// compressed data combined with that would not require us to store more than a page.
constexpr size_t ensure_threshold(size_t threshold) {
if (threshold + sizeof(zx_ticks_t) > PAGE_SIZE) {
return PAGE_SIZE - sizeof(zx_ticks_t);
}
return threshold;
}
constexpr int bucket_for_ticks(zx_ticks_t start, zx_ticks_t end) {
// Turn the ticks range into seconds. As we want whole seconds we are happy to tolerate the
// rounding behavior of integer division here.
const uint64_t seconds = end > start ? (end - start) / ticks_per_second() : 0;
// Calculate the bucket and return it, clamping to the maximum number of buckets.
return ktl::min(seconds == 0 ? 0 : __builtin_ctzl(seconds) + 1,
VmCompression::kNumLogBuckets - 1);
}
} // namespace
VmCompression::CompressorGuard::~CompressorGuard() {
// Ensure the compressor instance was not left in an in progress state before returning it.
ASSERT(instance_.IsIdle());
}
VmCompression::CompressorGuard VmCompression::AcquireCompressor() {
Guard<Mutex> guard_{&instance_lock_};
return CompressorGuard(instance_, ktl::move(guard_));
}
VmCompression::VmCompression(fbl::RefPtr<VmCompressedStorage> storage,
fbl::RefPtr<VmCompressionStrategy> strategy,
size_t compression_threshold)
: storage_(ktl::move(storage)),
strategy_(ktl::move(strategy)),
compression_threshold_(ensure_threshold(compression_threshold)),
instance_(*this, kTempReferenceValue) {
ASSERT(storage_);
ASSERT(strategy_);
// Ensure we can steal space to store the compression timestamp.
ASSERT(compression_threshold_ + sizeof(zx_ticks_t) <= PAGE_SIZE);
}
VmCompression::~VmCompression() {
if (buffer_page_) {
pmm_free_page(buffer_page_);
}
}
VmCompression::CompressResult VmCompression::Compress(const void* page_src, zx_ticks_t now) {
// Take the compression lock so we can use the buffer_page_.
Guard<Mutex> guard{&compression_lock_};
// Ensure buffer page exists.
if (!buffer_page_) {
// Explicitly do not use delayed allocation since we might be under memory pressure.
zx_status_t status = pmm_alloc_page(0, &buffer_page_);
if (status != ZX_OK) {
return FailTag{};
}
}
compression_attempts_.fetch_add(1);
// Compress into the buffer page, measuring the time taken to do so.
const zx_duration_t start_runtime = Thread::Current::Get()->Runtime();
void* buffer_ptr = paddr_to_physmap(buffer_page_->paddr());
auto result = strategy_->Compress(page_src, buffer_ptr, compression_threshold_);
const zx_duration_t end_runtime = Thread::Current::Get()->Runtime();
if (likely(end_runtime > start_runtime)) {
compression_time_.fetch_add(end_runtime - start_runtime);
}
// The result is a different type so we need to convert, and it gives us a chance to record
// statistics.
if (ktl::holds_alternative<FailTag>(result)) {
compression_fail_.fetch_add(1);
return FailTag{};
}
if (ktl::holds_alternative<ZeroTag>(result)) {
compression_zero_page_.fetch_add(1);
return ZeroTag{};
}
// Have actual data to store.
DEBUG_ASSERT(ktl::holds_alternative<size_t>(result));
const size_t compressed_size = *ktl::get_if<size_t>(&result);
DEBUG_ASSERT(compressed_size > 0 && compressed_size <= compression_threshold_);
// Store the current ticks for tracking how long pages remain compressed. We had previously
// validated in the constructor that we would always have space on the page.
const size_t storage_size = compressed_size + sizeof(zx_ticks_t);
DEBUG_ASSERT(storage_size <= PAGE_SIZE);
*reinterpret_cast<zx_ticks_t*>(reinterpret_cast<uintptr_t>(buffer_ptr) + compressed_size) = now;
// Store the data, it takes ownership of the buffer_page_ and might return ownership of a page.
auto [maybe_ref, page] = storage_->Store(buffer_page_, storage_size);
buffer_page_ = page;
if (auto ref = maybe_ref) {
// Make sure the storage system never produced the temp reference.
ASSERT(!IsTempReference(*ref));
compression_success_.fetch_add(1);
return *ref;
}
compression_fail_.fetch_add(1);
return FailTag{};
}
void VmCompression::Decompress(CompressedRef ref, void* page_dest, zx_ticks_t now) {
if (unlikely(IsTempReference(ref))) {
DecompressTempReference(ref, page_dest);
return;
}
decompressions_.fetch_add(1);
// Lookup the data so we can decompress out.
auto [src, len] = storage_->CompressedData(ref);
// pull out the timestamp and determine how long this was compressed for.
DEBUG_ASSERT(len >= sizeof(zx_ticks_t));
const zx_ticks_t compressed_ticks =
*reinterpret_cast<zx_ticks_t*>(reinterpret_cast<uintptr_t>(src) + len - sizeof(zx_ticks_t));
const int bucket = bucket_for_ticks(compressed_ticks, now);
decompressions_within_log_seconds_[bucket].fetch_add(1);
// Decompress the data, excluding our timestamp, and measure how long decompression takes.
const zx_duration_t start_runtime = Thread::Current::Get()->Runtime();
strategy_->Decompress(src, len - sizeof(zx_ticks_t), page_dest);
const zx_duration_t end_runtime = Thread::Current::Get()->Runtime();
if (end_runtime > start_runtime) {
decompression_time_.fetch_add(end_runtime - start_runtime);
}
// Now that decompression is finished, free the backing memory.
storage_->Free(ref);
}
void VmCompression::Free(CompressedRef ref) {
if (unlikely(IsTempReference(ref))) {
FreeTempReference(ref);
return;
}
storage_->Free(ref);
decompression_skipped_.fetch_add(1);
}
VmCompression::Stats VmCompression::GetStats() const {
VmCompression::Stats stats =
VmCompression::Stats{.memory_usage = storage_->GetMemoryUsage(),
.compression_time = compression_time_,
.decompression_time = decompression_time_,
.total_page_compression_attempts = compression_attempts_,
.failed_page_compression_attempts = compression_fail_,
.total_page_decompressions = decompressions_,
.compressed_page_evictions = decompression_skipped_};
for (int i = 0; i < kNumLogBuckets; i++) {
stats.pages_decompressed_within_log_seconds[i] = decompressions_within_log_seconds_[i];
}
return stats;
}
void VmCompression::Dump() const {
printf("[zram]: Compression / decompression time %" PRIi64 "/%" PRIi64 " ns\n",
compression_time_.load(), decompression_time_.load());
printf("[zram]: Compression attempts: %zu success: %zu zero page: %zu failed: %zu\n",
compression_attempts_.load(), compression_success_.load(), compression_zero_page_.load(),
compression_fail_.load());
printf(
"[zram]: Total decompressions: %zu skipped: %zu within log seconds counts: %zu, %zu, %zu, %zu, %zu, %zu, %zu, %zu\n",
decompressions_.load(), decompression_skipped_.load(),
decompressions_within_log_seconds_[0].load(), decompressions_within_log_seconds_[1].load(),
decompressions_within_log_seconds_[2].load(), decompressions_within_log_seconds_[3].load(),
decompressions_within_log_seconds_[4].load(), decompressions_within_log_seconds_[5].load(),
decompressions_within_log_seconds_[6].load(), decompressions_within_log_seconds_[7].load());
strategy_->Dump();
storage_->Dump();
}
// static
fbl::RefPtr<VmCompression> VmCompression::CreateDefault() {
// See if we even have a strategy.
if ((gBootOptions->compression_strategy == CompressionStrategy::kNone) ||
(gBootOptions->compression_storage_strategy == CompressionStorageStrategy::kNone)) {
// It is an error to only have one of a storage or compressor strategy set.
if ((gBootOptions->compression_strategy == CompressionStrategy::kNone) ^
(gBootOptions->compression_storage_strategy == CompressionStorageStrategy::kNone)) {
printf(
"ERROR: Exactly one of kernel.compression.strategy and "
"kernel.compression.storage-strategy was defined\n");
}
return nullptr;
}
fbl::AllocChecker ac;
fbl::RefPtr<VmCompressedStorage> storage;
switch (gBootOptions->compression_storage_strategy) {
case CompressionStorageStrategy::kTriPage:
storage = fbl::AdoptRef<VmTriPageStorage>(new (&ac) VmTriPageStorage());
if (!ac.check()) {
printf("[ZRAM]: Failed to create tri_page compressed storage area\n");
return nullptr;
}
printf("[ZRAM]: Using compressed storage strategy: tri_page\n");
break;
case CompressionStorageStrategy::kNone:
// Original check should have handled this.
panic("Unreachable");
break;
}
ASSERT(storage);
if (gBootOptions->compression_threshold == 0 || gBootOptions->compression_threshold > 100) {
panic("ERROR: kernel.compression.threshold must be between 1 and 100");
}
const uint32_t threshold =
static_cast<uint32_t>(PAGE_SIZE) * gBootOptions->compression_threshold / 100u;
fbl::RefPtr<VmCompressionStrategy> strategy;
switch (gBootOptions->compression_strategy) {
case CompressionStrategy::kLz4:
strategy = VmLz4Compressor::Create();
if (!strategy) {
printf("[ZRAM]: Failed to create lz4 compressor\n");
return nullptr;
}
printf("[ZRAM]: Using compression strategy: lz4\n");
break;
case CompressionStrategy::kNone:
// Original check should have handled this.
panic("Unreachable");
break;
}
ASSERT(strategy);
fbl::RefPtr<VmCompression> compression = fbl::MakeRefCountedChecked<VmCompression>(
&ac, ktl::move(storage), ktl::move(strategy), threshold);
if (!ac.check()) {
printf("[ZRAM]: Failed to create compressor\n");
return nullptr;
}
ASSERT(compression);
return compression;
}
// These need to disable analysis as there is a requirement on the caller that the lock for the VMO
// who created this temporary reference is held, however at this point here we have no way to refer
// to that lock.
ktl::optional<vm_page_t*> VmCompression::MoveTempReference(CompressedRef ref)
TA_NO_THREAD_SAFETY_ANALYSIS {
DEBUG_ASSERT(IsTempReference(ref));
// The owner of the temp ref is the owner as page. So if we are seeing the temporary reference
// then we know page cannot progress (i.e. FinalizeState can be called), so we can safely
// perform the copy.
ASSERT(instance_.using_temp_reference_);
ASSERT(instance_.page_);
ASSERT(instance_.spare_page_);
void* addr = paddr_to_physmap(instance_.spare_page_->paddr());
ASSERT(addr);
Decompress(ref, addr);
vm_page_t* ret = instance_.spare_page_;
instance_.spare_page_ = nullptr;
return ret;
}
void VmCompression::FreeTempReference(CompressedRef ref) TA_NO_THREAD_SAFETY_ANALYSIS {
DEBUG_ASSERT(IsTempReference(ref));
ASSERT(instance_.using_temp_reference_);
ASSERT(instance_.page_);
instance_.using_temp_reference_ = false;
}
void VmCompression::DecompressTempReference(CompressedRef ref,
void* page_dest) TA_NO_THREAD_SAFETY_ANALYSIS {
DEBUG_ASSERT(IsTempReference(ref));
ASSERT(instance_.using_temp_reference_);
ASSERT(instance_.page_);
void* addr = paddr_to_physmap(instance_.page_->paddr());
ASSERT(addr);
memcpy(page_dest, addr, PAGE_SIZE);
FreeTempReference(ref);
}