mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-15 15:59:18 +00:00
WebDAV performance optimizations (#543)
* Make ranged GET requests cancellable; reduce notification update frequency * Include original exception as a cause in WebDAV ErrnoException * Add KDoc for threading
This commit is contained in:
parent
f0eb140777
commit
fbed5c7d67
|
@ -40,6 +40,9 @@ class PagingReader(
|
|||
* - a file seek + read or
|
||||
* - a ranged WebDAV request.
|
||||
*
|
||||
* This function will not be called by multiple threads at the same time, so
|
||||
* thread-safety is not required.
|
||||
*
|
||||
* @param offset position to start
|
||||
* @param size number of bytes to load
|
||||
*
|
||||
|
@ -97,6 +100,9 @@ class PagingReader(
|
|||
* This method will determine the page that contains [position] and read only
|
||||
* from this page.
|
||||
*
|
||||
* This method is synchronized so that no concurrent modifications of [currentPage]
|
||||
* and no concurrent calls to [loader] will be made.
|
||||
*
|
||||
* @param position starting position
|
||||
* @param size number of bytes requested
|
||||
* @param dst destination where data are read into
|
||||
|
|
|
@ -26,6 +26,12 @@ import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
|
|||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.webdav.RandomAccessCallback.Wrapper.Companion.TIMEOUT_INTERVAL
|
||||
import at.bitfire.davdroid.webdav.cache.PageCacheBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
|
@ -77,6 +83,16 @@ class RandomAccessCallback private constructor(
|
|||
|
||||
private val pagingReader = PagingReader(fileSize, PageCacheBuilder.MAX_PAGE_SIZE, this)
|
||||
private val pageCache = PageCacheBuilder.getInstance()
|
||||
private var loadPageJobs: Set<Deferred<ByteArray>> = emptySet()
|
||||
|
||||
init {
|
||||
cancellationSignal?.let {
|
||||
Logger.log.info("Cancelling random access to $url")
|
||||
for (job in loadPageJobs)
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onFsync() { /* not used */ }
|
||||
|
||||
|
@ -90,17 +106,6 @@ class RandomAccessCallback private constructor(
|
|||
Logger.log.fine("onRead $url $offset $size")
|
||||
throwIfCancelled("onRead")
|
||||
|
||||
val progress =
|
||||
if (fileSize == 0L) // avoid division by zero
|
||||
100
|
||||
else
|
||||
(offset*100/fileSize).toInt()
|
||||
notificationManager.notifyIfPossible(
|
||||
notificationTag,
|
||||
NotificationUtils.NOTIFY_WEBDAV_ACCESS,
|
||||
notification.setProgress(100, progress, false).build()
|
||||
)
|
||||
|
||||
try {
|
||||
return pagingReader.read(offset, size, data)
|
||||
} catch (e: Exception) {
|
||||
|
@ -123,12 +128,27 @@ class RandomAccessCallback private constructor(
|
|||
|
||||
override fun loadPage(offset: Long, size: Int): ByteArray {
|
||||
Logger.log.fine("Loading page $url $offset/$size")
|
||||
return pageCache.getOrPut(PageCacheBuilder.PageIdentifier(url, offset, size)) {
|
||||
|
||||
// update notification
|
||||
val progress =
|
||||
if (fileSize == 0L) // avoid division by zero
|
||||
100
|
||||
else
|
||||
(offset * 100 / fileSize).toInt()
|
||||
notificationManager.notifyIfPossible(
|
||||
notificationTag,
|
||||
NotificationUtils.NOTIFY_WEBDAV_ACCESS,
|
||||
notification.setProgress(100, progress, false).build()
|
||||
)
|
||||
|
||||
// create async job that can be cancelled (and cancellation interrupts I/O)
|
||||
val job = CoroutineScope(Dispatchers.IO).async {
|
||||
runInterruptible {
|
||||
pageCache.getOrPut(PageCacheBuilder.PageIdentifier(url, offset, size)) {
|
||||
val ifMatch: Headers =
|
||||
documentState.eTag?.let { eTag ->
|
||||
Headers.headersOf("If-Match", "\"$eTag\"")
|
||||
} ?:
|
||||
documentState.lastModified?.let { lastModified ->
|
||||
} ?: documentState.lastModified?.let { lastModified ->
|
||||
Headers.headersOf("If-Unmodified-Since", HttpUtils.formatDate(lastModified))
|
||||
} ?: throw IllegalStateException("ETag/Last-Modified required for random access")
|
||||
|
||||
|
@ -149,6 +169,19 @@ class RandomAccessCallback private constructor(
|
|||
return@getOrPut result ?: throw DavException("No response body")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
loadPageJobs += job
|
||||
|
||||
// wait for result
|
||||
return runBlocking {
|
||||
job.await()
|
||||
}
|
||||
} finally {
|
||||
loadPageJobs -= job
|
||||
}
|
||||
}
|
||||
|
||||
private fun throwIfCancelled(functionName: String) {
|
||||
if (cancellationSignal?.isCanceled == true) {
|
||||
|
@ -158,7 +191,8 @@ class RandomAccessCallback private constructor(
|
|||
}
|
||||
|
||||
private fun Exception.toErrNoException(functionName: String) =
|
||||
ErrnoException(functionName,
|
||||
ErrnoException(
|
||||
functionName,
|
||||
when (this) {
|
||||
is HttpException ->
|
||||
when (code) {
|
||||
|
@ -170,15 +204,11 @@ class RandomAccessCallback private constructor(
|
|||
is InterruptedIOException -> OsConstants.EINTR
|
||||
is PartialContentNotSupportedException -> OsConstants.EOPNOTSUPP
|
||||
else -> OsConstants.EIO
|
||||
}
|
||||
},
|
||||
this
|
||||
)
|
||||
|
||||
|
||||
data class DocumentKey(
|
||||
val resource: HttpUrl,
|
||||
val state: DocumentState
|
||||
)
|
||||
|
||||
class PartialContentNotSupportedException: Exception()
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue