/*
* Copyright (C) 2010 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "DiskImageCache.h"
#if ENABLE(DISK_IMAGE_CACHE)
#include "FileSystem.h"
#include "Logging.h"
#include "WebCoreThread.h"
#include "WebCoreThreadMessage.h"
#include <errno.h>
#include <sys/mman.h>
#include <wtf/text/CString.h>
@interface UpdateBufferOnWebThreadDispatcher : NSObject {
RefPtr<WebCore::SharedBuffer> m_buffer;
}
- (id)initWithBuffer:(PassRefPtr<WebCore::SharedBuffer>)buffer;
- (void)runAndDispose;
@end
@implementation UpdateBufferOnWebThreadDispatcher
- (id)initWithBuffer:(PassRefPtr<WebCore::SharedBuffer>)buffer
{
ASSERT(buffer);
self = [super init];
if (!self)
return nil;
m_buffer = buffer;
return self;
}
- (void)runAndDispose
{
// Notify the buffer in the case of a successful mapping.
m_buffer->markAsMemoryMapped();
// Stop holding on to the buffer, we don't want to keep it alive.
m_buffer.clear();
// Dispose of ourselves.
[self release];
}
@end
namespace WebCore {
DiskImageCache* diskImageCache()
{
DEFINE_STATIC_LOCAL(DiskImageCache, staticCache, ());
return &staticCache;
}
void DiskImageCache::Entry::map(const String& path)
{
ASSERT(m_buffer.get());
ASSERT(!m_mapping);
// Optimization: Don't map the buffer if we are the only object holding a
// reference to the buffer. Just remove our reference and return. When the
// buffer deconstructs it will remove itself. The removal is asynchronous,
// so we don't need to worry about this entry being deleted immediately,
// so our caller is safe to check if the entry was mapped or not.
if (m_buffer.get()->hasOneRef()) {
m_buffer.clear();
return;
}
m_path = path;
m_size = m_buffer->size();
// Open the file for reading and writing.
PlatformFileHandle handle = open(m_path.utf8().data(), O_CREAT | O_RDWR | O_TRUNC, (mode_t)0600);
if (!isHandleValid(handle))
return;
// Write the data to the file.
if (writeToFile(handle, m_buffer->data(), m_size) == -1) {
closeFile(handle);
deleteFile(m_path);
return;
}
// Seek back to the beginning.
if (seekFile(handle, 0, SeekFromBeginning) == -1) {
closeFile(handle);
deleteFile(m_path);
return;
}
// Perform Memory mapping for reading.
// NOTE: This must not conflict with the open() above, which must also open for reading.
m_mapping = mmap(0, m_size, PROT_READ, MAP_SHARED, handle, 0);
closeFile(handle);
if (m_mapping == MAP_FAILED) {
LOG(DiskImageCache, "DiskImageCache: mapping failed (%d): (%s)", errno, strerror(errno));
m_mapping = NULL;
deleteFile(m_path);
return;
}
// Notify the buffer in the case of a successful mapping.
// This should happen on the WebThread, because this is being run
// asynchronously inside a dispatch queue.
ASSERT(WebThreadNotCurrent());
id dispatcher = [[UpdateBufferOnWebThreadDispatcher alloc] initWithBuffer:m_buffer.release()];
NSInvocation *invocation = WebThreadMakeNSInvocation(dispatcher, @selector(runAndDispose));
WebThreadCallAPI(invocation);
}
void DiskImageCache::Entry::unmap()
{
if (!m_mapping) {
ASSERT(!m_size);
return;
}
if (munmap(m_mapping, m_size) == -1)
LOG_ERROR("DiskImageCache: Could not munmap a memory mapped file with id (%d)", m_id);
m_mapping = NULL;
m_size = 0;
}
void DiskImageCache::Entry::removeFile()
{
ASSERT(!m_mapping);
ASSERT(!m_size);
if (!deleteFile(m_path))
LOG_ERROR("DiskImageCache: Could not delete memory mapped file (%s)", m_path.utf8().data());
}
DiskImageCache::DiskImageCache()
: m_enabled(false)
, m_size(0)
, m_maximumCacheSize(100 * 1024 * 1024)
, m_minimumImageSize(100 * 1024)
, m_nextAvailableId(DiskImageCache::invalidDiskCacheId + 1)
{
}
disk_cache_id_t DiskImageCache::writeItem(PassRefPtr<SharedBuffer> item)
{
if (!isEnabled() || !createDirectoryIfNeeded())
return DiskImageCache::invalidDiskCacheId;
// We are already full, cannot add anything until something is removed.
if (isFull()) {
LOG(DiskImageCache, "DiskImageCache: could not process an item because the cache was full at (%d). The \"max\" being (%d)", m_size, m_maximumCacheSize);
return DiskImageCache::invalidDiskCacheId;
}
// Create an entry.
disk_cache_id_t id = nextAvailableId();
RefPtr<DiskImageCache::Entry> entry = DiskImageCache::Entry::create(item, id);
m_table.add(id, entry);
// Create a temporary file path.
String path = temporaryFile();
LOG(DiskImageCache, "DiskImageCache: creating entry (%d) at (%s)", id, path.utf8().data());
if (path.isNull())
return DiskImageCache::invalidDiskCacheId;
// Map to disk asynchronously.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// The cache became full since the time we were added to the queue. Don't map.
if (diskImageCache()->isFull())
return;
entry->map(path);
// Update the size on a sucessful mapping.
if (entry->isMapped())
diskImageCache()->updateSize(entry->size());
});
return id;
}
void DiskImageCache::updateSize(unsigned delta)
{
MutexLocker lock(m_mutex);
m_size += delta;
}
void DiskImageCache::removeItem(disk_cache_id_t id)
{
LOG(DiskImageCache, "DiskImageCache: removeItem (%d)", id);
RefPtr<DiskImageCache::Entry> entry = m_table.get(id);
m_table.remove(id);
if (!entry->isMapped())
return;
updateSize(-(entry->size()));
dispatch_async(dispatch_get_global_queue(0, 0), ^{
entry->unmap();
entry->removeFile();
});
}
void* DiskImageCache::dataForItem(disk_cache_id_t id)
{
ASSERT(id);
RefPtr<DiskImageCache::Entry> entry = m_table.get(id);
ASSERT(entry->isMapped());
return entry->data();
}
bool DiskImageCache::createDirectoryIfNeeded()
{
if (!m_cacheDirectory.isNull())
return true;
m_cacheDirectory = temporaryDirectory();
LOG(DiskImageCache, "DiskImageCache: Created temporary directory (%s)", m_cacheDirectory.utf8().data());
if (m_cacheDirectory.isNull()) {
LOG_ERROR("DiskImageCache: could not create cache directory");
return false;
}
if (m_client)
m_client->didCreateDiskImageCacheDirectory(m_cacheDirectory);
return true;
}
disk_cache_id_t DiskImageCache::nextAvailableId()
{
disk_cache_id_t nextId = m_nextAvailableId;
m_nextAvailableId++;
return nextId;
}
} // namespace WebCore
#endif // ENABLE(DISK_IMAGE_CACHE)