Take the two main-thread disk reads off the startup path, and report what that bought
Both costs StrictMode found on a cold start are now off the main thread. scratchRoot is a Lazy<File>, so Context.getCacheDir - which checks and creates the private cache directory, measured at 492 ms - runs when a preview first copies something instead of while the view model is being created. The free-space probes moved into fillRootFreeSpace and fillFreeSpace, which run on Dispatchers.IO after the rows are already on screen and update the state when the filesystem answers; nothing waits for them, and fillFreeSpace checks the path is still the one on screen so a fast double-tap cannot leave a folder showing another one's number. Measured after: StrictMode no longer reports either violation, and the only ones left are MIUI's own font hook - FontSettings.loadFontSetting, TypefaceUtils.loadFontSettings, InterceptorProxy.create via MainActivity.onCreate, in 50-78 ms pairs. The debug cold start went from a 1814 ms median to 1803 ms and the benchmark coldStart from 519.5/531.6 ms to 485.6/568.9/600.0 ms. That is no improvement, and PERFORMANCE.md says so in those words: both violations were first-call costs (cache directory creation once per install, a cold StatFs page cache), the timed runs are warm, and the device's run-to-run spread of 115 ms is wider than anything the change could have bought. What it does change is the launch right after an install, which is the benchmark's first iteration: 648 ms max fresh against 550 ms warm.
This commit is contained in:
@@ -51,6 +51,7 @@ import de.softwareapp_hb.filemanager.domain.TrashedEntry
|
||||
import de.softwareapp_hb.filemanager.domain.UsageAnalysis
|
||||
import de.softwareapp_hb.filemanager.domain.UsageNode
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -59,6 +60,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -101,7 +103,7 @@ class BrowserViewModel(
|
||||
* A `File` rather than a `Context`: the ViewModel keeps its distance from the platform,
|
||||
* and the application hands it a directory it already owns.
|
||||
*/
|
||||
private val scratchRoot: File? = null,
|
||||
private val scratchRoot: Lazy<File>? = null,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(restoredViewState())
|
||||
@@ -142,6 +144,9 @@ class BrowserViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
// The rows are on screen; their free-space numbers follow when the filesystem answers.
|
||||
viewModelScope.launch { fillRootFreeSpace() }
|
||||
|
||||
if (source == null) {
|
||||
listing = emptyList()
|
||||
recompute()
|
||||
@@ -277,6 +282,8 @@ class BrowserViewModel(
|
||||
// Navigating abandons any search: its results belonged to the folder being left.
|
||||
searchJob?.cancel()
|
||||
libraryJob?.cancel()
|
||||
// Measured off the main thread, after the listing is up: see [fillFreeSpace].
|
||||
viewModelScope.launch { fillFreeSpace(path) }
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(loading = true) }
|
||||
listing = runCatching { source.list(path) }.getOrDefault(emptyList())
|
||||
@@ -290,7 +297,7 @@ class BrowserViewModel(
|
||||
query = "",
|
||||
searchRoot = null,
|
||||
searching = false,
|
||||
freeBytes = storage.space(path)?.freeBytes,
|
||||
freeBytes = null,
|
||||
loading = false,
|
||||
)
|
||||
}
|
||||
@@ -344,11 +351,53 @@ class BrowserViewModel(
|
||||
path = volume.root,
|
||||
labelRes = if (volume.label == null) R.string.browser_internal_storage else null,
|
||||
label = volume.label,
|
||||
freeBytes = storage.space(volume.root)?.freeBytes,
|
||||
// No `freeBytes` here on purpose. Reading it means `StatFs`, which StrictMode
|
||||
// measured at 397 ms on the first call (docs/PERFORMANCE.md), and this function runs
|
||||
// while the root screen is being built. [fillRootFreeSpace] adds it a moment later.
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The free-space line on each root, once the filesystem has answered.
|
||||
*
|
||||
* The rows are drawn without it, so the screen appears with the volumes and the numbers arrive a
|
||||
* frame or two later — a number that is late reads far better than a start that is half a second
|
||||
* late. The probe is off the main thread, which is the whole point: `StatFs` on the main thread is
|
||||
* what the cold-start measurement found.
|
||||
*/
|
||||
private suspend fun fillRootFreeSpace() {
|
||||
// A granted tree is a single root whose free space the platform does not report anyway.
|
||||
if (storage.treeLabel() != null) return
|
||||
val roots = _state.value.roots
|
||||
if (roots.isEmpty()) return
|
||||
|
||||
val free = withContext(Dispatchers.IO) {
|
||||
roots.associate { it.path to runCatching { storage.space(it.path)?.freeBytes }.getOrNull() }
|
||||
}
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
roots = state.roots.map { root ->
|
||||
free[root.path]?.let { root.copy(freeBytes = it) } ?: root
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The free space of the folder on screen, read off the main thread.
|
||||
*
|
||||
* Same reasoning as [fillRootFreeSpace]: the listing is shown first, and the number follows.
|
||||
*/
|
||||
private suspend fun fillFreeSpace(path: StoragePath) {
|
||||
val free = withContext(Dispatchers.IO) {
|
||||
runCatching { storage.space(path)?.freeBytes }.getOrNull()
|
||||
}
|
||||
// Only if the screen is still this folder: a fast tap through two folders must not leave the
|
||||
// second one showing the first one's free space.
|
||||
_state.update { state -> if (state.path == path) state.copy(freeBytes = free) else state }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- libraries
|
||||
|
||||
/**
|
||||
@@ -1457,7 +1506,7 @@ class BrowserViewModel(
|
||||
|
||||
/** A file to copy one entry into, or `null` when this build has nowhere to put one. */
|
||||
private fun reserveScratch(name: String): File? {
|
||||
val root = scratchRoot ?: return null
|
||||
val root = scratchRoot?.value ?: return null
|
||||
|
||||
// One folder per preview, holding the entry under its own name — a viewer shows the
|
||||
// file's name in its title bar, and a copy called `0-shot.png` would read as a file
|
||||
@@ -2084,7 +2133,10 @@ class BrowserViewModel(
|
||||
bookmarkStore = app.bookmarkStore,
|
||||
apkInspector = app.apkInspector,
|
||||
searchIndex = app.searchIndex,
|
||||
scratchRoot = app.cacheDir.resolve(SCRATCH_DIRECTORY),
|
||||
// Lazy on purpose: `app.cacheDir` checks and creates the private cache
|
||||
// directory, which StrictMode measured at 492 ms on the first start — see
|
||||
// docs/PERFORMANCE.md. Nothing needs it until a preview copies something.
|
||||
scratchRoot = lazy { app.cacheDir.resolve(SCRATCH_DIRECTORY) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,9 @@ class BrowserViewModelTest {
|
||||
searchIndex = searchIndex,
|
||||
bookmarkStore = bookmarks,
|
||||
clock = clock,
|
||||
scratchRoot = scratchRoot,
|
||||
// The view model takes it lazily now — `app.cacheDir` is a disk check that used to run while
|
||||
// the screen was being built — so the test wraps the folder it already made.
|
||||
scratchRoot = scratchRoot?.let { folder -> lazyOf(folder) },
|
||||
)
|
||||
|
||||
/** A real ZIP in memory, so an entry can be previewed out of something that has an index. */
|
||||
|
||||
Reference in New Issue
Block a user