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

Commit 4cefdc6d authored by Seigo Nonaka's avatar Seigo Nonaka
Browse files

Cache the variation instance of Typeface

Performance numbers on Pixel 8 Pro.

Non-Cached: 366,835 ns
    Cached:   2,427 ns

Bug: 355462362
Test: PaintTest
Flag: com.android.text.flags.typeface_cache_for_var_settings
Change-Id: I10f0697e4d158cdcf55e7e1196d1f37174de87bf
parent 9a72f7a6
Loading
Loading
Loading
Loading
+25 −3
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package android.text;
import android.graphics.Paint;
import android.graphics.RecordingCanvas;
import android.graphics.RenderNode;
import android.graphics.Typeface;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;

@@ -120,13 +121,34 @@ public class VariableFontPerfTest {
    public void testSetFontVariationSettings() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        final Paint paint = new Paint(PAINT);
        final Random random = new Random(0);
        while (state.keepRunning()) {
            state.pauseTiming();
            int weight = random.nextInt(1000);
            paint.setTypeface(null);
            paint.setFontVariationSettings(null);
            Typeface.clearTypefaceCachesForTestingPurpose();
            state.resumeTiming();

            paint.setFontVariationSettings("'wght' " + weight);
            paint.setFontVariationSettings("'wght' 450");
        }
        Typeface.clearTypefaceCachesForTestingPurpose();
    }

    @Test
    public void testSetFontVariationSettings_Cached() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        final Paint paint = new Paint(PAINT);
        Typeface.clearTypefaceCachesForTestingPurpose();

        while (state.keepRunning()) {
            state.pauseTiming();
            paint.setTypeface(null);
            paint.setFontVariationSettings(null);
            state.resumeTiming();

            paint.setFontVariationSettings("'wght' 450");
        }

        Typeface.clearTypefaceCachesForTestingPurpose();
    }

}
+10 −0
Original line number Diff line number Diff line
@@ -277,3 +277,13 @@ flag {
    purpose: PURPOSE_BUGFIX
  }
}

flag {
  name: "typeface_cache_for_var_settings"
  namespace: "text"
  description: "Cache Typeface instance for font variation settings."
  bug: "355462362"
  metadata {
    purpose: PURPOSE_BUGFIX
  }
}
 No newline at end of file
+74 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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 android.graphics;

import static com.google.common.truth.Truth.assertThat;

import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.test.InstrumentationTestCase;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.text.flags.Flags;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * PaintTest tests {@link Paint}.
 */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class PaintFontVariationTest extends InstrumentationTestCase {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
    @Test
    public void testDerivedFromSameTypeface() {
        final Paint p = new Paint();

        p.setTypeface(Typeface.SANS_SERIF);
        assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
        Typeface first = p.getTypeface();

        p.setTypeface(Typeface.SANS_SERIF);
        assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
        Typeface second = p.getTypeface();

        assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
    }

    @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
    @Test
    public void testDerivedFromChained() {
        final Paint p = new Paint();

        p.setTypeface(Typeface.SANS_SERIF);
        assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
        Typeface first = p.getTypeface();

        assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
        Typeface second = p.getTypeface();

        assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
    }
}
+41 −0
Original line number Diff line number Diff line
@@ -16,13 +16,22 @@

package android.graphics;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertNotEquals;

import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.test.InstrumentationTestCase;
import android.text.TextUtils;

import androidx.test.filters.SmallTest;

import com.android.text.flags.Flags;

import org.junit.Rule;

import java.util.Arrays;
import java.util.HashSet;

@@ -30,6 +39,9 @@ import java.util.HashSet;
 * PaintTest tests {@link Paint}.
 */
public class PaintTest extends InstrumentationTestCase {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf";

    static void assertEquals(String message, float[] expected, float[] actual) {
@@ -403,4 +415,33 @@ public class PaintTest extends InstrumentationTestCase {
        assertEquals(6, getClusterCount(p, rtlStr + ltrStr));
        assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr));
    }

    @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
    public void testDerivedFromSameTypeface() {
        final Paint p = new Paint();

        p.setTypeface(Typeface.SANS_SERIF);
        assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
        Typeface first = p.getTypeface();

        p.setTypeface(Typeface.SANS_SERIF);
        assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
        Typeface second = p.getTypeface();

        assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
    }

    @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
    public void testDerivedFromChained() {
        final Paint p = new Paint();

        p.setTypeface(Typeface.SANS_SERIF);
        assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
        Typeface first = p.getTypeface();

        assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
        Typeface second = p.getTypeface();

        assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
    }
}
+86 −2
Original line number Diff line number Diff line
@@ -56,6 +56,7 @@ import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.text.flags.Flags;

