blob: 53cfa6e506fad970fecd8f1e601b68ebc7447890 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <lib/zircon-internal/thread_annotations.h>
#include <algorithm>
#include <random>
#include <gtest/gtest.h>
#include "src/storage/f2fs/f2fs.h"
#include "src/storage/lib/block_client/cpp/fake_block_device.h"
#include "unit_lib.h"
namespace f2fs {
namespace {
constexpr uint64_t kDefaultBlockCount = 143360;
class GcManagerTest : public F2fsFakeDevTestFixture {
public:
GcManagerTest(TestOptions options = TestOptions{.block_count = kDefaultBlockCount})
: F2fsFakeDevTestFixture(std::move(options)) {}
protected:
std::vector<std::string> MakeGcTriggerCondition(uint32_t invalidate_ratio = 25,
bool sync = true) {
auto prng = std::default_random_engine(testing::UnitTest::GetInstance()->random_seed());
fs_->GetGcManager().DisableFgGc();
std::vector<std::string> total_file_names;
uint32_t count = 0;
while (true) {
if (fs_->GetSegmentManager().HasNotEnoughFreeSecs()) {
break;
}
std::vector<std::string> file_names;
for (uint32_t i = 0; i < fs_->GetSuperblockInfo().GetBlocksPerSeg() &&
!fs_->GetSegmentManager().HasNotEnoughFreeSecs();
++i, ++count) {
std::string file_name = std::to_string(count);
zx::result test_file = root_dir_->Create(file_name, fs::CreationType::kFile);
EXPECT_TRUE(test_file.is_ok()) << test_file.status_string();
auto file_vn = fbl::RefPtr<File>::Downcast(*std::move(test_file));
std::array<char, kPageSize> buf;
f2fs_hash_t hash = DentryHash(file_name);
std::memcpy(buf.data(), &hash, sizeof(hash));
FileTester::AppendToFile(file_vn.get(), buf.data(), buf.size());
file_names.push_back(file_name);
EXPECT_EQ(file_vn->Close(), ZX_OK);
WritebackOperation op;
file_vn->Writeback(op);
}
sync_completion_t completion;
fs_->ScheduleWriter(&completion);
sync_completion_wait(&completion, ZX_TIME_INFINITE);
if (sync) {
fs_->SyncFs();
}
std::shuffle(file_names.begin(), file_names.end(), prng);
const uint64_t kDeleteSize = file_names.size() * invalidate_ratio / 100;
auto iter = file_names.begin();
for (uint64_t i = 0; i < kDeleteSize; ++i) {
EXPECT_EQ(root_dir_->Unlink(*iter, false), ZX_OK);
iter = file_names.erase(iter);
}
if (sync) {
fs_->SyncFs();
}
total_file_names.insert(total_file_names.end(), file_names.begin(), file_names.end());
}
fs_->GetGcManager().EnableFgGc();
return total_file_names;
}
};
TEST_F(GcManagerTest, CpError) {
fs_->GetSuperblockInfo().SetCpFlags(CpFlag::kCpErrorFlag);
auto result = fs_->GetGcManager().Run();
ASSERT_TRUE(result.is_error());
ASSERT_EQ(result.error_value(), ZX_ERR_BAD_STATE);
}
TEST_F(GcManagerTest, CheckpointDiskReadFailOnSyncFs) TA_NO_THREAD_SAFETY_ANALYSIS {
DisableFsck();
WritebackOperation op;
op.bSync = true;
fs_->GetMetaVnode().Writeback(op);
pgoff_t target_addr = fs_->GetSegmentManager().CurrentSitAddr(0) * kDefaultSectorsPerBlock;
auto hook = [target_addr](const block_fifo_request_t &_req, const zx::vmo *_vmo) {
if (_req.dev_offset == target_addr) {
return ZX_ERR_PEER_CLOSED;
}
return ZX_OK;
};
MakeGcTriggerCondition();
// Increse dirty page count to perform GC
{
fs_->GetSuperblockInfo().IncreasePageCount(CountType::kDirtyData);
DeviceTester::SetHook(fs_.get(), hook);
ASSERT_EQ(fs_->SyncFs(true), ZX_ERR_INTERNAL);
ASSERT_TRUE(fs_->GetSuperblockInfo().TestCpFlags(CpFlag::kCpErrorFlag));
DeviceTester::SetHook(fs_.get(), nullptr);
fs_->GetSuperblockInfo().DecreasePageCount(CountType::kDirtyData);
}
}
TEST_F(GcManagerTest, CheckpointDiskReadFailOnGc) TA_NO_THREAD_SAFETY_ANALYSIS {
DisableFsck();
WritebackOperation op;
op.bSync = true;
fs_->GetMetaVnode().Writeback(op);
pgoff_t target_addr = fs_->GetSegmentManager().CurrentSitAddr(0) * kDefaultSectorsPerBlock;
auto hook = [target_addr](const block_fifo_request_t &_req, const zx::vmo *_vmo) {
if (_req.dev_offset == target_addr) {
return ZX_ERR_PEER_CLOSED;
}
return ZX_OK;
};
MakeGcTriggerCondition();
// Check disk peer closed exception case in Run()
{
DeviceTester::SetHook(fs_.get(), hook);
ASSERT_EQ(fs_->GetGcManager().Run().error_value(), ZX_ERR_PEER_CLOSED);
ASSERT_TRUE(fs_->GetSuperblockInfo().TestCpFlags(CpFlag::kCpErrorFlag));
DeviceTester::SetHook(fs_.get(), nullptr);
}
}
TEST_F(GcManagerTest, CheckpointDiskReadFailOnGcPreFree) TA_NO_THREAD_SAFETY_ANALYSIS {
DisableFsck();
WritebackOperation op;
op.bSync = true;
fs_->GetMetaVnode().Writeback(op);
pgoff_t target_addr = fs_->GetSegmentManager().CurrentSitAddr(0) * kDefaultSectorsPerBlock;
auto hook = [target_addr](const block_fifo_request_t &_req, const zx::vmo *_vmo) {
if (_req.dev_offset == target_addr) {
return ZX_ERR_PEER_CLOSED;
}
return ZX_OK;
};
uint32_t prefree_segno = fs_->GetSegmentManager().CURSEG_I(CursegType::kCursegWarmData)->segno;
MakeGcTriggerCondition(25, false);
fs_->GetSegmentManager().LocateDirtySegment(prefree_segno + 1, DirtyType::kPre);
// Check disk peer closed exception case in Run()
{
DeviceTester::SetHook(fs_.get(), hook);
ASSERT_EQ(fs_->GetGcManager().Run().error_value(), ZX_ERR_PEER_CLOSED);
DeviceTester::SetHook(fs_.get(), nullptr);
}
}
TEST_F(GcManagerTest, PageColdData) {
fs_->GetGcManager().DisableFgGc();
zx::result test_file = root_dir_->Create("file", fs::CreationType::kFile);
ASSERT_TRUE(test_file.is_ok()) << test_file.status_string();
auto file = fbl::RefPtr<File>::Downcast(*std::move(test_file));
char buf[kPageSize] = {
0,
};
FileTester::AppendToFile(file.get(), buf, sizeof(buf));
WritebackOperation op = {.bSync = true};
file->Writeback(op);
MakeGcTriggerCondition(10);
fs_->GetGcManager().DisableFgGc();
// Get old block address.
auto old_blk_addr_or = file->FindDataBlkAddr(0);
ASSERT_EQ(old_blk_addr_or.is_ok(), true);
{
fs::SharedLock lock(f2fs::GetGlobalLock());
auto pages_or = file->WriteBegin(0, kPageSize);
ASSERT_TRUE(pages_or.is_ok());
ASSERT_TRUE(pages_or->front()->IsDirty());
}
// If kPageColdData flag is not set, allocate its block as SSR or LFS.
ASSERT_NE(file->Writeback(op), 0UL);
auto new_blk_addr_or = file->FindDataBlkAddr(0);
ASSERT_EQ(old_blk_addr_or.is_error(), false);
ASSERT_NE(new_blk_addr_or.value(), old_blk_addr_or.value());
{
fs::SharedLock lock(f2fs::GetGlobalLock());
auto pages_or = file->WriteBegin(0, kPageSize);
ASSERT_TRUE(pages_or.is_ok());
ASSERT_TRUE(pages_or->front()->IsDirty());
pages_or->front()->SetColdData();
}
// If kPageColdData flag is set, allocate its block as LFS.
CursegInfo *cold_curseg = fs_->GetSegmentManager().CURSEG_I(CursegType::kCursegColdData);
auto expected_addr = fs_->GetSegmentManager().NextFreeBlkAddr(CursegType::kCursegColdData);
ASSERT_EQ(cold_curseg->alloc_type, static_cast<uint8_t>(AllocMode::kLFS));
ASSERT_NE(file->Writeback(op), 0UL);
new_blk_addr_or = file->FindDataBlkAddr(0);
ASSERT_EQ(old_blk_addr_or.is_error(), false);
ASSERT_NE(new_blk_addr_or.value(), old_blk_addr_or.value());
ASSERT_EQ(new_blk_addr_or.value(), expected_addr);
{
LockedPage data_page;
ASSERT_EQ(file->GrabCachePage(0, &data_page), ZX_OK);
ASSERT_FALSE(data_page->IsColdData());
}
file->Close();
}
TEST_F(GcManagerTest, OrphanFileGc) {
DirtySeglistInfo *dirty_info = &fs_->GetSegmentManager().GetDirtySegmentInfo();
FreeSegmapInfo *free_info = &fs_->GetSegmentManager().GetFreeSegmentInfo();
zx::result vn = root_dir_->Create("test", fs::CreationType::kFile);
ASSERT_TRUE(vn.is_ok()) << vn.status_string();
auto file = fbl::RefPtr<File>::Downcast(*std::move(vn));
uint8_t buffer[kPageSize] = {
0xAA,
};
FileTester::AppendToFile(file.get(), buffer, kPageSize);
WritebackOperation op = {.bSync = true};
file->Writeback(op);
fs_->GetSegmentManager().AllocateNewSegments();
fs_->SyncFs();
auto block_or = file->FindDataBlkAddr(0);
ASSERT_TRUE(block_or.is_ok());
// gc target segno
auto target_segno = fs_->GetSegmentManager().GetSegmentNumber(block_or.value());
// Check victim seg is dirty
ASSERT_TRUE(dirty_info->dirty_segmap[static_cast<int>(DirtyType::kDirty)].GetOne(target_segno));
ASSERT_TRUE(free_info->free_segmap.GetOne(target_segno));
// Make file orphan
FileTester::DeleteChild(root_dir_.get(), "test", false);
// Do gc
ASSERT_EQ(GcTester::DoGarbageCollect(fs_->GetGcManager(), target_segno, GcType::kFgGc), ZX_OK);
// Check victim seg is clean
ASSERT_FALSE(dirty_info->dirty_segmap[static_cast<int>(DirtyType::kDirty)].GetOne(target_segno));
// Check if the data is valid while the orphan |file| opens
uint8_t read[kPageSize] = {
0,
};
FileTester::ReadFromFile(file.get(), read, sizeof(read), 0);
ASSERT_EQ(std::memcmp(buffer, read, sizeof(read)), 0);
ASSERT_EQ(file->Close(), ZX_OK);
file = nullptr;
}
class GcManagerTestWithLargeSec
: public GcManagerTest,
public testing::WithParamInterface<std::pair<uint64_t, uint32_t>> {
public:
GcManagerTestWithLargeSec()
: GcManagerTest(TestOptions{.block_count = GetParam().first,
.mkfs_options = MkfsOptions{.segs_per_sec = GetParam().second}}) {
}
};
TEST_P(GcManagerTestWithLargeSec, SegmentDirtyInfo) TA_NO_THREAD_SAFETY_ANALYSIS {
MakeGcTriggerCondition();
DirtySeglistInfo *dirty_info = &fs_->GetSegmentManager().GetDirtySegmentInfo();
// Get Victim
uint32_t last_victim =
fs_->GetSegmentManager().GetLastVictim(static_cast<int>(GcMode::kGcGreedy));
auto victim_seg_or = fs_->GetSegmentManager().GetVictimByDefault(
GcType::kFgGc, CursegType::kNoCheckType, AllocMode::kLFS);
ASSERT_FALSE(victim_seg_or.is_error());
uint32_t victim_seg = victim_seg_or.value();
fs_->GetSegmentManager().SetLastVictim(static_cast<int>(GcMode::kGcGreedy), last_victim);
fs_->GetSegmentManager().SetCurVictimSec(kNullSecNo);
// Check at least one of victim seg is dirty
bool is_dirty = false;
const uint32_t start_segno = victim_seg - (victim_seg % fs_->GetSuperblockInfo().GetSegsPerSec());
for (uint32_t i = 0; i < fs_->GetSuperblockInfo().GetSegsPerSec(); ++i) {
is_dirty |=
dirty_info->dirty_segmap[static_cast<int>(DirtyType::kDirty)].GetOne(start_segno + i);
}
ASSERT_TRUE(is_dirty);
// Copy prev nr_dirty info
int prev_nr_dirty[static_cast<int>(DirtyType::kNrDirtytype)] = {};
memcpy(prev_nr_dirty, dirty_info->nr_dirty, sizeof(prev_nr_dirty));
// Trigger GC
auto result = fs_->GetGcManager().Run();
ASSERT_FALSE(result.is_error());
// Check victim seg is clean
for (uint32_t i = 0; i < fs_->GetSuperblockInfo().GetSegsPerSec(); ++i) {
ASSERT_FALSE(
dirty_info->dirty_segmap[static_cast<int>(DirtyType::kDirty)].GetOne(start_segno + i));
}
// Check nr_dirty decreased
for (int i = static_cast<int>(DirtyType::kDirtyHotData); i <= static_cast<int>(DirtyType::kDirty);
++i) {
ASSERT_TRUE(dirty_info->nr_dirty[i] <= prev_nr_dirty[i]);
}
}
TEST_P(GcManagerTestWithLargeSec, SegmentFreeInfo) TA_NO_THREAD_SAFETY_ANALYSIS {
MakeGcTriggerCondition();
FreeSegmapInfo *free_info = &fs_->GetSegmentManager().GetFreeSegmentInfo();
// Get Victim
uint32_t last_victim =
fs_->GetSegmentManager().GetLastVictim(static_cast<int>(GcMode::kGcGreedy));
auto victim_seg_or = fs_->GetSegmentManager().GetVictimByDefault(
GcType::kFgGc, CursegType::kNoCheckType, AllocMode::kLFS);
ASSERT_FALSE(victim_seg_or.is_error());
uint32_t victim_seg = victim_seg_or.value();
fs_->GetSegmentManager().SetLastVictim(static_cast<int>(GcMode::kGcGreedy), last_victim);
fs_->GetSegmentManager().SetCurVictimSec(kNullSecNo);
uint32_t victim_sec = fs_->GetSegmentManager().GetSecNo(victim_seg);
// Check victim sec is not free
ASSERT_TRUE(free_info->free_secmap.GetOne(victim_sec));
// Trigger GC
auto result = fs_->GetGcManager().Run();
ASSERT_FALSE(result.is_error());
// Check victim sec is freed
ASSERT_FALSE(free_info->free_secmap.GetOne(victim_sec));
}
TEST_P(GcManagerTestWithLargeSec, SecureSpace) {
MakeGcTriggerCondition();
// Set the number of blocks to be gc'ed as 2 sections or available space of the volume.
uint64_t blocks_to_secure =
std::min((safemath::CheckMul<uint64_t>(fs_->GetSuperblockInfo().GetTotalBlockCount(),
100 - fs_->GetSegmentManager().Utilization()) /
100)
.ValueOrDie(),
(safemath::CheckMul<uint64_t>(2, fs_->GetSuperblockInfo().GetBlocksPerSeg()) *
fs_->GetSuperblockInfo().GetSegsPerSec())
.ValueOrDie());
// It should be able to create new files on free blocks that gc acquires.
for (uint32_t i = 0; i < blocks_to_secure; ++i) {
std::string file_name = "_" + std::to_string(i);
zx::result test_file = root_dir_->Create(file_name, fs::CreationType::kFile);
ASSERT_TRUE(test_file.is_ok()) << test_file.status_string();
test_file->Close();
}
}
TEST_P(GcManagerTestWithLargeSec, GcConsistency) {
std::vector<std::string> file_names = MakeGcTriggerCondition();
// It secures enough free sections.
auto secs_or = fs_->GetGcManager().Run();
ASSERT_TRUE(secs_or.is_ok());
for (auto name : file_names) {
fbl::RefPtr<fs::Vnode> vn;
FileTester::Lookup(root_dir_.get(), name, &vn);
ASSERT_TRUE(vn);
auto file = fbl::RefPtr<File>::Downcast(std::move(vn));
char buf[kPageSize] = {
0,
};
FileTester::ReadFromFile(file.get(), buf, sizeof(buf), 0);
f2fs_hash_t hash = DentryHash(name);
ASSERT_EQ(std::memcmp(buf, &hash, sizeof(hash)), 0);
file->Close();
}
}
const std::array<std::pair<uint64_t, uint32_t>, 2> kSecParams = {
{{kDefaultBlockCount, 1}, {4 * kDefaultBlockCount, 4}}};
INSTANTIATE_TEST_SUITE_P(GcManagerTestWithLargeSec, GcManagerTestWithLargeSec,
::testing::ValuesIn(kSecParams));
} // namespace
} // namespace f2fs