tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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