4
4
5
5
import 'dart:async' ;
6
6
import 'dart:html' as html;
7
+ import 'dart:js_util' as js_util;
7
8
import 'dart:math' as math;
8
9
import 'dart:ui' ;
9
10
@@ -27,6 +28,16 @@ const int _kMeasuredSampleCount = 100;
27
28
/// The total number of samples collected by a benchmark.
28
29
const int kTotalSampleCount = _kWarmUpSampleCount + _kMeasuredSampleCount;
29
30
31
+ /// A benchmark metric that includes frame-related computations prior to
32
+ /// submitting layer and picture operations to the underlying renderer, such as
33
+ /// HTML and CanvasKit. During this phase we compute transforms, clips, and
34
+ /// other information needed for rendering.
35
+ const String kProfilePrerollFrame = 'preroll_frame' ;
36
+
37
+ /// A benchmark metric that includes submitting layer and picture information
38
+ /// to the renderer.
39
+ const String kProfileApplyFrame = 'apply_frame' ;
40
+
30
41
/// Measures the amount of time [action] takes.
31
42
Duration timeAction (VoidCallback action) {
32
43
final Stopwatch stopwatch = Stopwatch ()..start ();
@@ -221,9 +232,9 @@ abstract class SceneBuilderRecorder extends Recorder {
221
232
final Scene scene = sceneBuilder.build ();
222
233
profile.record ('windowRenderDuration' , () {
223
234
window.render (scene);
224
- });
225
- });
226
- });
235
+ }, reported : false );
236
+ }, reported : false );
237
+ }, reported : true );
227
238
endMeasureFrame ();
228
239
229
240
if (profile.shouldContinue ()) {
@@ -331,7 +342,7 @@ abstract class WidgetRecorder extends Recorder implements FrameRecorder {
331
342
@mustCallSuper
332
343
void frameDidDraw () {
333
344
endMeasureFrame ();
334
- profile.addDataPoint ('drawFrameDuration' , _drawFrameStopwatch.elapsed);
345
+ profile.addDataPoint ('drawFrameDuration' , _drawFrameStopwatch.elapsed, reported : true );
335
346
336
347
if (profile.shouldContinue ()) {
337
348
window.scheduleFrame ();
@@ -353,12 +364,30 @@ abstract class WidgetRecorder extends Recorder implements FrameRecorder {
353
364
final _RecordingWidgetsBinding binding =
354
365
_RecordingWidgetsBinding .ensureInitialized ();
355
366
final Widget widget = createWidget ();
367
+
368
+ registerEngineBenchmarkValueListener (kProfilePrerollFrame, (num value) {
369
+ localProfile.addDataPoint (
370
+ kProfilePrerollFrame,
371
+ Duration (microseconds: value.toInt ()),
372
+ reported: false ,
373
+ );
374
+ });
375
+ registerEngineBenchmarkValueListener (kProfileApplyFrame, (num value) {
376
+ localProfile.addDataPoint (
377
+ kProfileApplyFrame,
378
+ Duration (microseconds: value.toInt ()),
379
+ reported: false ,
380
+ );
381
+ });
382
+
356
383
binding._beginRecording (this , widget);
357
384
358
385
try {
359
386
await _runCompleter.future;
360
387
return localProfile;
361
388
} finally {
389
+ stopListeningToEngineBenchmarkValues (kProfilePrerollFrame);
390
+ stopListeningToEngineBenchmarkValues (kProfileApplyFrame);
362
391
_runCompleter = null ;
363
392
profile = null ;
364
393
}
@@ -421,7 +450,7 @@ abstract class WidgetBuildRecorder extends Recorder implements FrameRecorder {
421
450
// Only record frames that show the widget.
422
451
if (showWidget) {
423
452
endMeasureFrame ();
424
- profile.addDataPoint ('drawFrameDuration' , _drawFrameStopwatch.elapsed);
453
+ profile.addDataPoint ('drawFrameDuration' , _drawFrameStopwatch.elapsed, reported : true );
425
454
}
426
455
427
456
if (profile.shouldContinue ()) {
@@ -488,11 +517,21 @@ class _WidgetBuildRecorderHostState extends State<_WidgetBuildRecorderHost> {
488
517
/// calculations will only apply to the latest [_kMeasuredSampleCount] data
489
518
/// points.
490
519
class Timeseries {
491
- Timeseries (this .name);
520
+ Timeseries (this .name, this .isReported );
492
521
493
522
/// The label of this timeseries used for debugging and result inspection.
494
523
final String name;
495
524
525
+ /// Whether this timeseries is reported to the benchmark dashboard.
526
+ ///
527
+ /// If `true` a new benchmark card is created for the timeseries and is
528
+ /// visible on the dashboard.
529
+ ///
530
+ /// If `false` the data is stored but it does not show up on the dashboard.
531
+ /// Use unreported metrics for metrics that are useful for manual inspection
532
+ /// but that are too fine-grained to be useful for tracking on the dashboard.
533
+ final bool isReported;
534
+
496
535
/// List of all the values that have been recorded.
497
536
///
498
537
/// This list has no limit.
@@ -700,14 +739,20 @@ class Profile {
700
739
final Map <String , dynamic > extraData = < String , dynamic > {};
701
740
702
741
/// Invokes [callback] and records the duration of its execution under [key] .
703
- Duration record (String key, VoidCallback callback) {
742
+ Duration record (String key, VoidCallback callback, { @required bool reported } ) {
704
743
final Duration duration = timeAction (callback);
705
- addDataPoint (key, duration);
744
+ addDataPoint (key, duration, reported : reported );
706
745
return duration;
707
746
}
708
747
709
- void addDataPoint (String key, Duration duration) {
710
- scoreData.putIfAbsent (key, () => Timeseries (key)).add (duration.inMicroseconds.toDouble ());
748
+ /// Adds a timed sample to the timeseries corresponding to [key] .
749
+ ///
750
+ /// Set [reported] to `true` to report the timeseries to the dashboard UI.
751
+ ///
752
+ /// Set [reported] to `false` to store the data, but not show it on the
753
+ /// dashboard UI.
754
+ void addDataPoint (String key, Duration duration, { @required bool reported }) {
755
+ scoreData.putIfAbsent (key, () => Timeseries (key, reported)).add (duration.inMicroseconds.toDouble ());
711
756
}
712
757
713
758
/// Decides whether the data collected so far is sufficient to stop, or
@@ -740,9 +785,16 @@ class Profile {
740
785
};
741
786
742
787
for (final String key in scoreData.keys) {
743
- scoreKeys.add ('$key .average' );
744
- scoreKeys.add ('$key .outlierAverage' );
745
788
final Timeseries timeseries = scoreData[key];
789
+
790
+ if (timeseries.isReported) {
791
+ scoreKeys.add ('$key .average' );
792
+ // Report `outlierRatio` rather than `outlierAverage`, because
793
+ // the absolute value of outliers is less interesting than the
794
+ // ratio.
795
+ scoreKeys.add ('$key .outlierRatio' );
796
+ }
797
+
746
798
final TimeseriesStats stats = timeseries.computeStats ();
747
799
json['$key .average' ] = stats.average;
748
800
json['$key .outlierAverage' ] = stats.outlierAverage;
@@ -958,3 +1010,55 @@ void endMeasureFrame() {
958
1010
);
959
1011
_currentFrameNumber += 1 ;
960
1012
}
1013
+
1014
+ /// A function that receives a benchmark value from the framework.
1015
+ typedef EngineBenchmarkValueListener = void Function (num value);
1016
+
1017
+ // Maps from a value label name to a listener.
1018
+ final Map <String , EngineBenchmarkValueListener > _engineBenchmarkListeners = < String , EngineBenchmarkValueListener > {};
1019
+
1020
+ /// Registers a [listener] for engine benchmark values labeled by [name] .
1021
+ ///
1022
+ /// If another listener is already registered, overrides it.
1023
+ void registerEngineBenchmarkValueListener (String name, EngineBenchmarkValueListener listener) {
1024
+ if (listener == null ) {
1025
+ throw ArgumentError (
1026
+ 'Listener must not be null. To stop listening to engine benchmark values '
1027
+ 'under label "$name ", call stopListeningToEngineBenchmarkValues(\' $name \' ).' ,
1028
+ );
1029
+ }
1030
+
1031
+ if (_engineBenchmarkListeners.containsKey (name)) {
1032
+ throw StateError (
1033
+ 'A listener for "$name " is already registered.\n '
1034
+ 'Call `stopListeningToEngineBenchmarkValues` to unregister the previous '
1035
+ 'listener before registering a new one.'
1036
+ );
1037
+ }
1038
+
1039
+ if (_engineBenchmarkListeners.isEmpty) {
1040
+ // The first listener is being registered. Register the global listener.
1041
+ js_util.setProperty (html.window, '_flutter_internal_on_benchmark' , _dispatchEngineBenchmarkValue);
1042
+ }
1043
+
1044
+ _engineBenchmarkListeners[name] = listener;
1045
+ }
1046
+
1047
+ /// Stops listening to engine benchmark values under labeled by [name] .
1048
+ void stopListeningToEngineBenchmarkValues (String name) {
1049
+ _engineBenchmarkListeners.remove (name);
1050
+ if (_engineBenchmarkListeners.isEmpty) {
1051
+ // The last listener unregistered. Remove the global listener.
1052
+ js_util.setProperty (html.window, '_flutter_internal_on_benchmark' , null );
1053
+ }
1054
+ }
1055
+
1056
+ // Dispatches a benchmark value reported by the engine to the relevant listener.
1057
+ //
1058
+ // If there are no listeners registered for [name], ignores the value.
1059
+ void _dispatchEngineBenchmarkValue (String name, double value) {
1060
+ final EngineBenchmarkValueListener listener = _engineBenchmarkListeners[name];
1061
+ if (listener != null ) {
1062
+ listener (value);
1063
+ }
1064
+ }
0 commit comments