refactor(article): warm up the blocks that are actually rendered
The screen merges small parser blocks into larger render blocks, while the warm-up measured the unmerged parser blocks. Merge once in the view model and keep the merged blocks in the article state, so the screen and the warm-up share exactly what will be drawn.
This commit is contained in:
1 parent
381ce33017
commit
5ad184ba58
4 files changed
+69
-77
No files matched your search
@@ -14,8 +14,9 @@ import com.example.research.data.local.preferences.PreferencesManager
|
||||
import com.example.research.data.repository.LocalDictionaryRepository
|
||||
import com.example.research.ui.article.ArticleState
|
||||
import com.example.research.ui.article.ArticleTextWarmUp
|
||||
import com.example.research.ui.article.DslBlock
|
||||
import com.example.research.ui.article.DslAnnotatedParser
|
||||
import com.example.research.ui.article.HEAVY_ARTICLE_BLOCK_COUNT
|
||||
import com.example.research.ui.article.mergeDslBlocks
|
||||
import com.example.research.R
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
@@ -63,7 +64,7 @@ class SearchViewModel(
|
||||
private var indexWarmupJob: Job? = null
|
||||
private var debounceSaveQueryJob: Job? = null
|
||||
|
||||
private val parseCache = java.util.LinkedHashMap<String, List<DslBlock>>(PARSE_CACHE_SIZE, 0.75f, true)
|
||||
private val parseCache = java.util.LinkedHashMap<String, ArticleState.Success>(PARSE_CACHE_SIZE, 0.75f, true)
|
||||
private val articleHistory = ArrayDeque<ArticleState.Success>()
|
||||
|
||||
private val activeDictionaries: StateFlow<List<com.example.research.core.domain.model.Dictionary>> =
|
||||
@@ -211,12 +212,7 @@ class SearchViewModel(
|
||||
val cached = synchronized(parseCache) { parseCache[cacheKey] }
|
||||
if (cached != null) {
|
||||
mutableSelectedArticle.update {
|
||||
ArticleState.Success(
|
||||
title = entry.originalWord,
|
||||
dslContent = cached,
|
||||
dictionaryName = entry.dictionaryName,
|
||||
dictionaryPath = entry.dictionaryPath,
|
||||
)
|
||||
cached.copy(title = entry.originalWord, dictionaryName = entry.dictionaryName)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
@@ -228,6 +224,13 @@ class SearchViewModel(
|
||||
colorScheme = DslAnnotatedParser.ARTICLE_COLOR_SCHEME,
|
||||
)
|
||||
}
|
||||
val article = ArticleState.Success(
|
||||
title = entry.originalWord,
|
||||
dslContent = mergeDslBlocks(annotatedStringList),
|
||||
dictionaryName = entry.dictionaryName,
|
||||
dictionaryPath = entry.dictionaryPath,
|
||||
isHeavy = annotatedStringList.size > HEAVY_ARTICLE_BLOCK_COUNT,
|
||||
)
|
||||
synchronized(parseCache) {
|
||||
if (parseCache.size >= PARSE_CACHE_SIZE) {
|
||||
val iterator = parseCache.entries.iterator()
|
||||
@@ -235,19 +238,12 @@ class SearchViewModel(
|
||||
iterator.next().run { iterator.remove() }
|
||||
}
|
||||
}
|
||||
parseCache[cacheKey] = annotatedStringList
|
||||
parseCache[cacheKey] = article
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
ArticleTextWarmUp.warmUp(getApplication(), annotatedStringList)
|
||||
}
|
||||
mutableSelectedArticle.update {
|
||||
ArticleState.Success(
|
||||
title = entry.originalWord,
|
||||
dslContent = annotatedStringList,
|
||||
dictionaryName = entry.dictionaryName,
|
||||
dictionaryPath = entry.dictionaryPath,
|
||||
)
|
||||
ArticleTextWarmUp.warmUp(getApplication(), article.dslContent)
|
||||
}
|
||||
mutableSelectedArticle.update { article }
|
||||
}
|
||||
.onError { message, exception ->
|
||||
Log.e(TAG, "loadArticle() - error: $message", exception)
|
||||
@@ -324,10 +320,6 @@ class SearchViewModel(
|
||||
companion object {
|
||||
private const val TAG = "SearchViewModel"
|
||||
|
||||
// Parse cache: stores parsed DslBlock lists to avoid re-parsing articles
|
||||
// Size: 50 articles - typical user navigation depth in a session
|
||||
// Memory estimate: ~50KB per article average (text + formatting)
|
||||
// Total: ~2.5MB worst case
|
||||
private const val PARSE_CACHE_SIZE = 50
|
||||
|
||||
private const val KEY_SEARCH_QUERY = "search_query"
|
||||
|
||||
@@ -44,7 +44,6 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
@@ -55,54 +54,6 @@ import com.example.research.common.ui.theme.dictionaryTitleLarge
|
||||
import com.example.research.ui.theme.AppWindowInsets
|
||||
import com.example.research.ui.theme.Spacing
|
||||
|
||||
private const val MERGED_BLOCK_MAX_CHARS = 1500
|
||||
private const val MERGED_BLOCK_MAX_ITEMS = 12
|
||||
|
||||
private fun mergeDslBlocks(blocks: List<DslBlock>): List<DslBlock> {
|
||||
if (blocks.size <= MERGED_BLOCK_MAX_ITEMS) return blocks
|
||||
val merged = ArrayList<DslBlock>(blocks.size / 4)
|
||||
var i = 0
|
||||
while (i < blocks.size) {
|
||||
val builder = AnnotatedString.Builder()
|
||||
val refs = ArrayList<DslAnnotatedParser.LazyRef>()
|
||||
var j = i
|
||||
while (j < blocks.size) {
|
||||
val block = blocks[j]
|
||||
repeat(block.indent) { builder.append(' ') }
|
||||
val paraStart = builder.length
|
||||
builder.append(block.text)
|
||||
block.lazyRefs.forEach { ref ->
|
||||
refs.add(
|
||||
ref.copy(
|
||||
startIndex = ref.startIndex + paraStart,
|
||||
endIndex = ref.endIndex + paraStart
|
||||
)
|
||||
)
|
||||
}
|
||||
val more = j + 1 < blocks.size &&
|
||||
(j + 1 - i) < MERGED_BLOCK_MAX_ITEMS &&
|
||||
builder.length < MERGED_BLOCK_MAX_CHARS
|
||||
if (more) {
|
||||
builder.append('\n')
|
||||
}
|
||||
j++
|
||||
if (!more) {
|
||||
break
|
||||
}
|
||||
}
|
||||
merged.add(
|
||||
DslBlock(
|
||||
text = builder.toAnnotatedString(),
|
||||
indent = 0,
|
||||
lazyRefs = refs,
|
||||
continuesPrevious = blocks[i].continuesPrevious
|
||||
)
|
||||
)
|
||||
i = j
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ArticleContent(
|
||||
articleState: ArticleState?,
|
||||
@@ -183,8 +134,7 @@ fun ArticleContent(
|
||||
}
|
||||
}
|
||||
|
||||
val isHeavyArticle =
|
||||
articleState.dslContent.size > HEAVY_ARTICLE_BLOCK_COUNT
|
||||
val isHeavyArticle = articleState.isHeavy
|
||||
|
||||
val darkTheme = MaterialTheme.colorScheme.background.luminance() < 0.5f
|
||||
val displayBlocks = remember(
|
||||
@@ -192,14 +142,12 @@ fun ArticleContent(
|
||||
baseTextStyle,
|
||||
darkTheme
|
||||
) {
|
||||
val merged =
|
||||
mergeDslBlocks(articleState.dslContent)
|
||||
if (darkTheme) {
|
||||
merged.map {
|
||||
articleState.dslContent.map {
|
||||
it.copy(text = DslAnnotatedParser.remapForDarkTheme(it.text))
|
||||
}
|
||||
} else {
|
||||
merged
|
||||
articleState.dslContent
|
||||
}
|
||||
}
|
||||
val articleList: @Composable (Modifier) -> Unit = { listModifier ->
|
||||
|
||||
@@ -37,6 +37,7 @@ sealed class ArticleState {
|
||||
val dictionaryPath: String = "",
|
||||
val scrollIndex: Int = 0,
|
||||
val scrollOffset: Int = 0,
|
||||
val isHeavy: Boolean = false,
|
||||
) : ArticleState()
|
||||
@Immutable
|
||||
data class Error(
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.example.research.ui.article
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
private const val MERGED_BLOCK_MAX_CHARS = 1500
|
||||
private const val MERGED_BLOCK_MAX_ITEMS = 12
|
||||
|
||||
internal fun mergeDslBlocks(blocks: List<DslBlock>): List<DslBlock> {
|
||||
if (blocks.size <= MERGED_BLOCK_MAX_ITEMS) return blocks
|
||||
val merged = ArrayList<DslBlock>(blocks.size / 4)
|
||||
var i = 0
|
||||
while (i < blocks.size) {
|
||||
val builder = AnnotatedString.Builder()
|
||||
val refs = ArrayList<DslAnnotatedParser.LazyRef>()
|
||||
var j = i
|
||||
while (j < blocks.size) {
|
||||
val block = blocks[j]
|
||||
repeat(block.indent) { builder.append(' ') }
|
||||
val paraStart = builder.length
|
||||
builder.append(block.text)
|
||||
block.lazyRefs.forEach { ref ->
|
||||
refs.add(
|
||||
ref.copy(
|
||||
startIndex = ref.startIndex + paraStart,
|
||||
endIndex = ref.endIndex + paraStart
|
||||
)
|
||||
)
|
||||
}
|
||||
val more = j + 1 < blocks.size &&
|
||||
(j + 1 - i) < MERGED_BLOCK_MAX_ITEMS &&
|
||||
builder.length < MERGED_BLOCK_MAX_CHARS
|
||||
if (more) {
|
||||
builder.append('\n')
|
||||
}
|
||||
j++
|
||||
if (!more) {
|
||||
break
|
||||
}
|
||||
}
|
||||
merged.add(
|
||||
DslBlock(
|
||||
text = builder.toAnnotatedString(),
|
||||
indent = 0,
|
||||
lazyRefs = refs,
|
||||
continuesPrevious = blocks[i].continuesPrevious
|
||||
)
|
||||
)
|
||||
i = j
|
||||
}
|
||||
return merged
|
||||
}
|
||||
Reference in new issue
Block a user