/* -*- 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 "MemoryBlockCache.h" #include "mozilla/Atomics.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Logging.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_media.h" #include "nsWeakReference.h" #include "prsystem.h" namespace mozilla { #undef LOG LazyLogModule gMemoryBlockCacheLog("MemoryBlockCache"); #define LOG(x, ...) \ MOZ_LOG(gMemoryBlockCacheLog, LogLevel::Debug, ("%p " x, this, ##__VA_ARGS__)) // Combined sizes of all MemoryBlockCache buffers. // Initialized to 0 by non-local static initialization. // Increases when a buffer grows (during initialization or unexpected OOB // writes), decreases when a MemoryBlockCache (with its buffer) is destroyed. static Atomic gCombinedSizes; static int32_t CalculateMaxBlocks(int64_t aContentLength) { int64_t maxSize = int64_t(StaticPrefs::media_memory_cache_max_size()) * 1024; MOZ_ASSERT(aContentLength <= maxSize); MOZ_ASSERT(maxSize % MediaBlockCacheBase::BLOCK_SIZE == 0); // Note: It doesn't matter if calculations overflow, Init() would later fail. // We want at least enough blocks to contain the original content length. const int32_t requiredBlocks = maxSize / MediaBlockCacheBase::BLOCK_SIZE; // Allow at least 1s of ultra HD (25Mbps). const int32_t workableBlocks = 25 * 1024 * 1024 / 8 / MediaBlockCacheBase::BLOCK_SIZE; return std::max(requiredBlocks, workableBlocks); } MemoryBlockCache::MemoryBlockCache(int64_t aContentLength) // Buffer whole blocks. : mInitialContentLength((aContentLength >= 0) ? size_t(aContentLength) : 0), mMaxBlocks(CalculateMaxBlocks(aContentLength)), mMutex("MemoryBlockCache"), mHasGrown(false) { if (aContentLength <= 0) { LOG("MemoryBlockCache() MEMORYBLOCKCACHE_ERRORS='InitUnderuse'"); } } MemoryBlockCache::~MemoryBlockCache() { MOZ_ASSERT(gCombinedSizes >= mBuffer.Length()); size_t sizes = static_cast(gCombinedSizes -= mBuffer.Length()); LOG("~MemoryBlockCache() - destroying buffer of size %zu; combined sizes now " "%zu", mBuffer.Length(), sizes); } bool MemoryBlockCache::EnsureBufferCanContain(size_t aContentLength) { mMutex.AssertCurrentThreadOwns(); if (aContentLength == 0) { return true; } const size_t initialLength = mBuffer.Length(); const size_t desiredLength = ((aContentLength - 1) / BLOCK_SIZE + 1) * BLOCK_SIZE; if (initialLength >= desiredLength) { // Already large enough. return true; } // Need larger buffer. If we are allowed more memory, attempt to re-allocate. const size_t extra = desiredLength - initialLength; // Only check the very first allocation against the combined MemoryBlockCache // limit. Further growths will always be allowed, assuming MediaCache won't // go over GetMaxBlocks() by too much. if (initialLength == 0) { // Note: There is a small race between testing `atomic + extra > limit` and // committing to it with `atomic += extra` below; but this is acceptable, as // in the worst case it may allow a small number of buffers to go past the // limit. // The alternative would have been to reserve the space first with // `atomic += extra` and then undo it with `atomic -= extra` in case of // failure; but this would have meant potentially preventing other (small // but successful) allocations. static const size_t sysmem = std::max(PR_GetPhysicalMemorySize(), 32 * 1024 * 1024); const size_t limit = std::min( size_t(StaticPrefs::media_memory_caches_combined_limit_kb()) * 1024, sysmem * StaticPrefs::media_memory_caches_combined_limit_pc_sysmem() / 100); const size_t currentSizes = static_cast(gCombinedSizes); if (currentSizes + extra > limit) { LOG("EnsureBufferCanContain(%zu) - buffer size %zu, wanted + %zu = %zu;" " combined sizes %zu + %zu > limit %zu", aContentLength, initialLength, extra, desiredLength, currentSizes, extra, limit); return false; } } if (!mBuffer.SetLength(desiredLength, mozilla::fallible)) { LOG("EnsureBufferCanContain(%zu) - buffer size %zu, wanted + %zu = %zu, " "allocation failed", aContentLength, initialLength, extra, desiredLength); return false; } MOZ_ASSERT(mBuffer.Length() == desiredLength); const size_t capacity = mBuffer.Capacity(); const size_t extraCapacity = capacity - desiredLength; if (extraCapacity != 0) { // Our buffer was given a larger capacity than the requested length, we may // as well claim that extra capacity, both for our accounting, and to // possibly bypass some future growths that would fit in this new capacity. mBuffer.SetLength(capacity); } const size_t newSizes = gCombinedSizes += (extra + extraCapacity); LOG("EnsureBufferCanContain(%zu) - buffer size %zu + requested %zu + bonus " "%zu = %zu; combined sizes %zu", aContentLength, initialLength, extra, extraCapacity, capacity, newSizes); mHasGrown = true; return true; } nsresult MemoryBlockCache::Init() { LOG("Init()"); MutexAutoLock lock(mMutex); MOZ_ASSERT(mBuffer.IsEmpty()); // Attempt to pre-allocate buffer for expected content length. if (!EnsureBufferCanContain(mInitialContentLength)) { LOG("Init() MEMORYBLOCKCACHE_ERRORS='InitAllocation'"); return NS_ERROR_FAILURE; } return NS_OK; } void MemoryBlockCache::Flush() { LOG("Flush()"); MutexAutoLock lock(mMutex); MOZ_ASSERT(mBuffer.Length() >= mInitialContentLength); memset(mBuffer.Elements(), 0, mBuffer.Length()); mHasGrown = false; } nsresult MemoryBlockCache::WriteBlock(uint32_t aBlockIndex, Span aData1, Span aData2) { MutexAutoLock lock(mMutex); size_t offset = BlockIndexToOffset(aBlockIndex); if (offset + aData1.Length() + aData2.Length() > mBuffer.Length() && !mHasGrown) { LOG("WriteBlock() MEMORYBLOCKCACHE_ERRORS='WriteBlockOverflow'"); } if (!EnsureBufferCanContain(offset + aData1.Length() + aData2.Length())) { LOG("WriteBlock() MEMORYBLOCKCACHE_ERRORS='WriteBlockCannotGrow'"); return NS_ERROR_FAILURE; } memcpy(mBuffer.Elements() + offset, aData1.Elements(), aData1.Length()); if (aData2.Length() > 0) { memcpy(mBuffer.Elements() + offset + aData1.Length(), aData2.Elements(), aData2.Length()); } return NS_OK; } nsresult MemoryBlockCache::Read(int64_t aOffset, uint8_t* aData, int32_t aLength, int32_t* aBytes) { MutexAutoLock lock(mMutex); MOZ_ASSERT(aOffset >= 0); if (aOffset + aLength > int64_t(mBuffer.Length())) { LOG("Read() MEMORYBLOCKCACHE_ERRORS='ReadOverrun'"); return NS_ERROR_FAILURE; } memcpy(aData, mBuffer.Elements() + aOffset, aLength); *aBytes = aLength; return NS_OK; } nsresult MemoryBlockCache::MoveBlock(int32_t aSourceBlockIndex, int32_t aDestBlockIndex) { MutexAutoLock lock(mMutex); size_t sourceOffset = BlockIndexToOffset(aSourceBlockIndex); size_t destOffset = BlockIndexToOffset(aDestBlockIndex); if (sourceOffset + BLOCK_SIZE > mBuffer.Length()) { LOG("MoveBlock() MEMORYBLOCKCACHE_ERRORS='MoveBlockSourceOverrun'"); return NS_ERROR_FAILURE; } if (destOffset + BLOCK_SIZE > mBuffer.Length() && !mHasGrown) { LOG("MoveBlock() MEMORYBLOCKCACHE_ERRORS='MoveBlockDestOverflow'"); } if (!EnsureBufferCanContain(destOffset + BLOCK_SIZE)) { LOG("MoveBlock() MEMORYBLOCKCACHE_ERRORS='MoveBlockCannotGrow'"); return NS_ERROR_FAILURE; } memcpy(mBuffer.Elements() + destOffset, mBuffer.Elements() + sourceOffset, BLOCK_SIZE); return NS_OK; } } // End namespace mozilla. // avoid redefined macro in unified build #undef LOG