Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit de6d4442 authored by Hawkwood Glazier's avatar Hawkwood Glazier
Browse files

Reduce the number of Font instance creation

Bug: 378217740
Bug: 363273709
Flag: NONE Caching api change
Test: Manually checked some traces in perfetto
Change-Id: I7584764d4a0ea911c363430daf57c2b794cc6a99
parent 1ba241be
Loading
Loading
Loading
Loading
+59 −67
Original line number Diff line number Diff line
@@ -34,66 +34,60 @@ private const val FONT_ITALIC_MIN = 0f
private const val FONT_ITALIC_ANIMATION_STEP = 0.1f
private const val FONT_ITALIC_DEFAULT_VALUE = 0f

// Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in
// frame draw time on a Pixel 6.
@VisibleForTesting const val DEFAULT_FONT_CACHE_MAX_ENTRIES = 10
/** Caches for font interpolation */
interface FontCache {
    val animationFrameCount: Int

/** Provide interpolation of two fonts by adjusting font variation settings. */
class FontInterpolator(
    numberOfAnimationSteps: Int? = null,
) {
    /**
     * Cache key for the interpolated font.
     *
     * This class is mutable for recycling.
     */
    private data class InterpKey(var l: Font?, var r: Font?, var progress: Float) {
        fun set(l: Font, r: Font, progress: Float) {
            this.l = l
            this.r = r
            this.progress = progress
        }
    fun get(key: InterpKey): Font?

    fun get(key: VarFontKey): Font?

    fun put(key: InterpKey, font: Font)

    fun put(key: VarFontKey, font: Font)
}

    /**
     * Cache key for the font that has variable font.
     *
     * This class is mutable for recycling.
     */
    private data class VarFontKey(
        var sourceId: Int,
        var index: Int,
        val sortedAxes: MutableList<FontVariationAxis>
    ) {
/** Cache key for the interpolated font. */
data class InterpKey(val start: Font?, val end: Font?, val frame: Int)

/** Cache key for the font that has variable font. */
data class VarFontKey(val sourceId: Int, val index: Int, val sortedAxes: List<FontVariationAxis>) {
    constructor(
        font: Font,
            axes: List<FontVariationAxis>
        ) : this(
            font.sourceIdentifier,
            font.ttcIndex,
            axes.toMutableList().apply { sortBy { it.tag } }
        )

        fun set(font: Font, axes: List<FontVariationAxis>) {
            sourceId = font.sourceIdentifier
            index = font.ttcIndex
            sortedAxes.clear()
            sortedAxes.addAll(axes)
            sortedAxes.sortBy { it.tag }
        }
        axes: List<FontVariationAxis>,
    ) : this(font.sourceIdentifier, font.ttcIndex, axes.sortedBy { it.tag })
}

class FontCacheImpl(override val animationFrameCount: Int = DEFAULT_FONT_CACHE_MAX_ENTRIES / 2) :
    FontCache {
    // Font interpolator has two level caches: one for input and one for font with different
    // variation settings. No synchronization is needed since FontInterpolator is not designed to be
    // thread-safe and can be used only on UI thread.
    val cacheMaxEntries = numberOfAnimationSteps?.let { it * 2 } ?: DEFAULT_FONT_CACHE_MAX_ENTRIES
    val cacheMaxEntries = animationFrameCount * 2
    private val interpCache = LruCache<InterpKey, Font>(cacheMaxEntries)
    private val verFontCache = LruCache<VarFontKey, Font>(cacheMaxEntries)

    // Mutable keys for recycling.
    private val tmpInterpKey = InterpKey(null, null, 0f)
    private val tmpVarFontKey = VarFontKey(0, 0, mutableListOf())
    override fun get(key: InterpKey): Font? = interpCache[key]

    override fun get(key: VarFontKey): Font? = verFontCache[key]

    override fun put(key: InterpKey, font: Font) {
        interpCache.put(key, font)
    }

    override fun put(key: VarFontKey, font: Font) {
        verFontCache.put(key, font)
    }

    companion object {
        // Benchmarked via Perfetto, difference between 10 and 50 entries is about 0.3ms in frame
        // draw time on a Pixel 6.
        @VisibleForTesting const val DEFAULT_FONT_CACHE_MAX_ENTRIES = 10
    }
}

/** Provide interpolation of two fonts by adjusting font variation settings. */
class FontInterpolator(val fontCache: FontCache = FontCacheImpl()) {
    /** Linear interpolate the font variation settings. */
    fun lerp(start: Font, end: Font, progress: Float): Font {
        if (progress == 0f) {
@@ -111,13 +105,12 @@ class FontInterpolator(

        // Check we already know the result. This is commonly happens since we draws the different
        // text chunks with the same font.
        tmpInterpKey.set(start, end, progress)
        val cachedFont = interpCache[tmpInterpKey]
        if (cachedFont != null) {
        val iKey = InterpKey(start, end, (progress * fontCache.animationFrameCount).toInt())
        fontCache.get(iKey)?.let {
            if (DEBUG) {
                Log.d(LOG_TAG, "[$progress] Interp. cache hit for $tmpInterpKey")
                Log.d(LOG_TAG, "[$progress] Interp. cache hit for $iKey")
            }
            return cachedFont
            return it
        }

        // General axes interpolation takes O(N log N), this is came from sorting the axes. Usually
@@ -131,14 +124,14 @@ class FontInterpolator(
                        MathUtils.lerp(
                            startValue ?: FONT_WEIGHT_DEFAULT_VALUE,
                            endValue ?: FONT_WEIGHT_DEFAULT_VALUE,
                            progress
                            progress,
                        )
                    TAG_ITAL ->
                        adjustItalic(
                            MathUtils.lerp(
                                startValue ?: FONT_ITALIC_DEFAULT_VALUE,
                                endValue ?: FONT_ITALIC_DEFAULT_VALUE,
                                progress
                                progress,
                            )
                        )
                    else -> {
@@ -152,32 +145,31 @@ class FontInterpolator(

        // Check if we already make font for this axes. This is typically happens if the animation
        // happens backward.
        tmpVarFontKey.set(start, newAxes)
        val axesCachedFont = verFontCache[tmpVarFontKey]
        if (axesCachedFont != null) {
            interpCache.put(InterpKey(start, end, progress), axesCachedFont)
        val vKey = VarFontKey(start, newAxes)
        fontCache.get(vKey)?.let {
            fontCache.put(iKey, it)
            if (DEBUG) {
                Log.d(LOG_TAG, "[$progress] Axis cache hit for $tmpVarFontKey")
                Log.d(LOG_TAG, "[$progress] Axis cache hit for $vKey")
            }
            return axesCachedFont
            return it
        }

        // This is the first time to make the font for the axes. Build and store it to the cache.
        // Font.Builder#build won't throw IOException since creating fonts from existing fonts will
        // not do any IO work.
        val newFont = Font.Builder(start).setFontVariationSettings(newAxes.toTypedArray()).build()
        interpCache.put(InterpKey(start, end, progress), newFont)
        verFontCache.put(VarFontKey(start, newAxes), newFont)
        fontCache.put(iKey, newFont)
        fontCache.put(vKey, newFont)

        // Cache misses are likely to create memory leaks, so this is logged at error level.
        Log.e(LOG_TAG, "[$progress] Cache MISS for $tmpInterpKey / $tmpVarFontKey")
        Log.e(LOG_TAG, "[$progress] Cache MISS for $iKey / $vKey")
        return newFont
    }

    private fun lerp(
        start: Array<FontVariationAxis>,
        end: Array<FontVariationAxis>,
        filter: (tag: String, left: Float?, right: Float?) -> Float
        filter: (tag: String, left: Float?, right: Float?) -> Float,
    ): List<FontVariationAxis> {
        // Safe to modify result of Font#getAxes since it returns cloned object.
        start.sortBy { axis -> axis.tag }
+9 −13
Original line number Diff line number Diff line
@@ -35,6 +35,8 @@ private const val TYPEFACE_CACHE_MAX_ENTRIES = 5
typealias GlyphCallback = (TextAnimator.PositionedGlyph, Float) -> Unit

interface TypefaceVariantCache {
    val fontCache: FontCache
    val animationFrameCount: Int
    fun getTypefaceForVariant(fvar: String?): Typeface?

    companion object {
@@ -57,8 +59,10 @@ interface TypefaceVariantCache {

class TypefaceVariantCacheImpl(
    var baseTypeface: Typeface,
    override val animationFrameCount: Int,
) : TypefaceVariantCache {
    private val cache = LruCache<String, Typeface>(TYPEFACE_CACHE_MAX_ENTRIES)
    override val fontCache = FontCacheImpl(animationFrameCount)
    override fun getTypefaceForVariant(fvar: String?): Typeface? {
        if (fvar == null) {
            return baseTypeface
@@ -100,25 +104,17 @@ class TypefaceVariantCacheImpl(
 */
class TextAnimator(
    layout: Layout,
    numberOfAnimationSteps: Int? = null, // Only do this number of discrete animation steps.
    private val invalidateCallback: () -> Unit,
    private val typefaceCache: TypefaceVariantCache,
    private val invalidateCallback: () -> Unit = {},
) {
    var typefaceCache: TypefaceVariantCache = TypefaceVariantCacheImpl(layout.paint.typeface)
        get() = field
        set(value) {
            field = value
            textInterpolator.typefaceCache = value
        }

    // Following two members are for mutable for testing purposes.
    public var textInterpolator: TextInterpolator =
        TextInterpolator(layout, typefaceCache, numberOfAnimationSteps)
    public var animator: ValueAnimator =
    public var textInterpolator = TextInterpolator(layout, typefaceCache)
    public var animator =
        ValueAnimator.ofFloat(1f).apply {
            duration = DEFAULT_ANIMATION_DURATION
            addUpdateListener {
                textInterpolator.progress =
                    calculateProgress(it.animatedValue as Float, numberOfAnimationSteps)
                    calculateProgress(it.animatedValue as Float, typefaceCache.animationFrameCount)
                invalidateCallback()
            }
            addListener(
+12 −12
Original line number Diff line number Diff line
@@ -28,11 +28,7 @@ import com.android.internal.graphics.ColorUtils
import java.lang.Math.max

/** Provide text style linear interpolation for plain text. */
class TextInterpolator(
    layout: Layout,
    var typefaceCache: TypefaceVariantCache,
    numberOfAnimationSteps: Int? = null,
) {
class TextInterpolator(layout: Layout, var typefaceCache: TypefaceVariantCache) {
    /**
     * Returns base paint used for interpolation.
     *
@@ -66,7 +62,7 @@ class TextInterpolator(
        val start: Int, // inclusive
        val end: Int, // exclusive
        var baseFont: Font,
        var targetFont: Font
        var targetFont: Font,
    ) {
        val length: Int
            get() = end - start
@@ -79,14 +75,14 @@ class TextInterpolator(
        val baseY: FloatArray, // same length as glyphIds
        val targetX: FloatArray, // same length as glyphIds
        val targetY: FloatArray, // same length as glyphIds
        val fontRuns: List<FontRun>
        val fontRuns: List<FontRun>,
    )

    /** A class represents text layout of a single line. */
    private class Line(val runs: List<Run>)

    private var lines = listOf<Line>()
    private val fontInterpolator = FontInterpolator(numberOfAnimationSteps)
    private val fontInterpolator = FontInterpolator(typefaceCache.fontCache)

    // Recycling object for glyph drawing and tweaking.
    private val tmpPaint = TextPaint()
@@ -343,12 +339,16 @@ class TextInterpolator(
    private class MutablePositionedGlyph : TextAnimator.PositionedGlyph() {
        override var runStart: Int = 0
            public set

        override var runLength: Int = 0
            public set

        override var glyphIndex: Int = 0
            public set

        override lateinit var font: Font
            public set

        override var glyphId: Int = 0
            public set
    }
@@ -401,7 +401,7 @@ class TextInterpolator(
                    0,
                    i - prevStart,
                    font,
                    tmpPaintForGlyph
                    tmpPaintForGlyph,
                )
                prevStart = i
                arrayIndex = 0
@@ -418,13 +418,13 @@ class TextInterpolator(
            0,
            run.end - prevStart,
            font,
            tmpPaintForGlyph
            tmpPaintForGlyph,
        )
    }

    private fun updatePositionsAndFonts(
        layoutResult: List<List<PositionedGlyphs>>,
        updateBase: Boolean
        updateBase: Boolean,
    ) {
        // Update target positions with newly calculated text layout.
        check(layoutResult.size == lines.size) { "The new layout result has different line count." }
@@ -507,7 +507,7 @@ class TextInterpolator(
                lineStart,
                count,
                layout.textDirectionHeuristic,
                paint
                paint,
            ) { _, _, glyphs, _ ->
                runs.add(glyphs)
            }
+3 −1
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import com.android.app.animation.Interpolators
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.animation.GlyphCallback
import com.android.systemui.animation.TextAnimator
import com.android.systemui.animation.TypefaceVariantCacheImpl
import com.android.systemui.customization.R
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogcatOnlyMessageBuffer
@@ -98,7 +99,8 @@ constructor(

    @VisibleForTesting
    var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
        TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb)
        val cache = TypefaceVariantCacheImpl(layout.paint.typeface, NUM_CLOCK_FONT_ANIMATION_STEPS)
        TextAnimator(layout, cache, invalidateCb)
    }

    // Used by screenshot tests to provide stability
+0 −21
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.shared.clocks

object ClockAnimation {
    const val NUM_CLOCK_FONT_ANIMATION_STEPS = 30
}
Loading