[MUSIC PLAYING]
SIYAMED SINIR: Hi, everybody.
My name is Siyamed Sinir, and I work on Android Text.
And today, I would like to talk about why improving your text
measuring performance is important for
your overall app's performance.
In order to do this, I wanted to gather some numbers.
And therefore, I wrote a simple sample app that has a feed
and is very similar to the applications
that we use every day.
It has a list of items, and each item has an image and some text
into it.
When we look at the text, as you can see in the picture,
the user name and the title for each feed item
is what we call single style text.
And the content contains a text that has different colors, text
sizes, fonts, and et cetera.
One important point here that I would like to emphasize--
on our platform right now, the hyphenation
is turned on by default. However,
in this app, as you can see there,
is no hyphenation applied.
And the reason is Android works by some dictionaries
in order to be able to hyphenate the words.
And since these words are just random characters side by side,
it cannot do any hyphenation.
When we look at each feed item, we
see that it has a very simple structure--
a linear layout at top, an image with a relative layout,
and three TextViews for user name, title, and content.
And I'm pretty sure it's either the same
or very similar to one of your apps' existing design or layout
structure.
What I wanted to see was for each feed item,
what is the percentage of the time spent on text
versus the whole layout.
Therefore, what I did is I wrote a benchmark where I called
the linearLayout.measure() on one end.
And meanwhile, I recorded all the parameters
that were passed to those different TextViews,
and I simulated the same thing on those TextViews.
Because of the relative layout here,
the two TextViews at the top were getting two measure()
calls each.
In order to have consistent numbers
or repeatable measurements, I locked the CPU.
I ahead of time compiled my budget benchmark app,
and I set the debuggable false.
And these numbers that we will see soon are for a Pixel X
Large that is running Android P.
So the first question is how much time does it
require for linear layout to measure
versus how much time it requires to measure all those three
TextViews?
This number was interesting.
4.60 milliseconds for the whole layout,
and 98% of that time is spent on text.
Since the CPU is locked, maybe normally the numbers
on the real device would be lower than four.
But still, the ratio will remain the same.
The next thing I wondered was if I
were to disable hyphenation, how much improvement would I
get from my linear layout, the full layout?
And just turning the hyphenation off
drops all the times by 100%.
So it improves the measurement performance by 2x.
At this point, I want to remind you
that hyphenation was turned on, but there
was no visible hyphenation.
So turning off hyphenation did not have any effect on the UI.
Finally, I wanted to check how the numbers would improve
if I were to use a new API that we added on Android P and also
Android X--
PrecomputedText.
These numbers were crazier.
Four milliseconds become 0.2 milliseconds.
It's almost 20% to any X improvement.
I would like to describe what PrecomputedText does
and how it works.
But before that, I would like to talk about what kind of things
we went through while implementing
the PrecomputedText.
We were asking ourselves questions
like why is text measurement so expensive
and can we make it faster.
Since almost the beginning, we have been
improving the text performance.
But as you would understand from the number 98%,
it's always going to be a bottleneck.
Therefore, we try to see can we move
all these expensive measurement to a background thread
and provide an API.
In terms of why it is so expensive,
whenever the system calls a measure() on TextView,
it first does some preliminary work and tries to identify
the intrinsic width and height of the text that it contains.
Then, according to the parameters that are passed
through on measure() and also the numbers that it finds,
it creates a layout.
All these processes goes into our native code
because we have to use some open source
libraries such as Free Type and [INAUDIBLE]
in order to measure our text.
And when we look from the scope of our measure, 90%--
actually more than 90%--
of our time is spent on the native code.
And when we pass a string like this
to our native code, what it does is
it first divides it into words.
And then for each word, it applies text shaping.
It finds the form that it can render the character
and tries to bring those characters in the word
together.
Then we check how much time this takes
compared to all the time that's spent on the native code.
It is, again, more than 90%.
So a text measurement taking the 90% of the whole measurement,
native code taking the 90% of the text,
and shaping taking more than 90% of the native code
means that most of the measurement time,
at least for this app, is spent on text shaping.
The system applies the same rules for all of the words.
And for each word that is measured, it caches them.
One of the reasons to cache them is just after the measure
or layout there, will be a draw operation,
and the draw will need the same information.
Then it applies line breaking and hyphenation.
So one valid question here is why does hyphenation--
turning it off-- improve the performance twice?
It's mainly because of whenever it tries to hyphenate,
it has to apply text shaping for more words.
Let's say in here, example is divided into ex- and -ample.
It has to calculate two more--
it has to do two more shaping on two more words.
So the next question is, if we were
to move this expensive measurement to background
thread, what we would need.
The first issue we encountered was before Android P,
the native code was acquiring giant synchronization locks.
And we fixed it on Android P. We made the locks smaller.
Then we wondered, can we use the existing layout
classes for such a goal?
And there were two issues, two important issues,
at that point.
One of them-- layout classes need
a width to be provided to them so that they
can do their calculations.
And that width, you would not have that width
before measuring, so it was a chicken and egg problem.
The other one is the layout objects
are just blueprints for the text that contain kind
of a cache on the Java side.
And they do not know about the previously calculated
native word layout objects.
All they know is how many lines there,
where do they start and end, and what are their coordinates.
So we wondered, what if we created
a construct, first of all, that doesn't need the width?
Second, it should be able to create it
on a background thread safely.
And third, it has to have strong references
to those native layout objects.
This is important because in terms of layout,
if you were to create a layout before you need it,
then maybe when it's time to measure or render it,
you will lose the word layout objects.
They will be evicted from the cache,
and therefore there was no guarantee
that you would have the speed improvement.
We wanted to extend this construct from CharSequence
and Spannable because we wanted it
to be compatible with our current APIs
and also your applications.
Since it is mostly interested in how the text will look,
this construct needed some parameters
that would change the text styling,
such as the text size, color, locale, and et cetera.
One important point here is there
was a reason why previously those native objects were being
cached and evicted, and it is the amount of memory
that they use.
Right now when you use PrecomputedText,
you will be spending 20 kilobytes for 500 characters.
Even though it implements Spannable,
which is a mutable interface, every calculation
that PrecomputedText does is done at the construction time.
Therefore, you should not be calling SetSpan or RemoveSpan
functions with spans or styling information that will
change how the text will look.
Otherwise, you will get an exception
because that would invalidate all the computation,
and it would be useless.
Then we look at the parameters that PrecomputedText requires.
Since most of the text styling information
right now is encapsulated in the text pane,
it requires the text pane as a mandatory constructed argument.
The others are BreakStrategy, HyphenationFrequency,
and TextDirection are the functions
that you would already know from the static layout builder.
Since most of the time you will be designing your text styling
in your XML, either layout.xml or styles.xml,
we wanted to add a helper function
where you can create the PrecomputedText
parameters using a TextView.
However, at this point, I would like
to emphasize that TextView is not
required to create a PrecomputedText.Params.
It is a helper function, and this function
is going to make more sense when we
go through how to use PrecomputedText
with RecyclerView.
Since version 25, RecyclerView has a Prefetch feature,
and we wanted to provide ways of using PrecomputedText
with RecyclerView.Prefetch.
And normally during on your onBind View Holder,
you will call setText, and then the RecyclerView
would measure the whole layout that you just created.
And we know that the measurement part of the TextViews
are expensive.
Then you use RecyclerView with PrecomputedText,
you will just change setText text to setTextFeature.
And what Prefetch will do is before an item is shown
on the screen, it will go to the background thread,
do the text measurement, and as a result will
create the PrecomputedText.
When the computation is ready, it
will switch back to the [INAUDIBLE] thread,
and it will call setText with this PrecomputedText, which
will make the measurement part much
faster than the previous case.
Using it is pretty easy.
Instead of calling setText in your onBind View Holder,
you just change it to be setTextFeature
and some configuration for PrecomputedText.
And here, what you tell the system
is please run this task on a background thread
and precompute my text with this CharSequence, which is my data,
and this display configuration.
As you can see here, TextMetricsParamCompat()
becomes very handy to be able to create
the PrecomputedText.Params.
One point here is, though, if you are changing your text
styling according to the data you have,
you have to apply all the styling
on your TextView or Spannables before calling this function.
I will continue with how to turn off hyphenation in your app
globally so that you can only enable it when you need it.
This thing is also pretty simple,
and you already saw the effect.
It improves the measurement performance by 2x.
What you do is first you define a new style in your styles.xml
and turn off hyphenation for that style.
You extend this style from a base style
in order to handle pre-Lollipop and after-Lollipop cases.
And for version 21 and above, you extend this base TextView
style from Material TextView.
And finally, on your team, you set AndroidTextViewStyle
to be your new style.
If I were to summarize all the talk,
I would say, first go and turn off hyphenation in your app
globally so that your measurement can be faster.
And it's not only going to be the text measurement.
The whole screen measurement, depending
on how much text you have, will be almost twice faster.
And then please check the PrecomputedText API.
And if you are using RecyclerView,
please apply the PrecomputedText and RecyclerView.Prefetch
code that I showed you so that you
will get a more smooth scrolling experience for your users.
This was my talk.
Thank you very much for listening.
[APPLAUSE]
[MUSIC PLAYING]