forked from mirrors/0ad
add WIP file code to a new folder (not included in the build)
This was SVN commit r5443.
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* =========================================================================
|
||||
* File : file_cache.cpp
|
||||
* Project : 0 A.D.
|
||||
* Description : cache of file contents (supports zero-copy IO)
|
||||
* =========================================================================
|
||||
*/
|
||||
|
||||
// license: GPL; see lib/license.txt
|
||||
|
||||
#include "precompiled.h"
|
||||
#include "file_cache.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "path.h"
|
||||
#include "file_stats.h"
|
||||
#include "archive/trace.h"
|
||||
#include "lib/cache_adt.h" // Cache
|
||||
#include "lib/lockfree.h"
|
||||
#include "lib/bits.h" // round_up
|
||||
#include "lib/allocators/allocators.h"
|
||||
#include "lib/allocators/headerless.h"
|
||||
#include "lib/allocators/mem_util.h" // mem_PageSize
|
||||
|
||||
|
||||
// >= sys_max_sector_size or else waio will have to realign.
|
||||
// chosen as exactly 1 page: this allows write-protecting file buffers
|
||||
// without worrying about their (non-page-aligned) borders.
|
||||
// internal fragmentation is considerable but acceptable.
|
||||
static const size_t alignment = mem_PageSize();
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// allocator
|
||||
|
||||
/*
|
||||
the biggest worry of a file cache is external fragmentation. there are two
|
||||
basic ways to combat this:
|
||||
1) 'defragment' periodically - move blocks around to increase
|
||||
size of available 'holes'.
|
||||
2) prevent fragmentation from occurring at all via
|
||||
deliberate alloc/free policy.
|
||||
|
||||
file contents are returned directly to the user (zero-copy IO), so only
|
||||
currently unreferenced blocks can be moved. it is believed that this would
|
||||
severely hamper defragmentation; we therefore go with the latter approach.
|
||||
|
||||
the basic insight is: fragmentation occurs when a block is freed whose
|
||||
neighbors are not free (thus preventing coalescing). this can be prevented by
|
||||
allocating objects of similar lifetimes together. typical workloads
|
||||
(uniform access frequency) already show such behavior: the Landlord cache
|
||||
manager evicts files in an LRU manner, which matches the allocation policy.
|
||||
|
||||
references:
|
||||
"The Memory Fragmentation Problem - Solved?" (Johnstone and Wilson)
|
||||
"Dynamic Storage Allocation - A Survey and Critical Review" (Johnstone and Wilson)
|
||||
*/
|
||||
|
||||
class Allocator
|
||||
{
|
||||
public:
|
||||
Allocator(size_t maxSize)
|
||||
: m_allocator(maxSize)
|
||||
{
|
||||
}
|
||||
|
||||
IoBuf Allocate(size_t size)
|
||||
{
|
||||
const size_t alignedSize = round_up(size, alignment);
|
||||
stats_buf_alloc(size, alignedSize);
|
||||
|
||||
void* p = m_allocator.Allocate(alignedSize);
|
||||
#ifndef NDEBUG
|
||||
m_checker.notify_alloc(p, alignedSize);
|
||||
#endif
|
||||
|
||||
return (IoBuf)p;
|
||||
}
|
||||
|
||||
void Deallocate(IoBuf buf, size_t size)
|
||||
{
|
||||
void* const p = (void*)buf;
|
||||
|
||||
// (re)allow writes. it would be nice to un-map the buffer, but this is
|
||||
// not possible because HeaderlessAllocator needs to affix boundary tags.
|
||||
(void)mprotect(p, size, PROT_READ|PROT_WRITE);
|
||||
|
||||
const size_t alignedSize = round_up(size, alignment);
|
||||
#ifndef NDEBUG
|
||||
m_checker.notify_free(p, alignedSize);
|
||||
#endif
|
||||
m_allocator.Deallocate(p, alignedSize);
|
||||
|
||||
stats_buf_free();
|
||||
}
|
||||
|
||||
private:
|
||||
HeaderlessAllocator m_allocator;
|
||||
|
||||
#ifndef NDEBUG
|
||||
static AllocatorChecker m_checker;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
typedef LF_RefCountedMemRange FileContents;
|
||||
|
||||
/**
|
||||
* manages the active FileContents referenced by users.
|
||||
* ("active" means between Reserve() and the final Release())
|
||||
**/
|
||||
class ActiveList
|
||||
{
|
||||
public:
|
||||
~ActiveList()
|
||||
{
|
||||
// display leaks
|
||||
debug_printf("file_cache leaks:\n");
|
||||
for(MapIt it = m_map.begin(); it != m_map.end(); ++it)
|
||||
{
|
||||
const char* atom_fn = it->first;
|
||||
FileContents& fc = it->second;
|
||||
debug_printf(" %s (0x%P 0x%08x)\n", atom_fn, fc.mem, fc.size);
|
||||
}
|
||||
debug_printf("--------\n");
|
||||
}
|
||||
|
||||
void Add(const char* atom_fn, FileContents& fc)
|
||||
{
|
||||
const PairIB ret = m_map.insert(std::make_pair(atom_fn, fc));
|
||||
debug_assert(ret.second); // complain if already existed
|
||||
}
|
||||
|
||||
void Remove(const char* atom_fn)
|
||||
{
|
||||
const size_t numRemoved = m_map.erase(atom_fn);
|
||||
debug_assert(numRemoved == 1);
|
||||
}
|
||||
|
||||
FileContents* Find(const char* atom_fn)
|
||||
{
|
||||
MapIt it = m_map.find(atom_fn);
|
||||
if(it == m_map.end())
|
||||
return 0; // not an error
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
// (called by FileCache::Impl::AllocateCacheSpace; we can't pass
|
||||
// atom_fn because Cache only knows about buf and size.)
|
||||
bool Contains(IoBuf buf) const
|
||||
{
|
||||
for(MapCIt it = m_map.begin(); it != m_map.end(); ++it)
|
||||
{
|
||||
const FileContents& fc = it->second;
|
||||
if(fc.mem == buf)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
typedef std::map<const char*, FileContents> Map;
|
||||
typedef Map::iterator MapIt;
|
||||
typedef Map::const_iterator MapCIt;
|
||||
typedef std::pair<MapIt, bool> PairIB;
|
||||
Map m_map;
|
||||
};
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// FileCache::Impl
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// the organization of this cache is somewhat counterintuitive. one might
|
||||
// expect a simple mapping of filename to FileContents. however, since users
|
||||
// are strongly encouraged to only load/process one file at a time, there
|
||||
// will only be a few active references. with the cache holding many more
|
||||
// entries, looking up files there is more expensive than consulting a
|
||||
// separate list of active FileContents.
|
||||
// this list (the "manager") and the cache contents are not necessarily
|
||||
// related; no inclusion relation need hold. the only requirement is that
|
||||
// each consult the other on ownership issues. if the cache decides a file
|
||||
// should be evicted while references to it are active, or users release a
|
||||
// reference to FileContents that the cache wants to keep, the memory must
|
||||
// not actually be freed. (it is then logically 'owned' by the other)
|
||||
|
||||
class FileCache::Impl
|
||||
{
|
||||
public:
|
||||
Impl(size_t size)
|
||||
: m_allocator(size)
|
||||
{
|
||||
}
|
||||
|
||||
IoBuf Reserve(const char* atom_fn, size_t size)
|
||||
{
|
||||
// (this probably indicates a bug; caching 0-length files would
|
||||
// have no benefit, anyway)
|
||||
debug_assert(size != 0);
|
||||
|
||||
IoBuf buf = AllocateCacheSpace(size);
|
||||
|
||||
FileContents fc;
|
||||
fc.refs.AcquireExclusiveAccess();
|
||||
fc.mem = (void*)buf;
|
||||
fc.size = size;
|
||||
m_activeList.Add(atom_fn, fc);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
void MarkComplete(const char* atom_fn, uint cost)
|
||||
{
|
||||
FileContents* fc = m_activeList.Find(atom_fn);
|
||||
debug_assert(fc);
|
||||
|
||||
fc->refs.RelinquishExclusiveAccess();
|
||||
|
||||
// zero-copy cache => all users share the contents => must not
|
||||
// allow changes. this will be reverted when the buffer is freed.
|
||||
(void)mprotect(fc->mem, fc->size, PROT_READ);
|
||||
|
||||
m_cache.add(atom_fn, (IoBuf)fc->mem, fc->size, cost);
|
||||
}
|
||||
|
||||
IoBuf Retrieve(const char* atom_fn, size_t& size)
|
||||
{
|
||||
IoBuf buf;
|
||||
if(!m_cache.retrieve(atom_fn, buf, &size))
|
||||
return 0;
|
||||
|
||||
FileContents* pfc = m_activeList.Find(atom_fn);
|
||||
// was already active; add a reference.
|
||||
if(pfc)
|
||||
pfc->refs.AddReference();
|
||||
// in cache, but no active references; add to list.
|
||||
else
|
||||
{
|
||||
FileContents fc;
|
||||
fc.refs.AddReference();
|
||||
fc.mem = (void*)buf;
|
||||
fc.size = size;
|
||||
m_activeList.Add(atom_fn, fc);
|
||||
}
|
||||
|
||||
stats_buf_ref();
|
||||
return buf;
|
||||
}
|
||||
|
||||
void Release(const char* atom_fn)
|
||||
{
|
||||
FileContents* fc = m_activeList.Find(atom_fn);
|
||||
debug_assert(fc);
|
||||
|
||||
fc->refs.Release();
|
||||
if(fc->refs.ReferenceCount() == 0)
|
||||
{
|
||||
trace_notify_free(atom_fn);
|
||||
|
||||
if(!IsInCache(atom_fn))
|
||||
m_allocator.Deallocate((IoBuf)fc->mem, fc->size);
|
||||
|
||||
m_activeList.Remove(atom_fn);
|
||||
}
|
||||
}
|
||||
|
||||
LibError Invalidate(const char* atom_fn)
|
||||
{
|
||||
// remove from cache
|
||||
IoBuf cachedBuf; size_t cachedSize;
|
||||
if(m_cache.peek(atom_fn, cachedBuf, &cachedSize))
|
||||
{
|
||||
m_cache.remove(atom_fn);
|
||||
// note: we ensure cachedBuf is not active below.
|
||||
m_allocator.Deallocate(cachedBuf, cachedSize);
|
||||
}
|
||||
|
||||
// this could happen if a hotload notification comes while someone
|
||||
// is holding a reference to the file contents. atom_fn has been
|
||||
// removed from the cache, so subsequent Retrieve() calls will not
|
||||
// return old data. however, (re)loading the file would fail because
|
||||
// Reserve() ensures there's not already an extant buffer.
|
||||
// the correct way to handle this is to delay or cancel the reload,
|
||||
// so we notify our caller accordingly.
|
||||
if(IsActive(cachedBuf))
|
||||
WARN_RETURN(ERR::AGAIN); // if this actually happens, remove the warning.
|
||||
|
||||
return INFO::OK;
|
||||
}
|
||||
|
||||
private:
|
||||
bool IsActive(IoBuf buf) const
|
||||
{
|
||||
return m_activeList.Contains(buf);
|
||||
}
|
||||
|
||||
bool IsInCache(const char* atom_fn) const
|
||||
{
|
||||
IoBuf cachedBuf; size_t cachedSize; // unused
|
||||
return m_cache.peek(atom_fn, cachedBuf, &cachedSize);
|
||||
}
|
||||
|
||||
IoBuf AllocateCacheSpace(size_t size)
|
||||
{
|
||||
uint attempts = 0;
|
||||
for(;;)
|
||||
{
|
||||
IoBuf buf = m_allocator.Allocate(size);
|
||||
if(buf)
|
||||
return buf;
|
||||
|
||||
// remove least valuable entry from cache
|
||||
IoBuf discardedBuf; size_t discardedSize;
|
||||
bool removed = m_cache.remove_least_valuable(&discardedBuf, &discardedSize);
|
||||
// only false if cache is empty, which can't be the case because
|
||||
// allocation failed.
|
||||
debug_assert(removed);
|
||||
|
||||
// someone is holding a reference; we must not free the
|
||||
// underlying memory, nor count this iteration.
|
||||
if(IsActive(discardedBuf))
|
||||
continue;
|
||||
|
||||
m_allocator.Deallocate(discardedBuf, discardedSize);
|
||||
|
||||
// note: this may seem hefty, but 300 is known to be reached.
|
||||
// (after building an archive, the file cache will be full;
|
||||
// attempting to allocate a few MB can take a while if only
|
||||
// small scattered blocks are freed.)
|
||||
debug_assert(++attempts < 500); // otherwise: failed to make room in cache?!
|
||||
}
|
||||
}
|
||||
|
||||
ActiveList m_activeList;
|
||||
|
||||
// HACK: due to atom_fn, we are assured that strings are equal iff their
|
||||
// addresses match. however, Cache's STL (hash_)map stupidly assumes that
|
||||
// const char* keys are "strings". to avoid this behavior, we specify the
|
||||
// key as const void*.
|
||||
static Cache<const void*, IoBuf> m_cache;
|
||||
|
||||
Allocator m_allocator;
|
||||
};
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
FileCache::FileCache(size_t size)
|
||||
: impl(new Impl(size))
|
||||
{
|
||||
}
|
||||
|
||||
IoBuf FileCache::Reserve(const char* atom_fn, size_t size)
|
||||
{
|
||||
return impl.get()->Reserve(atom_fn, size);
|
||||
}
|
||||
|
||||
void FileCache::MarkComplete(const char* atom_fn, uint cost)
|
||||
{
|
||||
impl.get()->MarkComplete(atom_fn, cost);
|
||||
}
|
||||
|
||||
IoBuf FileCache::Retrieve(const char* atom_fn, size_t& size)
|
||||
{
|
||||
return impl.get()->Retrieve(atom_fn, size);
|
||||
}
|
||||
|
||||
void FileCache::Release(const char* atom_fn)
|
||||
{
|
||||
impl.get()->Release(atom_fn);
|
||||
}
|
||||
|
||||
LibError FileCache::Invalidate(const char* atom_fn)
|
||||
{
|
||||
return impl.get()->Invalidate(atom_fn);
|
||||
}
|
||||
Reference in New Issue
Block a user