Skip to content

Commit

Permalink
Update shaker jank guide
Browse files Browse the repository at this point in the history
  • Loading branch information
antfitch committed Dec 19, 2024
1 parent 9c72344 commit 13727cc
Showing 1 changed file with 54 additions and 121 deletions.
175 changes: 54 additions & 121 deletions src/content/perf/shader.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,61 +6,32 @@ description: What is shader jank and how to minimize it.

{% render docs/performance.md %}

If the animations on your mobile app appear to be janky,
but only on the first run,
this is likely due to shader compilation.
Flutter's long term solution to
shader compilation jank is [Impeller][],
which is the default renderer on iOS.
You can preview Impeller on Android by passing
`--enable-impeller` to `flutter run.
This document describes what shader compilation jank is and how to minimize it.

Shader compilation jank is the annoying stutter you sometimes see in
animations, especially the first time you run an app. This happens because the
app has to compile shaders while it's running.

If you are using [Impeller][], Flutter's default mobile renderer, you shouldn't
experience jank because all necessary shaders are precompiled. However, if
you're not using Impeller and your mobile app's animations appear to
stutter, this is likely due to shader compilation jank.

[Impeller]: /perf/impeller

While we work on making Impeller fully production ready,
you can mitigate shader compilation jank by bundling
precompiled shaders with an iOS app.
Unfortunately, this approach doesn't work well on Android
due to precompiled shaders being device or GPU-specific.
The Android hardware ecosystem is large enough that the
GPU-specific precompiled shaders bundled with an application
will work on only a small subset of devices,
and will likely make jank worse on the other devices,
or even create rendering errors.

Also, note that we aren't planning to make
improvements to the developer experience for creating
precompiled shaders described below. Instead,
we are focusing our energies on more robust
solution to this problem that Impeller offers.

## What is shader compilation jank?

A shader is a piece of code that runs on a
GPU (graphics processing unit).
When the Skia graphics backend that Flutter uses for rendering
sees a new sequence of draw commands for the first time,
it sometimes generates and compiles a
custom GPU shader for that sequence of commands.
This allows that sequence and potentially similar sequences
to render as fast as possible.

Unfortunately, Skia's shader generation and compilation
happens in sequence with the frame workload.
The compilation could cost up to a few hundred milliseconds
whereas a smooth frame needs to be drawn within 16 milliseconds
for a 60 fps (frame-per-second) display.
Therefore, a compilation could cause tens of frames
to be missed, and drop the fps from 60 to 6.
This is _compilation jank_.
After the compilation is complete,
the animation should be smooth.

On the other hand, Impeller generates and compiles all
necessary shaders when we build the Flutter Engine.
Therefore apps running on Impeller already have
all the shaders they need, and the shaders can be used
without introducing jank into animations.
## What causes shader compilation jank?

When the Skia graphics renderer is used instead of [Impeller][] and Skia sees a
new sequence of draw commands for the first time, it sometimes generates and
compiles a custom GPU shader for that sequence of commands. This allows that
sequence and potentially similar sequences to render as fast as possible.

Unfortunately, Skia's shader generation and compilation happens in sequence with
the frame workload. The compilation could cost up to a few hundred milliseconds
whereas a smooth frame needs to be drawn within 16 milliseconds for a 60 fps
(frame-per-second) display. Therefore, a compilation could cause tens of frames
to be missed, and drop the fps from 60 to 6. This is _compilation jank_.
After the compilation is complete, the animation should be smooth.

Definitive evidence for the presence of shader compilation jank
is to set `GrGLProgramBuilder::finalize` in the tracing
Expand All @@ -69,29 +40,39 @@ The following screenshot shows an example timeline tracing.

![A tracing screenshot verifying jank](/assets/images/docs/perf/render/tracing.png){:width="100%"}

## What do we mean by "first run"?
## How to reduce compilation jank

If you're not using Flutter's Impeller graphics renderer and you're creating an
iOS app, you can minimize shader compilation jank by bundling precompiled
shaders with your iOS app. To learn more, see the iOS app development
documentation.

On iOS, "first run" means that the user might see
jank when an animation first occurs every time
the user opens the app from scratch.
If you're not using Flutter's Impeller graphics renderer and you're creating an
Android app, you can minimize shader compilation jank with the `cache-sksl`
command line tool. To learn more, see [How to use SkSL warmup](#how-to-use-sksl-warmup).

## How to use SkSL warmup

Flutter provides command line tools
for app developers to collect shaders that might be needed
for end-users in the SkSL (Skia Shader Language) format.
The SkSL shaders can then be packaged into the app,
and get warmed up (pre-compiled) when an end-user first
opens the app, thereby reducing the compilation
jank in later animations.
Use the following instructions to collect
and package the SkSL shaders:
To reduce shader compilation jank when a user first opens an Android app, you
can use Flutter's `cache-sksl` command-line tool. This tool precompiles shaders
into SkSL (Skia Shading Language) format, which can then be bundled with your
Android app.

Important considerations:

* On Android, precompiled shaders are device or GPU-specific.
* The precompiled shaders for Android might only work with a subset of devices.
* Precompiled shaders could make jank worse or produce errors on some
types of Android devices while making jank better on others.
* With `cache-sksl`, you can precompile shaders for one specific class of
Android device at a time.

Use the following instructions to collect and package the SkSL shaders:

<ol>
<li>

Run the app with `--cache-sksl` turned on
to capture shaders in SkSL:
Run the app with `--cache-sksl` turned on to produce shaders in `SkSL` format:

```console
flutter run --profile --cache-sksl
Expand Down Expand Up @@ -139,63 +120,15 @@ as appropriate:
flutter build ios --bundle-sksl-path flutter_01.sksl.json
```

If it's built for a driver test like `test_driver/app.dart`,
make sure to also specify `--target=test_driver/app.dart`
(for example, `flutter build ios --bundle-sksl-path
flutter_01.sksl.json --target=test_driver/app.dart`).
If the app is built for a driver test like `test_driver/app.dart`,
make sure to specify a target. For example:

```console
flutter build ios --bundle-sksl-path flutter_01.sksl.json --target=test_driver/app.dart
```

</li>

<li> Test the newly built app.
</li>
</ol>

Alternatively, you can write some integration tests to
automate the first three steps using a single command.
For example:

```console
flutter drive --profile --cache-sksl --write-sksl-on-exit flutter_01.sksl.json -t test_driver/app.dart
```

With such [integration tests][],
you can easily and reliably get the
new SkSLs when the app code changes,
or when Flutter upgrades.
Such tests can also be used to verify the performance change
before and after the SkSL warm-up.
Even better, you can put those tests into a
CI (continuous integration) system so the
SkSLs are generated and tested automatically over the lifetime of an app.

[integration tests]: /cookbook/testing/integration/introduction

:::note
The integration_test package is now the recommended way
to write integration tests. Refer to the
[Integration testing](/testing/integration-tests/)
page for details.
:::

Take the original version of [Flutter Gallery][] as an example.
The CI system is set up to generate SkSLs for every Flutter commit,
and verifies the performance, in the [`transitions_perf_test.dart`][] test.
For more details,
check out the [`flutter_gallery_sksl_warmup__transition_perf`][] and
[`flutter_gallery_sksl_warmup__transition_perf_e2e_ios32`][] tasks.

[Flutter Gallery]: {{site.repo.flutter}}/tree/main/dev/integration_tests/flutter_gallery
[`flutter_gallery_sksl_warmup__transition_perf`]: {{site.repo.flutter}}/blob/master/dev/devicelab/bin/tasks/flutter_gallery_sksl_warmup__transition_perf.dart
[`flutter_gallery_sksl_warmup__transition_perf_e2e_ios32`]: {{site.repo.flutter}}/blob/master/dev/devicelab/bin/tasks/flutter_gallery_sksl_warmup__transition_perf_e2e_ios32.dart
[`transitions_perf_test.dart`]: {{site.repo.flutter}}/blob/master/dev/integration_tests/flutter_gallery/test_driver/transitions_perf_test.dart

The worst frame rasterization time is a useful metric from
such integration tests to indicate the severity of shader
compilation jank.
For instance,
the steps above reduce Flutter gallery's shader compilation
jank and speeds up its worst frame rasterization time on a
Moto G4 from ~90 ms to ~40 ms. On iPhone 4s,
it's reduced from ~300 ms to ~80 ms. That leads to the visual
difference as illustrated in the beginning of this article.

0 comments on commit 13727cc

Please sign in to comment.