FileBlockCache.cpp (18004B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "FileBlockCache.h" 8 9 #include <algorithm> 10 11 #include "MediaCache.h" 12 #include "VideoUtils.h" 13 #include "mozilla/ScopeExit.h" 14 #include "mozilla/dom/ContentChild.h" 15 #include "nsAnonymousTemporaryFile.h" 16 #include "nsIThreadManager.h" 17 #include "nsXULAppAPI.h" 18 #include "prio.h" 19 20 namespace mozilla { 21 22 #undef LOG 23 LazyLogModule gFileBlockCacheLog("FileBlockCache"); 24 #define LOG(x, ...) \ 25 MOZ_LOG(gFileBlockCacheLog, LogLevel::Debug, ("%p " x, this, ##__VA_ARGS__)) 26 27 static void CloseFD(PRFileDesc* aFD) { 28 PRStatus prrc; 29 prrc = PR_Close(aFD); 30 if (prrc != PR_SUCCESS) { 31 NS_WARNING("PR_Close() failed."); 32 } 33 } 34 35 void FileBlockCache::SetCacheFile(PRFileDesc* aFD) { 36 LOG("SetCacheFile aFD=%p", aFD); 37 if (!aFD) { 38 // Failed to get a temporary file. Shutdown. 39 Close(); 40 return; 41 } 42 { 43 MutexAutoLock lock(mFileMutex); 44 mFD = aFD; 45 } 46 { 47 MutexAutoLock lock(mDataMutex); 48 LOG("SetFileCache mBackgroundET=%p, mIsWriteScheduled %d", 49 mBackgroundET.get(), mIsWriteScheduled); 50 if (mBackgroundET) { 51 // Still open, complete the initialization. 52 mInitialized = true; 53 if (mIsWriteScheduled) { 54 // A write was scheduled while waiting for FD. We need to run/dispatch a 55 // task to service the request. 56 nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod( 57 "FileBlockCache::SetCacheFile -> PerformBlockIOs", this, 58 &FileBlockCache::PerformBlockIOs); 59 mBackgroundET->Dispatch(event.forget(), NS_DISPATCH_EVENT_MAY_BLOCK); 60 } 61 return; 62 } 63 } 64 // We've been closed while waiting for the file descriptor. 65 // Close the file descriptor we've just received, if still there. 66 MutexAutoLock lock(mFileMutex); 67 if (mFD) { 68 CloseFD(mFD); 69 mFD = nullptr; 70 } 71 } 72 73 nsresult FileBlockCache::Init() { 74 LOG("Init()"); 75 MutexAutoLock mon(mDataMutex); 76 MOZ_ASSERT(!mBackgroundET); 77 nsresult rv = NS_CreateBackgroundTaskQueue("FileBlockCache", 78 getter_AddRefs(mBackgroundET)); 79 if (NS_FAILED(rv)) { 80 return rv; 81 } 82 83 if (XRE_IsParentProcess()) { 84 RefPtr<FileBlockCache> self = this; 85 rv = mBackgroundET->Dispatch( 86 NS_NewRunnableFunction("FileBlockCache::Init", 87 [self] { 88 PRFileDesc* fd = nullptr; 89 nsresult rv = 90 NS_OpenAnonymousTemporaryFile(&fd); 91 if (NS_SUCCEEDED(rv)) { 92 self->SetCacheFile(fd); 93 } else { 94 self->Close(); 95 } 96 }), 97 NS_DISPATCH_EVENT_MAY_BLOCK); 98 } else { 99 // We must request a temporary file descriptor from the parent process. 100 RefPtr<FileBlockCache> self = this; 101 rv = dom::ContentChild::GetSingleton()->AsyncOpenAnonymousTemporaryFile( 102 [self](PRFileDesc* aFD) { self->SetCacheFile(aFD); }); 103 } 104 105 if (NS_FAILED(rv)) { 106 Close(); 107 } 108 109 return rv; 110 } 111 112 void FileBlockCache::Flush() { 113 LOG("Flush()"); 114 MutexAutoLock mon(mDataMutex); 115 MOZ_ASSERT(mBackgroundET); 116 117 // Dispatch a task so we won't clear the arrays while PerformBlockIOs() is 118 // dropping the data lock and cause InvalidArrayIndex. 119 RefPtr<FileBlockCache> self = this; 120 mBackgroundET->Dispatch( 121 NS_NewRunnableFunction("FileBlockCache::Flush", [self]() { 122 MutexAutoLock mon(self->mDataMutex); 123 // Just discard pending changes, assume MediaCache won't read from 124 // blocks it hasn't written to. 125 self->mChangeIndexList.clear(); 126 self->mBlockChanges.Clear(); 127 })); 128 } 129 130 size_t FileBlockCache::GetMaxBlocks(size_t aCacheSizeInKB) const { 131 // We look up the cache size every time. This means dynamic changes 132 // to the pref are applied. 133 // Ensure we can divide BLOCK_SIZE by 1024. 134 static_assert(MediaCacheStream::BLOCK_SIZE % 1024 == 0, 135 "BLOCK_SIZE should be a multiple of 1024"); 136 // Ensure BLOCK_SIZE/1024 is at least 2. 137 static_assert(MediaCacheStream::BLOCK_SIZE / 1024 >= 2, 138 "BLOCK_SIZE / 1024 should be at least 2"); 139 // Ensure we can convert BLOCK_SIZE/1024 to a uint32_t without truncation. 140 static_assert(MediaCacheStream::BLOCK_SIZE / 1024 <= int64_t(UINT32_MAX), 141 "BLOCK_SIZE / 1024 should be at most UINT32_MAX"); 142 // Since BLOCK_SIZE is a strict multiple of 1024, 143 // aCacheSizeInKB * 1024 / BLOCK_SIZE == aCacheSizeInKB / (BLOCK_SIZE / 144 // 1024), but the latter formula avoids a potential overflow from `* 1024`. 145 // And because BLOCK_SIZE/1024 is at least 2, the maximum cache size 146 // INT32_MAX*2 will give a maxBlocks that can fit in an int32_t. 147 constexpr size_t blockSizeKb = size_t(MediaCacheStream::BLOCK_SIZE / 1024); 148 const size_t maxBlocks = aCacheSizeInKB / blockSizeKb; 149 return std::max(maxBlocks, size_t(1)); 150 } 151 152 FileBlockCache::FileBlockCache() 153 : mFileMutex("MediaCache.Writer.IO.Mutex"), 154 mFD(nullptr), 155 mFDCurrentPos(0), 156 mDataMutex("MediaCache.Writer.Data.Mutex"), 157 mIsWriteScheduled(false), 158 mIsReading(false) {} 159 160 FileBlockCache::~FileBlockCache() { Close(); } 161 162 void FileBlockCache::Close() { 163 LOG("Close()"); 164 165 nsCOMPtr<nsISerialEventTarget> thread; 166 { 167 MutexAutoLock mon(mDataMutex); 168 if (!mBackgroundET) { 169 return; 170 } 171 thread.swap(mBackgroundET); 172 } 173 174 PRFileDesc* fd; 175 { 176 MutexAutoLock lock(mFileMutex); 177 fd = mFD; 178 mFD = nullptr; 179 } 180 181 // Let the thread close the FD, and then trigger its own shutdown. 182 // Note that mBackgroundET is now empty, so no other task will be posted 183 // there. Also mBackgroundET and mFD are empty and therefore can be reused 184 // immediately. 185 nsresult rv = thread->Dispatch(NS_NewRunnableFunction("FileBlockCache::Close", 186 [thread, fd] { 187 if (fd) { 188 CloseFD(fd); 189 } 190 // No need to shutdown 191 // background task 192 // queues. 193 }), 194 NS_DISPATCH_EVENT_MAY_BLOCK); 195 NS_ENSURE_SUCCESS_VOID(rv); 196 } 197 198 template <typename Container, typename Value> 199 bool ContainerContains(const Container& aContainer, const Value& value) { 200 return std::find(aContainer.begin(), aContainer.end(), value) != 201 aContainer.end(); 202 } 203 204 nsresult FileBlockCache::WriteBlock(uint32_t aBlockIndex, 205 Span<const uint8_t> aData1, 206 Span<const uint8_t> aData2) { 207 MutexAutoLock mon(mDataMutex); 208 209 if (!mBackgroundET) { 210 return NS_ERROR_FAILURE; 211 } 212 213 // Check if we've already got a pending write scheduled for this block. 214 mBlockChanges.EnsureLengthAtLeast(aBlockIndex + 1); 215 bool blockAlreadyHadPendingChange = mBlockChanges[aBlockIndex] != nullptr; 216 mBlockChanges[aBlockIndex] = new BlockChange(aData1, aData2); 217 218 if (!blockAlreadyHadPendingChange || 219 !ContainerContains(mChangeIndexList, aBlockIndex)) { 220 // We either didn't already have a pending change for this block, or we 221 // did but we didn't have an entry for it in mChangeIndexList (we're in the 222 // process of writing it and have removed the block's index out of 223 // mChangeIndexList in Run() but not finished writing the block to file 224 // yet). Add the blocks index to the end of mChangeIndexList to ensure the 225 // block is written as as soon as possible. 226 mChangeIndexList.push_back(aBlockIndex); 227 } 228 NS_ASSERTION(ContainerContains(mChangeIndexList, aBlockIndex), 229 "Must have entry for new block"); 230 231 EnsureWriteScheduled(); 232 233 return NS_OK; 234 } 235 236 void FileBlockCache::EnsureWriteScheduled() { 237 mDataMutex.AssertCurrentThreadOwns(); 238 MOZ_ASSERT(mBackgroundET); 239 240 if (mIsWriteScheduled || mIsReading) { 241 return; 242 } 243 mIsWriteScheduled = true; 244 if (!mInitialized) { 245 // We're still waiting on a file descriptor. When it arrives, 246 // the write will be scheduled. 247 return; 248 } 249 nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod( 250 "FileBlockCache::EnsureWriteScheduled -> PerformBlockIOs", this, 251 &FileBlockCache::PerformBlockIOs); 252 mBackgroundET->Dispatch(event.forget(), NS_DISPATCH_EVENT_MAY_BLOCK); 253 } 254 255 nsresult FileBlockCache::Seek(int64_t aOffset) { 256 mFileMutex.AssertCurrentThreadOwns(); 257 258 if (mFDCurrentPos != aOffset) { 259 MOZ_ASSERT(mFD); 260 int64_t result = PR_Seek64(mFD, aOffset, PR_SEEK_SET); 261 if (result != aOffset) { 262 NS_WARNING("Failed to seek media cache file"); 263 return NS_ERROR_FAILURE; 264 } 265 mFDCurrentPos = result; 266 } 267 return NS_OK; 268 } 269 270 nsresult FileBlockCache::ReadFromFile(int64_t aOffset, uint8_t* aDest, 271 int32_t aBytesToRead, 272 int32_t& aBytesRead) { 273 LOG("ReadFromFile(offset=%" PRIu64 ", len=%u)", aOffset, aBytesToRead); 274 mFileMutex.AssertCurrentThreadOwns(); 275 MOZ_ASSERT(mFD); 276 277 nsresult res = Seek(aOffset); 278 if (NS_FAILED(res)) return res; 279 280 aBytesRead = PR_Read(mFD, aDest, aBytesToRead); 281 if (aBytesRead <= 0) return NS_ERROR_FAILURE; 282 mFDCurrentPos += aBytesRead; 283 284 return NS_OK; 285 } 286 287 nsresult FileBlockCache::WriteBlockToFile(int32_t aBlockIndex, 288 const uint8_t* aBlockData) { 289 LOG("WriteBlockToFile(index=%u)", aBlockIndex); 290 291 mFileMutex.AssertCurrentThreadOwns(); 292 MOZ_ASSERT(mFD); 293 294 nsresult rv = Seek(BlockIndexToOffset(aBlockIndex)); 295 if (NS_FAILED(rv)) return rv; 296 297 int32_t amount = PR_Write(mFD, aBlockData, BLOCK_SIZE); 298 if (amount < BLOCK_SIZE) { 299 NS_WARNING("Failed to write media cache block!"); 300 return NS_ERROR_FAILURE; 301 } 302 mFDCurrentPos += BLOCK_SIZE; 303 304 return NS_OK; 305 } 306 307 nsresult FileBlockCache::MoveBlockInFile(int32_t aSourceBlockIndex, 308 int32_t aDestBlockIndex) { 309 LOG("MoveBlockInFile(src=%u, dest=%u)", aSourceBlockIndex, aDestBlockIndex); 310 311 mFileMutex.AssertCurrentThreadOwns(); 312 313 uint8_t buf[BLOCK_SIZE]; 314 int32_t bytesRead = 0; 315 if (NS_FAILED(ReadFromFile(BlockIndexToOffset(aSourceBlockIndex), buf, 316 BLOCK_SIZE, bytesRead))) { 317 return NS_ERROR_FAILURE; 318 } 319 return WriteBlockToFile(aDestBlockIndex, buf); 320 } 321 322 void FileBlockCache::PerformBlockIOs() { 323 MutexAutoLock mon(mDataMutex); 324 MOZ_ASSERT(mBackgroundET->IsOnCurrentThread()); 325 NS_ASSERTION(mIsWriteScheduled, "Should report write running or scheduled."); 326 327 LOG("Run() mFD=%p mBackgroundET=%p", mFD, mBackgroundET.get()); 328 329 while (!mChangeIndexList.empty()) { 330 if (!mBackgroundET) { 331 // We've been closed, abort, discarding unwritten changes. 332 mIsWriteScheduled = false; 333 return; 334 } 335 336 if (mIsReading) { 337 // We're trying to read; postpone all writes. (Reader will resume writes.) 338 mIsWriteScheduled = false; 339 return; 340 } 341 342 // Process each pending change. We pop the index out of the change 343 // list, but leave the BlockChange in mBlockChanges until the change 344 // is written to file. This is so that any read which happens while 345 // we drop mDataMutex to write will refer to the data's source in 346 // memory, rather than the not-yet up to date data written to file. 347 // This also ensures we will insert a new index into mChangeIndexList 348 // when this happens. 349 350 // Hold a reference to the change, in case another change 351 // overwrites the mBlockChanges entry for this block while we drop 352 // mDataMutex to take mFileMutex. 353 int32_t blockIndex = mChangeIndexList.front(); 354 RefPtr<BlockChange> change = mBlockChanges[blockIndex]; 355 MOZ_ASSERT(change, 356 "Change index list should only contain entries for blocks " 357 "with changes"); 358 { 359 MutexAutoUnlock unlock(mDataMutex); 360 MutexAutoLock lock(mFileMutex); 361 if (!mFD) { 362 // We may be here if mFD has been reset because we're closing, so we 363 // don't care anymore about writes. 364 return; 365 } 366 if (change->IsWrite()) { 367 WriteBlockToFile(blockIndex, change->mData.get()); 368 } else if (change->IsMove()) { 369 MoveBlockInFile(change->mSourceBlockIndex, blockIndex); 370 } 371 } 372 mChangeIndexList.pop_front(); // MonitorAutoUnlock above 373 // If a new change has not been made to the block while we dropped 374 // mDataMutex, clear reference to the old change. Otherwise, the old 375 // reference has been cleared already. 376 if (mBlockChanges[blockIndex] == change) { // MonitorAutoUnlock above 377 mBlockChanges[blockIndex] = nullptr; // MonitorAutoUnlock above 378 } 379 } 380 381 mIsWriteScheduled = false; 382 } 383 384 nsresult FileBlockCache::Read(int64_t aOffset, uint8_t* aData, 385 int32_t aLength) { 386 MutexAutoLock mon(mDataMutex); 387 388 if (!mBackgroundET || (aOffset / BLOCK_SIZE) > INT32_MAX) { 389 return NS_ERROR_FAILURE; 390 } 391 392 mIsReading = true; 393 auto exitRead = MakeScopeExit([&] { 394 mDataMutex.AssertCurrentThreadOwns(); 395 mIsReading = false; 396 if (!mChangeIndexList.empty()) { 397 // mReading has stopped or prevented pending writes, resume them. 398 EnsureWriteScheduled(); 399 } 400 }); 401 402 int32_t bytesToRead = aLength; 403 int64_t offset = aOffset; 404 uint8_t* dst = aData; 405 while (bytesToRead > 0) { 406 int32_t blockIndex = static_cast<int32_t>(offset / BLOCK_SIZE); 407 int32_t start = offset % BLOCK_SIZE; 408 int32_t amount = std::min(BLOCK_SIZE - start, bytesToRead); 409 410 // If the block is not yet written to file, we can just read from 411 // the memory buffer, otherwise we need to read from file. 412 int32_t bytesRead = 0; 413 MOZ_ASSERT(!mBlockChanges.IsEmpty()); 414 MOZ_ASSERT(blockIndex >= 0 && 415 static_cast<uint32_t>(blockIndex) < mBlockChanges.Length()); 416 RefPtr<BlockChange> change = mBlockChanges.SafeElementAt(blockIndex); 417 if (change && change->IsWrite()) { 418 // Block isn't yet written to file. Read from memory buffer. 419 const uint8_t* blockData = change->mData.get(); 420 memcpy(dst, blockData + start, amount); 421 bytesRead = amount; 422 } else { 423 if (change && change->IsMove()) { 424 // The target block is the destination of a not-yet-completed move 425 // action, so read from the move's source block from file. Note we 426 // *don't* follow a chain of moves here, as a move's source index 427 // is resolved when MoveBlock() is called, and the move's source's 428 // block could be have itself been subject to a move (or write) 429 // which happened *after* this move was recorded. 430 blockIndex = change->mSourceBlockIndex; 431 } 432 // Block has been written to file, either as the source block of a move, 433 // or as a stable (all changes made) block. Read the data directly 434 // from file. 435 nsresult res; 436 { 437 MutexAutoUnlock unlock(mDataMutex); 438 MutexAutoLock lock(mFileMutex); 439 if (!mFD) { 440 // Not initialized yet, or closed. 441 return NS_ERROR_FAILURE; 442 } 443 res = ReadFromFile(BlockIndexToOffset(blockIndex) + start, dst, amount, 444 bytesRead); 445 } 446 NS_ENSURE_SUCCESS(res, res); 447 } 448 dst += bytesRead; 449 offset += bytesRead; 450 bytesToRead -= bytesRead; 451 } 452 return NS_OK; 453 } 454 455 nsresult FileBlockCache::MoveBlock(int32_t aSourceBlockIndex, 456 int32_t aDestBlockIndex) { 457 MutexAutoLock mon(mDataMutex); 458 459 if (!mBackgroundET) { 460 return NS_ERROR_FAILURE; 461 } 462 463 mBlockChanges.EnsureLengthAtLeast( 464 std::max(aSourceBlockIndex, aDestBlockIndex) + 1); 465 466 // The source block's contents may be the destination of another pending 467 // move, which in turn can be the destination of another pending move, 468 // etc. Resolve the final source block, so that if one of the blocks in 469 // the chain of moves is overwritten, we don't lose the reference to the 470 // contents of the destination block. 471 int32_t sourceIndex = aSourceBlockIndex; 472 BlockChange* sourceBlock = nullptr; 473 while ((sourceBlock = mBlockChanges[sourceIndex]) && sourceBlock->IsMove()) { 474 sourceIndex = sourceBlock->mSourceBlockIndex; 475 } 476 477 if (mBlockChanges[aDestBlockIndex] == nullptr || 478 !ContainerContains(mChangeIndexList, aDestBlockIndex)) { 479 // Only add another entry to the change index list if we don't already 480 // have one for this block. We won't have an entry when either there's 481 // no pending change for this block, or if there is a pending change for 482 // this block and we're in the process of writing it (we've popped the 483 // block's index out of mChangeIndexList in Run() but not finished writing 484 // the block to file yet. 485 mChangeIndexList.push_back(aDestBlockIndex); 486 } 487 488 // If the source block hasn't yet been written to file then the dest block 489 // simply contains that same write. Resolve this as a write instead. 490 if (sourceBlock && sourceBlock->IsWrite()) { 491 mBlockChanges[aDestBlockIndex] = new BlockChange(sourceBlock->mData.get()); 492 } else { 493 mBlockChanges[aDestBlockIndex] = new BlockChange(sourceIndex); 494 } 495 496 EnsureWriteScheduled(); 497 498 NS_ASSERTION(ContainerContains(mChangeIndexList, aDestBlockIndex), 499 "Should have scheduled block for change"); 500 501 return NS_OK; 502 } 503 504 } // End namespace mozilla. 505 506 // avoid redefined macro in unified build 507 #undef LOG