block_cache: Compute the timeout dynamically based on write speeds.

We now record how long it takes to write a block (on average), and then
utilize this information to reduce the timeout write thread's timeout
(to 2 * block_count * average_block_time, so we don't completely
congest the drive.) Remove the "TODO" about the I/O scheduler;
this new logic will be just fine even under an I/O scheduler.

Note that this change goes both ways: while faster writes mean more
writes and quicker, slower writes will increase the timeout before
we do another one also. This then also guards against queueing
another write while one is already in progress, which was
not handled before.

Tested in KVM. Even on a SATA-backed spinning HDD, this reduces
the timeout to around *200ms* on average (!!), so a 10x improvement.
On a ramdisk, it reduces the timeout to *10-30ms* (!!!) on average,
so a 100-200x improvement, so this change will benefit everyone
but SSDs especially.

Since BFS inode and journal writes always go through the block_cache,
this very dramatically improves inode-related write performance.
The "stop and start" stutters when emptying or moving items to Trash
seem totally gone, among a lot of other things.

Change-Id: I41f46a6432ce1f50f896a853abdfe22dde0ba327
This commit is contained in:
Augustin Cavalier
2019-07-12 21:19:47 -04:00
parent 9d06690ede
commit 6d336fda4a
+50 -8
View File
@@ -226,6 +226,9 @@ struct block_cache : DoublyLinkedListLinkImpl<block_cache> {
uint32 busy_writing_count; uint32 busy_writing_count;
bool busy_writing_waiters; bool busy_writing_waiters;
bigtime_t last_block_write;
bigtime_t last_block_write_duration;
uint32 num_dirty_blocks; uint32 num_dirty_blocks;
bool read_only; bool read_only;
@@ -1203,6 +1206,8 @@ BlockWriter::Write(cache_transaction* transaction, bool canUnlock)
qsort(fBlocks, fCount, sizeof(void*), &_CompareBlocks); qsort(fBlocks, fCount, sizeof(void*), &_CompareBlocks);
fDeletedTransaction = false; fDeletedTransaction = false;
bigtime_t start = system_time();
for (uint32 i = 0; i < fCount; i++) { for (uint32 i = 0; i < fCount; i++) {
status_t status = _WriteBlock(fBlocks[i]); status_t status = _WriteBlock(fBlocks[i]);
if (status != B_OK) { if (status != B_OK) {
@@ -1216,9 +1221,17 @@ BlockWriter::Write(cache_transaction* transaction, bool canUnlock)
} }
} }
bigtime_t finish = system_time();
if (canUnlock) if (canUnlock)
mutex_lock(&fCache->lock); mutex_lock(&fCache->lock);
if (fStatus == B_OK && fCount >= 8) {
fCache->last_block_write = finish;
fCache->last_block_write_duration = (fCache->last_block_write - start)
/ fCount;
}
for (uint32 i = 0; i < fCount; i++) for (uint32 i = 0; i < fCount; i++)
_BlockDone(fBlocks[i], transaction); _BlockDone(fBlocks[i], transaction);
@@ -1392,6 +1405,8 @@ block_cache::block_cache(int _fd, off_t numBlocks, size_t blockSize,
busy_reading_waiters(false), busy_reading_waiters(false),
busy_writing_count(0), busy_writing_count(0),
busy_writing_waiters(0), busy_writing_waiters(0),
last_block_write(0),
last_block_write_duration(0),
num_dirty_blocks(0), num_dirty_blocks(0),
read_only(readOnly) read_only(readOnly)
{ {
@@ -2538,8 +2553,8 @@ get_next_locked_block_cache(block_cache* last)
static status_t static status_t
block_notifier_and_writer(void* /*data*/) block_notifier_and_writer(void* /*data*/)
{ {
const bigtime_t kTimeout = 2000000LL; const bigtime_t kDefaultTimeout = 2000000LL;
bigtime_t timeout = kTimeout; bigtime_t timeout = kDefaultTimeout;
while (true) { while (true) {
bigtime_t start = system_time(); bigtime_t start = system_time();
@@ -2552,15 +2567,32 @@ block_notifier_and_writer(void* /*data*/)
continue; continue;
} }
// write 64 blocks of each block_cache every two seconds // Write 64 blocks of each block_cache roughly every 2 seconds,
// TODO: change this once we have an I/O scheduler // potentially more or less depending on congestion and drive speeds
timeout = kTimeout; // (usually much less.) We do not want to queue everything at once
// because a future transaction might then get held up waiting for
// a specific block to be written.
timeout = kDefaultTimeout;
size_t usedMemory; size_t usedMemory;
object_cache_get_usage(sBlockCache, &usedMemory); object_cache_get_usage(sBlockCache, &usedMemory);
block_cache* cache = NULL; block_cache* cache = NULL;
while ((cache = get_next_locked_block_cache(cache)) != NULL) { while ((cache = get_next_locked_block_cache(cache)) != NULL) {
// Give some breathing room: wait 2x the length of the potential
// maximum block count-sized write between writes, and also skip
// if there are more than 16 blocks currently being written.
const bigtime_t next = cache->last_block_write
+ cache->last_block_write_duration * 2 * 64;
if (cache->busy_writing_count > 16 || system_time() < next) {
if (cache->last_block_write_duration > 0) {
timeout = min_c(timeout,
cache->last_block_write_duration * 2 * 64);
}
continue;
}
BlockWriter writer(cache, 64); BlockWriter writer(cache, 64);
bool hasMoreBlocks = false;
size_t cacheUsedMemory; size_t cacheUsedMemory;
object_cache_get_usage(cache->buffer_cache, &cacheUsedMemory); object_cache_get_usage(cache->buffer_cache, &cacheUsedMemory);
@@ -2573,10 +2605,11 @@ block_notifier_and_writer(void* /*data*/)
while (iterator.HasNext()) { while (iterator.HasNext()) {
cached_block* block = iterator.Next(); cached_block* block = iterator.Next();
if (block->CanBeWritten() && !writer.Add(block)) if (block->CanBeWritten() && !writer.Add(block)) {
hasMoreBlocks = true;
break; break;
} }
}
} else { } else {
TransactionTable::Iterator iterator(cache->transaction_hash); TransactionTable::Iterator iterator(cache->transaction_hash);
@@ -2594,13 +2627,22 @@ block_notifier_and_writer(void* /*data*/)
bool hasLeftOvers; bool hasLeftOvers;
// we ignore this one // we ignore this one
if (!writer.Add(transaction, hasLeftOvers)) if (!writer.Add(transaction, hasLeftOvers)) {
hasMoreBlocks = true;
break; break;
} }
} }
}
writer.Write(); writer.Write();
if (hasMoreBlocks && cache->last_block_write_duration > 0) {
// There are probably still more blocks that we could write, so
// see if we can decrease the timeout.
timeout = min_c(timeout,
cache->last_block_write_duration * 2 * 64);
}
if ((block_cache_used_memory() / B_PAGE_SIZE) if ((block_cache_used_memory() / B_PAGE_SIZE)
> vm_page_num_pages() / 2) { > vm_page_num_pages() / 2) {
// Try to reduce memory usage to half of the available // Try to reduce memory usage to half of the available