Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[framework,web] add FlutterTimeline and semantics benchmarks that use it #128366

Merged
merged 7 commits into from Jun 21, 2023

Conversation

yjbanov
Copy link
Contributor

@yjbanov yjbanov commented Jun 7, 2023

FlutterTimeline

Add a new class FlutterTimeline that's a drop-in replacement for Timeline from dart:developer. In addition to forwarding invocations of startSync, finishSync, timeSync, and instantSync to dart:developer, provides the following extra methods that make is easy to collect timings for code blocks on a frame-by-frame basis:

  • debugCollect() - aggregates timings since the last reset, or since the app launched.
  • debugReset() - forgets all data collected since the previous reset, or since the app launched. This allows clearing data from previous frames so timings can be attributed to the current frame.
  • now - this was enhanced so that it works on the web by calling window.performance.now (in Timeline this is a noop in Dart web compilers).
  • collectionEnabled - a field that controls whether FlutterTimeline stores timings in memory. By default this is disabled to avoid unexpected overhead (although the class is designed for minimal and predictable overhead). Specific benchmarks can enable collection to report to Skia Perf.

Semantics benchmarks

Add BenchMaterial3Semantics that benchmarks the cost of semantics when constructing a screen full of Material 3 widgets from nothing. It is expected that semantics will have non-trivial cost in this case, but we should strive to keep it much lower than the rendering cost. This is the case already. This benchmark shows that the cost of semantics is <10%.

Add BenchMaterial3ScrollSemantics that benchmarks the cost of scrolling a previously constructed screen full of Material 3 widgets. The expectation should be that semantics will have trivial cost, since we're just shifting some widgets around. As of today, the numbers are not great, with semantics taking >50% of frame time, which is what prompted this PR in the first place. As we optimize this, we want to see this number improve.

@flutter-dashboard flutter-dashboard bot added a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. team Infra upgrades, team productivity, code health, technical debt. See also team: labels. labels Jun 7, 2023
@github-actions github-actions bot added the a: text input Entering text in a text field or keyboard related problems label Jun 7, 2023
@yjbanov yjbanov changed the title [web] add FlutterTimeline and semantics benchmarks that use it [framework,web] add FlutterTimeline and semantics benchmarks that use it Jun 7, 2023
Copy link
Contributor

@mdebbar mdebbar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some nitpicks.

