Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ jobs:
generate_badges: pr


impeller_model:
name: Test impeller_model
impeeler:
name: Test impeeler
timeout-minutes: 10
runs-on: ubuntu-latest
permissions:
Expand All @@ -66,37 +66,37 @@ jobs:
uses: subosito/flutter-action@v2
with:
# The model depends on private engine semantics — pin to the
# verified version (see packages/impeller_model/FEASIBILITY.md).
# verified version (see packages/impeeler/FEASIBILITY.md).
flutter-version: '3.47.1'
channel: 'stable'
cache: true

- name: 🧪 Test
run: flutter test
working-directory: packages/impeller_model
working-directory: packages/impeeler

# SwiftShader Vulkan on CI — the flakiest leg; if it ever
# flakes, suspect this step first.
- name: 🧪 Test under Impeller
run: flutter test --enable-impeller
working-directory: packages/impeller_model
working-directory: packages/impeeler

- name: 🔍 Analyze
run: flutter analyze
working-directory: packages/impeller_model
working-directory: packages/impeeler

- name: 📜 Engine quotes match the pinned SDK
run: dart run tool/engine_refs.dart --flutter-root "$FLUTTER_ROOT"
working-directory: packages/impeller_model
working-directory: packages/impeeler

- name: 🧪 Example
run: flutter pub get && flutter analyze && flutter test
working-directory: packages/impeller_model/example
working-directory: packages/impeeler/example

# Checks every prediction against the committed device traces.
- name: 🧪 Predictions match the traces
run: flutter pub get && flutter analyze && flutter test
working-directory: packages/impeller_model/validation
working-directory: packages/impeeler/validation

check_generation:
name: Check Code Generation
Expand Down
6 changes: 3 additions & 3 deletions apps/build_to_burn/lib/slides/closing_slides.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ class SkillsSlide extends FlutterDeckSlideWidget {
),
);

/// The impeller_model skills in this repo.
/// The impeeler skills in this repo.
static const _skillsUrl =
'https://github.com/whynotmake-it/talks/tree/main/packages/impeller_model/skills';
'https://github.com/whynotmake-it/talks/tree/main/packages/impeeler/skills';

static const _skills = [
('Why is this screen expensive?', 'GPU cost'),
Expand Down Expand Up @@ -155,7 +155,7 @@ class SkillsSlide extends FlutterDeckSlideWidget {
),
const Spacer(),
Text(
'Works on any app: add impeller_model, write a '
'Works on any app: add impeeler, write a '
'widget test, ask your agent.',
style: p.body,
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: impeller_model-engine-upgrade
description: Move impeller_model to a new Flutter release and re-verify it against the engine source and real GPU traces.
name: impeeler-engine-upgrade
description: Move impeeler to a new Flutter release and re-verify it against the engine source and real GPU traces.
disable-model-invocation: true
---

# Upgrade impeller_model to a new Flutter release
# Upgrade impeeler to a new Flutter release

Run from `packages/impeller_model`. `OLD` is `pinnedFlutterVersion` in
Run from `packages/impeeler`. `OLD` is `pinnedFlutterVersion` in
`lib/src/engine/revision.dart`, `NEW` the target tag (e.g. `3.48.0`). The
package's conventions are in `MAINTAINING.md`.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Reports written by estimateGpu and snaptest
.impeller_model/
.impeeler/
.snaptest/
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Feasibility: estimating Impeller GPU work from a widget test

**Status: usable for the talk and for budgets in tests.** From a plain
`flutter test`, `impeller_model` reconstructs the render passes Impeller
`flutter test`, `impeeler` reconstructs the render passes Impeller
encodes for a frame, per device class, and the frames a screen requests while
idle. The pass structure was checked against real GPU traces of 14 scenes on
macOS Metal and on a Pixel 10 (Vulkan without framebuffer fetch): 14/14 match
Expand All @@ -15,7 +15,7 @@ milliseconds or milliwatts".

## How it works

1. **Capture.** `ImpellerModelBinding` (an
1. **Capture.** `ImpeelerBinding` (an
`AutomatedTestWidgetsFlutterBinding`) overrides `createPictureRecorder` and
`createCanvas`, wraps every picture canvas in a recording proxy and keeps
the ops per `ui.Picture` in an `Expando`. Walking `RenderView.debugLayer`
Expand Down Expand Up @@ -127,7 +127,7 @@ MSAA storage.
fetch (`pixel9`, `galaxyS25`): ported, not traced.
- **Morphology and shader image filters, unusual mask blurs**: pass shapes
are approximations and are flagged `approximate` in reports.
- **GPU time and energy**: not modeled. The `impeller_model-gpu-profiling`
- **GPU time and energy**: not modeled. The `impeeler-gpu-profiling`
skill measures them.

## Memory
Expand Down Expand Up @@ -174,7 +174,7 @@ For the slides: a 1179×2556 iPhone texture is ≈ 12 MB at 4 B/px but

## Frame demand

`ImpellerModelBinding` overrides `scheduleFrame` and `scheduleFrameCallback`
`ImpeelerBinding` overrides `scheduleFrame` and `scheduleFrameCallback`
and records a stack for every request; callbacks remember who scheduled
them, so a ticker's frame is attributed to the `State` that started it.
`measureFrameDemand` pumps fake time in vsync steps (after a settle period)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Maintaining impeller_model
# Maintaining impeeler

The model mirrors engine and framework internals that are not stable API, so
it is pinned to one Flutter release (`lib/src/engine/revision.dart`) and
Expand All @@ -9,7 +9,7 @@ built to make an SDK upgrade a mechanical review.
| Directory | Contents | Coupled to |
| --- | --- | --- |
| `lib/src/engine/` | Ports of Impeller and DisplayList decisions: `canvas.dart` (`Canvas` save/restore/flip), `display_list.dart` (layer tree to ops, `dl_builder` bookkeeping), `dl_dispatcher.dart`, `gaussian_blur.dart`, `color_filter.dart`, `capabilities.dart` (formats, framebuffer fetch per backend), `labels.dart` (debug labels). | Engine source |
| `lib/src/capture/` | `ImpellerModelBinding`: canvas and scene recording, layer walk, frame-request attribution, ticker discovery, widget creation locations. | Framework internals (`createCanvas`, `createSceneBuilder`, `debugLayer`, layer fields and `engineLayer`, `Ticker` diagnostics) |
| `lib/src/capture/` | `ImpeelerBinding`: canvas and scene recording, layer walk, frame-request attribution, ticker discovery, widget creation locations. | Framework internals (`createCanvas`, `createSceneBuilder`, `debugLayer`, layer fields and `engineLayer`, `Ticker` diagnostics) |
| `lib/src/model/` | Memory traffic arithmetic over the pass list. | Engine types only (`ModelPass`), no engine logic |
| `lib/src/api/` | Public API: `estimateGpu`, `measureFrameDemand`, device presets, JSON/HTML report. | `flutter_test` pumping (`frame_demand.dart`), per-platform widget defaults (`on_device.dart`), both quoted |
| `tool/engine_refs.dart` | Quote checker and upgrade diff. | |
Expand Down Expand Up @@ -44,7 +44,7 @@ report all quotes found.
## Upgrading Flutter

Follow the steps in
[`.agents/skills/impeller_model-engine-upgrade/SKILL.md`](.agents/skills/impeller_model-engine-upgrade/SKILL.md)
[`.agents/skills/impeeler-engine-upgrade/SKILL.md`](.agents/skills/impeeler-engine-upgrade/SKILL.md)
(written so an agent can run it; it reads the same for a person).

## Validation
Expand Down
30 changes: 16 additions & 14 deletions packages/impeller_model/README.md → packages/impeeler/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# impeller_model
# impeeler

Estimates the Impeller GPU work of a Flutter screen from a widget test:
Peels a frame apart to estimate the Impeller GPU work of a Flutter screen,
from a widget test:

- **Render passes per frame** and which widget caused each one
(`BackdropFilter (lib/home.dart:42)`), named like Impeller labels them in a
Expand All @@ -11,8 +12,9 @@ Estimates the Impeller GPU work of a Flutter screen from a widget test:
(tickers, a blinking caret, timers), who requests them, and how many of
them draw nothing new.

It is a model of one Flutter release (3.47.1), checked against real GPU
traces on macOS Metal and a Pixel 10 (14/14 scenes each, see
It is a thin peel of the Impeller engine — a model of one Flutter release
(3.47.1) — checked against real GPU traces on macOS Metal and a Pixel 10
(14/14 scenes each, see
[`validation/`](validation/README.md)). Treat it as an estimate that tells
you *whether a change adds or removes passes*; measure GPU time and energy on
a device.
Expand All @@ -21,19 +23,19 @@ a device.

```yaml
dev_dependencies:
impeller_model:
path: packages/impeller_model # not published
impeeler:
path: packages/impeeler # not published
```

Install the binding in `test/flutter_test_config.dart`:

```dart
import 'dart:async';

import 'package:impeller_model/impeller_model.dart';
import 'package:impeeler/impeeler.dart';

Future<void> testExecutable(FutureOr<void> Function() testMain) async {
ImpellerModelBinding.ensureInitialized();
ImpeelerBinding.ensureInitialized();
await testMain();
}
```
Expand All @@ -55,7 +57,7 @@ testWidgets('name dialog', (tester) async {

`estimateGpu` resizes the test view to each device, captures one frame per
device, watches frame demand for a second, and writes
`test/.impeller_model/<test name>.{json,html,png}`. The HTML file shows:
`test/.impeeler/<test name>.{json,html,png}`. The HTML file shows:

- a screenshot of the screen,
- render passes and memory traffic per device, with a device picker,
Expand All @@ -73,14 +75,14 @@ engine receives, so they count like the framework's layers.

See [`example/`](example/) for the talk's hook screen and a `fixed_ticker`
comparison; [`doc/hook_screen_with_the_name_dialog.html`](doc/hook_screen_with_the_name_dialog.html)
is the report it writes. Add `.impeller_model/` to `.gitignore`.
is the report it writes. Add `.impeeler/` to `.gitignore`.

### Lower-level API

| Call | Gives |
| --- | --- |
| `estimateGpu(tester, devices: [...])` | `GpuReport`: one `FrameEstimate` per device, `FrameDemand`, written files. |
| `estimateFrame(capture, device)` | One `FrameEstimate` from a `FrameCapture` of the last drawn frame (`ImpellerModelBinding.instance.captureFrame()`). |
| `estimateFrame(capture, device)` | One `FrameEstimate` from a `FrameCapture` of the last drawn frame (`ImpeelerBinding.instance.captureFrame()`). |
| `measureFrameDemand(tester)` | `FrameDemand`: verdict, frames, sources, tickers. |
| `findTickers()` | Every ticker in the tree, its owner widget and creation location. |
| `GpuDevice.iPhone16`, `.pixel10`, ..., `GpuDevice.custom(screen, gpu: ...)` | Device presets: a `device_frame` screen plus a `CapabilityProfile`. |
Expand Down Expand Up @@ -120,9 +122,9 @@ skill tooling, or copy them into `.agents/skills/`):

| Skill | Use it to |
| --- | --- |
| [`impeller_model-gpu-cost`](skills/impeller_model-gpu-cost/SKILL.md) | Estimate a screen's passes in a widget test, read the report, and reduce costly widgets. |
| [`impeller_model-frame-demand`](skills/impeller_model-frame-demand/SKILL.md) | Find what keeps an idle screen rendering; slow decorative animations with `fixed_ticker`. |
| [`impeller_model-gpu-profiling`](skills/impeller_model-gpu-profiling/SKILL.md) | Measure on a device: Xcode/Metal captures, Metal System Trace, energy; AGI, RenderDoc, Perfetto on Android. |
| [`impeeler-gpu-cost`](skills/impeeler-gpu-cost/SKILL.md) | Estimate a screen's passes in a widget test, read the report, and reduce costly widgets. |
| [`impeeler-frame-demand`](skills/impeeler-frame-demand/SKILL.md) | Find what keeps an idle screen rendering; slow decorative animations with `fixed_ticker`. |
| [`impeeler-gpu-profiling`](skills/impeeler-gpu-profiling/SKILL.md) | Measure on a device: Xcode/Metal captures, Metal System Trace, energy; AGI, RenderDoc, Perfetto on Android. |

## Maintaining

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ <h2>Frames while idle</h2>
// Header
document.title = report.name + ' · GPU estimate';
byId('name').textContent = report.name;
byId('meta').textContent = 'Estimated by impeller_model for Flutter ' + report.flutter + '. Confirm GPU time on a device.';
byId('meta').textContent = 'Estimated by impeeler for Flutter ' + report.flutter + '. Confirm GPU time on a device.';

// Warnings: only what makes a number untrustworthy. One line per warning,
// naming the devices only when it does not hold for all of them.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: impeller_model_example
name: impeeler_example
description: "The build_to_burn hook screen and a widget test that estimates its GPU cost."
publish_to: 'none'
version: 0.1.0
Expand All @@ -15,7 +15,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
impeller_model:
impeeler:
path: ..
lintervention: ^0.3.1

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'dart:async';

import 'package:impeller_model/impeller_model.dart';
import 'package:impeeler/impeeler.dart';

/// Installs the binding that records every picture's draw calls. It must be
/// in place before the first `testWidgets`, so it lives here rather than in
/// `main()`.
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
ImpellerModelBinding.ensureInitialized();
ImpeelerBinding.ensureInitialized();
await testMain();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:impeller_model/impeller_model.dart';
import 'package:impeller_model_example/main.dart';
import 'package:impeeler/impeeler.dart';
import 'package:impeeler_example/main.dart';

void main() {
testWidgets('hook screen with the name dialog', (tester) async {
await tester.pumpWidget(const HookApp());

// Writes test/.impeller_model/hook_screen_with_the_name_dialog.{json,html}.
// Writes test/.impeeler/hook_screen_with_the_name_dialog.{json,html}.
final report = await estimateGpu(tester, screenshot: true);

final iPhone = report.frames.first;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:impeller_model/impeller_model.dart';
import 'package:impeller_model_example/live_dot.dart';
import 'package:impeeler/impeeler.dart';
import 'package:impeeler_example/live_dot.dart';

Widget _app(double? fps) => Directionality(
textDirection: TextDirection.ltr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export 'src/api/frame_estimate.dart'
show CostCenter, FrameEstimate, estimateFrame;
export 'src/api/gpu_device.dart' show GpuDevice;
export 'src/capture/binding.dart'
show DrawnFrame, FrameCapture, ImpellerModelBinding;
show DrawnFrame, FrameCapture, ImpeelerBinding;
export 'src/capture/creation_location.dart' show WidgetOrigin;
export 'src/capture/frame_requests.dart' show FrameRequestOrigin;
export 'src/capture/layer_walk.dart' show CapturedLayer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class GpuReport {
/// device is written next to them and shown in the report. With
/// [writeFiles] false nothing is written, not even the screenshot.
///
/// Requires [ImpellerModelBinding]. Call it after the screen is in the
/// Requires [ImpeelerBinding]. Call it after the screen is in the
/// state you want to measure:
///
/// ```dart
Expand All @@ -90,10 +90,10 @@ Future<GpuReport> estimateGpu(
Duration frameDemandSettle = const Duration(milliseconds: 500),
int refreshRate = 60,
bool screenshot = false,
String outputDir = '.impeller_model',
String outputDir = '.impeeler',
bool writeFiles = true,
}) async {
final binding = ImpellerModelBinding.instance;
final binding = ImpeelerBinding.instance;
final targets = devices ?? GpuDevice.phones;
if (targets.isEmpty) {
throw ArgumentError.value(devices, 'devices', 'must not be empty');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ Future<FrameDemand> _measure(
int refreshRate,
GpuDevice? device,
) async {
final binding = ImpellerModelBinding.instance;
final binding = ImpeelerBinding.instance;
final step = Duration(
microseconds: Duration.microsecondsPerSecond ~/ refreshRate,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ function splitLabel(label) {
// Header
document.title = report.name + ' · GPU estimate';
byId('name').textContent = report.name;
byId('meta').textContent = 'Estimated by impeller_model for Flutter ' + report.flutter + '. Confirm GPU time on a device.';
byId('meta').textContent = 'Estimated by impeeler for Flutter ' + report.flutter + '. Confirm GPU time on a device.';

// Warnings: only what makes a number untrustworthy. One line per warning,
// naming the devices only when it does not hold for all of them.
Expand Down
Loading
Loading