Refactor benchmark configuration and improve file handling with shared streams
This commit is contained in:
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user