Merge pull request #6565 from leoetlino/fs-timing

IOS: Emulate filesystem timings
This commit is contained in:
Léo Lam 2018-04-07 23:47:09 +02:00 committed by GitHub
commit 75574b7b97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 174 additions and 27 deletions

View file

@ -4,6 +4,7 @@
#include "Core/IOS/FS/FileSystemProxy.h"
#include <algorithm>
#include <cstring>
#include "Common/Assert.h"
@ -29,12 +30,20 @@ static s32 ConvertResult(ResultCode code)
return -(static_cast<s32>(code) + 100);
}
// XXX: timing is not the same for all commands and in all cases.
static IPCCommandResult GetFSReply(s32 return_value)
static IPCCommandResult GetFSReply(s32 return_value, u64 extra_tb_ticks = 0)
{
return {return_value, true, SystemTimers::GetTicksPerSecond() / 500};
// According to hardware tests, FS takes at least 2700 TB ticks to reply to commands.
return {return_value, true, (2700 + extra_tb_ticks) * SystemTimers::TIMER_RATIO};
}
/// Amount of TB ticks required for a superblock write to complete.
constexpr u64 SUPERBLOCK_WRITE_TICKS = 3370000;
/// Amount of TB ticks required to write a cluster (for a file).
constexpr u64 CLUSTER_WRITE_TICKS = 300000;
/// Amount of TB ticks required to read a cluster (for a file).
constexpr u64 CLUSTER_READ_TICKS = 115000;
constexpr size_t CLUSTER_DATA_SIZE = 0x4000;
FS::FS(Kernel& ios, const std::string& device_name) : Device(ios, device_name)
{
}
@ -42,6 +51,9 @@ FS::FS(Kernel& ios, const std::string& device_name) : Device(ios, device_name)
void FS::DoState(PointerWrap& p)
{
p.Do(m_fd_map);
p.Do(m_cache_fd);
p.Do(m_cache_chain_index);
p.Do(m_dirty_cache);
}
static void LogResult(const std::string& command, ResultCode code)
@ -56,35 +68,80 @@ static void LogResult(const std::string& command, const Result<T>& result)
LogResult(command, result.Succeeded() ? ResultCode::Success : result.Error());
}
enum class FileLookupMode
{
Normal,
/// Timing model to use when FS splits the path into a parent and a file name
/// and looks up each of them individually.
Split,
};
// Note: lookups normally stop at the first non existing path (as the FST cannot be traversed
// further when a directory doesn't exist). However for the sake of simplicity we assume that the
// entire lookup succeeds because otherwise we'd need to check whether every path component exists.
static u64 EstimateFileLookupTicks(const std::string& path, FileLookupMode mode)
{
const size_t number_of_path_components = std::count(path.cbegin(), path.cend(), '/');
if (number_of_path_components == 0)
return 0;
// Paths that end with a slash are invalid and rejected early in FS.
if (!path.empty() && *path.rbegin() == '/')
return 300;
if (mode == FileLookupMode::Normal)
return 680 * number_of_path_components;
return 1000 + 340 * number_of_path_components;
}
/// Get a reply with the correct timing for operations that modify the superblock.
///
/// A superblock flush takes a very large amount of time, so other delays are ignored
/// to simplify the implementation as they are insignificant.
static IPCCommandResult GetReplyForSuperblockOperation(ResultCode result)
{
const u64 ticks = result == ResultCode::Success ? SUPERBLOCK_WRITE_TICKS : 0;
return GetFSReply(ConvertResult(result), ticks);
}
IPCCommandResult FS::Open(const OpenRequest& request)
{
if (m_fd_map.size() >= 16)
return GetDefaultReply(ConvertResult(ResultCode::NoFreeHandle));
return GetFSReply(ConvertResult(ResultCode::NoFreeHandle));
if (request.path.size() >= 64)
return GetDefaultReply(ConvertResult(ResultCode::Invalid));
return GetFSReply(ConvertResult(ResultCode::Invalid));
if (request.path == "/dev/fs")
{
m_fd_map[request.fd] = {request.gid, request.uid, INVALID_FD};
return GetDefaultReply(IPC_SUCCESS);
return GetFSReply(IPC_SUCCESS);
}
const u64 ticks = EstimateFileLookupTicks(request.path, FileLookupMode::Normal);
auto backend_fd = m_ios.GetFS()->OpenFile(request.uid, request.gid, request.path,
static_cast<Mode>(request.flags & 3));
LogResult(StringFromFormat("OpenFile(%s)", request.path.c_str()), backend_fd);
if (!backend_fd)
return GetFSReply(ConvertResult(backend_fd.Error()));
return GetFSReply(ConvertResult(backend_fd.Error()), ticks);
m_fd_map[request.fd] = {request.gid, request.uid, backend_fd->Release()};
std::strncpy(m_fd_map[request.fd].name.data(), request.path.c_str(), 64);
return GetFSReply(IPC_SUCCESS);
return GetFSReply(IPC_SUCCESS, ticks);
}
IPCCommandResult FS::Close(u32 fd)
{
u64 ticks = 0;
if (m_fd_map[fd].fs_fd != INVALID_FD)
{
if (fd == m_cache_fd)
{
ticks += SimulateFlushFileCache();
m_cache_fd = INVALID_FD;
}
if (m_fd_map[fd].superblock_flush_needed)
ticks += SUPERBLOCK_WRITE_TICKS;
const ResultCode result = m_ios.GetFS()->Close(m_fd_map[fd].fs_fd);
LogResult(StringFromFormat("Close(%s)", m_fd_map[fd].name.data()), result);
m_fd_map.erase(fd);
@ -95,14 +152,89 @@ IPCCommandResult FS::Close(u32 fd)
{
m_fd_map.erase(fd);
}
return GetFSReply(IPC_SUCCESS);
return GetFSReply(IPC_SUCCESS, ticks);
}
u64 FS::SimulatePopulateFileCache(u32 fd, u32 offset, u32 file_size)
{
if (HasCacheForFile(fd, offset))
return 0;
u64 ticks = SimulateFlushFileCache();
if ((offset % CLUSTER_DATA_SIZE != 0 || offset != file_size) && offset < file_size)
ticks += CLUSTER_READ_TICKS;
m_cache_fd = fd;
m_cache_chain_index = static_cast<u16>(offset / CLUSTER_DATA_SIZE);
return ticks;
}
u64 FS::SimulateFlushFileCache()
{
if (m_cache_fd == INVALID_FD || !m_dirty_cache)
return 0;
m_dirty_cache = false;
m_fd_map[m_cache_fd].superblock_flush_needed = true;
return CLUSTER_WRITE_TICKS;
}
// Simulate parts of the FS read/write logic to estimate ticks for file operations correctly.
u64 FS::EstimateTicksForReadWrite(const Handle& handle, const ReadWriteRequest& request)
{
u64 ticks = 0;
const bool is_write = request.command == IPC_CMD_WRITE;
const Result<FileStatus> status = m_ios.GetFS()->GetFileStatus(handle.fs_fd);
u32 offset = status->offset;
u32 count = request.size;
if (!is_write && count + offset > status->size)
count = status->size - offset;
while (count != 0)
{
u32 copy_length;
// Fast path (if not cached): FS copies an entire cluster directly from/to the request.
if (!HasCacheForFile(request.fd, offset) && count >= CLUSTER_DATA_SIZE &&
offset % CLUSTER_DATA_SIZE == 0)
{
ticks += is_write ? CLUSTER_WRITE_TICKS : CLUSTER_READ_TICKS;
copy_length = CLUSTER_DATA_SIZE;
if (is_write)
m_fd_map[request.fd].superblock_flush_needed = true;
}
else
{
ticks += SimulatePopulateFileCache(request.fd, offset, status->size);
const u32 start = offset - m_cache_chain_index * CLUSTER_DATA_SIZE;
copy_length = std::min<u32>(CLUSTER_DATA_SIZE - start, count);
// FS essentially just does a memcpy from/to an internal buffer.
ticks += 0.63f * copy_length + (request.command == IPC_CMD_WRITE ? 1000 : 0);
m_dirty_cache = is_write;
if (is_write && (offset + copy_length) % CLUSTER_DATA_SIZE == 0)
ticks += SimulateFlushFileCache();
}
offset += copy_length;
count -= copy_length;
}
return ticks;
}
bool FS::HasCacheForFile(u32 fd, u32 offset) const
{
const u16 chain_index = static_cast<u16>(offset / CLUSTER_DATA_SIZE);
return m_cache_fd == fd && m_cache_chain_index == chain_index;
}
IPCCommandResult FS::Read(const ReadWriteRequest& request)
{
const Handle& handle = m_fd_map[request.fd];
if (handle.fs_fd == INVALID_FD)
return GetDefaultReply(ConvertResult(ResultCode::Invalid));
return GetFSReply(ConvertResult(ResultCode::Invalid));
// Simulate the FS read logic to estimate ticks. Note: this must be done before reading.
const u64 ticks = EstimateTicksForReadWrite(handle, request);
const Result<u32> result = m_ios.GetFS()->ReadBytesFromFile(
handle.fs_fd, Memory::GetPointer(request.buffer), request.size);
@ -110,15 +242,19 @@ IPCCommandResult FS::Read(const ReadWriteRequest& request)
StringFromFormat("Read(%s, 0x%08x, %u)", handle.name.data(), request.buffer, request.size),
result);
if (!result)
return GetDefaultReply(ConvertResult(result.Error()));
return GetDefaultReply(*result);
return GetFSReply(ConvertResult(result.Error()));
return GetFSReply(*result, ticks);
}
IPCCommandResult FS::Write(const ReadWriteRequest& request)
{
const Handle& handle = m_fd_map[request.fd];
if (handle.fs_fd == INVALID_FD)
return GetDefaultReply(ConvertResult(ResultCode::Invalid));
return GetFSReply(ConvertResult(ResultCode::Invalid));
// Simulate the FS write logic to estimate ticks. Must be done before writing.
const u64 ticks = EstimateTicksForReadWrite(handle, request);
const Result<u32> result = m_ios.GetFS()->WriteBytesToFile(
handle.fs_fd, Memory::GetPointer(request.buffer), request.size);
@ -126,15 +262,16 @@ IPCCommandResult FS::Write(const ReadWriteRequest& request)
StringFromFormat("Write(%s, 0x%08x, %u)", handle.name.data(), request.buffer, request.size),
result);
if (!result)
return GetDefaultReply(ConvertResult(result.Error()));
return GetDefaultReply(*result);
return GetFSReply(ConvertResult(result.Error()));
return GetFSReply(*result, ticks);
}
IPCCommandResult FS::Seek(const SeekRequest& request)
{
const Handle& handle = m_fd_map[request.fd];
if (handle.fs_fd == INVALID_FD)
return GetDefaultReply(ConvertResult(ResultCode::Invalid));
return GetFSReply(ConvertResult(ResultCode::Invalid));
const Result<u32> result =
m_ios.GetFS()->SeekFile(handle.fs_fd, request.offset, IOS::HLE::FS::SeekMode(request.mode));
@ -142,8 +279,8 @@ IPCCommandResult FS::Seek(const SeekRequest& request)
StringFromFormat("Seek(%s, 0x%08x, %u)", handle.name.data(), request.offset, request.mode),
result);
if (!result)
return GetDefaultReply(ConvertResult(result.Error()));
return GetDefaultReply(*result);
return GetFSReply(ConvertResult(result.Error()));
return GetFSReply(*result);
}
#pragma pack(push, 1)
@ -245,7 +382,7 @@ IPCCommandResult FS::Format(const Handle& handle, const IOCtlRequest& request)
return GetFSReply(ConvertResult(ResultCode::AccessDenied));
const ResultCode result = m_ios.GetFS()->Format(handle.uid);
return GetFSReply(ConvertResult(result));
return GetReplyForSuperblockOperation(result);
}
IPCCommandResult FS::GetStats(const Handle& handle, const IOCtlRequest& request)
@ -280,7 +417,7 @@ IPCCommandResult FS::CreateDirectory(const Handle& handle, const IOCtlRequest& r
m_ios.GetFS()->CreateDirectory(handle.uid, handle.gid, params->path, params->attribute,
params->owner_mode, params->group_mode, params->other_mode);
LogResult(StringFromFormat("CreateDirectory(%s)", params->path), result);
return GetFSReply(ConvertResult(result));
return GetReplyForSuperblockOperation(result);
}
IPCCommandResult FS::ReadDirectory(const Handle& handle, const IOCtlVRequest& request)
@ -345,7 +482,7 @@ IPCCommandResult FS::SetAttribute(const Handle& handle, const IOCtlRequest& requ
handle.uid, params->path, params->uid, params->gid, params->attribute, params->owner_mode,
params->group_mode, params->other_mode);
LogResult(StringFromFormat("SetMetadata(%s)", params->path), result);
return GetFSReply(ConvertResult(result));
return GetReplyForSuperblockOperation(result);
}
IPCCommandResult FS::GetAttribute(const Handle& handle, const IOCtlRequest& request)
@ -354,10 +491,11 @@ IPCCommandResult FS::GetAttribute(const Handle& handle, const IOCtlRequest& requ
return GetFSReply(ConvertResult(ResultCode::Invalid));
const std::string path = Memory::GetString(request.buffer_in, 64);
const u64 ticks = EstimateFileLookupTicks(path, FileLookupMode::Split);
const Result<Metadata> metadata = m_ios.GetFS()->GetMetadata(handle.uid, handle.gid, path);
LogResult(StringFromFormat("GetMetadata(%s)", path.c_str()), metadata);
if (!metadata)
return GetFSReply(ConvertResult(metadata.Error()));
return GetFSReply(ConvertResult(metadata.Error()), ticks);
// Yes, the other members aren't copied at all. Actually, IOS does not even memset
// the struct at all, which means uninitialised bytes from the stack are returned.
@ -370,7 +508,7 @@ IPCCommandResult FS::GetAttribute(const Handle& handle, const IOCtlRequest& requ
out.group_mode = metadata->group_mode;
out.other_mode = metadata->other_mode;
Memory::CopyToEmu(request.buffer_out, &out, sizeof(out));
return GetFSReply(IPC_SUCCESS);
return GetFSReply(IPC_SUCCESS, ticks);
}
IPCCommandResult FS::DeleteFile(const Handle& handle, const IOCtlRequest& request)
@ -381,7 +519,7 @@ IPCCommandResult FS::DeleteFile(const Handle& handle, const IOCtlRequest& reques
const std::string path = Memory::GetString(request.buffer_in, 64);
const ResultCode result = m_ios.GetFS()->Delete(handle.uid, handle.gid, path);
LogResult(StringFromFormat("Delete(%s)", path.c_str()), result);
return GetFSReply(ConvertResult(result));
return GetReplyForSuperblockOperation(result);
}
IPCCommandResult FS::RenameFile(const Handle& handle, const IOCtlRequest& request)
@ -393,7 +531,7 @@ IPCCommandResult FS::RenameFile(const Handle& handle, const IOCtlRequest& reques
const std::string new_path = Memory::GetString(request.buffer_in + 64, 64);
const ResultCode result = m_ios.GetFS()->Rename(handle.uid, handle.gid, old_path, new_path);
LogResult(StringFromFormat("Rename(%s, %s)", old_path.c_str(), new_path.c_str()), result);
return GetFSReply(ConvertResult(result));
return GetReplyForSuperblockOperation(result);
}
IPCCommandResult FS::CreateFile(const Handle& handle, const IOCtlRequest& request)
@ -406,7 +544,7 @@ IPCCommandResult FS::CreateFile(const Handle& handle, const IOCtlRequest& reques
m_ios.GetFS()->CreateFile(handle.uid, handle.gid, params->path, params->attribute,
params->owner_mode, params->group_mode, params->other_mode);
LogResult(StringFromFormat("CreateFile(%s)", params->path), result);
return GetFSReply(ConvertResult(result));
return GetReplyForSuperblockOperation(result);
}
IPCCommandResult FS::SetFileVersionControl(const Handle& handle, const IOCtlRequest& request)

View file

@ -46,6 +46,7 @@ private:
IOS::HLE::FS::Fd fs_fd = INVALID_FD;
// We use a std::array to keep this savestate friendly.
std::array<char, 64> name{};
bool superblock_flush_needed = false;
};
enum
@ -79,7 +80,15 @@ private:
IPCCommandResult GetUsage(const Handle& handle, const IOCtlVRequest& request);
IPCCommandResult Shutdown(const Handle& handle, const IOCtlRequest& request);
u64 EstimateTicksForReadWrite(const Handle& handle, const ReadWriteRequest& request);
u64 SimulatePopulateFileCache(u32 fd, u32 offset, u32 file_size);
u64 SimulateFlushFileCache();
bool HasCacheForFile(u32 fd, u32 offset) const;
std::map<u32, Handle> m_fd_map;
u32 m_cache_fd = INVALID_FD;
u16 m_cache_chain_index = 0;
bool m_dirty_cache = false;
};
} // namespace Device
} // namespace HLE

View file

@ -74,7 +74,7 @@ static Common::Event g_compressAndDumpStateSyncEvent;
static std::thread g_save_thread;
// Don't forget to increase this after doing changes on the savestate system
static const u32 STATE_VERSION = 95; // Last changed in PR 6421
static const u32 STATE_VERSION = 96; // Last changed in PR 6565
// Maps savestate versions to Dolphin versions.
// Versions after 42 don't need to be added to this list,