summaryrefslogtreecommitdiffstats
path: root/dom/media/FileBlockCache.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/FileBlockCache.cpp')
-rw-r--r--dom/media/FileBlockCache.cpp506
1 files changed, 506 insertions, 0 deletions
diff --git a/dom/media/FileBlockCache.cpp b/dom/media/FileBlockCache.cpp
new file mode 100644
index 0000000000..3989c05833
--- /dev/null
+++ b/dom/media/FileBlockCache.cpp
@@ -0,0 +1,506 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileBlockCache.h"
+#include "MediaCache.h"
+#include "VideoUtils.h"
+#include "prio.h"
+#include <algorithm>
+#include "nsAnonymousTemporaryFile.h"
+#include "nsIThreadManager.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/ScopeExit.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+
+#undef LOG
+LazyLogModule gFileBlockCacheLog("FileBlockCache");
+#define LOG(x, ...) \
+ MOZ_LOG(gFileBlockCacheLog, LogLevel::Debug, ("%p " x, this, ##__VA_ARGS__))
+
+static void CloseFD(PRFileDesc* aFD) {
+ PRStatus prrc;
+ prrc = PR_Close(aFD);
+ if (prrc != PR_SUCCESS) {
+ NS_WARNING("PR_Close() failed.");
+ }
+}
+
+void FileBlockCache::SetCacheFile(PRFileDesc* aFD) {
+ LOG("SetCacheFile aFD=%p", aFD);
+ if (!aFD) {
+ // Failed to get a temporary file. Shutdown.
+ Close();
+ return;
+ }
+ {
+ MutexAutoLock lock(mFileMutex);
+ mFD = aFD;
+ }
+ {
+ MutexAutoLock lock(mDataMutex);
+ LOG("SetFileCache mBackgroundET=%p, mIsWriteScheduled %d",
+ mBackgroundET.get(), mIsWriteScheduled);
+ if (mBackgroundET) {
+ // Still open, complete the initialization.
+ mInitialized = true;
+ if (mIsWriteScheduled) {
+ // A write was scheduled while waiting for FD. We need to run/dispatch a
+ // task to service the request.
+ nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod(
+ "FileBlockCache::SetCacheFile -> PerformBlockIOs", this,
+ &FileBlockCache::PerformBlockIOs);
+ mBackgroundET->Dispatch(event.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
+ }
+ return;
+ }
+ }
+ // We've been closed while waiting for the file descriptor.
+ // Close the file descriptor we've just received, if still there.
+ MutexAutoLock lock(mFileMutex);
+ if (mFD) {
+ CloseFD(mFD);
+ mFD = nullptr;
+ }
+}
+
+nsresult FileBlockCache::Init() {
+ LOG("Init()");
+ MutexAutoLock mon(mDataMutex);
+ MOZ_ASSERT(!mBackgroundET);
+ nsresult rv = NS_CreateBackgroundTaskQueue("FileBlockCache",
+ getter_AddRefs(mBackgroundET));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (XRE_IsParentProcess()) {
+ RefPtr<FileBlockCache> self = this;
+ rv = mBackgroundET->Dispatch(
+ NS_NewRunnableFunction("FileBlockCache::Init",
+ [self] {
+ PRFileDesc* fd = nullptr;
+ nsresult rv =
+ NS_OpenAnonymousTemporaryFile(&fd);
+ if (NS_SUCCEEDED(rv)) {
+ self->SetCacheFile(fd);
+ } else {
+ self->Close();
+ }
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ } else {
+ // We must request a temporary file descriptor from the parent process.
+ RefPtr<FileBlockCache> self = this;
+ rv = dom::ContentChild::GetSingleton()->AsyncOpenAnonymousTemporaryFile(
+ [self](PRFileDesc* aFD) { self->SetCacheFile(aFD); });
+ }
+
+ if (NS_FAILED(rv)) {
+ Close();
+ }
+
+ return rv;
+}
+
+void FileBlockCache::Flush() {
+ LOG("Flush()");
+ MutexAutoLock mon(mDataMutex);
+ MOZ_ASSERT(mBackgroundET);
+
+ // Dispatch a task so we won't clear the arrays while PerformBlockIOs() is
+ // dropping the data lock and cause InvalidArrayIndex.
+ RefPtr<FileBlockCache> self = this;
+ mBackgroundET->Dispatch(
+ NS_NewRunnableFunction("FileBlockCache::Flush", [self]() {
+ MutexAutoLock mon(self->mDataMutex);
+ // Just discard pending changes, assume MediaCache won't read from
+ // blocks it hasn't written to.
+ self->mChangeIndexList.clear();
+ self->mBlockChanges.Clear();
+ }));
+}
+
+size_t FileBlockCache::GetMaxBlocks(size_t aCacheSizeInKB) const {
+ // We look up the cache size every time. This means dynamic changes
+ // to the pref are applied.
+ // Ensure we can divide BLOCK_SIZE by 1024.
+ static_assert(MediaCacheStream::BLOCK_SIZE % 1024 == 0,
+ "BLOCK_SIZE should be a multiple of 1024");
+ // Ensure BLOCK_SIZE/1024 is at least 2.
+ static_assert(MediaCacheStream::BLOCK_SIZE / 1024 >= 2,
+ "BLOCK_SIZE / 1024 should be at least 2");
+ // Ensure we can convert BLOCK_SIZE/1024 to a uint32_t without truncation.
+ static_assert(MediaCacheStream::BLOCK_SIZE / 1024 <= int64_t(UINT32_MAX),
+ "BLOCK_SIZE / 1024 should be at most UINT32_MAX");
+ // Since BLOCK_SIZE is a strict multiple of 1024,
+ // aCacheSizeInKB * 1024 / BLOCK_SIZE == aCacheSizeInKB / (BLOCK_SIZE /
+ // 1024), but the latter formula avoids a potential overflow from `* 1024`.
+ // And because BLOCK_SIZE/1024 is at least 2, the maximum cache size
+ // INT32_MAX*2 will give a maxBlocks that can fit in an int32_t.
+ constexpr size_t blockSizeKb = size_t(MediaCacheStream::BLOCK_SIZE / 1024);
+ const size_t maxBlocks = aCacheSizeInKB / blockSizeKb;
+ return std::max(maxBlocks, size_t(1));
+}
+
+FileBlockCache::FileBlockCache()
+ : mFileMutex("MediaCache.Writer.IO.Mutex"),
+ mFD(nullptr),
+ mFDCurrentPos(0),
+ mDataMutex("MediaCache.Writer.Data.Mutex"),
+ mIsWriteScheduled(false),
+ mIsReading(false) {}
+
+FileBlockCache::~FileBlockCache() { Close(); }
+
+void FileBlockCache::Close() {
+ LOG("Close()");
+
+ nsCOMPtr<nsISerialEventTarget> thread;
+ {
+ MutexAutoLock mon(mDataMutex);
+ if (!mBackgroundET) {
+ return;
+ }
+ thread.swap(mBackgroundET);
+ }
+
+ PRFileDesc* fd;
+ {
+ MutexAutoLock lock(mFileMutex);
+ fd = mFD;
+ mFD = nullptr;
+ }
+
+ // Let the thread close the FD, and then trigger its own shutdown.
+ // Note that mBackgroundET is now empty, so no other task will be posted
+ // there. Also mBackgroundET and mFD are empty and therefore can be reused
+ // immediately.
+ nsresult rv = thread->Dispatch(NS_NewRunnableFunction("FileBlockCache::Close",
+ [thread, fd] {
+ if (fd) {
+ CloseFD(fd);
+ }
+ // No need to shutdown
+ // background task
+ // queues.
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+template <typename Container, typename Value>
+bool ContainerContains(const Container& aContainer, const Value& value) {
+ return std::find(aContainer.begin(), aContainer.end(), value) !=
+ aContainer.end();
+}
+
+nsresult FileBlockCache::WriteBlock(uint32_t aBlockIndex,
+ Span<const uint8_t> aData1,
+ Span<const uint8_t> aData2) {
+ MutexAutoLock mon(mDataMutex);
+
+ if (!mBackgroundET) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check if we've already got a pending write scheduled for this block.
+ mBlockChanges.EnsureLengthAtLeast(aBlockIndex + 1);
+ bool blockAlreadyHadPendingChange = mBlockChanges[aBlockIndex] != nullptr;
+ mBlockChanges[aBlockIndex] = new BlockChange(aData1, aData2);
+
+ if (!blockAlreadyHadPendingChange ||
+ !ContainerContains(mChangeIndexList, aBlockIndex)) {
+ // We either didn't already have a pending change for this block, or we
+ // did but we didn't have an entry for it in mChangeIndexList (we're in the
+ // process of writing it and have removed the block's index out of
+ // mChangeIndexList in Run() but not finished writing the block to file
+ // yet). Add the blocks index to the end of mChangeIndexList to ensure the
+ // block is written as as soon as possible.
+ mChangeIndexList.push_back(aBlockIndex);
+ }
+ NS_ASSERTION(ContainerContains(mChangeIndexList, aBlockIndex),
+ "Must have entry for new block");
+
+ EnsureWriteScheduled();
+
+ return NS_OK;
+}
+
+void FileBlockCache::EnsureWriteScheduled() {
+ mDataMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(mBackgroundET);
+
+ if (mIsWriteScheduled || mIsReading) {
+ return;
+ }
+ mIsWriteScheduled = true;
+ if (!mInitialized) {
+ // We're still waiting on a file descriptor. When it arrives,
+ // the write will be scheduled.
+ return;
+ }
+ nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod(
+ "FileBlockCache::EnsureWriteScheduled -> PerformBlockIOs", this,
+ &FileBlockCache::PerformBlockIOs);
+ mBackgroundET->Dispatch(event.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
+}
+
+nsresult FileBlockCache::Seek(int64_t aOffset) {
+ mFileMutex.AssertCurrentThreadOwns();
+
+ if (mFDCurrentPos != aOffset) {
+ MOZ_ASSERT(mFD);
+ int64_t result = PR_Seek64(mFD, aOffset, PR_SEEK_SET);
+ if (result != aOffset) {
+ NS_WARNING("Failed to seek media cache file");
+ return NS_ERROR_FAILURE;
+ }
+ mFDCurrentPos = result;
+ }
+ return NS_OK;
+}
+
+nsresult FileBlockCache::ReadFromFile(int64_t aOffset, uint8_t* aDest,
+ int32_t aBytesToRead,
+ int32_t& aBytesRead) {
+ LOG("ReadFromFile(offset=%" PRIu64 ", len=%u)", aOffset, aBytesToRead);
+ mFileMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(mFD);
+
+ nsresult res = Seek(aOffset);
+ if (NS_FAILED(res)) return res;
+
+ aBytesRead = PR_Read(mFD, aDest, aBytesToRead);
+ if (aBytesRead <= 0) return NS_ERROR_FAILURE;
+ mFDCurrentPos += aBytesRead;
+
+ return NS_OK;
+}
+
+nsresult FileBlockCache::WriteBlockToFile(int32_t aBlockIndex,
+ const uint8_t* aBlockData) {
+ LOG("WriteBlockToFile(index=%u)", aBlockIndex);
+
+ mFileMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(mFD);
+
+ nsresult rv = Seek(BlockIndexToOffset(aBlockIndex));
+ if (NS_FAILED(rv)) return rv;
+
+ int32_t amount = PR_Write(mFD, aBlockData, BLOCK_SIZE);
+ if (amount < BLOCK_SIZE) {
+ NS_WARNING("Failed to write media cache block!");
+ return NS_ERROR_FAILURE;
+ }
+ mFDCurrentPos += BLOCK_SIZE;
+
+ return NS_OK;
+}
+
+nsresult FileBlockCache::MoveBlockInFile(int32_t aSourceBlockIndex,
+ int32_t aDestBlockIndex) {
+ LOG("MoveBlockInFile(src=%u, dest=%u)", aSourceBlockIndex, aDestBlockIndex);
+
+ mFileMutex.AssertCurrentThreadOwns();
+
+ uint8_t buf[BLOCK_SIZE];
+ int32_t bytesRead = 0;
+ if (NS_FAILED(ReadFromFile(BlockIndexToOffset(aSourceBlockIndex), buf,
+ BLOCK_SIZE, bytesRead))) {
+ return NS_ERROR_FAILURE;
+ }
+ return WriteBlockToFile(aDestBlockIndex, buf);
+}
+
+void FileBlockCache::PerformBlockIOs() {
+ MutexAutoLock mon(mDataMutex);
+ MOZ_ASSERT(mBackgroundET->IsOnCurrentThread());
+ NS_ASSERTION(mIsWriteScheduled, "Should report write running or scheduled.");
+
+ LOG("Run() mFD=%p mBackgroundET=%p", mFD, mBackgroundET.get());
+
+ while (!mChangeIndexList.empty()) {
+ if (!mBackgroundET) {
+ // We've been closed, abort, discarding unwritten changes.
+ mIsWriteScheduled = false;
+ return;
+ }
+
+ if (mIsReading) {
+ // We're trying to read; postpone all writes. (Reader will resume writes.)
+ mIsWriteScheduled = false;
+ return;
+ }
+
+ // Process each pending change. We pop the index out of the change
+ // list, but leave the BlockChange in mBlockChanges until the change
+ // is written to file. This is so that any read which happens while
+ // we drop mDataMutex to write will refer to the data's source in
+ // memory, rather than the not-yet up to date data written to file.
+ // This also ensures we will insert a new index into mChangeIndexList
+ // when this happens.
+
+ // Hold a reference to the change, in case another change
+ // overwrites the mBlockChanges entry for this block while we drop
+ // mDataMutex to take mFileMutex.
+ int32_t blockIndex = mChangeIndexList.front();
+ RefPtr<BlockChange> change = mBlockChanges[blockIndex];
+ MOZ_ASSERT(change,
+ "Change index list should only contain entries for blocks "
+ "with changes");
+ {
+ MutexAutoUnlock unlock(mDataMutex);
+ MutexAutoLock lock(mFileMutex);
+ if (!mFD) {
+ // We may be here if mFD has been reset because we're closing, so we
+ // don't care anymore about writes.
+ return;
+ }
+ if (change->IsWrite()) {
+ WriteBlockToFile(blockIndex, change->mData.get());
+ } else if (change->IsMove()) {
+ MoveBlockInFile(change->mSourceBlockIndex, blockIndex);
+ }
+ }
+ mChangeIndexList.pop_front(); // MonitorAutoUnlock above
+ // If a new change has not been made to the block while we dropped
+ // mDataMutex, clear reference to the old change. Otherwise, the old
+ // reference has been cleared already.
+ if (mBlockChanges[blockIndex] == change) { // MonitorAutoUnlock above
+ mBlockChanges[blockIndex] = nullptr; // MonitorAutoUnlock above
+ }
+ }
+
+ mIsWriteScheduled = false;
+}
+
+nsresult FileBlockCache::Read(int64_t aOffset, uint8_t* aData, int32_t aLength,
+ int32_t* aBytes) {
+ MutexAutoLock mon(mDataMutex);
+
+ if (!mBackgroundET || (aOffset / BLOCK_SIZE) > INT32_MAX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mIsReading = true;
+ auto exitRead = MakeScopeExit([&] {
+ mDataMutex.AssertCurrentThreadOwns();
+ mIsReading = false;
+ if (!mChangeIndexList.empty()) {
+ // mReading has stopped or prevented pending writes, resume them.
+ EnsureWriteScheduled();
+ }
+ });
+
+ int32_t bytesToRead = aLength;
+ int64_t offset = aOffset;
+ uint8_t* dst = aData;
+ while (bytesToRead > 0) {
+ int32_t blockIndex = static_cast<int32_t>(offset / BLOCK_SIZE);
+ int32_t start = offset % BLOCK_SIZE;
+ int32_t amount = std::min(BLOCK_SIZE - start, bytesToRead);
+
+ // If the block is not yet written to file, we can just read from
+ // the memory buffer, otherwise we need to read from file.
+ int32_t bytesRead = 0;
+ MOZ_ASSERT(!mBlockChanges.IsEmpty());
+ MOZ_ASSERT(blockIndex >= 0 &&
+ static_cast<uint32_t>(blockIndex) < mBlockChanges.Length());
+ RefPtr<BlockChange> change = mBlockChanges.SafeElementAt(blockIndex);
+ if (change && change->IsWrite()) {
+ // Block isn't yet written to file. Read from memory buffer.
+ const uint8_t* blockData = change->mData.get();
+ memcpy(dst, blockData + start, amount);
+ bytesRead = amount;
+ } else {
+ if (change && change->IsMove()) {
+ // The target block is the destination of a not-yet-completed move
+ // action, so read from the move's source block from file. Note we
+ // *don't* follow a chain of moves here, as a move's source index
+ // is resolved when MoveBlock() is called, and the move's source's
+ // block could be have itself been subject to a move (or write)
+ // which happened *after* this move was recorded.
+ blockIndex = change->mSourceBlockIndex;
+ }
+ // Block has been written to file, either as the source block of a move,
+ // or as a stable (all changes made) block. Read the data directly
+ // from file.
+ nsresult res;
+ {
+ MutexAutoUnlock unlock(mDataMutex);
+ MutexAutoLock lock(mFileMutex);
+ if (!mFD) {
+ // Not initialized yet, or closed.
+ return NS_ERROR_FAILURE;
+ }
+ res = ReadFromFile(BlockIndexToOffset(blockIndex) + start, dst, amount,
+ bytesRead);
+ }
+ NS_ENSURE_SUCCESS(res, res);
+ }
+ dst += bytesRead;
+ offset += bytesRead;
+ bytesToRead -= bytesRead;
+ }
+ *aBytes = aLength - bytesToRead;
+ return NS_OK;
+}
+
+nsresult FileBlockCache::MoveBlock(int32_t aSourceBlockIndex,
+ int32_t aDestBlockIndex) {
+ MutexAutoLock mon(mDataMutex);
+
+ if (!mBackgroundET) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mBlockChanges.EnsureLengthAtLeast(
+ std::max(aSourceBlockIndex, aDestBlockIndex) + 1);
+
+ // The source block's contents may be the destination of another pending
+ // move, which in turn can be the destination of another pending move,
+ // etc. Resolve the final source block, so that if one of the blocks in
+ // the chain of moves is overwritten, we don't lose the reference to the
+ // contents of the destination block.
+ int32_t sourceIndex = aSourceBlockIndex;
+ BlockChange* sourceBlock = nullptr;
+ while ((sourceBlock = mBlockChanges[sourceIndex]) && sourceBlock->IsMove()) {
+ sourceIndex = sourceBlock->mSourceBlockIndex;
+ }
+
+ if (mBlockChanges[aDestBlockIndex] == nullptr ||
+ !ContainerContains(mChangeIndexList, aDestBlockIndex)) {
+ // Only add another entry to the change index list if we don't already
+ // have one for this block. We won't have an entry when either there's
+ // no pending change for this block, or if there is a pending change for
+ // this block and we're in the process of writing it (we've popped the
+ // block's index out of mChangeIndexList in Run() but not finished writing
+ // the block to file yet.
+ mChangeIndexList.push_back(aDestBlockIndex);
+ }
+
+ // If the source block hasn't yet been written to file then the dest block
+ // simply contains that same write. Resolve this as a write instead.
+ if (sourceBlock && sourceBlock->IsWrite()) {
+ mBlockChanges[aDestBlockIndex] = new BlockChange(sourceBlock->mData.get());
+ } else {
+ mBlockChanges[aDestBlockIndex] = new BlockChange(sourceIndex);
+ }
+
+ EnsureWriteScheduled();
+
+ NS_ASSERTION(ContainerContains(mChangeIndexList, aDestBlockIndex),
+ "Should have scheduled block for change");
+
+ return NS_OK;
+}
+
+} // End namespace mozilla.
+
+// avoid redefined macro in unified build
+#undef LOG