Refactor benchmark configuration and improve file handling with shared streams

This commit is contained in:
ImBenji
2025-12-13 16:45:06 +00:00
parent c97f36cfb6
commit bdd1fab997
9 changed files with 249 additions and 465 deletions

View File

@@ -2,9 +2,7 @@
#include <fstream>
#include <string>
#include <memory>
#include <iostream>
#include <unordered_map>
#include <mutex>
#ifdef _WIN32
#include <windows.h>
@@ -22,15 +20,15 @@ class SweepstoreFileHandle {
private:
std::string path;
std::ios::openmode openMode;
#ifdef WITH_UNREAL
IFileHandle* unrealHandle;
#else
// Thread-local cache: each thread gets its own fstream per path
static thread_local std::unordered_map<std::string, std::unique_ptr<std::fstream>> streamCache;
// Single shared stream for all threads
std::fstream stream;
// Get or create the fstream for this thread
std::fstream& getThreadStream();
const std::fstream& getThreadStream() const;
// Mutex protecting the stream
std::mutex streamMutex;
#endif
public:
@@ -38,76 +36,23 @@ public:
const std::string& getPath() const { return path; }
#ifndef WITH_UNREAL
std::fstream& getStream() { return getThreadStream(); }
const std::fstream& getStream() const { return getThreadStream(); }
// Smart pointer-like interface
std::fstream* operator->() { return &getThreadStream(); }
const std::fstream* operator->() const { return &getThreadStream(); }
std::fstream& operator*() { return getThreadStream(); }
const std::fstream& operator*() const { return getThreadStream(); }
#endif
bool isOpen() const;
void close();
// Windows-compatible I/O wrappers
#if defined(_WIN32) || defined(WITH_UNREAL)
// Main I/O API - atomic seek+read/write operations
void seekAndRead(uint64_t offset, char* buffer, size_t size);
void seekAndWrite(uint64_t offset, const char* buffer, size_t size);
// Explicit flush
void flush();
void readSeek(std::streampos pos, std::ios::seekdir dir = std::ios::beg);
void writeSeek(std::streampos pos, std::ios::seekdir dir = std::ios::beg);
void readBytes(char* buffer, std::streamsize size);
void writeBytes(const char* buffer, std::streamsize size);
#else
// Inline for non-Windows to avoid overhead
inline void flush() {
getThreadStream().flush();
}
inline void readSeek(std::streampos pos, std::ios::seekdir dir = std::ios::beg) {
getThreadStream().seekg(pos, dir);
}
inline void writeSeek(std::streampos pos, std::ios::seekdir dir = std::ios::beg) {
getThreadStream().seekp(pos, dir);
}
inline void readBytes(char* buffer, std::streamsize size) {
getThreadStream().read(buffer, size);
}
inline void writeBytes(const char* buffer, std::streamsize size) {
getThreadStream().write(buffer, size);
}
#endif
SweepstoreFileHandle(SweepstoreFileHandle&& other) noexcept
: path(std::move(other.path))
, openMode(other.openMode)
#ifdef WITH_UNREAL
, unrealHandle(other.unrealHandle)
#endif
{
#ifdef WITH_UNREAL
other.unrealHandle = nullptr;
#endif
}
SweepstoreFileHandle& operator=(SweepstoreFileHandle&& other) noexcept {
if (this != &other) {
close();
path = std::move(other.path);
openMode = other.openMode;
#ifdef WITH_UNREAL
unrealHandle = other.unrealHandle;
other.unrealHandle = nullptr;
#endif
}
return *this;
}
// Move semantics
SweepstoreFileHandle(SweepstoreFileHandle&& other) noexcept;
SweepstoreFileHandle& operator=(SweepstoreFileHandle&& other) noexcept;
// Delete copy semantics
SweepstoreFileHandle(const SweepstoreFileHandle&) = delete;
SweepstoreFileHandle& operator=(const SweepstoreFileHandle&) = delete;
~SweepstoreFileHandle() {
close();
}
};
~SweepstoreFileHandle();
};

View File

