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