HapeLee 1 месяц назад
Родитель
Сommit
d1b488b04e

+ 7 - 5
app/src/main/java/io/legado/app/feature/reader/core/layout/ReaderChapterBlockMeasurer.kt

@@ -7,6 +7,7 @@ import io.legado.app.feature.reader.core.source.ReaderChapterSourceBlock
 import io.legado.app.feature.reader.core.source.ReaderInlineSourceStyle
 import io.legado.app.feature.reader.core.style.ReaderCharacterStyle
 import io.legado.app.feature.reader.core.style.ReaderCharacterStyleResolver
+import io.legado.app.feature.reader.core.style.ReaderCompiledStyleRanges
 import io.legado.app.feature.reader.core.style.ReaderStyleRange
 import kotlin.coroutines.cancellation.CancellationException
 
@@ -108,13 +109,13 @@ class ReaderChapterMeasureMetrics(private val nanoTime: () -> Long) {
     }
 
     fun resolveStyle(
-        ranges: List<ReaderStyleRange>,
+        ranges: ReaderCompiledStyleRanges,
         position: Int,
         isTitle: Boolean
     ): ReaderCharacterStyle? {
         val start = nanoTime()
         return try {
-            ReaderCharacterStyleResolver.resolve(ranges, position, isTitle)
+            ranges.resolve(position, isTitle)
         } finally {
             styleLookupNs += nanoTime() - start
             styleLookups++
@@ -141,6 +142,8 @@ class ReaderChapterBlockMeasurer(
         style: ReaderChapterMeasureStyle,
         onBlock: ((ReaderMeasuredBlock) -> Unit)? = null,
     ): ReaderChapterMeasureResult {
+        val compiledStyleRanges = style.styleRanges.takeIf { it.isNotEmpty() }
+            ?.let(ReaderCharacterStyleResolver::compile)
         // `blocks += x` 就是 `add(x)`:覆写 add 即可在每个追加点回调,无需在六处追加点重复。
         val blocks = object : ArrayList<ReaderMeasuredBlock>(source.blocks.size) {
             override fun add(element: ReaderMeasuredBlock): Boolean {
@@ -246,11 +249,10 @@ class ReaderChapterBlockMeasurer(
                         var offset = 0
                         initiallyShaped.text.forEachIndexed { clusterIndex, cluster ->
                             val position = item.chapterPosition + offset
-                            val rangeStyle = style.styleRanges
-                                .takeIf(List<ReaderStyleRange>::isNotEmpty)
+                            val rangeStyle = compiledStyleRanges
                                 ?.let {
                                     if (metrics != null) metrics.resolveStyle(it, position, isTitle)
-                                    else ReaderCharacterStyleResolver.resolve(it, position, isTitle)
+                                    else it.resolve(position, isTitle)
                                 }
                             val textStyle = htmlStyle.merge(rangeStyle)
                             val textShaper = shaper(textStyle)

+ 21 - 1
app/src/main/java/io/legado/app/feature/reader/core/layout/ReaderPaginator.kt

@@ -253,7 +253,17 @@ object ReaderPaginator {
  * 因为只有章末才知道它的堆叠高度要加 [ReaderPaginationConfig.chapterEndPaddingPx]
  * (旧版同样在章末才 `textPage.height = durY + 20dp`)。
  */
-internal class ReaderPaginationSession(private val config: ReaderPaginationConfig) {
+internal class ReaderPaginationMetrics {
+    var lineBreakNs = 0L
+    var lineRefineNs = 0L
+    var linePlacementNs = 0L
+    var inlineParagraphs = 0L
+}
+
+internal class ReaderPaginationSession(
+    private val config: ReaderPaginationConfig,
+    private val metrics: ReaderPaginationMetrics? = null,
+) {
 
     /** 每页成型即回调;章末页在 [finish] 内回调。 */
     var onPage: ((ReaderPage) -> Unit)? = null
@@ -624,6 +634,7 @@ internal class ReaderPaginationSession(private val config: ReaderPaginationConfi
         appendSeparator: Boolean,
     ) {
         if (paragraph.items.isEmpty()) return
+        val setupStartNs = if (metrics != null) System.nanoTime() else 0L
         val letterSpacing = paragraph.letterSpacingPx ?: config.letterSpacingPx
         val indentWidth = paragraph.indentWidthPx
             ?: (paragraph.baseTextSizePx + letterSpacing) * paragraph.indentCharacters
@@ -704,6 +715,7 @@ internal class ReaderPaginationSession(private val config: ReaderPaginationConfi
                 .coerceAtLeast(0f).toInt(),
         )
         val originalEnds = breaker.lineClusterStarts.drop(1)
+        val refineStartNs = if (metrics != null) System.nanoTime() else 0L
         val starts = mutableListOf(0)
         // 每行的纵向预算在断行阶段就定死,绘制阶段直接复用——排版预留与绘制的外框必须
         // 用同一份 inset,否则框会压到相邻文字上或者留出多余的空档。
@@ -757,6 +769,7 @@ internal class ReaderPaginationSession(private val config: ReaderPaginationConfi
             lineBottomBudgets += bottomBudgetPx
             previousLineHadFrame = lineHasFrame(from, until)
         }
+        val placementStartNs = if (metrics != null) System.nanoTime() else 0L
         for (lineIndex in 0 until starts.lastIndex) {
             val from = starts[lineIndex]
             val until = starts[lineIndex + 1]
@@ -943,6 +956,13 @@ internal class ReaderPaginationSession(private val config: ReaderPaginationConfi
                 ?: config.paragraphSpacingPx) * paragraph.titleSpacingScale
             else config.paragraphSpacingPx
         }
+        if (metrics != null) {
+            val endNs = System.nanoTime()
+            metrics.lineBreakNs += refineStartNs - setupStartNs
+            metrics.lineRefineNs += placementStartNs - refineStartNs
+            metrics.linePlacementNs += endNs - placementStartNs
+            metrics.inlineParagraphs++
+        }
     }
 
     private fun addRule(rule: ReaderMeasuredBlock.Rule) {

+ 38 - 0
app/src/main/java/io/legado/app/feature/reader/core/style/ReaderCharacterStyle.kt

@@ -33,6 +33,24 @@ data class ReaderStyleRange(
 }
 
 object ReaderCharacterStyleResolver {
+    /** Build the winning style once per interval instead of scanning every range for each glyph. */
+    fun compile(ranges: List<ReaderStyleRange>): ReaderCompiledStyleRanges {
+        val boundaries = ranges.asSequence()
+            .filter { it.start < it.endExclusive }
+            .flatMap { sequenceOf(it.start, it.endExclusive) }
+            .distinct()
+            .sorted()
+            .toList()
+            .toIntArray()
+        val bodyStyles = Array<ReaderCharacterStyle?>(boundaries.size.coerceAtLeast(1) - 1) {
+            resolve(ranges, boundaries[it], false)
+        }
+        val titleStyles = Array<ReaderCharacterStyle?>(bodyStyles.size) {
+            resolve(ranges, boundaries[it], true)
+        }
+        return ReaderCompiledStyleRanges(boundaries, bodyStyles, titleStyles)
+    }
+
     fun resolve(
         ranges: List<ReaderStyleRange>,
         position: Int,
@@ -50,3 +68,23 @@ object ReaderCharacterStyleResolver {
         return winner?.style
     }
 }
+
+class ReaderCompiledStyleRanges internal constructor(
+    private val boundaries: IntArray,
+    private val bodyStyles: Array<ReaderCharacterStyle?>,
+    private val titleStyles: Array<ReaderCharacterStyle?>,
+) {
+    fun resolve(position: Int, isTitle: Boolean): ReaderCharacterStyle? {
+        var low = 0
+        var high = boundaries.size - 2
+        while (low <= high) {
+            val middle = (low + high) ushr 1
+            when {
+                position < boundaries[middle] -> high = middle - 1
+                position >= boundaries[middle + 1] -> low = middle + 1
+                else -> return if (isTitle) titleStyles[middle] else bodyStyles[middle]
+            }
+        }
+        return null
+    }
+}

+ 19 - 1
app/src/main/java/io/legado/app/feature/reader/legacy/LegacyReaderChapterPaginator.kt

@@ -11,6 +11,7 @@ import io.legado.app.feature.reader.core.layout.ReaderChapterMeasureStyle
 import io.legado.app.feature.reader.core.layout.ReaderImageDimensions
 import io.legado.app.feature.reader.core.layout.ReaderImageLayoutMode
 import io.legado.app.feature.reader.core.layout.ReaderPaginationConfig
+import io.legado.app.feature.reader.core.layout.ReaderPaginationMetrics
 import io.legado.app.feature.reader.core.layout.ReaderPaginationSession
 import io.legado.app.feature.reader.core.layout.ReaderTextAlignment
 import io.legado.app.feature.reader.core.layout.ReaderTextShaperFactory
@@ -208,7 +209,8 @@ object LegacyReaderChapterPaginator {
             letterSpacingPx = bodyPaint.letterSpacing * bodyPaint.textSize,
             revision = revision,
         )
-        val paginationSession = ReaderPaginationSession(paginationConfig)
+        val paginationMetrics = if (tracing) ReaderPaginationMetrics() else null
+        val paginationSession = ReaderPaginationSession(paginationConfig, paginationMetrics)
         paginationSession.onPage = onPage
         if (tracing) {
             ReaderPerfTrace.counter(
@@ -287,6 +289,22 @@ object LegacyReaderChapterPaginator {
             )
             ReaderPerfTrace.counter("pagination.shape-calls", measureMetrics?.shapingCalls ?: 0)
             ReaderPerfTrace.counter("pagination.style-lookups", measureMetrics?.styleLookups ?: 0)
+            ReaderPerfTrace.counter(
+                "pagination.line-break.us",
+                paginationMetrics?.lineBreakNs?.div(1_000) ?: 0
+            )
+            ReaderPerfTrace.counter(
+                "pagination.line-refine.us",
+                paginationMetrics?.lineRefineNs?.div(1_000) ?: 0
+            )
+            ReaderPerfTrace.counter(
+                "pagination.line-placement.us",
+                paginationMetrics?.linePlacementNs?.div(1_000) ?: 0
+            )
+            ReaderPerfTrace.counter(
+                "pagination.inline-paragraphs",
+                paginationMetrics?.inlineParagraphs ?: 0
+            )
         }
         if (measured is ReaderChapterMeasureResult.Unsupported) {
             // 测量失败说明这一章的页不可信:已流出的部分页由调用方按 Unsupported 丢弃。

+ 26 - 0
app/src/test/java/io/legado/app/feature/reader/core/style/ReaderCharacterStyleTest.kt

@@ -3,8 +3,34 @@ package io.legado.app.feature.reader.core.style
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNull
 import org.junit.Test
+import kotlin.random.Random
 
 class ReaderCharacterStyleTest {
+    @Test
+    fun compiledRangesMatchDirectResolutionAtEveryBoundaryAndOverlap() {
+        val random = Random(21)
+        repeat(100) {
+            val ranges = List(random.nextInt(0, 20)) { index ->
+                ReaderStyleRange(
+                    start = random.nextInt(-5, 25),
+                    endExclusive = random.nextInt(-5, 25),
+                    target = ReaderStyleTarget.entries[random.nextInt(3)],
+                    style = ReaderCharacterStyle(colorArgb = index),
+                    priority = random.nextInt(-2, 3),
+                )
+            }
+            val compiled = ReaderCharacterStyleResolver.compile(ranges)
+            for (position in -6..25) {
+                for (isTitle in listOf(false, true)) {
+                    assertEquals(
+                        ReaderCharacterStyleResolver.resolve(ranges, position, isTitle),
+                        compiled.resolve(position, isTitle),
+                    )
+                }
+            }
+        }
+    }
+
     @Test
     fun higherPriorityMarkingOverridesRegexStyle() {
         val ranges = listOf(