import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -74,6 +75,7 @@ import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -143,6 +145,23 @@ public class Typeface {
    private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16);
    private static final Object sDynamicCacheLock = new Object();

    private static final LruCache<Long, LruCache<String, Typeface>> sVariableCache =
            new LruCache<>(16);
    private static final Object sVariableCacheLock = new Object();

    /** @hide */
    @VisibleForTesting
    public static void clearTypefaceCachesForTestingPurpose() {
        synchronized (sWeightCacheLock) {
            sWeightTypefaceCache.clear();
        }
        synchronized (sDynamicCacheLock) {
            sDynamicTypefaceCache.evictAll();
        }
        synchronized (sVariableCacheLock) {
            sVariableCache.evictAll();
        }
    }

    @GuardedBy("SYSTEM_FONT_MAP_LOCK")
    static Typeface sDefaultTypeface;
@@ -195,6 +214,8 @@ public class Typeface {
    @UnsupportedAppUsage
    public final long native_instance;

    private final Typeface mDerivedFrom;

    private final String mSystemFontFamilyName;

    private final Runnable mCleaner;
@@ -273,6 +294,18 @@ public class Typeface {
        return (mStyle & ITALIC) != 0;
    }

    /**
     * Returns the Typeface used for creating this Typeface.
     *
     * Maybe null if this is not derived from other Typeface.
     * TODO(b/357707916): Make this public API.
     * @hide
     */
    @VisibleForTesting
    public final @Nullable Typeface getDerivedFrom() {
        return mDerivedFrom;
    }

    /**
     * Returns the system font family name if the typeface was created from a system font family,
     * otherwise returns null.
@@ -1021,9 +1054,51 @@ public class Typeface {
        return typeface;
    }

    /** @hide */
    private static String axesToVarKey(@NonNull List<FontVariationAxis> axes) {
        // The given list can be mutated because it is allocated in Paint#setFontVariationSettings.
        // Currently, Paint#setFontVariationSettings is the only code path reaches this method.
        axes.sort(Comparator.comparingInt(FontVariationAxis::getOpenTypeTagValue));
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < axes.size(); ++i) {
            final FontVariationAxis fva = axes.get(i);
            sb.append(fva.getTag());
            sb.append(fva.getStyleValue());
        }
        return sb.toString();
    }

    /**
     * TODO(b/357707916): Make this public API.
     * @hide
     */
    public static Typeface createFromTypefaceWithVariation(@Nullable Typeface family,
            @NonNull List<FontVariationAxis> axes) {
        if (Flags.typefaceCacheForVarSettings()) {
            final Typeface target = (family == null) ? Typeface.DEFAULT : family;
            final Typeface base = (target.mDerivedFrom == null) ? target : target.mDerivedFrom;

            final String key = axesToVarKey(axes);

            synchronized (sVariableCacheLock) {
                LruCache<String, Typeface> innerCache = sVariableCache.get(base.native_instance);
                if (innerCache == null) {
                    // Cache up to 16 var instance per root Typeface
                    innerCache = new LruCache<>(16);
                    sVariableCache.put(base.native_instance, innerCache);
                } else {
                    Typeface cached = innerCache.get(key);
                    if (cached != null) {
                        return cached;
                    }
                }
                Typeface typeface = new Typeface(
                        nativeCreateFromTypefaceWithVariation(base.native_instance, axes),
                        base.getSystemFontFamilyName(), base);
                innerCache.put(key, typeface);
                return typeface;
            }
        }

        final Typeface base = family == null ? Typeface.DEFAULT : family;
        Typeface typeface = new Typeface(
                nativeCreateFromTypefaceWithVariation(base.native_instance, axes),
@@ -1184,11 +1259,19 @@ public class Typeface {
    // don't allow clients to call this directly
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private Typeface(long ni) {
        this(ni, null);
        this(ni, null, null);
    }


    // don't allow clients to call this directly
    // This is kept for robolectric.
    private Typeface(long ni, @Nullable String systemFontFamilyName) {
        this(ni, systemFontFamilyName, null);
    }

    // don't allow clients to call this directly
    private Typeface(long ni, @Nullable String systemFontFamilyName,
            @Nullable Typeface derivedFrom) {
        if (ni == 0) {
            throw new RuntimeException("native typeface cannot be made");
        }
@@ -1198,6 +1281,7 @@ public class Typeface {
        mStyle = nativeGetStyle(ni);
        mWeight = nativeGetWeight(ni);
        mSystemFontFamilyName = systemFontFamilyName;
        mDerivedFrom = derivedFrom;
    }

    /**