Compare commits
9 Commits
main
...
users/cach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd9ca0bab8 | ||
|
|
3bbd0c336e | ||
|
|
2bea3456e2 | ||
|
|
2ac7fe0c72 | ||
|
|
9b7be34863 | ||
|
|
68bcf8dd0f | ||
|
|
ec88f7d2fa | ||
|
|
a22304526f | ||
|
|
0ae1094b5c |
@ -890,6 +890,7 @@ option (LLVM_ENABLE_SPHINX "Use Sphinx to generate llvm documentation." OFF)
|
||||
option (LLVM_ENABLE_OCAMLDOC "Build OCaml bindings documentation." ON)
|
||||
option (LLVM_ENABLE_BINDINGS "Build bindings." ON)
|
||||
option (LLVM_ENABLE_TELEMETRY "Enable the telemetry library. If set to OFF, library cannot be enabled after build (eg., at runtime)" ON)
|
||||
option (LLVM_ENABLE_ONDISK_CAS "Build OnDiskCAS." ON)
|
||||
|
||||
set(LLVM_INSTALL_DOXYGEN_HTML_DIR "${CMAKE_INSTALL_DOCDIR}/llvm/doxygen-html"
|
||||
CACHE STRING "Doxygen-generated HTML documentation install directory")
|
||||
|
||||
123
llvm/include/llvm/CAS/MappedFileRegionBumpPtr.h
Normal file
123
llvm/include/llvm/CAS/MappedFileRegionBumpPtr.h
Normal file
@ -0,0 +1,123 @@
|
||||
//===- MappedFileRegionBumpPtr.h --------------------------------*- C++ -*-===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
/// \file
|
||||
/// This file declares interface for MappedFileRegionBumpPtr, a bump pointer
|
||||
/// allocator, backed by a memory-mapped file.
|
||||
///
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef LLVM_CAS_MAPPEDFILEREGIONBUMPPTR_H
|
||||
#define LLVM_CAS_MAPPEDFILEREGIONBUMPPTR_H
|
||||
|
||||
#include "llvm/Support/Alignment.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include <atomic>
|
||||
|
||||
namespace llvm::cas {
|
||||
|
||||
/// Allocator for an owned mapped file region that supports thread-safe and
|
||||
/// process-safe bump pointer allocation.
|
||||
///
|
||||
/// This allocator is designed to create a sparse file when supported by the
|
||||
/// filesystem's \c ftruncate so that it can be used with a large maximum size.
|
||||
/// It will also attempt to shrink the underlying file down to its current
|
||||
/// allocation size when the last concurrent mapping is closed.
|
||||
///
|
||||
/// Process-safe. Uses file locks when resizing the file during initialization
|
||||
/// and destruction.
|
||||
///
|
||||
/// Thread-safe, assuming all threads use the same instance to talk to a given
|
||||
/// file/mapping. Unsafe to have multiple instances talking to the same file
|
||||
/// in the same process since file locks will misbehave. Clients should
|
||||
/// coordinate (somehow).
|
||||
///
|
||||
/// Provides 8-byte alignment for all allocations.
|
||||
class MappedFileRegionBumpPtr {
|
||||
public:
|
||||
using RegionT = sys::fs::mapped_file_region;
|
||||
|
||||
/// Create a \c MappedFileRegionBumpPtr.
|
||||
///
|
||||
/// \param Path the path to open the mapped region.
|
||||
/// \param Capacity the maximum size for the mapped file region.
|
||||
/// \param BumpPtrOffset the offset at which to store the bump pointer.
|
||||
/// \param NewFileConstructor is for constructing new files. It has exclusive
|
||||
/// access to the file. Must call \c initializeBumpPtr.
|
||||
static Expected<MappedFileRegionBumpPtr>
|
||||
create(const Twine &Path, uint64_t Capacity, int64_t BumpPtrOffset,
|
||||
function_ref<Error(MappedFileRegionBumpPtr &)> NewFileConstructor);
|
||||
|
||||
/// Finish initializing the bump pointer. Must be called by
|
||||
/// \c NewFileConstructor.
|
||||
void initializeBumpPtr(int64_t BumpPtrOffset);
|
||||
|
||||
/// Minimum alignment for allocations, currently hardcoded to 8B.
|
||||
static constexpr Align getAlign() {
|
||||
// Trick Align into giving us '8' as a constexpr.
|
||||
struct alignas(8) T {};
|
||||
static_assert(alignof(T) == 8, "Tautology failed?");
|
||||
return Align::Of<T>();
|
||||
}
|
||||
|
||||
/// Allocate at least \p AllocSize. Rounds up to \a getAlign().
|
||||
Expected<char *> allocate(uint64_t AllocSize) {
|
||||
auto Offset = allocateOffset(AllocSize);
|
||||
if (LLVM_UNLIKELY(!Offset))
|
||||
return Offset.takeError();
|
||||
return data() + *Offset;
|
||||
}
|
||||
/// Allocate, returning the offset from \a data() instead of a pointer.
|
||||
Expected<int64_t> allocateOffset(uint64_t AllocSize);
|
||||
|
||||
char *data() const { return Region.data(); }
|
||||
uint64_t size() const { return H->BumpPtr; }
|
||||
uint64_t capacity() const { return Region.size(); }
|
||||
|
||||
RegionT &getRegion() { return Region; }
|
||||
|
||||
~MappedFileRegionBumpPtr() { destroyImpl(); }
|
||||
|
||||
MappedFileRegionBumpPtr() = default;
|
||||
MappedFileRegionBumpPtr(MappedFileRegionBumpPtr &&RHS) { moveImpl(RHS); }
|
||||
MappedFileRegionBumpPtr &operator=(MappedFileRegionBumpPtr &&RHS) {
|
||||
destroyImpl();
|
||||
moveImpl(RHS);
|
||||
return *this;
|
||||
}
|
||||
|
||||
MappedFileRegionBumpPtr(const MappedFileRegionBumpPtr &) = delete;
|
||||
MappedFileRegionBumpPtr &operator=(const MappedFileRegionBumpPtr &) = delete;
|
||||
|
||||
private:
|
||||
void destroyImpl();
|
||||
void moveImpl(MappedFileRegionBumpPtr &RHS) {
|
||||
std::swap(Region, RHS.Region);
|
||||
std::swap(H, RHS.H);
|
||||
std::swap(Path, RHS.Path);
|
||||
std::swap(FD, RHS.FD);
|
||||
std::swap(SharedLockFD, RHS.SharedLockFD);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Header {
|
||||
std::atomic<int64_t> BumpPtr;
|
||||
std::atomic<int64_t> AllocatedSize;
|
||||
};
|
||||
RegionT Region;
|
||||
Header *H = nullptr;
|
||||
std::string Path;
|
||||
// File descriptor for the main storage file.
|
||||
std::optional<int> FD;
|
||||
// File descriptor for the file used as reader/writer lock.
|
||||
std::optional<int> SharedLockFD;
|
||||
};
|
||||
|
||||
} // namespace llvm::cas
|
||||
|
||||
#endif // LLVM_CAS_MAPPEDFILEREGIONBUMPPTR_H
|
||||
@ -146,4 +146,7 @@
|
||||
coverage bugs, and to 0 otherwise. */
|
||||
#cmakedefine01 LLVM_ENABLE_DEBUGLOC_TRACKING_ORIGIN
|
||||
|
||||
/* Define to 1 to enable LLVM OnDisk Content Addressable Storage*/
|
||||
#cmakedefine01 LLVM_ENABLE_ONDISK_CAS
|
||||
|
||||
#endif
|
||||
|
||||
@ -410,6 +410,11 @@ LLVM_ABI std::error_code copy_file(const Twine &From, int ToFD);
|
||||
/// platform-specific error_code.
|
||||
LLVM_ABI std::error_code resize_file(int FD, uint64_t Size);
|
||||
|
||||
/// Resize path to size with sparse files explicitly enabled. It uses
|
||||
/// FSCTL_SET_SPARSE On Windows. This is the same as resize_file on
|
||||
/// non-Windows
|
||||
LLVM_ABI std::error_code resize_file_sparse(int FD, uint64_t Size);
|
||||
|
||||
/// Resize \p FD to \p Size before mapping \a mapped_file_region::readwrite. On
|
||||
/// non-Windows, this calls \a resize_file(). On Windows, this is a no-op,
|
||||
/// since the subsequent mapping (via \c CreateFileMapping) automatically
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
add_llvm_component_library(LLVMCAS
|
||||
BuiltinCAS.cpp
|
||||
InMemoryCAS.cpp
|
||||
MappedFileRegionBumpPtr.cpp
|
||||
ObjectStore.cpp
|
||||
OnDiskCommon.cpp
|
||||
|
||||
ADDITIONAL_HEADER_DIRS
|
||||
${LLVM_MAIN_INCLUDE_DIR}/llvm/CAS
|
||||
|
||||
318
llvm/lib/CAS/MappedFileRegionBumpPtr.cpp
Normal file
318
llvm/lib/CAS/MappedFileRegionBumpPtr.cpp
Normal file
@ -0,0 +1,318 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
/// \file Implements MappedFileRegionBumpPtr.
|
||||
///
|
||||
/// A bump pointer allocator, backed by a memory-mapped file.
|
||||
///
|
||||
/// The effect we want is:
|
||||
///
|
||||
/// 1. If it doesn't exist, create the file with an initial size.
|
||||
/// 2. Reserve virtual memory large enough for the max file size.
|
||||
/// 3. Map the file into memory in the reserved region.
|
||||
/// 4. Increase the file size and update the mapping when necessary.
|
||||
///
|
||||
/// However, updating the mapping is challenging when it needs to work portably,
|
||||
/// and across multiple processes without locking for every read. Our current
|
||||
/// implementation strategy is:
|
||||
///
|
||||
/// 1. Use \c sys::fs::resize_file_sparse to grow the file to its max size
|
||||
/// (typically several GB). If the file system doesn't support sparse file,
|
||||
/// this may return a fully allocated file.
|
||||
/// 2. Call \c sys::fs::mapped_file_region to map the entire file.
|
||||
/// 3. [Automatic as part of 2.]
|
||||
/// 4. If supported, use \c fallocate or similiar APIs to ensure the file system
|
||||
/// storage for the sparse file so we won't end up with partial file if the
|
||||
/// disk is out of space.
|
||||
///
|
||||
/// Additionally, we attempt to resize the file to its actual data size when
|
||||
/// closing the mapping, if this is the only concurrent instance. This is done
|
||||
/// using file locks. Shrinking the file mitigates problems with having large
|
||||
/// files: on filesystems without sparse files it avoids unnecessary space use;
|
||||
/// it also avoids allocating the full size if another process copies the file,
|
||||
/// which typically loses sparseness. These mitigations only work while the file
|
||||
/// is not in use.
|
||||
///
|
||||
/// TODO: we assume that all concurrent users of the file will use the same
|
||||
/// value for Capacity. Otherwise a process with a larger capacity can write
|
||||
/// data that is "out of bounds" for processes with smaller capacity. Currently
|
||||
/// this is true in the CAS.
|
||||
///
|
||||
/// To support resizing, we use two separate file locks:
|
||||
/// 1. We use a shared reader lock on a ".shared" file until destruction.
|
||||
/// 2. We use a lock on the main file during initialization - shared to check
|
||||
/// the status, upgraded to exclusive to resize/initialize the file.
|
||||
///
|
||||
/// Then during destruction we attempt to get exclusive access on (1), which
|
||||
/// requires no concurrent readers. If so, we shrink the file. Using two
|
||||
/// separate locks simplifies the implementation and enables it to work on
|
||||
/// platforms (e.g. Windows) where a shared/reader lock prevents writing.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "llvm/CAS/MappedFileRegionBumpPtr.h"
|
||||
#include "OnDiskCommon.h"
|
||||
|
||||
#if LLVM_ON_UNIX
|
||||
#include <sys/stat.h>
|
||||
#if __has_include(<sys/param.h>)
|
||||
#include <sys/param.h>
|
||||
#endif
|
||||
#ifdef DEV_BSIZE
|
||||
#define MAPPED_FILE_BSIZE DEV_BSIZE
|
||||
#elif __linux__
|
||||
#define MAPPED_FILE_BSIZE 512
|
||||
#endif
|
||||
#endif
|
||||
|
||||
using namespace llvm;
|
||||
using namespace llvm::cas;
|
||||
using namespace llvm::cas::ondisk;
|
||||
|
||||
namespace {
|
||||
struct FileLockRAII {
|
||||
std::string Path;
|
||||
int FD;
|
||||
std::optional<sys::fs::LockKind> Locked;
|
||||
|
||||
FileLockRAII(std::string Path, int FD) : Path(std::move(Path)), FD(FD) {}
|
||||
~FileLockRAII() { consumeError(unlock()); }
|
||||
|
||||
Error lock(sys::fs::LockKind LK) {
|
||||
if (std::error_code EC = lockFileThreadSafe(FD, LK))
|
||||
return createFileError(Path, EC);
|
||||
Locked = LK;
|
||||
return Error::success();
|
||||
}
|
||||
|
||||
Error unlock() {
|
||||
if (Locked) {
|
||||
Locked = std::nullopt;
|
||||
if (std::error_code EC = unlockFileThreadSafe(FD))
|
||||
return createFileError(Path, EC);
|
||||
}
|
||||
return Error::success();
|
||||
}
|
||||
};
|
||||
|
||||
struct FileSizeInfo {
|
||||
uint64_t Size;
|
||||
uint64_t AllocatedSize;
|
||||
|
||||
static ErrorOr<FileSizeInfo> get(sys::fs::file_t File);
|
||||
};
|
||||
} // end anonymous namespace
|
||||
|
||||
Expected<MappedFileRegionBumpPtr> MappedFileRegionBumpPtr::create(
|
||||
const Twine &Path, uint64_t Capacity, int64_t BumpPtrOffset,
|
||||
function_ref<Error(MappedFileRegionBumpPtr &)> NewFileConstructor) {
|
||||
MappedFileRegionBumpPtr Result;
|
||||
Result.Path = Path.str();
|
||||
// Open the main file.
|
||||
int FD;
|
||||
if (std::error_code EC = sys::fs::openFileForReadWrite(
|
||||
Result.Path, FD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
|
||||
return createFileError(Path, EC);
|
||||
Result.FD = FD;
|
||||
|
||||
// Open the shared lock file. See file comment for details of locking scheme.
|
||||
SmallString<128> SharedLockPath(Result.Path);
|
||||
SharedLockPath.append(".shared");
|
||||
int SharedLockFD;
|
||||
if (std::error_code EC = sys::fs::openFileForReadWrite(
|
||||
SharedLockPath, SharedLockFD, sys::fs::CD_OpenAlways,
|
||||
sys::fs::OF_None))
|
||||
return createFileError(SharedLockPath, EC);
|
||||
Result.SharedLockFD = SharedLockFD;
|
||||
|
||||
// Take shared/reader lock that will be held until we close the file; unlocked
|
||||
// by destroyImpl.
|
||||
if (std::error_code EC =
|
||||
lockFileThreadSafe(SharedLockFD, sys::fs::LockKind::Exclusive))
|
||||
return createFileError(Path, EC);
|
||||
|
||||
// Take shared/reader lock for initialization.
|
||||
FileLockRAII InitLock(Result.Path, FD);
|
||||
if (Error E = InitLock.lock(sys::fs::LockKind::Shared))
|
||||
return std::move(E);
|
||||
|
||||
sys::fs::file_t File = sys::fs::convertFDToNativeFile(FD);
|
||||
auto FileSize = FileSizeInfo::get(File);
|
||||
if (!FileSize)
|
||||
return createFileError(Result.Path, FileSize.getError());
|
||||
|
||||
if (FileSize->Size < Capacity) {
|
||||
// Lock the file exclusively so only one process will do the initialization.
|
||||
if (Error E = InitLock.unlock())
|
||||
return std::move(E);
|
||||
if (Error E = InitLock.lock(sys::fs::LockKind::Exclusive))
|
||||
return std::move(E);
|
||||
// Retrieve the current size now that we have exclusive access.
|
||||
FileSize = FileSizeInfo::get(File);
|
||||
if (!FileSize)
|
||||
return createFileError(Result.Path, FileSize.getError());
|
||||
}
|
||||
|
||||
// At this point either the file is still under-sized, or we have the size for
|
||||
// the completely initialized file.
|
||||
|
||||
if (FileSize->Size < Capacity) {
|
||||
// We are initializing the file; it may be empty, or may have been shrunk
|
||||
// during a previous close.
|
||||
// TODO: Detect a case where someone opened it with a smaller capacity.
|
||||
assert(InitLock.Locked == sys::fs::LockKind::Exclusive);
|
||||
if (std::error_code EC = sys::fs::resize_file_sparse(FD, Capacity))
|
||||
return createFileError(Result.Path, EC);
|
||||
} else {
|
||||
// Someone else initialized it.
|
||||
Capacity = FileSize->Size;
|
||||
}
|
||||
|
||||
// Create the mapped region.
|
||||
{
|
||||
std::error_code EC;
|
||||
sys::fs::mapped_file_region Map(
|
||||
File, sys::fs::mapped_file_region::readwrite, Capacity, 0, EC);
|
||||
if (EC)
|
||||
return createFileError(Result.Path, EC);
|
||||
Result.Region = std::move(Map);
|
||||
}
|
||||
|
||||
if (FileSize->Size == 0) {
|
||||
assert(InitLock.Locked == sys::fs::LockKind::Exclusive);
|
||||
// We are creating a new file; run the constructor.
|
||||
if (Error E = NewFileConstructor(Result))
|
||||
return std::move(E);
|
||||
} else {
|
||||
Result.initializeBumpPtr(BumpPtrOffset);
|
||||
}
|
||||
|
||||
if (FileSize->Size < Capacity && FileSize->AllocatedSize < Capacity) {
|
||||
// We are initializing the file; sync the allocated size in case it
|
||||
// changed when truncating or during construction.
|
||||
FileSize = FileSizeInfo::get(File);
|
||||
if (!FileSize)
|
||||
return createFileError(Result.Path, FileSize.getError());
|
||||
assert(InitLock.Locked == sys::fs::LockKind::Exclusive);
|
||||
Result.H->AllocatedSize.exchange(FileSize->AllocatedSize);
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
void MappedFileRegionBumpPtr::destroyImpl() {
|
||||
if (!FD)
|
||||
return;
|
||||
|
||||
// Drop the shared lock indicating we are no longer accessing the file.
|
||||
if (SharedLockFD)
|
||||
(void)unlockFileThreadSafe(*SharedLockFD);
|
||||
|
||||
// Attempt to truncate the file if we can get exclusive access. Ignore any
|
||||
// errors.
|
||||
if (H) {
|
||||
assert(SharedLockFD && "Must have shared lock file open");
|
||||
if (tryLockFileThreadSafe(*SharedLockFD) == std::error_code()) {
|
||||
size_t Size = size();
|
||||
size_t Capacity = capacity();
|
||||
assert(Size < Capacity);
|
||||
// sync to file system to make sure all contents are up-to-date.
|
||||
(void)Region.sync();
|
||||
// unmap the file before resizing since that is the requirement for
|
||||
// some platforms.
|
||||
Region.unmap();
|
||||
(void)sys::fs::resize_file(*FD, Size);
|
||||
(void)unlockFileThreadSafe(*SharedLockFD);
|
||||
}
|
||||
}
|
||||
|
||||
auto Close = [](std::optional<int> &FD) {
|
||||
if (FD) {
|
||||
sys::fs::file_t File = sys::fs::convertFDToNativeFile(*FD);
|
||||
sys::fs::closeFile(File);
|
||||
FD = std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
// Close the file and shared lock.
|
||||
Close(FD);
|
||||
Close(SharedLockFD);
|
||||
}
|
||||
|
||||
void MappedFileRegionBumpPtr::initializeBumpPtr(int64_t BumpPtrOffset) {
|
||||
assert(capacity() < (uint64_t)INT64_MAX && "capacity must fit in int64_t");
|
||||
int64_t BumpPtrEndOffset = BumpPtrOffset + sizeof(decltype(*H));
|
||||
assert(BumpPtrEndOffset <= (int64_t)capacity() &&
|
||||
"Expected end offset to be pre-allocated");
|
||||
assert(isAligned(Align::Of<decltype(*H)>(), BumpPtrOffset) &&
|
||||
"Expected end offset to be aligned");
|
||||
H = reinterpret_cast<decltype(H)>(data() + BumpPtrOffset);
|
||||
|
||||
int64_t ExistingValue = 0;
|
||||
if (!H->BumpPtr.compare_exchange_strong(ExistingValue, BumpPtrEndOffset))
|
||||
assert(ExistingValue >= BumpPtrEndOffset &&
|
||||
"Expected 0, or past the end of the BumpPtr itself");
|
||||
}
|
||||
|
||||
static Error createAllocatorOutOfSpaceError() {
|
||||
return createStringError(std::make_error_code(std::errc::not_enough_memory),
|
||||
"memory mapped file allocator is out of space");
|
||||
}
|
||||
|
||||
Expected<int64_t> MappedFileRegionBumpPtr::allocateOffset(uint64_t AllocSize) {
|
||||
AllocSize = alignTo(AllocSize, getAlign());
|
||||
int64_t OldEnd = H->BumpPtr.fetch_add(AllocSize);
|
||||
int64_t NewEnd = OldEnd + AllocSize;
|
||||
if (LLVM_UNLIKELY(NewEnd > (int64_t)capacity())) {
|
||||
// Return the allocation. If the start already passed the end, that means
|
||||
// some other concurrent allocations already consumed all the capacity.
|
||||
// There is no need to return the original value. If the start was not
|
||||
// passed the end, current allocation certainly bumped it passed the end.
|
||||
// All other allocation afterwards must have failed and current allocation
|
||||
// is in charge of return the allocation back to a valid value.
|
||||
if (OldEnd <= (int64_t)capacity())
|
||||
(void)H->BumpPtr.exchange(OldEnd);
|
||||
|
||||
return createAllocatorOutOfSpaceError();
|
||||
}
|
||||
|
||||
int64_t DiskSize = H->AllocatedSize;
|
||||
if (LLVM_UNLIKELY(NewEnd > DiskSize)) {
|
||||
int64_t NewSize;
|
||||
// The minimum increment is a page, but allocate more to amortize the cost.
|
||||
constexpr int64_t Increment = 1 * 1024 * 1024; // 1 MB
|
||||
if (Error E = preallocateFileTail(*FD, DiskSize, DiskSize + Increment)
|
||||
.moveInto(NewSize))
|
||||
return std::move(E);
|
||||
assert(NewSize >= DiskSize + Increment);
|
||||
// FIXME: on Darwin this can under-count the size if there is a race to
|
||||
// preallocate disk, because the semantics of F_PREALLOCATE are to add bytes
|
||||
// to the end of the file, not to allocate up to a fixed size.
|
||||
// Any discrepancy will be resolved the next time the file is truncated and
|
||||
// then reopend.
|
||||
while (DiskSize < NewSize)
|
||||
H->AllocatedSize.compare_exchange_strong(DiskSize, NewSize);
|
||||
}
|
||||
return OldEnd;
|
||||
}
|
||||
|
||||
ErrorOr<FileSizeInfo> FileSizeInfo::get(sys::fs::file_t File) {
|
||||
#if LLVM_ON_UNIX && defined(MAPPED_FILE_BSIZE)
|
||||
struct stat Status;
|
||||
int StatRet = ::fstat(File, &Status);
|
||||
if (StatRet)
|
||||
return errnoAsErrorCode();
|
||||
uint64_t AllocatedSize = uint64_t(Status.st_blksize) * MAPPED_FILE_BSIZE;
|
||||
return FileSizeInfo{uint64_t(Status.st_size), AllocatedSize};
|
||||
#else
|
||||
// Fallback: assume the file is fully allocated. Note: this may result in
|
||||
// data loss on out-of-space.
|
||||
sys::fs::file_status Status;
|
||||
if (std::error_code EC = sys::fs::status(File, Status))
|
||||
return EC;
|
||||
return FileSizeInfo{Status.getSize(), Status.getSize()};
|
||||
#endif
|
||||
}
|
||||
120
llvm/lib/CAS/OnDiskCommon.cpp
Normal file
120
llvm/lib/CAS/OnDiskCommon.cpp
Normal file
@ -0,0 +1,120 @@
|
||||
//===- OnDiskCommon.cpp ---------------------------------------------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "OnDiskCommon.h"
|
||||
#include "llvm/Config/config.h"
|
||||
#include "llvm/Support/Error.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include <thread>
|
||||
|
||||
#if __has_include(<sys/file.h>)
|
||||
#include <sys/file.h>
|
||||
#ifdef LOCK_SH
|
||||
#define HAVE_FLOCK 1
|
||||
#else
|
||||
#define HAVE_FLOCK 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if __has_include(<fcntl.h>)
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
using namespace llvm;
|
||||
|
||||
std::error_code cas::ondisk::lockFileThreadSafe(int FD,
|
||||
sys::fs::LockKind Kind) {
|
||||
#if HAVE_FLOCK
|
||||
if (flock(FD, Kind == sys::fs::LockKind::Exclusive ? LOCK_EX : LOCK_SH) == 0)
|
||||
return std::error_code();
|
||||
return std::error_code(errno, std::generic_category());
|
||||
#elif defined(_WIN32)
|
||||
// On Windows this implementation is thread-safe.
|
||||
return sys::fs::lockFile(FD, Kind);
|
||||
#else
|
||||
return make_error_code(std::errc::no_lock_available);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::error_code cas::ondisk::unlockFileThreadSafe(int FD) {
|
||||
#if HAVE_FLOCK
|
||||
if (flock(FD, LOCK_UN) == 0)
|
||||
return std::error_code();
|
||||
return std::error_code(errno, std::generic_category());
|
||||
#elif defined(_WIN32)
|
||||
// On Windows this implementation is thread-safe.
|
||||
return sys::fs::unlockFile(FD);
|
||||
#else
|
||||
return make_error_code(std::errc::no_lock_available);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::error_code
|
||||
cas::ondisk::tryLockFileThreadSafe(int FD, std::chrono::milliseconds Timeout,
|
||||
sys::fs::LockKind Kind) {
|
||||
#if HAVE_FLOCK
|
||||
auto Start = std::chrono::steady_clock::now();
|
||||
auto End = Start + Timeout;
|
||||
do {
|
||||
if (flock(FD, (Kind == sys::fs::LockKind::Exclusive ? LOCK_EX : LOCK_SH) |
|
||||
LOCK_NB) == 0)
|
||||
return std::error_code();
|
||||
int Error = errno;
|
||||
if (Error == EWOULDBLOCK) {
|
||||
// Match sys::fs::tryLockFile, which sleeps for 1 ms per attempt.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
continue;
|
||||
}
|
||||
return std::error_code(Error, std::generic_category());
|
||||
} while (std::chrono::steady_clock::now() < End);
|
||||
return make_error_code(std::errc::no_lock_available);
|
||||
#elif defined(_WIN32)
|
||||
// On Windows this implementation is thread-safe.
|
||||
return sys::fs::tryLockFile(FD, Timeout, Kind);
|
||||
#else
|
||||
return make_error_code(std::errc::no_lock_available);
|
||||
#endif
|
||||
}
|
||||
|
||||
Expected<size_t> cas::ondisk::preallocateFileTail(int FD, size_t CurrentSize,
|
||||
size_t NewSize) {
|
||||
auto CreateError = [&](std::error_code EC) -> Expected<size_t> {
|
||||
if (EC == std::errc::not_supported)
|
||||
// Ignore ENOTSUP in case the filesystem cannot preallocate.
|
||||
return NewSize;
|
||||
#if defined(HAVE_POSIX_FALLOCATE)
|
||||
if (EC == std::errc::invalid_argument && CurrentSize < NewSize && // len > 0
|
||||
NewSize < std::numeric_limits<off_t>::max()) // 0 <= offset, len < max
|
||||
// Prior to 2024, POSIX required EINVAL for cases that should be ENOTSUP,
|
||||
// so handle it the same as above if it is not one of the other ways to
|
||||
// get EINVAL.
|
||||
return NewSize;
|
||||
#endif
|
||||
return createStringError(EC,
|
||||
"failed to allocate to CAS file: " + EC.message());
|
||||
};
|
||||
#if defined(HAVE_POSIX_FALLOCATE)
|
||||
// Note: posix_fallocate returns its error directly, not via errno.
|
||||
if (int Err = posix_fallocate(FD, CurrentSize, NewSize - CurrentSize))
|
||||
return CreateError(std::error_code(Err, std::generic_category()));
|
||||
return NewSize;
|
||||
#elif defined(__APPLE__)
|
||||
fstore_t FAlloc;
|
||||
FAlloc.fst_flags = F_ALLOCATEALL | F_ALLOCATEPERSIST;
|
||||
FAlloc.fst_posmode = F_PEOFPOSMODE;
|
||||
FAlloc.fst_offset = 0;
|
||||
FAlloc.fst_length = NewSize - CurrentSize;
|
||||
FAlloc.fst_bytesalloc = 0;
|
||||
if (fcntl(FD, F_PREALLOCATE, &FAlloc))
|
||||
return CreateError(errnoAsErrorCode());
|
||||
assert(CurrentSize + FAlloc.fst_bytesalloc >= NewSize);
|
||||
return CurrentSize + FAlloc.fst_bytesalloc;
|
||||
#else
|
||||
return NewSize; // Pretend it worked.
|
||||
#endif
|
||||
}
|
||||
46
llvm/lib/CAS/OnDiskCommon.h
Normal file
46
llvm/lib/CAS/OnDiskCommon.h
Normal file
@ -0,0 +1,46 @@
|
||||
//===- OnDiskCommon.h -------------------------------------------*- C++ -*-===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef LLVM_LIB_CAS_ONDISKCOMMON_H
|
||||
#define LLVM_LIB_CAS_ONDISKCOMMON_H
|
||||
|
||||
#include "llvm/Support/Error.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include <chrono>
|
||||
|
||||
namespace llvm::cas::ondisk {
|
||||
|
||||
/// Thread-safe alternative to \c sys::fs::lockFile. This does not support all
|
||||
/// the platforms that \c sys::fs::lockFile does, so keep it in the CAS library
|
||||
/// for now.
|
||||
std::error_code lockFileThreadSafe(int FD, llvm::sys::fs::LockKind Kind);
|
||||
|
||||
/// Thread-safe alternative to \c sys::fs::unlockFile. This does not support all
|
||||
/// the platforms that \c sys::fs::lockFile does, so keep it in the CAS library
|
||||
/// for now.
|
||||
std::error_code unlockFileThreadSafe(int FD);
|
||||
|
||||
/// Thread-safe alternative to \c sys::fs::tryLockFile. This does not support
|
||||
/// all the platforms that \c sys::fs::lockFile does, so keep it in the CAS
|
||||
/// library for now.
|
||||
std::error_code tryLockFileThreadSafe(
|
||||
int FD, std::chrono::milliseconds Timeout = std::chrono::milliseconds(0),
|
||||
llvm::sys::fs::LockKind Kind = llvm::sys::fs::LockKind::Exclusive);
|
||||
|
||||
/// Allocate space for the file \p FD on disk, if the filesystem supports it.
|
||||
///
|
||||
/// On filesystems that support this operation, this ensures errors such as
|
||||
/// \c std::errc::no_space_on_device are detected before we write data.
|
||||
///
|
||||
/// \returns the new size of the file, or an \c Error.
|
||||
Expected<size_t> preallocateFileTail(int FD, size_t CurrentSize,
|
||||
size_t NewSize);
|
||||
|
||||
} // namespace llvm::cas::ondisk
|
||||
|
||||
#endif // LLVM_LIB_CAS_ONDISKCOMMON_H
|
||||
@ -600,6 +600,11 @@ std::error_code resize_file(int FD, uint64_t Size) {
|
||||
return std::error_code();
|
||||
}
|
||||
|
||||
std::error_code resize_file_sparse(int FD, uint64_t Size) {
|
||||
// On Unix, this is the same as `resize_file`.
|
||||
return resize_file(FD, Size);
|
||||
}
|
||||
|
||||
static int convertAccessMode(AccessMode Mode) {
|
||||
switch (Mode) {
|
||||
case AccessMode::Exist:
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
#include "llvm/Support/Windows/WindowsSupport.h"
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <winioctl.h>
|
||||
|
||||
#undef max
|
||||
|
||||
@ -617,6 +618,22 @@ std::error_code resize_file(int FD, uint64_t Size) {
|
||||
return std::error_code(error, std::generic_category());
|
||||
}
|
||||
|
||||
std::error_code resize_file_sparse(int FD, uint64_t Size) {
|
||||
HANDLE hFile = reinterpret_cast<HANDLE>(::_get_osfhandle(FD));
|
||||
DWORD temp;
|
||||
if (!DeviceIoControl(hFile, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &temp,
|
||||
NULL)) {
|
||||
return mapWindowsError(GetLastError());
|
||||
}
|
||||
LARGE_INTEGER liSize;
|
||||
liSize.QuadPart = Size;
|
||||
if (!SetFilePointerEx(hFile, liSize, NULL, FILE_BEGIN) ||
|
||||
!SetEndOfFile(hFile)) {
|
||||
return mapWindowsError(GetLastError());
|
||||
}
|
||||
return std::error_code();
|
||||
}
|
||||
|
||||
std::error_code access(const Twine &Path, AccessMode Mode) {
|
||||
SmallVector<wchar_t, 128> PathUtf16;
|
||||
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
if (LLVM_ENABLE_ONDISK_CAS)
|
||||
add_definitions(-DLLVM_ENABLE_ONDISK_CAS=1)
|
||||
endif()
|
||||
|
||||
set(LLVM_LINK_COMPONENTS
|
||||
Support
|
||||
CAS
|
||||
@ -7,6 +11,7 @@ set(LLVM_LINK_COMPONENTS
|
||||
add_llvm_unittest(CASTests
|
||||
CASTestConfig.cpp
|
||||
ObjectStoreTest.cpp
|
||||
ProgramTest.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(CASTests PRIVATE LLVMTestingSupport)
|
||||
|
||||
153
llvm/unittests/CAS/ProgramTest.cpp
Normal file
153
llvm/unittests/CAS/ProgramTest.cpp
Normal file
@ -0,0 +1,153 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#include "llvm/Support/Program.h"
|
||||
#include "llvm/CAS/MappedFileRegionBumpPtr.h"
|
||||
#include "llvm/Config/llvm-config.h"
|
||||
#include "llvm/Support/ConvertUTF.h"
|
||||
#include "llvm/Support/FileSystem.h"
|
||||
#include "llvm/Support/Path.h"
|
||||
#include "llvm/Support/ThreadPool.h"
|
||||
#include "gtest/gtest.h"
|
||||
#if defined(__APPLE__)
|
||||
#include <crt_externs.h>
|
||||
#elif !defined(_MSC_VER)
|
||||
// Forward declare environ in case it's not provided by stdlib.h.
|
||||
extern char **environ;
|
||||
#endif
|
||||
|
||||
using namespace llvm;
|
||||
using namespace llvm::cas;
|
||||
|
||||
extern const char *TestMainArgv0;
|
||||
static char ProgramID = 0;
|
||||
|
||||
class CASProgramTest : public testing::Test {
|
||||
std::vector<StringRef> EnvTable;
|
||||
std::vector<std::string> EnvStorage;
|
||||
|
||||
protected:
|
||||
void SetUp() override {
|
||||
auto EnvP = [] {
|
||||
#if defined(_WIN32)
|
||||
_wgetenv(L"TMP"); // Populate _wenviron, initially is null
|
||||
return _wenviron;
|
||||
#elif defined(__APPLE__)
|
||||
return *_NSGetEnviron();
|
||||
#else
|
||||
return environ;
|
||||
#endif
|
||||
}();
|
||||
ASSERT_TRUE(EnvP);
|
||||
|
||||
auto prepareEnvVar = [this](decltype(*EnvP) Var) -> StringRef {
|
||||
#if defined(_WIN32)
|
||||
// On Windows convert UTF16 encoded variable to UTF8
|
||||
auto Len = wcslen(Var);
|
||||
ArrayRef<char> Ref{reinterpret_cast<char const *>(Var),
|
||||
Len * sizeof(*Var)};
|
||||
EnvStorage.emplace_back();
|
||||
auto convStatus = llvm::convertUTF16ToUTF8String(Ref, EnvStorage.back());
|
||||
EXPECT_TRUE(convStatus);
|
||||
return EnvStorage.back();
|
||||
#else
|
||||
(void)this;
|
||||
return StringRef(Var);
|
||||
#endif
|
||||
};
|
||||
|
||||
while (*EnvP != nullptr) {
|
||||
auto S = prepareEnvVar(*EnvP);
|
||||
if (!StringRef(S).starts_with("GTEST_"))
|
||||
EnvTable.emplace_back(S);
|
||||
++EnvP;
|
||||
}
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
EnvTable.clear();
|
||||
EnvStorage.clear();
|
||||
}
|
||||
|
||||
void addEnvVar(StringRef Var) { EnvTable.emplace_back(Var); }
|
||||
|
||||
ArrayRef<StringRef> getEnviron() const { return EnvTable; }
|
||||
};
|
||||
|
||||
#if LLVM_ENABLE_ONDISK_CAS
|
||||
|
||||
TEST_F(CASProgramTest, MappedFileRegionBumpPtrTest) {
|
||||
auto TestAllocator = [](StringRef Path) {
|
||||
auto NewFileConstructor = [&](MappedFileRegionBumpPtr &Alloc) -> Error {
|
||||
Alloc.initializeBumpPtr(0);
|
||||
return Error::success();
|
||||
};
|
||||
|
||||
auto Alloc = MappedFileRegionBumpPtr::create(
|
||||
Path, /*Capacity=*/10 * 1024 * 1024,
|
||||
/*BumpPtrOffset=*/0, NewFileConstructor);
|
||||
if (!Alloc)
|
||||
ASSERT_TRUE(false);
|
||||
|
||||
std::vector<unsigned *> AllocatedPtr;
|
||||
AllocatedPtr.resize(100);
|
||||
DefaultThreadPool Threads;
|
||||
for (unsigned I = 0; I < 100; ++I) {
|
||||
Threads.async(
|
||||
[&](unsigned Idx) {
|
||||
// Allocate a buffer that is larger than needed so allocator hits
|
||||
// additional pages for test coverage.
|
||||
unsigned *P = (unsigned *)cantFail(Alloc->allocate(100));
|
||||
*P = Idx;
|
||||
AllocatedPtr[Idx] = P;
|
||||
},
|
||||
I);
|
||||
}
|
||||
|
||||
Threads.wait();
|
||||
for (unsigned I = 0; I < 100; ++I)
|
||||
EXPECT_EQ(*AllocatedPtr[I], I);
|
||||
};
|
||||
|
||||
if (const char *File = getenv("LLVM_CAS_TEST_MAPPED_FILE_REGION")) {
|
||||
TestAllocator(File);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
SmallString<128> FilePath;
|
||||
sys::fs::createUniqueDirectory("MappedFileRegionBumpPtr", FilePath);
|
||||
sys::path::append(FilePath, "allocation-file");
|
||||
|
||||
std::string Executable =
|
||||
sys::fs::getMainExecutable(TestMainArgv0, &ProgramID);
|
||||
StringRef Argv[] = {
|
||||
Executable, "--gtest_filter=CASProgramTest.MappedFileRegionBumpPtrTest"};
|
||||
|
||||
// Add LLVM_PROGRAM_TEST_LOCKED_FILE to the environment of the child.
|
||||
std::string EnvVar = "LLVM_CAS_TEST_MAPPED_FILE_REGION=";
|
||||
EnvVar += FilePath.str();
|
||||
addEnvVar(EnvVar);
|
||||
|
||||
std::string Error;
|
||||
bool ExecutionFailed;
|
||||
sys::ProcessInfo PI = sys::ExecuteNoWait(Executable, Argv, getEnviron(), {},
|
||||
0, &Error, &ExecutionFailed);
|
||||
TestAllocator(FilePath);
|
||||
|
||||
ASSERT_FALSE(ExecutionFailed) << Error;
|
||||
ASSERT_TRUE(Error.empty());
|
||||
ASSERT_NE(PI.Pid, sys::ProcessInfo::InvalidPid) << "Invalid process id";
|
||||
llvm::sys::Wait(PI, /*SecondsToWait=*/5, &Error);
|
||||
ASSERT_TRUE(Error.empty());
|
||||
|
||||
// Clean up after both processes finish testing.
|
||||
sys::fs::remove(FilePath);
|
||||
sys::fs::remove_directories(sys::path::parent_path(FilePath));
|
||||
}
|
||||
|
||||
#endif // LLVM_ENABLE_ONDISK_CAS
|
||||
Loading…
x
Reference in New Issue
Block a user