@@ -2,43 +2,46 @@
#include <string>
#include <cstdint>
#include <unordered_map>
#include <stdexcept>
#include <mutex>
#include <condition_variable>
#include <map>
#include <vector>
#include <algorithm>
#include <memory>
#include "sweepstore/utils/timing.h"
#ifdef _WIN32
#include <windows.h>
#else
#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#endif
// Simple file lock using flock() with thread-local FD cache
// Each thread has its own FD, flock() is per-FD, so threads don't conflict
// Matches Dart's paradigm: each isolate has its own RandomAccessFile
// C++ level byte-range locking
// Allows Thread A (ticket 5) and Thread B (ticket 10) to work in parallel on different byte ranges
// Uses static shared state to coordinate locks across all SweepstoreFileLock instances
class SweepstoreFileLock {
public:
enum class Mode { Shared, Exclusive };
private:
// Key: file path + offset, Value: Mode
struct LockKey {
std::string path;
struct LockRange {
uint64_t offset;
uint64_t length;
Mode mode;
bool operator<(const LockKey& other) const {
if (path != other.path) return path < other.path;
if (offset != other.offset) return offset < other.offset;
return length < other.length;
uint64_t end() const { return offset + length; }
bool overlaps(uint64_t otherOffset, uint64_t otherLength) const {
uint64_t otherEnd = otherOffset + otherLength;
return offset < otherEnd && otherOffset < end();
}
};
// Track active locks per thread to prevent self-deadlock
static thread_local std::map<LockKey, Mode> activeLocks;
// Static shared state for all locks across all instances
struct SharedLockState {
std::mutex mutex;
std::condition_variable cv;
// Map: file path -> list of active lock ranges
std::map<std::string, std::vector<LockRange>> activeLocks;
};
static SharedLockState& getSharedState() {
static SharedLockState state;
return state;
}
std::string filePath;
uint64_t offset;
@@ -46,183 +49,79 @@ private:
Mode mode;
bool locked = false;
#ifdef _WIN32
// Thread-local HANDLE cache for Windows
static thread_local std::unordered_map<std::string, HANDLE> handleCache;
static HANDLE getOrOpenHandle(const std::string& path) {
auto it = handleCache.find(path);
if (it != handleCache.end()) {
return it->second;
// Check if acquiring this lock would conflict with existing locks
bool wouldConflict(const std::vector<LockRange>& existingLocks) const {
for (const auto& existing : existingLocks) {
if (existing.overlaps(offset, length)) {
// Conflict if either lock is exclusive
if (mode == Mode::Exclusive || existing.mode == Mode::Exclusive) {
return true;
}
// Shared locks don't conflict with each other
}
}
HANDLE handle = CreateFileA(
path.c_str(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (handle == INVALID_HANDLE_VALUE) {
throw std::runtime_error("Failed to open file for locking: " + path);
}
handleCache[path] = handle;
return handle;
return false;
}
void acquire() {
LockKey key{filePath, offset, length};
auto& state = getSharedState();
std::unique_lock<std::mutex> lock(state.mutex);
// Check if we already hold a lock on this region
auto it = activeLocks.find(key);
if (it != activeLocks.end()) {
// If we're trying to upgrade from shared to exclusive, release first
if (it->second == Mode::Shared && mode == Mode::Exclusive) {
releaseInternal(); // Release the old shared lock
activeLocks.erase(it);
// Small delay to allow OS to process the unlock before re-locking
// This prevents deadlock when multiple threads upgrade simultaneously
Sleep(1);
} else {
// Already hold compatible or same lock
locked = true;
return;
// Wait until no conflicts
state.cv.wait(lock, [&]() {
auto it = state.activeLocks.find(filePath);
if (it == state.activeLocks.end()) {
return true; // No locks on this file yet
}
}
return !wouldConflict(it->second);
});
HANDLE handle = getOrOpenHandle(filePath);
OVERLAPPED overlapped = {}; // Proper zero-initialization
overlapped.Offset = static_cast<DWORD>(offset & 0xFFFFFFFF);
overlapped.OffsetHigh = static_cast<DWORD>(offset >> 32);
DWORD length_low = static_cast<DWORD>(length & 0xFFFFFFFF);
DWORD length_high = static_cast<DWORD>(length >> 32);
DWORD flags = (mode == Mode::Exclusive) ? LOCKFILE_EXCLUSIVE_LOCK : 0;
if (!LockFileEx(handle, flags, 0, length_low, length_high, &overlapped)) {
throw std::runtime_error("Failed to acquire file lock");
}
// Add our lock
state.activeLocks[filePath].push_back({offset, length, mode});
locked = true;
activeLocks[key] = mode;
}
void releaseInternal() {
if (locked) {
HANDLE handle = getOrOpenHandle(filePath);
OVERLAPPED overlapped = {};
overlapped.Offset = static_cast<DWORD>(offset & 0xFFFFFFFF);
overlapped.OffsetHigh = static_cast<DWORD>(offset >> 32);
DWORD length_low = static_cast<DWORD>(length & 0xFFFFFFFF);
DWORD length_high = static_cast<DWORD>(length >> 32);
UnlockFileEx(handle, 0, length_low, length_high, &overlapped);
locked = false;
}
}
void release() {
if (locked) {
LockKey key{filePath, offset, length};
releaseInternal();
activeLocks.erase(key);
}
}
#else
// Thread-local FD cache - each thread has its own FD per file
static thread_local std::unordered_map<std::string, int> fdCache;
if (!locked) return;
static int getOrOpenFD(const std::string& path) {
auto it = fdCache.find(path);
if (it != fdCache.end()) {
return it->second;
}
auto& state = getSharedState();
std::unique_lock<std::mutex> lock(state.mutex);
int fd = open(path.c_str(), O_RDWR);
if (fd == -1) {
throw std::runtime_error("Failed to open file for locking: " + path);
}
auto it = state.activeLocks.find(filePath);
if (it != state.activeLocks.end()) {
auto& locks = it->second;
fdCache[path] = fd;
return fd;
}
// Remove our lock (find by offset/length/mode match)
locks.erase(
std::remove_if(locks.begin(), locks.end(), [&](const LockRange& r) {
return r.offset == offset && r.length == length && r.mode == mode;
}),
locks.end()
);
void acquire() {
LockKey key{filePath, offset, length};
// Check if we already hold a lock on this region
auto it = activeLocks.find(key);
if (it != activeLocks.end()) {
// If we're trying to upgrade from shared to exclusive, release first
if (it->second == Mode::Shared && mode == Mode::Exclusive) {
releaseInternal(); // Release the old shared lock
activeLocks.erase(it);
} else {
// Already hold compatible or same lock
locked = true;
return;
// Clean up if no more locks on this file
if (locks.empty()) {
state.activeLocks.erase(it);
}
}
int fd = getOrOpenFD(filePath);
locked = false;
struct flock lock_info;
lock_info.l_type = (mode == Mode::Exclusive) ? F_WRLCK : F_RDLCK;
lock_info.l_whence = SEEK_SET;
lock_info.l_start = offset;
lock_info.l_len = length;
lock_info.l_pid = 0;
if (fcntl(fd, F_SETLKW, &lock_info) == -1) {
throw std::runtime_error("Failed to acquire file lock");
}
locked = true;
activeLocks[key] = mode;
// Notify waiting threads
state.cv.notify_all();
}
void releaseInternal() {
if (locked) {
int fd = getOrOpenFD(filePath);
struct flock lock_info;
lock_info.l_type = F_UNLCK;
lock_info.l_whence = SEEK_SET;
lock_info.l_start = offset;
lock_info.l_len = length;
lock_info.l_pid = 0;
fcntl(fd, F_SETLK, &lock_info);
locked = false;
}
}
void release() {
if (locked) {
LockKey key{filePath, offset, length};
releaseInternal();
activeLocks.erase(key);
}
}
#endif
public:
// Constructor accepts offset/length for byte-range locking
SweepstoreFileLock(const std::string& path, uint64_t off, uint64_t len, Mode m)
: filePath(path), offset(off), length(len), mode(m) {}
~SweepstoreFileLock() { release(); }
void lock() {
SWEEPSTORE_TIME_FUNCTION();
if (!locked) acquire();
}
void unlock() {
SWEEPSTORE_TIME_FUNCTION();
release();
}
@@ -230,59 +129,16 @@ public:
return locked;
}
// Check if file is currently locked (non-blocking test)
bool isLocked() {
#ifdef _WIN32
HANDLE handle = getOrOpenHandle(filePath);
OVERLAPPED overlapped = {};
overlapped.Offset = static_cast<DWORD>(offset & 0xFFFFFFFF);
overlapped.OffsetHigh = static_cast<DWORD>(offset >> 32);
DWORD length_low = static_cast<DWORD>(length & 0xFFFFFFFF);
DWORD length_high = static_cast<DWORD>(length >> 32);
DWORD flags = (mode == Mode::Exclusive) ? LOCKFILE_EXCLUSIVE_LOCK : 0;
flags |= LOCKFILE_FAIL_IMMEDIATELY;
// Try non-blocking lock
if (!LockFileEx(handle, flags, 0, length_low, length_high, &overlapped)) {
return true; // Already locked
}
// Got the lock, release immediately
UnlockFileEx(handle, 0, length_low, length_high, &overlapped);
return false;
#else
int fd = getOrOpenFD(filePath);
struct flock lock_info;
lock_info.l_type = (mode == Mode::Exclusive) ? F_WRLCK : F_RDLCK;
lock_info.l_whence = SEEK_SET;
lock_info.l_start = offset;
lock_info.l_len = length;
lock_info.l_pid = 0;
// Try non-blocking lock
if (fcntl(fd, F_SETLK, &lock_info) == -1) {
return true; // Already locked
}
// Got the lock, release immediately
lock_info.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock_info);
return false;
#endif
}
// RAII helper for scoped locking
class Scoped {
SweepstoreFileLock& lock;
SweepstoreFileLock& lockRef;
public:
Scoped(SweepstoreFileLock& l) : lock(l) {
lock.lock();
Scoped(SweepstoreFileLock& l) : lockRef(l) {
lockRef.lock();
}
~Scoped() {
lock.unlock();
lockRef.unlock();
}
Scoped(const Scoped&) = delete;
@@ -292,4 +148,4 @@ public:
// Disable copying
SweepstoreFileLock(const SweepstoreFileLock&) = delete;
SweepstoreFileLock& operator=(const SweepstoreFileLock&) = delete;
};
};