From 840e109e0718361f70bcb23a3980e407de10859b Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 10 Dec 2021 10:09:24 -0800 Subject: [PATCH] Improve tracing (#93086) --- .ci.yaml | 12 +- dev/bots/pubspec.yaml | 2 +- dev/bots/test.dart | 90 ++++++++- .../flutter_gallery/lib/demo/video_demo.dart | 4 - .../test/flutter_test_config.dart | 18 +- .../flutter_gallery/test/smoke_test.dart | 2 +- dev/tracing_tests/README.md | 11 ++ dev/tracing_tests/android/.gitignore | 13 ++ dev/tracing_tests/android/app/build.gradle | 72 +++++++ .../android/app/src/debug/AndroidManifest.xml | 11 ++ .../android/app/src/main/AndroidManifest.xml | 38 ++++ .../com/example/tracing_tests/MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 15 ++ .../main/res/drawable/launch_background.xml | 15 ++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 21 ++ .../app/src/main/res/values/styles.xml | 21 ++ .../app/src/profile/AndroidManifest.xml | 11 ++ dev/tracing_tests/android/build.gradle | 35 ++++ dev/tracing_tests/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + dev/tracing_tests/android/settings.gradle | 15 ++ dev/tracing_tests/lib/control.dart | 16 ++ dev/tracing_tests/lib/test.dart | 70 +++++++ dev/tracing_tests/test/common.dart | 34 ++++ .../test/image_cache_tracing_test.dart | 27 +-- dev/tracing_tests/test/timeline_test.dart | 155 +++++++++++++++ .../flutter/lib/src/foundation/debug.dart | 15 ++ .../lib/src/foundation/diagnostics.dart | 50 ++++- .../lib/src/material/input_decorator.dart | 6 + .../flutter/lib/src/material/toggleable.dart | 4 + .../flutter/lib/src/rendering/binding.dart | 8 +- packages/flutter/lib/src/rendering/box.dart | 59 +++++- .../lib/src/rendering/custom_paint.dart | 10 + packages/flutter/lib/src/rendering/debug.dart | 29 ++- packages/flutter/lib/src/rendering/flex.dart | 6 +- .../flutter/lib/src/rendering/object.dart | 109 +++++++---- .../lib/src/rendering/shifted_box.dart | 6 +- packages/flutter/lib/src/rendering/view.dart | 8 +- packages/flutter/lib/src/widgets/debug.dart | 15 +- .../flutter/lib/src/widgets/framework.dart | 181 ++++++++++++++---- .../lib/src/widgets/overscroll_indicator.dart | 13 ++ .../flutter/lib/src/widgets/placeholder.dart | 11 ++ .../flutter/lib/src/widgets/scrollbar.dart | 3 + .../flutter/test/widgets/keep_alive_test.dart | 41 +++- .../test_release/diagnostics_test.dart | 2 +- 50 files changed, 1166 insertions(+), 133 deletions(-) create mode 100644 dev/tracing_tests/android/.gitignore create mode 100644 dev/tracing_tests/android/app/build.gradle create mode 100644 dev/tracing_tests/android/app/src/debug/AndroidManifest.xml create mode 100644 dev/tracing_tests/android/app/src/main/AndroidManifest.xml create mode 100644 dev/tracing_tests/android/app/src/main/kotlin/com/example/tracing_tests/MainActivity.kt create mode 100644 dev/tracing_tests/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 dev/tracing_tests/android/app/src/main/res/drawable/launch_background.xml create mode 100644 dev/tracing_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 dev/tracing_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 dev/tracing_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 dev/tracing_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 dev/tracing_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 dev/tracing_tests/android/app/src/main/res/values-night/styles.xml create mode 100644 dev/tracing_tests/android/app/src/main/res/values/styles.xml create mode 100644 dev/tracing_tests/android/app/src/profile/AndroidManifest.xml create mode 100644 dev/tracing_tests/android/build.gradle create mode 100644 dev/tracing_tests/android/gradle.properties create mode 100644 dev/tracing_tests/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 dev/tracing_tests/android/settings.gradle create mode 100644 dev/tracing_tests/lib/control.dart create mode 100644 dev/tracing_tests/lib/test.dart create mode 100644 dev/tracing_tests/test/common.dart create mode 100644 dev/tracing_tests/test/timeline_test.dart diff --git a/.ci.yaml b/.ci.yaml index 0db0450027..716ce299fd 100755 --- a/.ci.yaml +++ b/.ci.yaml @@ -398,7 +398,9 @@ targets: {"dependency": "goldctl"}, {"dependency": "clang"}, {"dependency": "cmake"}, - {"dependency": "ninja"} + {"dependency": "ninja"}, + {"dependency": "open_jdk"}, + {"dependency": "android_sdk", "version": "version:29.0"} ] shard: framework_tests subshard: misc @@ -2108,7 +2110,9 @@ targets: [ {"dependency": "goldctl"}, {"dependency": "xcode"}, - {"dependency": "gems"} + {"dependency": "gems"}, + {"dependency": "open_jdk"}, + {"dependency": "android_sdk", "version": "version:29.0"} ] shard: framework_tests subshard: misc @@ -3642,7 +3646,9 @@ targets: dependencies: >- [ {"dependency": "goldctl"}, - {"dependency": "vs_build", "version": "version:vs2019"} + {"dependency": "vs_build", "version": "version:vs2019"}, + {"dependency": "open_jdk"}, + {"dependency": "android_sdk", "version": "version:29.0"} ] shard: framework_tests subshard: misc diff --git a/dev/bots/pubspec.yaml b/dev/bots/pubspec.yaml index 74e10a1d21..ca162dca53 100644 --- a/dev/bots/pubspec.yaml +++ b/dev/bots/pubspec.yaml @@ -15,11 +15,11 @@ dependencies: platform: 3.1.0 process: 4.2.4 test: 1.19.5 + archive: 3.1.6 _discoveryapis_commons: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" _fe_analyzer_shared: 31.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" analyzer: 2.8.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - archive: 3.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" async: 2.8.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" boolean_selector: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" charcode: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 8284a17b63..e066dcc64e 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -6,7 +6,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math' as math; +import 'dart:typed_data'; +import 'package:archive/archive.dart'; import 'package:file/file.dart' as fs; import 'package:file/local.dart'; import 'package:path/path.dart' as path; @@ -744,6 +746,89 @@ Future _runFrameworkTests() async { await _runFlutterTest(path.join(flutterRoot, 'examples', 'layers'), options: soundNullSafetyOptions); } + Future runTracingTests() async { + final String tracingDirectory = path.join(flutterRoot, 'dev', 'tracing_tests'); + + // run the tests for debug mode + await _runFlutterTest(tracingDirectory, options: ['--enable-vmservice']); + + Future> verifyTracingAppBuild({ + required String modeArgument, + required String sourceFile, + required Set allowed, + required Set disallowed, + }) async { + await runCommand( + flutter, + [ + 'build', 'appbundle', '--$modeArgument', path.join('lib', sourceFile), + ], + workingDirectory: tracingDirectory, + ); + final Archive archive = ZipDecoder().decodeBytes(File(path.join(tracingDirectory, 'build', 'app', 'outputs', 'bundle', modeArgument, 'app-$modeArgument.aab')).readAsBytesSync()); + final ArchiveFile libapp = archive.findFile('base/lib/arm64-v8a/libapp.so')!; + final Uint8List libappBytes = libapp.content as Uint8List; // bytes decompressed here + final String libappStrings = utf8.decode(libappBytes, allowMalformed: true); + await runCommand(flutter, ['clean'], workingDirectory: tracingDirectory); + final List results = []; + for (final String pattern in allowed) { + if (!libappStrings.contains(pattern)) { + results.add('When building with --$modeArgument, expected to find "$pattern" in libapp.so but could not find it.'); + } + } + for (final String pattern in disallowed) { + if (libappStrings.contains(pattern)) { + results.add('When building with --$modeArgument, expected to not find "$pattern" in libapp.so but did find it.'); + } + } + return results; + } + + final List results = []; + results.addAll(await verifyTracingAppBuild( + modeArgument: 'profile', + sourceFile: 'control.dart', // this is the control, the other two below are the actual test + allowed: { + 'TIMELINE ARGUMENTS TEST CONTROL FILE', + 'toTimelineArguments used in non-debug build', // we call toTimelineArguments directly to check the message does exist + }, + disallowed: { + 'BUILT IN DEBUG MODE', 'BUILT IN RELEASE MODE', + }, + )); + results.addAll(await verifyTracingAppBuild( + modeArgument: 'profile', + sourceFile: 'test.dart', + allowed: { + 'BUILT IN PROFILE MODE', 'RenderTest.performResize called', // controls + 'BUILD', 'LAYOUT', 'PAINT', // we output these to the timeline in profile builds + // (LAYOUT and PAINT also exist because of NEEDS-LAYOUT and NEEDS-PAINT in RenderObject.toStringShort) + }, + disallowed: { + 'BUILT IN DEBUG MODE', 'BUILT IN RELEASE MODE', + 'TestWidget.debugFillProperties called', 'RenderTest.debugFillProperties called', // debug only + 'toTimelineArguments used in non-debug build', // entire function should get dropped by tree shaker + }, + )); + results.addAll(await verifyTracingAppBuild( + modeArgument: 'release', + sourceFile: 'test.dart', + allowed: { + 'BUILT IN RELEASE MODE', 'RenderTest.performResize called', // controls + }, + disallowed: { + 'BUILT IN DEBUG MODE', 'BUILT IN PROFILE MODE', + 'BUILD', 'LAYOUT', 'PAINT', // these are only used in Timeline.startSync calls that should not appear in release builds + 'TestWidget.debugFillProperties called', 'RenderTest.debugFillProperties called', // debug only + 'toTimelineArguments used in non-debug build', // not included in release builds + }, + )); + if (results.isNotEmpty) { + print(results.join('\n')); + exit(1); + } + } + Future runFixTests() async { final List args = [ 'fix', @@ -808,10 +893,7 @@ Future _runFrameworkTests() async { await _runFlutterTest(path.join(flutterRoot, 'packages', 'flutter_test'), options: soundNullSafetyOptions); await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'), options: soundNullSafetyOptions); await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'non_nullable'), options: mixedModeNullSafetyOptions); - await _runFlutterTest( - path.join(flutterRoot, 'dev', 'tracing_tests'), - options: ['--enable-vmservice'], - ); + await runTracingTests(); await runFixTests(); await runPrivateTests(); const String httpClientWarning = diff --git a/dev/integration_tests/flutter_gallery/lib/demo/video_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/video_demo.dart index 0df6ff2183..33fe67f109 100644 --- a/dev/integration_tests/flutter_gallery/lib/demo/video_demo.dart +++ b/dev/integration_tests/flutter_gallery/lib/demo/video_demo.dart @@ -382,14 +382,12 @@ class _VideoDemoState extends State with SingleTickerProviderStateMix super.initState(); Future initController(VideoPlayerController controller, String name) async { - print('> VideoDemo initController "$name" ${isDisposed ? "DISPOSED" : ""}'); controller.setLooping(true); controller.setVolume(0.0); controller.play(); await connectedCompleter.future; await controller.initialize(); if (mounted) { - print('< VideoDemo initController "$name" done ${isDisposed ? "DISPOSED" : ""}'); setState(() { }); } } @@ -403,11 +401,9 @@ class _VideoDemoState extends State with SingleTickerProviderStateMix @override void dispose() { - print('> VideoDemo dispose'); isDisposed = true; butterflyController.dispose(); beeController.dispose(); - print('< VideoDemo dispose'); super.dispose(); } diff --git a/dev/integration_tests/flutter_gallery/test/flutter_test_config.dart b/dev/integration_tests/flutter_gallery/test/flutter_test_config.dart index d552235d69..7f81bd9d4a 100644 --- a/dev/integration_tests/flutter_gallery/test/flutter_test_config.dart +++ b/dev/integration_tests/flutter_gallery/test/flutter_test_config.dart @@ -2,4 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'package:flutter_goldens/flutter_goldens.dart' show testExecutable; +import 'dart:async'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter_goldens/flutter_goldens.dart' as flutter_goldens show testExecutable; +import 'package:flutter_test/flutter_test.dart'; + +Future testExecutable(FutureOr Function() testMain) { + // Enable extra checks since this package exercises a lot of the framework. + debugCheckIntrinsicSizes = true; + + // Make tap() et al fail if the given finder specifies a widget that would not + // receive the event. + WidgetController.hitTestWarningShouldBeFatal = true; + + // Enable golden file testing using Skia Gold. + return flutter_goldens.testExecutable(testMain); +} diff --git a/dev/integration_tests/flutter_gallery/test/smoke_test.dart b/dev/integration_tests/flutter_gallery/test/smoke_test.dart index ee448eabe8..2de7e2f973 100644 --- a/dev/integration_tests/flutter_gallery/test/smoke_test.dart +++ b/dev/integration_tests/flutter_gallery/test/smoke_test.dart @@ -118,7 +118,7 @@ Future smokeOptionsPage(WidgetTester tester) async { // Switch back to system theme setting: first menu button, choose 'System Default' await tester.tap(find.byIcon(Icons.arrow_drop_down).first); await tester.pumpAndSettle(); - await tester.tap(find.text('System Default').at(1)); + await tester.tap(find.text('System Default').at(1), warnIfMissed: false); // https://github.com/flutter/flutter/issues/82908 await tester.pumpAndSettle(); // Switch text direction: first switch diff --git a/dev/tracing_tests/README.md b/dev/tracing_tests/README.md index 9801c10592..b09135a9f5 100644 --- a/dev/tracing_tests/README.md +++ b/dev/tracing_tests/README.md @@ -1,5 +1,16 @@ # Tracing tests +## "Application" + +The `lib/test.dart` and `lib/control.dart` files in this directory are +used by `dev/bots/test.dart`'s `runTracingTests` function to check +whether aspects of the tracing logic in the framework get compiled out +in profile and release builds. They're not meant to be run directly. + +The strings in these files are used in `dev/bots/test.dart`. + +## Tests + The tests in this folder must be run with `flutter test --enable-vmservice`, since they test that trace data is written to the timeline by connecting to the observatory. diff --git a/dev/tracing_tests/android/.gitignore b/dev/tracing_tests/android/.gitignore new file mode 100644 index 0000000000..6f568019d3 --- /dev/null +++ b/dev/tracing_tests/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/dev/tracing_tests/android/app/build.gradle b/dev/tracing_tests/android/app/build.gradle new file mode 100644 index 0000000000..ad537193a0 --- /dev/null +++ b/dev/tracing_tests/android/app/build.gradle @@ -0,0 +1,72 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.tracing_tests" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/dev/tracing_tests/android/app/src/debug/AndroidManifest.xml b/dev/tracing_tests/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..f628cb7d7e --- /dev/null +++ b/dev/tracing_tests/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev/tracing_tests/android/app/src/main/AndroidManifest.xml b/dev/tracing_tests/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..dab8bc6b07 --- /dev/null +++ b/dev/tracing_tests/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/dev/tracing_tests/android/app/src/main/kotlin/com/example/tracing_tests/MainActivity.kt b/dev/tracing_tests/android/app/src/main/kotlin/com/example/tracing_tests/MainActivity.kt new file mode 100644 index 0000000000..a44690912c --- /dev/null +++ b/dev/tracing_tests/android/app/src/main/kotlin/com/example/tracing_tests/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.tracing_tests + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/dev/tracing_tests/android/app/src/main/res/drawable-v21/launch_background.xml b/dev/tracing_tests/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000..70e372f4ec --- /dev/null +++ b/dev/tracing_tests/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tracing_tests/android/app/src/main/res/drawable/launch_background.xml b/dev/tracing_tests/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000..bb6811b2ee --- /dev/null +++ b/dev/tracing_tests/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/dev/tracing_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/dev/tracing_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/dev/tracing_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/dev/tracing_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/dev/tracing_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/dev/tracing_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/dev/tracing_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/dev/tracing_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/dev/tracing_tests/android/app/src/main/res/values-night/styles.xml b/dev/tracing_tests/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..60accd81dc --- /dev/null +++ b/dev/tracing_tests/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/dev/tracing_tests/android/app/src/main/res/values/styles.xml b/dev/tracing_tests/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..8c27610492 --- /dev/null +++ b/dev/tracing_tests/android/app/src/main/res/values/styles.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/dev/tracing_tests/android/app/src/profile/AndroidManifest.xml b/dev/tracing_tests/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000..f628cb7d7e --- /dev/null +++ b/dev/tracing_tests/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/dev/tracing_tests/android/build.gradle b/dev/tracing_tests/android/build.gradle new file mode 100644 index 0000000000..ccf18861d7 --- /dev/null +++ b/dev/tracing_tests/android/build.gradle @@ -0,0 +1,35 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +buildscript { + ext.kotlin_version = '1.6.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/dev/tracing_tests/android/gradle.properties b/dev/tracing_tests/android/gradle.properties new file mode 100644 index 0000000000..94adc3a3f9 --- /dev/null +++ b/dev/tracing_tests/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/dev/tracing_tests/android/gradle/wrapper/gradle-wrapper.properties b/dev/tracing_tests/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..bc6a58afdd --- /dev/null +++ b/dev/tracing_tests/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/dev/tracing_tests/android/settings.gradle b/dev/tracing_tests/android/settings.gradle new file mode 100644 index 0000000000..d3b6a4013d --- /dev/null +++ b/dev/tracing_tests/android/settings.gradle @@ -0,0 +1,15 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/dev/tracing_tests/lib/control.dart b/dev/tracing_tests/lib/control.dart new file mode 100644 index 0000000000..c0c73f6173 --- /dev/null +++ b/dev/tracing_tests/lib/control.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file is part of dev/bots/test.dart's runTracingTests test. + +import 'package:flutter/foundation.dart'; + +void main() { + // This file is intended to be compiled in profile mode. + // In that mode, the function below throws an exception. + // The dev/bots/test.dart test looks for the string from that exception. + // The string below is matched verbatim in dev/bots/test.dart as a control + // to make sure this file did get compiled. + DiagnosticsNode.message('TIMELINE ARGUMENTS TEST CONTROL FILE').toTimelineArguments(); +} diff --git a/dev/tracing_tests/lib/test.dart b/dev/tracing_tests/lib/test.dart new file mode 100644 index 0000000000..d99d77b92d --- /dev/null +++ b/dev/tracing_tests/lib/test.dart @@ -0,0 +1,70 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +class TestWidget extends LeafRenderObjectWidget { + const TestWidget({ + Key? key, + }) : super(key: key); + + @override + RenderObject createRenderObject(BuildContext context) => RenderTest(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + // This string is searched for verbatim by dev/bots/test.dart: + properties.add(MessageProperty('test', 'TestWidget.debugFillProperties called')); + } +} + +class RenderTest extends RenderBox { + @override + bool get sizedByParent => true; + + @override + void performResize() { + Timeline.instantSync('RenderTest.performResize called'); + size = constraints.biggest; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + // This string is searched for verbatim by dev/bots/test.dart: + properties.add(MessageProperty('test', 'RenderTest.debugFillProperties called')); + } +} + + +Future main() async { + // This section introduces strings that we can search for in dev/bots/test.dart + // as a sanity check: + if (kDebugMode) { + print('BUILT IN DEBUG MODE'); + } + if (kProfileMode) { + print('BUILT IN PROFILE MODE'); + } + if (kReleaseMode) { + print('BUILT IN RELEASE MODE'); + } + + // The point of this file is to make sure that toTimelineArguments is not + // called when we have debugProfileBuildsEnabled (et al) turned on. If that + // method is not called then the debugFillProperties methods above should also + // not get called and we should end up tree-shaking the entire Diagnostics + // logic out of the app. The dev/bots/test.dart test checks for this by + // looking for the strings in the methods above. + + debugProfileBuildsEnabled = true; + debugProfileLayoutsEnabled = true; + debugProfilePaintsEnabled = true; + runApp(const TestWidget()); +} diff --git a/dev/tracing_tests/test/common.dart b/dev/tracing_tests/test/common.dart new file mode 100644 index 0000000000..13b684ca4a --- /dev/null +++ b/dev/tracing_tests/test/common.dart @@ -0,0 +1,34 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:developer' as developer; +import 'dart:isolate' as isolate; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:vm_service/vm_service.dart'; +import 'package:vm_service/vm_service_io.dart'; + +export 'package:vm_service/vm_service.dart' show TimelineEvent; + +late String isolateId; + +late VmService _vmService; + +void initTimelineTests() { + setUpAll(() async { + final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); + if (info.serverUri == null) { + fail('This test _must_ be run with --enable-vmservice.'); + } + _vmService = await vmServiceConnectUri('ws://localhost:${info.serverUri!.port}${info.serverUri!.path}ws'); + await _vmService.setVMTimelineFlags(['Dart']); + isolateId = developer.Service.getIsolateID(isolate.Isolate.current)!; + }); +} + +Future> fetchTimelineEvents() async { + final Timeline timeline = await _vmService.getVMTimeline(); + await _vmService.clearVMTimeline(); + return timeline.traceEvents!; +} diff --git a/dev/tracing_tests/test/image_cache_tracing_test.dart b/dev/tracing_tests/test/image_cache_tracing_test.dart index 524be6ca85..af99af6922 100644 --- a/dev/tracing_tests/test/image_cache_tracing_test.dart +++ b/dev/tracing_tests/test/image_cache_tracing_test.dart @@ -3,31 +3,15 @@ // found in the LICENSE file. import 'dart:convert'; -import 'dart:developer' as developer; -import 'dart:isolate' as isolate; import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:vm_service/vm_service.dart'; -import 'package:vm_service/vm_service_io.dart'; + +import 'common.dart'; void main() { - late VmService vmService; - late String isolateId; - setUpAll(() async { - final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); - - if (info.serverUri == null) { - fail('This test _must_ be run with --enable-vmservice.'); - } - - vmService = await vmServiceConnectUri('ws://localhost:${info.serverUri!.port}${info.serverUri!.path}ws'); - await vmService.setVMTimelineFlags(['Dart']); - isolateId = developer.Service.getIsolateID(isolate.Isolate.current)!; - - // Initialize the image cache. - TestWidgetsFlutterBinding.ensureInitialized(); - }); + initTimelineTests(); + TestWidgetsFlutterBinding.ensureInitialized(); test('Image cache tracing', () async { final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); @@ -45,9 +29,8 @@ void main() { ); PaintingBinding.instance!.imageCache!.evict('Test2'); - final Timeline timeline = await vmService.getVMTimeline(); _expectTimelineEvents( - timeline.traceEvents!, + await fetchTimelineEvents(), >[ { 'name': 'ImageCache.putIfAbsent', diff --git a/dev/tracing_tests/test/timeline_test.dart b/dev/tracing_tests/test/timeline_test.dart new file mode 100644 index 0000000000..567734ac3f --- /dev/null +++ b/dev/tracing_tests/test/timeline_test.dart @@ -0,0 +1,155 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'common.dart'; + +final Set interestingLabels = { + 'BUILD', + 'LAYOUT', + 'UPDATING COMPOSITING BITS', + 'PAINT', + 'COMPOSITING', + 'FINALIZE TREE', + '$Placeholder', + '$CustomPaint', + '$RenderCustomPaint', +}; + +Future> fetchInterestingEvents() async { + return (await fetchTimelineEvents()).where((TimelineEvent event) { + return interestingLabels.contains(event.json!['name']) + && event.json!['ph'] == 'B'; // "Begin" mark of events, vs E which is for the "End" mark of events. + }).toList(); +} + +String eventToName(TimelineEvent event) => event.json!['name'] as String; + +class TestRoot extends StatefulWidget { + const TestRoot({ Key? key }) : super(key: key); + + static late final TestRootState state; + + @override + State createState() => TestRootState(); +} + +class TestRootState extends State { + @override + void initState() { + super.initState(); + TestRoot.state = this; + } + + Widget _widget = const Placeholder(); + + void updateWidget(Widget newWidget) { + setState(() { + _widget = newWidget; + }); + } + + void rebuild() { + setState(() { + // no change, just force a rebuild + }); + } + + @override + Widget build(BuildContext context) { + return _widget; + } +} + +Future runFrame(VoidCallback callback) { + final Future result = SchedulerBinding.instance!.endOfFrame; // schedules a frame + callback(); + return result; +} + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + initTimelineTests(); + test('Timeline', () async { + // We don't have expectations around the first frame because there's a race around + // the warm-up frame that we don't want to get involved in here. + await runFrame(() { runApp(const TestRoot()); }); + await SchedulerBinding.instance!.endOfFrame; + await fetchInterestingEvents(); + + // The next few cases build the exact same tree so should have no effect. + + debugProfileBuildsEnabled = true; + await runFrame(() { TestRoot.state.rebuild(); }); + expect( + (await fetchInterestingEvents()).map(eventToName), + ['BUILD', 'LAYOUT', 'UPDATING COMPOSITING BITS', 'PAINT', 'COMPOSITING', 'FINALIZE TREE'], + ); + debugProfileBuildsEnabled = false; + + debugProfileLayoutsEnabled = true; + await runFrame(() { TestRoot.state.rebuild(); }); + expect( + (await fetchInterestingEvents()).map(eventToName), + ['BUILD', 'LAYOUT', 'UPDATING COMPOSITING BITS', 'PAINT', 'COMPOSITING', 'FINALIZE TREE'], + ); + debugProfileLayoutsEnabled = false; + + debugProfilePaintsEnabled = true; + await runFrame(() { TestRoot.state.rebuild(); }); + expect( + (await fetchInterestingEvents()).map(eventToName), + ['BUILD', 'LAYOUT', 'UPDATING COMPOSITING BITS', 'PAINT', 'COMPOSITING', 'FINALIZE TREE'], + ); + debugProfilePaintsEnabled = false; + + + // Now we replace the widgets each time to cause a rebuild. + + List events; + Map args; + + debugProfileBuildsEnabled = true; + await runFrame(() { TestRoot.state.updateWidget(Placeholder(key: UniqueKey(), color: const Color(0xFFFFFFFF))); }); + events = await fetchInterestingEvents(); + expect( + events.map(eventToName), + ['BUILD', 'Placeholder', 'CustomPaint', 'LAYOUT', 'UPDATING COMPOSITING BITS', 'PAINT', 'COMPOSITING', 'FINALIZE TREE'], + ); + args = (events.where((TimelineEvent event) => event.json!['name'] == '$Placeholder').single.json!['args'] as Map).cast(); + expect(args['color'], 'Color(0xffffffff)'); + debugProfileBuildsEnabled = false; + + debugProfileLayoutsEnabled = true; + await runFrame(() { TestRoot.state.updateWidget(Placeholder(key: UniqueKey())); }); + events = await fetchInterestingEvents(); + expect( + events.map(eventToName), + ['BUILD', 'LAYOUT', 'RenderCustomPaint', 'UPDATING COMPOSITING BITS', 'PAINT', 'COMPOSITING', 'FINALIZE TREE'], + ); + args = (events.where((TimelineEvent event) => event.json!['name'] == '$RenderCustomPaint').single.json!['args'] as Map).cast(); + expect(args['creator'], startsWith('CustomPaint')); + expect(args['creator'], contains('Placeholder')); + expect(args['foregroundPainter'], startsWith('_PlaceholderPainter#')); + debugProfileLayoutsEnabled = false; + + debugProfilePaintsEnabled = true; + await runFrame(() { TestRoot.state.updateWidget(Placeholder(key: UniqueKey())); }); + events = await fetchInterestingEvents(); + expect( + events.map(eventToName), + ['BUILD', 'LAYOUT', 'UPDATING COMPOSITING BITS', 'PAINT', 'RenderCustomPaint', 'COMPOSITING', 'FINALIZE TREE'], + ); + args = (events.where((TimelineEvent event) => event.json!['name'] == '$RenderCustomPaint').single.json!['args'] as Map).cast(); + expect(args['creator'], startsWith('CustomPaint')); + expect(args['creator'], contains('Placeholder')); + expect(args['foregroundPainter'], startsWith('_PlaceholderPainter#')); + debugProfilePaintsEnabled = false; + + }, skip: isBrowser); // [intended] uses dart:isolate and io. +} diff --git a/packages/flutter/lib/src/foundation/debug.dart b/packages/flutter/lib/src/foundation/debug.dart index 66251f29c4..55969f16e3 100644 --- a/packages/flutter/lib/src/foundation/debug.dart +++ b/packages/flutter/lib/src/foundation/debug.dart @@ -35,6 +35,18 @@ bool debugAssertAllFoundationVarsUnset(String reason, { DebugPrintCallback debug /// Boolean value indicating whether [debugInstrumentAction] will instrument /// actions in debug builds. +/// +/// The framework does not use [debugInstrumentAction] internally, so this +/// does not enable any additional instrumentation for the framework itself. +/// +/// See also: +/// +/// * [debugProfileBuildsEnabled], which enables additional tracing of builds +/// in [Widget]s. +/// * [debugProfileLayoutsEnabled], which enables additional tracing of layout +/// events in [RenderObject]s. +/// * [debugProfilePaintsEnabled], which enables additional tracing of paint +/// events in [RenderObject]s. bool debugInstrumentationEnabled = false; /// Runs the specified [action], timing how long the action takes in debug @@ -76,6 +88,9 @@ Future debugInstrumentAction(String description, Future Function() acti /// /// Generally these indicate landmark events such as the build phase or layout. /// +/// [DiagnosticsNode.toTimelineArguments] includes these properties in its +/// result. +/// /// See also: /// /// * [dart:developer.Timeline.startSync], which typically takes this value as diff --git a/packages/flutter/lib/src/foundation/diagnostics.dart b/packages/flutter/lib/src/foundation/diagnostics.dart index e9c0468579..73008ee3a8 100644 --- a/packages/flutter/lib/src/foundation/diagnostics.dart +++ b/packages/flutter/lib/src/foundation/diagnostics.dart @@ -1265,7 +1265,7 @@ class TextTreeRenderer { // we should not place a separator between the name and the value. // Essentially in this case the properties are treated a bit like a value. if ((properties.isNotEmpty || children.isNotEmpty || node.emptyBodyDescription != null) && - (node.showSeparator || description?.isNotEmpty == true)) { + (node.showSeparator || description.isNotEmpty)) { builder.write(config.afterDescriptionIfBody); } @@ -1474,7 +1474,7 @@ abstract class DiagnosticsNode { /// `parentConfiguration` specifies how the parent is rendered as text art. /// For example, if the parent does not line break between properties, the /// description of a property should also be a single line if possible. - String? toDescription({ TextTreeConfiguration? parentConfiguration }); + String toDescription({ TextTreeConfiguration? parentConfiguration }); /// Whether to show a separator between [name] and description. /// @@ -1544,6 +1544,44 @@ abstract class DiagnosticsNode { String get _separator => showSeparator ? ':' : ''; + /// Converts the properties ([getProperties]) of this node to a form useful + /// for [Timeline] event arguments (as in [Timeline.startSync]). + /// + /// The properties specified by [timelineArgumentsIndicatingLandmarkEvent] are + /// included in the result. + /// + /// Children ([getChildren]) are omitted. + /// + /// This method is only valid in debug builds. In profile builds, this method + /// throws an exception. In release builds, it returns a copy of + /// [timelineArgumentsIndicatingLandmarkEvent] with no arguments added. + /// + /// See also: + /// + /// * [toJsonMap], which converts this node to a structured form intended for + /// data exchange (e.g. with an IDE). + Map toTimelineArguments() { + final Map result = Map.of(timelineArgumentsIndicatingLandmarkEvent); + if (!kReleaseMode) { + // We don't throw in release builds, to avoid hurting users. We also don't do anything useful. + if (kProfileMode) { + throw FlutterError( + // Parts of this string are searched for verbatim by a test in dev/bots/test.dart. + '$DiagnosticsNode.toTimelineArguments used in non-debug build.\n' + 'The $DiagnosticsNode.toTimelineArguments API is expensive and causes timeline traces ' + 'to be non-representative. As such, it should not be used in profile builds. However, ' + 'this application is compiled in profile mode and yet still invoked the method.' + ); + } + for (final DiagnosticsNode property in getProperties()) { + if (property.name != null) { + result[property.name!] = property.toDescription(parentConfiguration: singleLineTextConfiguration); + } + } + } + return result; + } + /// Serialize the node to a JSON map according to the configuration provided /// in the [DiagnosticsSerializationDelegate]. /// @@ -1655,7 +1693,7 @@ abstract class DiagnosticsNode { if (_isSingleLine(style)) { result = toStringDeep(parentConfiguration: parentConfiguration, minLevel: minLevel); } else { - final String description = toDescription(parentConfiguration: parentConfiguration)!; + final String description = toDescription(parentConfiguration: parentConfiguration); if (name == null || name!.isEmpty || !showName) { result = description; @@ -3558,7 +3596,7 @@ class DiagnosticsBlock extends DiagnosticsNode { this.allowTruncate = false, List children = const[], List properties = const [], - }) : _description = description, + }) : _description = description ?? '', _children = children, _properties = properties, super( @@ -3575,7 +3613,7 @@ class DiagnosticsBlock extends DiagnosticsNode { @override final DiagnosticLevel level; - final String? _description; + final String _description; @override final Object? value; @@ -3590,7 +3628,7 @@ class DiagnosticsBlock extends DiagnosticsNode { List getProperties() => _properties; @override - String? toDescription({TextTreeConfiguration? parentConfiguration}) => _description; + String toDescription({TextTreeConfiguration? parentConfiguration}) => _description; } /// A delegate that configures how a hierarchy of [DiagnosticsNode]s should be diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 088afeb83e..aea7da52bb 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -56,6 +56,9 @@ class _InputBorderGap extends ChangeNotifier { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes, this class is not used in collection int get hashCode => hashValues(start, extent); + + @override + String toString() => describeIdentity(this); } // Used to interpolate between two InputBorders. @@ -124,6 +127,9 @@ class _InputBorderPainter extends CustomPainter { || gap != oldPainter.gap || textDirection != oldPainter.textDirection; } + + @override + String toString() => describeIdentity(this); } // An analog of AnimatedContainer, which can animate its shaped border, for diff --git a/packages/flutter/lib/src/material/toggleable.dart b/packages/flutter/lib/src/material/toggleable.dart index 6b5c7b3445..284321fad0 100644 --- a/packages/flutter/lib/src/material/toggleable.dart +++ b/packages/flutter/lib/src/material/toggleable.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; @@ -589,4 +590,7 @@ abstract class ToggleablePainter extends ChangeNotifier implements CustomPainter @override bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => false; + + @override + String toString() => describeIdentity(this); } diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index ff77ca58d3..a12cade253 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -508,11 +508,15 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture Future performReassemble() async { await super.performReassemble(); if (BindingBase.debugReassembleConfig?.widgetName == null) { - Timeline.startSync('Dirty Render Tree', arguments: timelineArgumentsIndicatingLandmarkEvent); + if (!kReleaseMode) { + Timeline.startSync('Preparing Hot Reload (layout)', arguments: timelineArgumentsIndicatingLandmarkEvent); + } try { renderView.reassemble(); } finally { - Timeline.finishSync(); + if (!kReleaseMode) { + Timeline.finishSync(); + } } } scheduleWarmUpFrame(); diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 6b82436270..1687ce1522 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:developer' show Timeline; import 'dart:math' as math; import 'dart:ui' as ui show lerpDouble; @@ -1358,6 +1359,7 @@ abstract class RenderBox extends RenderObject { } Map<_IntrinsicDimensionsCacheEntry, double>? _cachedIntrinsicDimensions; + static int _debugIntrinsicsDepth = 0; double _computeIntrinsicDimension(_IntrinsicDimension dimension, double argument, double Function(double argument) computer) { assert(RenderObject.debugCheckingIntrinsics || !debugDoingThisResize); // performResize should not depend on anything except the incoming constraints @@ -1370,11 +1372,38 @@ abstract class RenderBox extends RenderObject { return true; }()); if (shouldCache) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + if (debugProfileLayoutsEnabled) { + debugTimelineArguments = toDiagnosticsNode().toTimelineArguments(); + } else { + debugTimelineArguments = Map.of(debugTimelineArguments); + } + debugTimelineArguments['intrinsics dimension'] = describeEnum(dimension); + debugTimelineArguments['intrinsics argument'] = '$argument'; + return true; + }()); + if (!kReleaseMode) { + if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { + Timeline.startSync( + '$runtimeType intrinsics', + arguments: debugTimelineArguments, + ); + } + _debugIntrinsicsDepth += 1; + } _cachedIntrinsicDimensions ??= <_IntrinsicDimensionsCacheEntry, double>{}; - return _cachedIntrinsicDimensions!.putIfAbsent( + final double result = _cachedIntrinsicDimensions!.putIfAbsent( _IntrinsicDimensionsCacheEntry(dimension, argument), () => computer(argument), ); + if (!kReleaseMode) { + _debugIntrinsicsDepth -= 1; + if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { + Timeline.finishSync(); + } + } + return result; } return computer(argument); } @@ -1807,8 +1836,34 @@ abstract class RenderBox extends RenderObject { return true; }()); if (shouldCache) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + if (debugProfileLayoutsEnabled) { + debugTimelineArguments = toDiagnosticsNode().toTimelineArguments(); + } else { + debugTimelineArguments = Map.of(debugTimelineArguments); + } + debugTimelineArguments['getDryLayout constraints'] = '$constraints'; + return true; + }()); + if (!kReleaseMode) { + if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { + Timeline.startSync( + '$runtimeType.getDryLayout', + arguments: debugTimelineArguments, + ); + } + _debugIntrinsicsDepth += 1; + } _cachedDryLayoutSizes ??= {}; - return _cachedDryLayoutSizes!.putIfAbsent(constraints, () => _computeDryLayout(constraints)); + final Size result = _cachedDryLayoutSizes!.putIfAbsent(constraints, () => _computeDryLayout(constraints)); + if (!kReleaseMode) { + _debugIntrinsicsDepth -= 1; + if (debugProfileLayoutsEnabled || _debugIntrinsicsDepth == 0) { + Timeline.finishSync(); + } + } + return result; } return _computeDryLayout(constraints); } diff --git a/packages/flutter/lib/src/rendering/custom_paint.dart b/packages/flutter/lib/src/rendering/custom_paint.dart index 0339fc2585..508e82a839 100644 --- a/packages/flutter/lib/src/rendering/custom_paint.dart +++ b/packages/flutter/lib/src/rendering/custom_paint.dart @@ -1017,4 +1017,14 @@ class RenderCustomPaint extends RenderProxyBox { return newChild; } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(MessageProperty('painter', '$painter')); + properties.add(MessageProperty('foregroundPainter', '$foregroundPainter', level: foregroundPainter != null ? DiagnosticLevel.info : DiagnosticLevel.fine)); + properties.add(DiagnosticsProperty('preferredSize', preferredSize, defaultValue: Size.zero)); + properties.add(DiagnosticsProperty('isComplex', isComplex, defaultValue: false)); + properties.add(DiagnosticsProperty('willChange', willChange, defaultValue: false)); + } } diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart index b709076400..0c0e84babc 100644 --- a/packages/flutter/lib/src/rendering/debug.dart +++ b/packages/flutter/lib/src/rendering/debug.dart @@ -96,9 +96,18 @@ bool debugCheckIntrinsicSizes = false; /// Adds [dart:developer.Timeline] events for every [RenderObject] layout. /// -/// For details on how to use [dart:developer.Timeline] events in the Dart -/// Observatory to optimize your app, see: -/// +/// The timing information this flag exposes is not representative of the actual +/// cost of layout, because the overhead of adding timeline events is +/// significant relative to the time each object takes to lay out. However, it +/// can expose unexpected layout behavior in the timeline. +/// +/// In debug builds, additional information is included in the trace (such as +/// the properties of render objects being laid out). Collecting this data is +/// expensive and further makes these traces non-representative of actual +/// performance. This data is omitted in profile builds. +/// +/// For more information about performance debugging in Flutter, see +/// . /// /// See also: /// @@ -112,11 +121,17 @@ bool debugProfileLayoutsEnabled = false; /// Adds [dart:developer.Timeline] events for every [RenderObject] painted. /// /// The timing information this flag exposes is not representative of actual -/// paints. However, it can expose unexpected painting in the timeline. +/// paints, because the overhead of adding timeline events is significant +/// relative to the time each object takes to paint. However, it can expose +/// unexpected painting in the timeline. /// -/// For details on how to use [dart:developer.Timeline] events in the Dart -/// Observatory to optimize your app, see: -/// +/// In debug builds, additional information is included in the trace (such as +/// the properties of render objects being painted). Collecting this data is +/// expensive and further makes these traces non-representative of actual +/// performance. This data is omitted in profile builds. +/// +/// For more information about performance debugging in Flutter, see +/// . /// /// See also: /// diff --git a/packages/flutter/lib/src/rendering/flex.dart b/packages/flutter/lib/src/rendering/flex.dart index 730c835273..99107df196 100644 --- a/packages/flutter/lib/src/rendering/flex.dart +++ b/packages/flutter/lib/src/rendering/flex.dart @@ -1155,8 +1155,10 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + if (debugProfileLayoutsEnabled) { + debugTimelineArguments = { + ...debugTimelineArguments, + 'dirty count': '${_nodesNeedingLayout.length}', + 'dirty list': '$_nodesNeedingLayout', + }; + } + return true; + }()); + Timeline.startSync( + 'LAYOUT', + arguments: debugTimelineArguments, + ); } assert(() { _debugDoingLayout = true; return true; }()); try { - // TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselves while (_nodesNeedingLayout.isNotEmpty) { final List dirtyNodes = _nodesNeedingLayout; _nodesNeedingLayout = []; @@ -923,7 +931,7 @@ class PipelineOwner { /// [flushPaint]. void flushCompositingBits() { if (!kReleaseMode) { - Timeline.startSync('Compositing bits'); + Timeline.startSync('UPDATING COMPOSITING BITS', arguments: timelineArgumentsIndicatingLandmarkEvent); } _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth); for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) { @@ -956,7 +964,21 @@ class PipelineOwner { /// See [RendererBinding] for an example of how this function is used. void flushPaint() { if (!kReleaseMode) { - Timeline.startSync('Paint', arguments: timelineArgumentsIndicatingLandmarkEvent); + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + if (debugProfilePaintsEnabled) { + debugTimelineArguments = { + ...debugTimelineArguments, + 'dirty count': '${_nodesNeedingPaint.length}', + 'dirty list': '$_nodesNeedingPaint', + }; + } + return true; + }()); + Timeline.startSync( + 'PAINT', + arguments: debugTimelineArguments, + ); } assert(() { _debugDoingPaint = true; @@ -1058,7 +1080,7 @@ class PipelineOwner { if (_semanticsOwner == null) return; if (!kReleaseMode) { - Timeline.startSync('Semantics'); + Timeline.startSync('SEMANTICS', arguments: timelineArgumentsIndicatingLandmarkEvent); } assert(_semanticsOwner != null); assert(() { @@ -1745,9 +1767,17 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im @pragma('vm:notify-debugger-on-exception') void layout(Constraints constraints, { bool parentUsesSize = false }) { assert(!_debugDisposed); - if (!kReleaseMode && debugProfileLayoutsEnabled) - Timeline.startSync('$runtimeType', arguments: timelineArgumentsIndicatingLandmarkEvent); - + if (!kReleaseMode && debugProfileLayoutsEnabled) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + debugTimelineArguments = toDiagnosticsNode().toTimelineArguments(); + return true; + }()); + Timeline.startSync( + '$runtimeType', + arguments: debugTimelineArguments, + ); + } assert(constraints != null); assert(constraints.debugAssertIsValid( isAppliedConstraint: true, @@ -2336,6 +2366,17 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im // a different layer, because there's a repaint boundary between us. if (_needsLayout) return; + if (!kReleaseMode && debugProfilePaintsEnabled) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + debugTimelineArguments = toDiagnosticsNode().toTimelineArguments(); + return true; + }()); + Timeline.startSync( + '$runtimeType', + arguments: debugTimelineArguments, + ); + } assert(() { if (_needsCompositingBitsUpdate) { if (parent is RenderObject) { @@ -2412,6 +2453,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im _debugDoingThisPaint = false; return true; }()); + if (!kReleaseMode && debugProfilePaintsEnabled) + Timeline.finishSync(); } /// An estimate of the bounds within which this render object will paint. @@ -2885,27 +2928,29 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im @override String toStringShort() { String header = describeIdentity(this); - if (_debugDisposed) { - header += ' DISPOSED'; - return header; - } - if (_relayoutBoundary != null && _relayoutBoundary != this) { - int count = 1; - RenderObject? target = parent as RenderObject?; - while (target != null && target != _relayoutBoundary) { - target = target.parent as RenderObject?; - count += 1; + if (!kReleaseMode) { + if (_debugDisposed) { + header += ' DISPOSED'; + return header; } - header += ' relayoutBoundary=up$count'; + if (_relayoutBoundary != null && _relayoutBoundary != this) { + int count = 1; + RenderObject? target = parent as RenderObject?; + while (target != null && target != _relayoutBoundary) { + target = target.parent as RenderObject?; + count += 1; + } + header += ' relayoutBoundary=up$count'; + } + if (_needsLayout) + header += ' NEEDS-LAYOUT'; + if (_needsPaint) + header += ' NEEDS-PAINT'; + if (_needsCompositingBitsUpdate) + header += ' NEEDS-COMPOSITING-BITS-UPDATE'; + if (!attached) + header += ' DETACHED'; } - if (_needsLayout) - header += ' NEEDS-LAYOUT'; - if (_needsPaint) - header += ' NEEDS-PAINT'; - if (_needsCompositingBitsUpdate) - header += ' NEEDS-COMPOSITING-BITS-UPDATE'; - if (!attached) - header += ' DETACHED'; return header; } diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart index 1d8b39b6c9..3be9cad449 100644 --- a/packages/flutter/lib/src/rendering/shifted_box.dart +++ b/packages/flutter/lib/src/rendering/shifted_box.dart @@ -838,8 +838,10 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO @override String toStringShort() { String header = super.toStringShort(); - if (_isOverflowing) - header += ' OVERFLOWING'; + if (!kReleaseMode) { + if (_isOverflowing) + header += ' OVERFLOWING'; + } return header; } } diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index 3e4b1a2b5f..a4ab69cf05 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -220,7 +220,9 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin /// /// Actually causes the output of the rendering pipeline to appear on screen. void compositeFrame() { - Timeline.startSync('Compositing', arguments: timelineArgumentsIndicatingLandmarkEvent); + if (!kReleaseMode) { + Timeline.startSync('COMPOSITING', arguments: timelineArgumentsIndicatingLandmarkEvent); + } try { final ui.SceneBuilder builder = ui.SceneBuilder(); final ui.Scene scene = layer!.buildScene(builder); @@ -234,7 +236,9 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin return true; }()); } finally { - Timeline.finishSync(); + if (!kReleaseMode) { + Timeline.finishSync(); + } } } diff --git a/packages/flutter/lib/src/widgets/debug.dart b/packages/flutter/lib/src/widgets/debug.dart index 8c364c2b33..be19d6b60e 100644 --- a/packages/flutter/lib/src/widgets/debug.dart +++ b/packages/flutter/lib/src/widgets/debug.dart @@ -94,9 +94,18 @@ bool debugPrintGlobalKeyedWidgetLifecycle = false; /// Adds [Timeline] events for every Widget built. /// -/// For details on how to use [Timeline] events in the Dart Observatory to -/// optimize your app, see https://flutter.dev/docs/testing/debugging#tracing-any-dart-code-performance -/// and https://fuchsia.googlesource.com/topaz/+/master/shell/docs/performance.md +/// The timing information this flag exposes is not representative of the actual +/// cost of building, because the overhead of adding timeline events is +/// significant relative to the time each object takes to build. However, it can +/// expose unexpected widget behavior in the timeline. +/// +/// In debug builds, additional information is included in the trace (such as +/// the properties of widgets being built). Collecting this data is +/// expensive and further makes these traces non-representative of actual +/// performance. This data is omitted in profile builds. +/// +/// For more information about performance debugging in Flutter, see +/// . /// /// See also: /// diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 488980ee09..7b3699fc61 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -2306,10 +2306,18 @@ abstract class BuildContext { /// data down to them. void visitChildElements(ElementVisitor visitor); - /// Returns a description of an [Element] from the current build context. + /// Returns a description of the [Element] associated with the current build context. + /// + /// The `name` is typically something like "The element being rebuilt was". + /// + /// See also: + /// + /// * [Element.describeElements], which can be used to describe a list of elements. DiagnosticsNode describeElement(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}); /// Returns a description of the [Widget] associated with the current build context. + /// + /// The `name` is typically something like "The widget being rebuilt was". DiagnosticsNode describeWidget(String name, {DiagnosticsTreeStyle style = DiagnosticsTreeStyle.errorProperty}); /// Adds a description of a specific type of widget missing from the current @@ -2525,7 +2533,25 @@ class BuildOwner { _debugBuilding = true; return true; }()); - Timeline.startSync('Build', arguments: timelineArgumentsIndicatingLandmarkEvent); + if (!kReleaseMode) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + if (debugProfileBuildsEnabled) { + debugTimelineArguments = { + ...debugTimelineArguments, + 'dirty count': '${_dirtyElements.length}', + 'dirty list': '$_dirtyElements', + 'lock level': '$_debugStateLockLevel', + 'scope context': '$context', + }; + } + return true; + }()); + Timeline.startSync( + 'BUILD', + arguments: debugTimelineArguments + ); + } try { _scheduledFlushDirtyElements = true; if (callback != null) { @@ -2555,10 +2581,11 @@ class BuildOwner { int dirtyCount = _dirtyElements.length; int index = 0; while (index < dirtyCount) { - assert(_dirtyElements[index] != null); - assert(_dirtyElements[index]._inDirtyList); + final Element element = _dirtyElements[index]; + assert(element != null); + assert(element._inDirtyList); assert(() { - if (_dirtyElements[index]._lifecycleState == _ElementLifecycle.active && !_dirtyElements[index]._debugIsInScope(context)) { + if (element._lifecycleState == _ElementLifecycle.active && !element._debugIsInScope(context)) { throw FlutterError.fromParts([ ErrorSummary('Tried to build dirty widget in the wrong build scope.'), ErrorDescription( @@ -2578,15 +2605,26 @@ class BuildOwner { ), DiagnosticsProperty( 'The offending element (which does not appear to be a descendant of the root of the build scope) was', - _dirtyElements[index], + element, style: DiagnosticsTreeStyle.errorProperty, ), ]); } return true; }()); + if (!kReleaseMode && debugProfileBuildsEnabled) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + debugTimelineArguments = element.widget.toDiagnosticsNode().toTimelineArguments(); + return true; + }()); + Timeline.startSync( + '${element.widget.runtimeType}', + arguments: debugTimelineArguments, + ); + } try { - _dirtyElements[index].rebuild(); + element.rebuild(); } catch (e, stack) { _debugReportException( ErrorDescription('while rebuilding dirty elements'), @@ -2594,14 +2632,16 @@ class BuildOwner { stack, informationCollector: () sync* { if (index < _dirtyElements.length) { - yield DiagnosticsDebugCreator(DebugCreator(_dirtyElements[index])); - yield _dirtyElements[index].describeElement('The element being rebuilt at the time was index $index of $dirtyCount'); + yield DiagnosticsDebugCreator(DebugCreator(element)); + yield element.describeElement('The element being rebuilt at the time was index $index of $dirtyCount'); } else { yield ErrorHint('The element being rebuilt at the time was index $index of $dirtyCount, but _dirtyElements only had ${_dirtyElements.length} entries. This suggests some confusion in the framework internals.'); } }, ); } + if (!kReleaseMode && debugProfileBuildsEnabled) + Timeline.finishSync(); index += 1; if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) { _dirtyElements.sort(Element._sort); @@ -2637,7 +2677,9 @@ class BuildOwner { _dirtyElements.clear(); _scheduledFlushDirtyElements = false; _dirtyElementsNeedsResorting = null; - Timeline.finishSync(); + if (!kReleaseMode) { + Timeline.finishSync(); + } assert(_debugBuilding); assert(() { _debugBuilding = false; @@ -2842,7 +2884,9 @@ class BuildOwner { /// about changes to global keys will run. @pragma('vm:notify-debugger-on-exception') void finalizeTree() { - Timeline.startSync('Finalize tree', arguments: timelineArgumentsIndicatingLandmarkEvent); + if (!kReleaseMode) { + Timeline.startSync('FINALIZE TREE', arguments: timelineArgumentsIndicatingLandmarkEvent); + } try { lockState(() { _inactiveElements._unmountAll(); // this unregisters the GlobalKeys @@ -2937,7 +2981,9 @@ class BuildOwner { // cause more exceptions. _debugReportException(ErrorSummary('while finalizing the widget tree'), e, stack); } finally { - Timeline.finishSync(); + if (!kReleaseMode) { + Timeline.finishSync(); + } } } @@ -2948,14 +2994,18 @@ class BuildOwner { /// /// This is expensive and should not be called except during development. void reassemble(Element root, DebugReassembleConfig? reassembleConfig) { - Timeline.startSync('Dirty Element Tree'); + if (!kReleaseMode) { + Timeline.startSync('Preparing Hot Reload (widgets)'); + } try { assert(root._parent == null); assert(root.owner == this); root._debugReassembleConfig = reassembleConfig; root.reassemble(); } finally { - Timeline.finishSync(); + if (!kReleaseMode) { + Timeline.finishSync(); + } } } } @@ -3021,8 +3071,24 @@ abstract class Element extends DiagnosticableTree implements BuildContext { Element? _parent; DebugReassembleConfig? _debugReassembleConfig; - // Custom implementation of `operator ==` optimized for the ".of" pattern - // used with `InheritedWidgets`. + /// Compare two widgets for equality. + /// + /// When a widget is rebuilt with another that compares equal according + /// to `operator ==`, it is assumed that the update is redundant and the + /// work to update that branch of the tree is skipped. + /// + /// It is generally discouraged to override `operator ==` on any widget that + /// has children, since a correct implementation would have to defer to the + /// children's equality operator also, and that is an O(N²) operation: each + /// child would need to itself walk all its children, each step of the tree. + /// + /// It is sometimes reasonable for a leaf widget (one with no children) to + /// implement this method, if rebuilding the widget is known to be much more + /// expensive than checking the widgets' parameters for equality and if the + /// widget is expected to often be rebuilt with identical parameters. + /// + /// In general, however, it is more efficient to cache the widgets used + /// in a build method if it is known that they will not change. @nonVirtual @override // ignore: avoid_equals_and_hash_code_on_mutable_classes @@ -3347,6 +3413,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { deactivateChild(child); return null; } + final Element newChild; if (child != null) { bool hasSameSuperclass = true; @@ -3372,13 +3439,29 @@ abstract class Element extends DiagnosticableTree implements BuildContext { return true; }()); if (hasSameSuperclass && child.widget == newWidget) { + // We don't insert a timeline event here, because otherwise it's + // confusing that widgets that "don't update" (because they didn't + // change) get "charged" on the timeline. if (child.slot != newSlot) updateSlotForChild(child, newSlot); newChild = child; } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); + if (!kReleaseMode && debugProfileBuildsEnabled) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + debugTimelineArguments = newWidget.toDiagnosticsNode().toTimelineArguments(); + return true; + }()); + Timeline.startSync( + '${newWidget.runtimeType}', + arguments: debugTimelineArguments, + ); + } child.update(newWidget); + if (!kReleaseMode && debugProfileBuildsEnabled) + Timeline.finishSync(); assert(child.widget == newWidget); assert(() { child.owner!._debugElementWasRebuilt(child); @@ -3388,10 +3471,36 @@ abstract class Element extends DiagnosticableTree implements BuildContext { } else { deactivateChild(child); assert(child._parent == null); + if (!kReleaseMode && debugProfileBuildsEnabled) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + debugTimelineArguments = newWidget.toDiagnosticsNode().toTimelineArguments(); + return true; + }()); + Timeline.startSync( + '${newWidget.runtimeType}', + arguments: debugTimelineArguments, + ); + } newChild = inflateWidget(newWidget, newSlot); + if (!kReleaseMode && debugProfileBuildsEnabled) + Timeline.finishSync(); } } else { + if (!kReleaseMode && debugProfileBuildsEnabled) { + Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent; + assert(() { + debugTimelineArguments = newWidget.toDiagnosticsNode().toTimelineArguments(); + return true; + }()); + Timeline.startSync( + '${newWidget.runtimeType}', + arguments: debugTimelineArguments, + ); + } newChild = inflateWidget(newWidget, newSlot); + if (!kReleaseMode && debugProfileBuildsEnabled) + Timeline.finishSync(); } assert(() { @@ -4291,6 +4400,9 @@ abstract class Element extends DiagnosticableTree implements BuildContext { owner!.scheduleBuildFor(this); } + /// Cause the widget to update itself. In debug builds, also verify various + /// invariants. + /// /// Called by the [BuildOwner] when [BuildOwner.scheduleBuildFor] has been /// called to mark this element dirty, by [mount] when the element is first /// built, and by [update] when the widget has changed. @@ -4328,7 +4440,9 @@ abstract class Element extends DiagnosticableTree implements BuildContext { assert(!_dirty); } - /// Called by rebuild() after the appropriate checks have been made. + /// Cause the widget to update itself. + /// + /// Called by [rebuild] after the appropriate checks have been made. @protected void performRebuild(); } @@ -4574,7 +4688,8 @@ abstract class ComponentElement extends Element { } void _firstBuild() { - rebuild(); + // StatefulElement overrides this to also call state.didChangeDependencies. + rebuild(); // This eventually calls performRebuild. } /// Calls the [StatelessWidget.build] method of the [StatelessWidget] object @@ -4586,9 +4701,6 @@ abstract class ComponentElement extends Element { @override @pragma('vm:notify-debugger-on-exception') void performRebuild() { - if (!kReleaseMode && debugProfileBuildsEnabled) - Timeline.startSync('${widget.runtimeType}', arguments: timelineArgumentsIndicatingLandmarkEvent); - assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true)); Widget? built; try { @@ -4636,9 +4748,6 @@ abstract class ComponentElement extends Element { ); _child = updateChild(null, built, slot); } - - if (!kReleaseMode && debugProfileBuildsEnabled) - Timeline.finishSync(); } /// Subclasses should override this function to actually call the appropriate @@ -5490,16 +5599,7 @@ abstract class RenderObjectElement extends Element { _debugUpdateRenderObjectOwner(); return true; }()); - assert(() { - _debugDoingBuild = true; - return true; - }()); - widget.updateRenderObject(this, renderObject); - assert(() { - _debugDoingBuild = false; - return true; - }()); - _dirty = false; + _performRebuild(); // calls widget.updateRenderObject() } void _debugUpdateRenderObjectOwner() { @@ -5511,6 +5611,11 @@ abstract class RenderObjectElement extends Element { @override void performRebuild() { + _performRebuild(); // calls widget.updateRenderObject() + } + + @pragma('vm:prefer-inline') + void _performRebuild() { assert(() { _debugDoingBuild = true; return true; @@ -6367,8 +6472,10 @@ class IndexedSlot { int get hashCode => hashValues(index, value); } +/// Used as a placeholder in [List] objects when the actual +/// elements are not yet determined. class _NullElement extends Element { - _NullElement() : super(_NullWidget()); + _NullElement() : super(const _NullWidget()); static _NullElement instance = _NullElement(); @@ -6376,10 +6483,12 @@ class _NullElement extends Element { bool get debugDoingBuild => throw UnimplementedError(); @override - void performRebuild() { } + void performRebuild() => throw UnimplementedError(); } class _NullWidget extends Widget { + const _NullWidget(); + @override Element createElement() => throw UnimplementedError(); } diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart index 35b196931a..93168bfe61 100644 --- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart +++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart @@ -526,6 +526,11 @@ class _GlowController extends ChangeNotifier { canvas.drawCircle(center, radius, paint); canvas.restore(); } + + @override + String toString() { + return '_GlowController(color: $color, axis: ${describeEnum(axis)})'; + } } class _GlowingOverscrollIndicatorPainter extends CustomPainter { @@ -595,6 +600,11 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter { return oldDelegate.leadingController != leadingController || oldDelegate.trailingController != trailingController; } + + @override + String toString() { + return '_GlowingOverscrollIndicatorPainter($leadingController, $trailingController)'; + } } /// A Material Design visual indication that a scroll view has overscrolled. @@ -891,6 +901,9 @@ class _StretchController extends ChangeNotifier { _stretchController.dispose(); super.dispose(); } + + @override + String toString() => '_StretchController()'; } /// A notification that either a [GlowingOverscrollIndicator] or a diff --git a/packages/flutter/lib/src/widgets/placeholder.dart b/packages/flutter/lib/src/widgets/placeholder.dart index 01b4a6eaf1..ec6ee1176e 100644 --- a/packages/flutter/lib/src/widgets/placeholder.dart +++ b/packages/flutter/lib/src/widgets/placeholder.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; + import 'basic.dart'; import 'framework.dart'; @@ -95,4 +97,13 @@ class Placeholder extends StatelessWidget { ), ); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('color', color, defaultValue: const Color(0xFF455A64))); + properties.add(DoubleProperty('strokeWidth', strokeWidth, defaultValue: 2.0)); + properties.add(DoubleProperty('fallbackWidth', fallbackWidth, defaultValue: 400.0)); + properties.add(DoubleProperty('fallbackHeight', fallbackHeight, defaultValue: 400.0)); + } } diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart index 4ffce2c7f0..82e36eaed6 100644 --- a/packages/flutter/lib/src/widgets/scrollbar.dart +++ b/packages/flutter/lib/src/widgets/scrollbar.dart @@ -750,6 +750,9 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter { @override SemanticsBuilderCallback? get semanticsBuilder => null; + + @override + String toString() => describeIdentity(this); } /// An extendable base class for building scrollbars that fade in and out. diff --git a/packages/flutter/test/widgets/keep_alive_test.dart b/packages/flutter/test/widgets/keep_alive_test.dart index b276ee938e..2a3bdf30ea 100644 --- a/packages/flutter/test/widgets/keep_alive_test.dart +++ b/packages/flutter/test/widgets/keep_alive_test.dart @@ -226,6 +226,11 @@ void main() { ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' + ' │ painter: null\n' + ' │ foregroundPainter:\n' + ' │ _GlowingOverscrollIndicatorPainter(_GlowController(color:\n' + ' │ Color(0xffffffff), axis: vertical), _GlowController(color:\n' + ' │ Color(0xffffffff), axis: vertical))\n' ' │\n' ' └─child: RenderRepaintBoundary#00000\n' ' │ needs compositing\n' @@ -322,6 +327,9 @@ void main() { ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n' ' │ size: Size(800.0, 400.0)\n' + ' │ painter: null\n' + ' │ foregroundPainter: _PlaceholderPainter#00000()\n' + ' │ preferredSize: Size(Infinity, Infinity)\n' ' │\n' ' ├─child with index 1: RenderLimitedBox#00000\n' // <----- no dashed line starts here ' │ │ parentData: index=1; layoutOffset=400.0\n' @@ -334,6 +342,9 @@ void main() { ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n' ' │ size: Size(800.0, 400.0)\n' + ' │ painter: null\n' + ' │ foregroundPainter: _PlaceholderPainter#00000()\n' + ' │ preferredSize: Size(Infinity, Infinity)\n' ' │\n' ' └─child with index 2: RenderLimitedBox#00000 NEEDS-PAINT\n' ' │ parentData: index=2; layoutOffset=800.0\n' @@ -345,7 +356,10 @@ void main() { ' └─child: RenderCustomPaint#00000 NEEDS-PAINT\n' ' parentData: (can use size)\n' ' constraints: BoxConstraints(w=800.0, h=400.0)\n' - ' size: Size(800.0, 400.0)\n', + ' size: Size(800.0, 400.0)\n' + ' painter: null\n' + ' foregroundPainter: _PlaceholderPainter#00000()\n' + ' preferredSize: Size(Infinity, Infinity)\n', )); const GlobalObjectKey<_LeafState>(0).currentState!.setKeepAlive(true); await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); @@ -375,6 +389,11 @@ void main() { ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' + ' │ painter: null\n' + ' │ foregroundPainter:\n' + ' │ _GlowingOverscrollIndicatorPainter(_GlowController(color:\n' + ' │ Color(0xffffffff), axis: vertical), _GlowController(color:\n' + ' │ Color(0xffffffff), axis: vertical))\n' ' │\n' ' └─child: RenderRepaintBoundary#00000\n' ' │ needs compositing\n' @@ -471,6 +490,9 @@ void main() { ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n' ' │ size: Size(800.0, 400.0)\n' + ' │ painter: null\n' + ' │ foregroundPainter: _PlaceholderPainter#00000()\n' + ' │ preferredSize: Size(Infinity, Infinity)\n' ' │\n' ' ├─child with index 5: RenderLimitedBox#00000\n' // <----- this is index 5, not 0 ' │ │ parentData: index=5; layoutOffset=2000.0\n' @@ -483,6 +505,9 @@ void main() { ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n' ' │ size: Size(800.0, 400.0)\n' + ' │ painter: null\n' + ' │ foregroundPainter: _PlaceholderPainter#00000()\n' + ' │ preferredSize: Size(Infinity, Infinity)\n' ' │\n' ' ├─child with index 6: RenderLimitedBox#00000\n' ' │ │ parentData: index=6; layoutOffset=2400.0\n' @@ -495,6 +520,9 @@ void main() { ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n' ' │ size: Size(800.0, 400.0)\n' + ' │ painter: null\n' + ' │ foregroundPainter: _PlaceholderPainter#00000()\n' + ' │ preferredSize: Size(Infinity, Infinity)\n' ' │\n' ' ├─child with index 7: RenderLimitedBox#00000 NEEDS-PAINT\n' ' ╎ │ parentData: index=7; layoutOffset=2800.0\n' @@ -507,6 +535,9 @@ void main() { ' ╎ parentData: (can use size)\n' ' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n' ' ╎ size: Size(800.0, 400.0)\n' + ' ╎ painter: null\n' + ' ╎ foregroundPainter: _PlaceholderPainter#00000()\n' + ' ╎ preferredSize: Size(Infinity, Infinity)\n' ' ╎\n' ' ╎╌child with index 0 (kept alive but not laid out): RenderLimitedBox#00000\n' // <----- this one is index 0 and is marked as being kept alive but not laid out ' ╎ │ parentData: index=0; keepAlive; layoutOffset=0.0\n' @@ -519,6 +550,9 @@ void main() { ' ╎ parentData: (can use size)\n' ' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n' ' ╎ size: Size(800.0, 400.0)\n' + ' ╎ painter: null\n' + ' ╎ foregroundPainter: _PlaceholderPainter#00000()\n' + ' ╎ preferredSize: Size(Infinity, Infinity)\n' ' ╎\n' // <----- dashed line ends here ' └╌child with index 3 (kept alive but not laid out): RenderLimitedBox#00000\n' ' │ parentData: index=3; keepAlive; layoutOffset=1200.0\n' @@ -530,7 +564,10 @@ void main() { ' └─child: RenderCustomPaint#00000\n' ' parentData: (can use size)\n' ' constraints: BoxConstraints(w=800.0, h=400.0)\n' - ' size: Size(800.0, 400.0)\n', + ' size: Size(800.0, 400.0)\n' + ' painter: null\n' + ' foregroundPainter: _PlaceholderPainter#00000()\n' + ' preferredSize: Size(Infinity, Infinity)\n', )); }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87876 diff --git a/packages/flutter/test_release/diagnostics_test.dart b/packages/flutter/test_release/diagnostics_test.dart index 038137d5b8..a253670d04 100644 --- a/packages/flutter/test_release/diagnostics_test.dart +++ b/packages/flutter/test_release/diagnostics_test.dart @@ -31,7 +31,7 @@ class TestDiagnosticsNode extends DiagnosticsNode { } @override - String? toDescription({TextTreeConfiguration? parentConfiguration}) { + String toDescription({TextTreeConfiguration? parentConfiguration}) { return 'Test Description'; }