Future<void> setUpAll() async {
FlutterTimeline.collectionEnabled = true;
super.setUpAll();
SemanticsBinding.instance.ensureSemantics();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this have the potential of turning semantics on for other benchmarks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because we reload the page between the benchmarks.

/// add/remove widgets, but its enough to trigger the framework to recompute
/// some of the semantics.
///
/// The expect output numbers of this benchmarks should be very small as
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// The expect output numbers of this benchmarks should be very small as
/// The expected output numbers of this benchmarks should be very small as

/// The expect output numbers of this benchmarks should be very small as
/// scrolling a list view should be a matter of shifting some widgets and
/// updating the projected clip imposed by the viewport. As of June 2023, the
/// numbers are not great. Semantics consumes >50% of frame time.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the 50% ratio is more important than the absolute numbers. Should we report the ratio instead? i.e. run the same widget with semantics and without semantics, and report the ratio between the two.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually attempted it just to rediscover that our benchmark system is designed to only accept timeline data in microseconds. Maybe we should generalize it to accept other units.

bool forward = true;

// A one-off timer is necessary to allow the framework to measure the
// available scroll extends before the scroll controller can be exercised
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"extent"?

// A one-off timer is necessary to allow the framework to measure the
// available scroll extends before the scroll controller can be exercised
// to change the scroll position.
Timer.run(() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check for mounted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's already mounted by the time initState is called.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean inside Timer.run(() { ...here... });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I don't think we need it. mounted being false inside the timer implies the benchmark ended before pumping any interesting frames we care to measure, which can't happen because WidgetRecorder waits for a minimum number of frames before stopping the benchmark.

/// This getter is not optimized to be fast. It is assumed that when metrics
/// are read back, they do not affect the timings of the work being
/// benchmarked.
List<double> get elements {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 277 to 281
final List<double> result = <double>[];
for (int i = 0; i < _pointer; i++) {
result.add(_slice[i]);
}
_chain.forEach(result.addAll);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the order of elements matter? If yes, then _chain.forEach(result.addAll) should be moved above the for loop. If no, then let's mention that in the dartdoc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does! Good catch. Fixed, and added a test for it.

///
/// See:
/// * https://developer.mozilla.org/en-US/docs/Web/API/Performance/now
double get performanceTimestamp => 1000 * _performance.now();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason performanceTimestamp is a double instead of 1000 * _performance.now().toInt()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, but since there's no int64 or Int64List on the web, I have to store these as doubles anyway. Otherwise, I'd have to store values in List<int> (which on the web is an array of Numbers, i.e. double) and lose some performance predictability. This code relies on the fact that I can allocate small fixed-sized Float64List instances for a constant cost.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I was confused by this line:

static int get now => impl.performanceTimestamp.toInt();

But I see that it's being retained as a double elsewhere.

///
/// When disabled, resets collected data by calling [reset].
///
/// Throws a [StateError] if invoked in release mode.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part doesn't seem to be true? It should be, though!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I removed the assertion to debug something, and forgot to put it back.

/// When disabled, resets collected data by calling [reset].
///
/// Throws a [StateError] if invoked in release mode.
static set collectionEnabled(bool value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call this debugCollectionEnabled to make clear that this is non-release only functionlaity.


static bool _collectionEnabled = false;

/// Start a synchronous operation labeled [name].
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use backticks for arguments that are not also a property: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#dartdoc-specific-requirements

(here and everywhere else)


/// Returns timings collected since [collectionEnabled] was set to true, or
/// since the previous [reset], whichever was last.
static AggregatedTimings collect() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call this debugCollect and document that it can only be called in non-release mode.

/// Forgets all previously collected timing data.
///
/// This method can be used to break up the data by frames. To do that, call
/// [collect] at the end of the frame, then call [reset] so that the next
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering if collect should just always imply reset? Is there reason to collect, wait a little and then collect again later where you want the previously collected data to be included?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Done.

/// This method can be used to break up the data by frames. To do that, call
/// [collect] at the end of the frame, then call [reset] so that the next
/// frame collects independent data.
static void reset() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also be called debutReset.

/// [finishSync] before returning to the event queue.
static void startSync(String name, { Map<String, Object?>? arguments, Flow? flow }) {
Timeline.startSync(name, arguments: arguments, flow: flow);
if (_collectionEnabled) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all these should probably also be guarded by !kReleaseMode to ensure that they are compiled out in release mode to lower the overhead even further.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

aggregate[block.name] = (previousValue.$1 + block.duration, previousValue.$2 + 1);
}

aggregatedBlocks = aggregate.entries.map<AggregatedTimedBlock>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these maybe only be computed if someone accesses aggregatedBlocks and/or getAggregated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@yjbanov
Copy link
Contributor Author

yjbanov commented Jun 9, 2023

@goderbauer I started adding the debug prefix everywhere but then I thought maybe we should encourage people to use this in release mode, if it's cheap enough. I collected these numbers:

  • Pixel 6 Dart AOT: ~0.4 microseconds/sample
  • M1 macOS Dart AOT: ~0.15 microseconds/sample
  • Pixel 6 dart2js: ~2 microseconds/sample
  • M1 macOS dart2js: ~0.4 microseconds/sample

So a few 10s of invocations won't cut into the frame budget, but may provide much better understanding of an app's real performance. Most of the cost comes from dart:developer. So I don't call it in release mode.

LMK which way you prefer to go. I'm open to either.

@goderbauer
Copy link
Member

I thought maybe we should encourage people to use this in release mode

I think that's what profile mode is for. It's release mode + hocks to measure performance. We should make this available in profile mode for sure. But I don't see the appeal for release mode.

@yjbanov
Copy link
Contributor Author

yjbanov commented Jun 9, 2023

I think that's what profile mode is for.

Oh, what meant was that apps could collect performance analytics from installed apps in production. But if we're not interested in supporting that, it's fine. That's out of scope for this PR anyway. Just a drive-by thought. The only issue is that the debug prefix will stick forever if we change our minds later.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

/// Enables metric collection.
///
/// Metric collection can only be enabled in non-release modes. It is most
/// useful in the profile mode where application performance is representative
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uber-nit:

Suggested change
/// useful in the profile mode where application performance is representative
/// useful in profile mode where application performance is representative

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}

/// Emit an instant event.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add the doc line about this being a drop-in replacement for [Timeline.instantSync]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

static int get now => impl.performanceTimestamp.toInt();

/// Returns timings collected since [debugCollectionEnabled] was set to true, or
/// since the previous [debugReset], whichever was last.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add: ... or the previous debugCollect ... ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

/// Forgets all previously collected timing data.
///
/// This method can be used to break up the data by frames. To do that, call
/// [debugCollect] at the end of the frame, then call [debugReset] so that the next
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update this since debugCollect automatically does a debugReset?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@yjbanov yjbanov added the autosubmit Merge PR when tree becomes green via auto submit App label Jun 21, 2023
@auto-submit auto-submit bot merged commit 07772a3 into flutter:master Jun 21, 2023
138 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 22, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 22, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 22, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 22, 2023
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Jun 22, 2023
flutter/flutter@c40baf4...042c036

2023-06-22 ahmedelsaayid@gmail.com Remove unnecessary variable `_hasPrimaryFocus` (flutter/flutter#129066)
2023-06-22 tessertaha@gmail.com Fix Material 3 Scrollable `TabBar` (flutter/flutter#125974)
2023-06-22 utisam@gmail.com Fix: Closing bottom sheet and removing FAB cause assertion failure (flutter/flutter#128566)
2023-06-22 tessertaha@gmail.com Add `InputDecorationTheme.merge` (flutter/flutter#129011)
2023-06-22 engine-flutter-autoroll@skia.org Roll Packages from 9af50d4 to 95bc1c6 (6 revisions) (flutter/flutter#129351)
2023-06-22 42216813+eliasyishak@users.noreply.github.com Prevent crashes on range errors when selecting device (flutter/flutter#129290)
2023-06-22 zanderso@users.noreply.github.com Revert "Roll Flutter Engine from 703c9a14ac7f to 8cc6d6d5efdb (1 revision)" (flutter/flutter#129353)
2023-06-22 engine-flutter-autoroll@skia.org Roll Flutter Engine from 703c9a14ac7f to 8cc6d6d5efdb (1 revision) (flutter/flutter#129339)
2023-06-22 engine-flutter-autoroll@skia.org Roll Flutter Engine from d9530e2b87de to 703c9a14ac7f (1 revision) (flutter/flutter#129337)
2023-06-22 engine-flutter-autoroll@skia.org Roll Flutter Engine from c6251a69a09a to d9530e2b87de (1 revision) (flutter/flutter#129334)
2023-06-22 engine-flutter-autoroll@skia.org Roll Flutter Engine from 08aaa88bf67f to c6251a69a09a (10 revisions) (flutter/flutter#129331)
2023-06-22 jason-simmons@users.noreply.github.com Manual roll of packages to 9af50d4 (flutter/flutter#129328)
2023-06-21 engine-flutter-autoroll@skia.org Roll Flutter Engine from 090fae83548a to 08aaa88bf67f (3 revisions) (flutter/flutter#129306)
2023-06-21 yjbanov@google.com [framework,web] add FlutterTimeline and semantics benchmarks that use it (flutter/flutter#128366)
2023-06-21 fluttergithubbot@gmail.com Roll pub packages (flutter/flutter#128966)
2023-06-21 6655696+guidezpl@users.noreply.github.com Remove incorrect non-nullable assumption from `ShapeDecoration.lerp` (flutter/flutter#129298)
2023-06-21 jmccandless@google.com Gracefully handle negative position in getWordAtOffset (flutter/flutter#128464)
2023-06-21 christopherfujino@gmail.com [flutter_tools] add a gradle error handler for could not open cache directory (flutter/flutter#129222)
2023-06-21 engine-flutter-autoroll@skia.org Roll Flutter Engine from f973fb4636d3 to 090fae83548a (5 revisions) (flutter/flutter#129293)
2023-06-21 rmolivares@renzo-olivares.dev Selection area right click behavior should match native (flutter/flutter#128224)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC dit@google.com,rmistry@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 16, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 17, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 17, 2023
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. team Infra upgrades, team productivity, code health, technical debt. See also team: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants