From 91f19bc6414db0a4ccbb574612908899dd094878 Mon Sep 17 00:00:00 2001 From: Kenzie Schmoll Date: Mon, 1 Aug 2022 13:50:03 -0700 Subject: [PATCH 1/4] Fix layoutPhase calculation --- .../frame_analysis/frame_analysis_model.dart | 39 ++++++++++++------- .../frame_analysis_model_test.dart | 2 +- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart index b10bed114b2..c0f0fcb473c 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart @@ -57,7 +57,9 @@ class FrameAnalysis { /// /// This is drawn from the "Layout" timeline event on the UI thread. This data /// may overlap with a portion of the [buildPhase] if "Build" timeline events - /// are children of the "Layout" event. + /// are children of the "Layout" event. If this is the case, the + /// [FramePhase.duration] for this phase will only include time that is spent + /// in the Layout event, outside of the Build events. /// /// Example: /// [-----------------Layout-----------------] @@ -66,19 +68,30 @@ class FrameAnalysis { FramePhase _generateLayoutPhase() { final uiEvent = frame.timelineEventData.uiEvent; - if (uiEvent == null) { - return FramePhase.layout(events: []); + if (uiEvent != null) { + final layoutEvent = uiEvent.firstChildWithCondition( + (event) => + event.name + ?.caseInsensitiveEquals(FramePhaseType.layout.eventName) ?? + false, + ); + + if (layoutEvent != null) { + final _buildChildren = layoutEvent.shallowNodesWithCondition( + (event) => event.name == FramePhaseType.build.eventName, + ); + final buildDuration = _buildChildren.fold(Duration.zero, + (previous, TimelineEvent event) { + return previous + event.time.duration; + }); + + return FramePhase.layout( + events: [layoutEvent as SyncTimelineEvent], + duration: layoutEvent.time.duration - buildDuration, + ); + } } - final layoutEvent = uiEvent.firstChildWithCondition( - (event) => - event.name?.caseInsensitiveEquals(FramePhaseType.layout.eventName) ?? - false, - ); - return FramePhase.layout( - events: [ - if (layoutEvent != null) layoutEvent as SyncTimelineEvent, - ], - ); + return FramePhase.layout(events: []); } /// Data for the Paint phase of [frame]. diff --git a/packages/devtools_app/test/performance/frame_analysis_model_test.dart b/packages/devtools_app/test/performance/frame_analysis_model_test.dart index c3c5351e172..132091bf64c 100644 --- a/packages/devtools_app/test/performance/frame_analysis_model_test.dart +++ b/packages/devtools_app/test/performance/frame_analysis_model_test.dart @@ -29,7 +29,7 @@ void main() { test('layoutPhase', () { final layoutPhase = frameAnalysis.layoutPhase; expect(layoutPhase.events.length, equals(1)); - expect(layoutPhase.duration.inMicroseconds, equals(211)); + expect(layoutPhase.duration.inMicroseconds, equals(128)); }); test('paintPhase', () { From fd3df821a7630b154ad0d32b2c63ae467c1677a8 Mon Sep 17 00:00:00 2001 From: Kenzie Schmoll Date: Mon, 1 Aug 2022 15:00:24 -0700 Subject: [PATCH 2/4] Rewrite FrameTimeVisualizer in performance page --- .../panes/frame_analysis/frame_analysis.dart | 165 +-------- .../frame_analysis/frame_analysis_model.dart | 90 +++-- .../frame_analysis/frame_time_visualizer.dart | 325 ++++++++++++++++++ .../frame_analysis_model_test.dart | 31 +- .../frame_analysis/frame_analysis_test.dart | 190 ++++++++++ .../performance/frame_time_visualizer.png | Bin 0 -> 18925 bytes .../frame_time_visualizer_icons_only.png | Bin 0 -> 3277 bytes ...ime_visualizer_with_shader_compilation.png | Bin 0 -> 19078 bytes .../lib/src/mocks/generated.dart | 1 + 9 files changed, 599 insertions(+), 203 deletions(-) create mode 100644 packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart rename packages/devtools_app/test/performance/{ => frame_analysis}/frame_analysis_model_test.dart (69%) create mode 100644 packages/devtools_app/test/performance/frame_analysis/frame_analysis_test.dart create mode 100644 packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer.png create mode 100644 packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_icons_only.png create mode 100644 packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_with_shader_compilation.png diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis.dart index 0e2a0011d3b..6bdc16adb9d 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis.dart @@ -4,14 +4,12 @@ import 'package:flutter/material.dart'; -import '../../../../primitives/utils.dart'; import '../../../../shared/common_widgets.dart'; import '../../../../shared/theme.dart'; -import '../../../../ui/colors.dart'; -import '../../../../ui/utils.dart'; import '../controls/enhance_tracing/enhance_tracing_controller.dart'; import 'frame_analysis_model.dart'; import 'frame_hints.dart'; +import 'frame_time_visualizer.dart'; class FlutterFrameAnalysisView extends StatelessWidget { const FlutterFrameAnalysisView({ @@ -47,7 +45,6 @@ class FlutterFrameAnalysisView extends StatelessWidget { bottom: denseSpacing, ), ), - // TODO(kenz): handle missing timeline events. Expanded( child: FrameTimeVisualizer(frameAnalysis: frameAnalysis), ), @@ -56,163 +53,3 @@ class FlutterFrameAnalysisView extends StatelessWidget { ); } } - -class FrameTimeVisualizer extends StatefulWidget { - const FrameTimeVisualizer({ - Key? key, - required this.frameAnalysis, - }) : super(key: key); - - final FrameAnalysis frameAnalysis; - - @override - State createState() => _FrameTimeVisualizerState(); -} - -class _FrameTimeVisualizerState extends State { - late FrameAnalysis frameAnalysis; - - @override - void initState() { - super.initState(); - frameAnalysis = widget.frameAnalysis; - frameAnalysis.selectFramePhase(frameAnalysis.longestUiPhase); - } - - @override - Widget build(BuildContext context) { - // TODO(kenz): calculate ratios to use as flex values. This will be a bit - // tricky because sometimes the Build event(s) are children of Layout. - // final buildTimeRatio = widget.frameAnalysis.buildTimeRatio(); - // final layoutTimeRatio = widget.frameAnalysis.layoutTimeRatio(); - // final paintTimeRatio = widget.frameAnalysis.paintTimeRatio(); - return ValueListenableBuilder( - valueListenable: frameAnalysis.selectedPhase, - builder: (context, selectedPhase, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('UI phases:'), - const SizedBox(height: denseSpacing), - Row( - children: [ - Flexible( - child: FramePhaseBlock( - framePhase: frameAnalysis.buildPhase, - icon: Icons.build, - isSelected: selectedPhase == frameAnalysis.buildPhase, - onSelected: frameAnalysis.selectFramePhase, - ), - ), - Flexible( - child: FramePhaseBlock( - framePhase: frameAnalysis.layoutPhase, - icon: Icons.auto_awesome_mosaic, - isSelected: selectedPhase == frameAnalysis.layoutPhase, - onSelected: frameAnalysis.selectFramePhase, - ), - ), - Flexible( - fit: FlexFit.tight, - child: FramePhaseBlock( - framePhase: frameAnalysis.paintPhase, - icon: Icons.format_paint, - isSelected: selectedPhase == frameAnalysis.paintPhase, - onSelected: frameAnalysis.selectFramePhase, - ), - ), - ], - ), - const SizedBox(height: denseSpacing), - const Text('Raster phase:'), - const SizedBox(height: denseSpacing), - Row( - children: [ - Expanded( - child: FramePhaseBlock( - framePhase: frameAnalysis.rasterPhase, - icon: Icons.grid_on, - isSelected: selectedPhase == frameAnalysis.rasterPhase, - onSelected: frameAnalysis.selectFramePhase, - ), - ) - ], - ), - // TODO(kenz): show flame chart of selected events here. - ], - ); - }, - ); - } -} - -class FramePhaseBlock extends StatelessWidget { - const FramePhaseBlock({ - Key? key, - required this.framePhase, - required this.icon, - required this.isSelected, - required this.onSelected, - }) : super(key: key); - - static const _height = 30.0; - - static const _selectedIndicatorHeight = 4.0; - - static const _backgroundColor = ThemedColor( - light: Color(0xFFEEEEEE), - dark: Color(0xFF3C4043), - ); - - static const _selectedBackgroundColor = ThemedColor( - light: Color(0xFFFFFFFF), - dark: Color(0xFF5F6367), - ); - - final FramePhase framePhase; - - final IconData icon; - - final bool isSelected; - - final void Function(FramePhase) onSelected; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final durationText = framePhase.duration != Duration.zero - ? msText(framePhase.duration) - : '--'; - return InkWell( - onTap: () => onSelected(framePhase), - child: Stack( - alignment: AlignmentDirectional.bottomStart, - children: [ - Container( - color: isSelected - ? _selectedBackgroundColor.colorFor(colorScheme) - : _backgroundColor.colorFor(colorScheme), - height: _height, - padding: const EdgeInsets.symmetric(horizontal: densePadding), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - icon, - size: defaultIconSize, - ), - const SizedBox(width: denseSpacing), - Text('${framePhase.title} - $durationText'), - ], - ), - ), - if (isSelected) - Container( - color: defaultSelectionColor, - height: _selectedIndicatorHeight, - ), - ], - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart index c0f0fcb473c..43475e2f925 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart @@ -2,8 +2,6 @@ // 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 '../../../../primitives/trees.dart'; import '../../../../primitives/utils.dart'; import '../../performance_model.dart'; @@ -17,14 +15,6 @@ class FrameAnalysis { static const intrinsicsEventSuffix = ' intrinsics'; - ValueListenable get selectedPhase => _selectedPhase; - - final _selectedPhase = ValueNotifier(null); - - void selectFramePhase(FramePhase block) { - _selectedPhase.value = block; - } - /// Data for the build phase of [frame]. /// /// This is drawn from all the "Build" events on the UI thread. For a single @@ -128,6 +118,18 @@ class FrameAnalysis { late FramePhase longestUiPhase = _calculateLongestFramePhase(); + bool get hasUiData => _hasUiData ??= [ + ...buildPhase.events, + ...layoutPhase.events, + ...paintPhase.events + ].isNotEmpty; + + bool? _hasUiData; + + bool get hasRasterData => _hasRasterData ??= rasterPhase.events.isNotEmpty; + + bool? _hasRasterData; + FramePhase _calculateLongestFramePhase() { var longestPhaseTime = Duration.zero; late FramePhase longest; @@ -189,33 +191,45 @@ class FrameAnalysis { _intrinsicOperationsCount = _intrinsics; } -// TODO(kenz): calculate ratios to use as flex values. This will be a bit -// tricky because sometimes the Build event(s) are children of Layout. -// int buildTimeRatio() { -// final totalBuildEventTimeMicros = buildTime.inMicroseconds; -// final uiEvent = frame.timelineEventData.uiEvent; -// if (uiEvent == null) return 1; -// final totalUiTimeMicros = uiEvent.time.duration.inMicroseconds; -// return ((totalBuildEventTimeMicros / totalUiTimeMicros) * 1000000).round(); -// } -// -// int layoutTimeRatio() { -// final totalLayoutTimeMicros = layoutTime.inMicroseconds; -// final uiEvent = frame.timelineEventData.uiEvent; -// if (uiEvent == null) return 1; -// final totalUiTimeMicros = -// frame.timelineEventData.uiEvent.time.duration.inMicroseconds; -// return ((totalLayoutTimeMicros / totalUiTimeMicros) * 1000000).round(); -// } -// -// int paintTimeRatio() { -// final totalPaintTimeMicros = paintTime.inMicroseconds; -// final uiEvent = frame.timelineEventData.uiEvent; -// if (uiEvent == null) return 1; -// final totalUiTimeMicros = -// frame.timelineEventData.uiEvent.time.duration.inMicroseconds; -// return ((totalPaintTimeMicros / totalUiTimeMicros) * 1000000).round(); -// } + int? buildFlex; + + int? layoutFlex; + + int? paintFlex; + + int? rasterFlex; + + int? shaderCompilationFlex; + + void calculateFramePhaseFlexValues() { + final totalUiTimeMicros = + (buildPhase.duration + layoutPhase.duration + paintPhase.duration) + .inMicroseconds; + buildFlex = _flexForPhase(buildPhase, totalUiTimeMicros); + layoutFlex = _flexForPhase(layoutPhase, totalUiTimeMicros); + paintFlex = _flexForPhase(paintPhase, totalUiTimeMicros); + + if (frame.hasShaderTime) { + final totalRasterMicros = frame.rasterTime.inMicroseconds; + final shaderMicros = frame.shaderDuration.inMicroseconds; + final otherRasterMicros = totalRasterMicros - shaderMicros; + shaderCompilationFlex = _calculateFlex(shaderMicros, totalRasterMicros); + rasterFlex = _calculateFlex(otherRasterMicros, totalRasterMicros); + } else { + rasterFlex = 1; + } + } + + int _flexForPhase(FramePhase phase, int totalTimeMicros) { + final totalPaintTimeMicros = phase.duration.inMicroseconds; + final uiEvent = frame.timelineEventData.uiEvent; + if (uiEvent == null) return 1; + return _calculateFlex(totalPaintTimeMicros, totalTimeMicros); + } + + int _calculateFlex(int numeratorMicros, int denominatorMicros) { + return ((numeratorMicros / denominatorMicros) * 100).round(); + } } enum FramePhaseType { @@ -253,7 +267,7 @@ class FramePhase { Duration? duration, }) : title = type.eventName, duration = duration ?? - events.fold(Duration.zero, (previous, SyncTimelineEvent event) { + events.fold(Duration.zero, (previous, event) { return previous + event.time.duration; }); diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart new file mode 100644 index 00000000000..b6776dc003f --- /dev/null +++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart @@ -0,0 +1,325 @@ +// Copyright 2022 The Chromium 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:math' as math; + +import 'package:flutter/material.dart'; + +import '../../../../primitives/utils.dart'; +import '../../../../shared/common_widgets.dart'; +import '../../../../shared/theme.dart'; +import '../../../../ui/utils.dart'; +import 'frame_analysis_model.dart'; + +class FrameTimeVisualizer extends StatefulWidget { + const FrameTimeVisualizer({ + Key? key, + required this.frameAnalysis, + }) : super(key: key); + + final FrameAnalysis frameAnalysis; + + @override + State createState() => _FrameTimeVisualizerState(); +} + +class _FrameTimeVisualizerState extends State { + @override + void initState() { + super.initState(); + // Do this in initState so that we do not have to pay the cost in build. + widget.frameAnalysis.calculateFramePhaseFlexValues(); + } + + @override + void didUpdateWidget(FrameTimeVisualizer oldWidget) { + super.didUpdateWidget(oldWidget); + widget.frameAnalysis.calculateFramePhaseFlexValues(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _UiPhases(frameAnalysis: widget.frameAnalysis), + const SizedBox(height: denseSpacing), + _RasterPhases(frameAnalysis: widget.frameAnalysis), + ], + ); + } +} + +class _UiPhases extends StatelessWidget { + const _UiPhases({Key? key, required this.frameAnalysis}) : super(key: key); + + final FrameAnalysis frameAnalysis; + + @override + Widget build(BuildContext context) { + return _FrameBlockGroup( + title: 'UI phases:', + data: _generateBlockData(frameAnalysis), + hasData: frameAnalysis.hasUiData, + ); + } + + List<_FramePhaseBlockData> _generateBlockData(FrameAnalysis frameAnalysis) { + final buildPhase = frameAnalysis.buildPhase; + final layoutPhase = frameAnalysis.layoutPhase; + final paintPhase = frameAnalysis.paintPhase; + return [ + _FramePhaseBlockData( + title: buildPhase.title, + duration: buildPhase.duration, + flex: frameAnalysis.buildFlex!, + icon: Icons.build, + ), + _FramePhaseBlockData( + title: layoutPhase.title, + duration: layoutPhase.duration, + flex: frameAnalysis.layoutFlex!, + icon: Icons.auto_awesome_mosaic, + ), + _FramePhaseBlockData( + title: paintPhase.title, + duration: paintPhase.duration, + flex: frameAnalysis.paintFlex!, + icon: Icons.format_paint, + ), + ]; + } +} + +class _RasterPhases extends StatelessWidget { + const _RasterPhases({Key? key, required this.frameAnalysis}) + : super(key: key); + + final FrameAnalysis frameAnalysis; + + @override + Widget build(BuildContext context) { + final data = _generateBlockData(frameAnalysis); + return _FrameBlockGroup( + title: 'Raster ${pluralize('phase', data.length)}:', + data: _generateBlockData(frameAnalysis), + hasData: frameAnalysis.hasRasterData, + ); + } + + List<_FramePhaseBlockData> _generateBlockData(FrameAnalysis frameAnalysis) { + final frame = frameAnalysis.frame; + if (frame.hasShaderTime) { + return [ + _FramePhaseBlockData( + title: 'Shader compilation', + duration: frame.shaderDuration, + flex: frameAnalysis.shaderCompilationFlex!, + icon: Icons.image_outlined, + ), + _FramePhaseBlockData( + title: 'Other raster', + duration: frame.rasterTime - frame.shaderDuration, + flex: frameAnalysis.rasterFlex!, + icon: Icons.grid_on, + ), + ]; + } + final rasterPhase = frameAnalysis.rasterPhase; + return [ + _FramePhaseBlockData( + title: rasterPhase.title, + duration: rasterPhase.duration, + flex: frameAnalysis.rasterFlex!, + icon: Icons.grid_on, + ), + ]; + } +} + +class _FrameBlockGroup extends StatelessWidget { + const _FrameBlockGroup({ + Key? key, + required this.title, + required this.data, + required this.hasData, + }) : super(key: key); + + final String title; + + final List<_FramePhaseBlockData> data; + + final bool hasData; + + @override + Widget build(BuildContext context) { + Widget content; + if (hasData) { + final totalFlex = + data.fold(0, (current, block) => current + block.flex); + content = LayoutBuilder( + builder: (context, constraints) { + final adjustedBlockWidths = + adjustedWidthsForBlocks(constraints, totalFlex); + return Row( + children: [ + for (var i = 0; i < data.length; i++) + _FramePhaseBlock( + blockData: data[i], + width: adjustedBlockWidths[i], + ), + ], + ); + }, + ); + } else { + content = const Text('Data not available.'); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title), + const SizedBox(height: denseSpacing), + content, + ], + ); + } + + /// Returns a list of adjusted widths for each block. + /// + /// The adjusted widths will ensure each block is at least + /// [_FramePhaseBlock.minBlockWidth] wide, and will modify surrounding block + /// widths to accomodate. f + List adjustedWidthsForBlocks( + BoxConstraints constraints, + int totalFlex, + ) { + final unadjustedBlockWidths = data + .map( + (blockData) => constraints.maxWidth * blockData.flex / totalFlex, + ) + .toList(); + + var adjustment = 0.0; + var widestBlockIndex = 0; + for (var i = 0; i < unadjustedBlockWidths.length; i++) { + final unadjustedWidth = unadjustedBlockWidths[i]; + final currentWidestBlock = unadjustedBlockWidths[widestBlockIndex]; + if (unadjustedWidth > currentWidestBlock) { + widestBlockIndex = i; + } + if (unadjustedWidth < _FramePhaseBlock.minBlockWidth) { + adjustment += _FramePhaseBlock.minBlockWidth - unadjustedWidth; + } + } + + final adjustedBlockWidths = []; + for (var i = 0; i < unadjustedBlockWidths.length; i++) { + final blockWidth = unadjustedBlockWidths[i]; + var adjustedWidth = blockWidth; + if (i == widestBlockIndex) { + adjustedWidth -= adjustment; + } + adjustedWidth = math.max(adjustedWidth, _FramePhaseBlock.minBlockWidth); + adjustedBlockWidths.add(adjustedWidth); + } + + return adjustedBlockWidths; + } +} + +class _FramePhaseBlock extends StatelessWidget { + const _FramePhaseBlock({ + Key? key, + required this.blockData, + required this.width, + }) : super(key: key); + + static const _height = 30.0; + + static const minBlockWidth = defaultIconSizeBeforeScaling + densePadding * 8; + + static const _backgroundColor = ThemedColor( + light: Color(0xFFEEEEEE), + dark: Color(0xFF3C4043), + ); + + final _FramePhaseBlockData blockData; + + final double width; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + return DevToolsTooltip( + message: blockData.display, + child: Container( + decoration: BoxDecoration( + color: _backgroundColor.colorFor(colorScheme), + border: Border.all(color: theme.focusColor), + ), + height: _height, + width: width, + padding: const EdgeInsets.symmetric(horizontal: densePadding), + child: LayoutBuilder( + builder: (context, constraints) { + final minWidthForText = defaultIconSize + + densePadding * 2 + + denseSpacing + + calculateTextSpanWidth(TextSpan(text: blockData.display)); + bool includeText = true; + if (constraints.maxWidth < minWidthForText) { + includeText = false; + } + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + blockData.icon, + size: defaultIconSize, + ), + if (includeText) ...[ + const SizedBox(width: denseSpacing), + Text( + blockData.display, + overflow: TextOverflow.ellipsis, + ), + ] + ], + ); + }, + ), + ), + ); + } +} + +class _FramePhaseBlockData { + _FramePhaseBlockData({ + required this.title, + required this.duration, + required this.flex, + required this.icon, + }); + + final String title; + + final Duration duration; + + final int flex; + + final IconData icon; + + String get display { + final durationText = duration != Duration.zero + ? msText( + duration, + allowRoundingToZero: false, + ) + : '--'; + return '$title - $durationText'; + } +} diff --git a/packages/devtools_app/test/performance/frame_analysis_model_test.dart b/packages/devtools_app/test/performance/frame_analysis/frame_analysis_model_test.dart similarity index 69% rename from packages/devtools_app/test/performance/frame_analysis_model_test.dart rename to packages/devtools_app/test/performance/frame_analysis/frame_analysis_model_test.dart index 132091bf64c..13454c542ee 100644 --- a/packages/devtools_app/test/performance/frame_analysis_model_test.dart +++ b/packages/devtools_app/test/performance/frame_analysis/frame_analysis_model_test.dart @@ -6,7 +6,7 @@ import 'package:devtools_app/src/screens/performance/panes/frame_analysis/frame_ import 'package:devtools_app/src/screens/performance/performance_model.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../test_data/performance.dart'; +import '../../test_data/performance.dart'; void main() { group('FrameAnalysis', () { @@ -77,5 +77,34 @@ void main() { frameAnalysis = FrameAnalysis(frame); expect(frameAnalysis.hasExpensiveOperations, isFalse); }); + + test('calculateFramePhaseFlexValues', () { + expect(frameAnalysis.buildFlex, isNull); + expect(frameAnalysis.layoutFlex, isNull); + expect(frameAnalysis.paintFlex, isNull); + expect(frameAnalysis.rasterFlex, isNull); + expect(frameAnalysis.shaderCompilationFlex, isNull); + + frameAnalysis.calculateFramePhaseFlexValues(); + + expect(frameAnalysis.buildFlex, equals(29)); + expect(frameAnalysis.layoutFlex, equals(45)); + expect(frameAnalysis.paintFlex, equals(26)); + expect(frameAnalysis.rasterFlex, equals(1)); + expect(frameAnalysis.shaderCompilationFlex, isNull); + + frame = testFrame0.shallowCopy() + ..setEventFlow(goldenUiTimelineEvent) + ..setEventFlow(rasterTimelineEventWithSubtleShaderJank); + frameAnalysis = FrameAnalysis(frame); + + frameAnalysis.calculateFramePhaseFlexValues(); + + expect(frameAnalysis.buildFlex, equals(29)); + expect(frameAnalysis.layoutFlex, equals(45)); + expect(frameAnalysis.paintFlex, equals(26)); + expect(frameAnalysis.rasterFlex, equals(67)); + expect(frameAnalysis.shaderCompilationFlex, equals(33)); + }); }); } diff --git a/packages/devtools_app/test/performance/frame_analysis/frame_analysis_test.dart b/packages/devtools_app/test/performance/frame_analysis/frame_analysis_test.dart new file mode 100644 index 00000000000..3813af26f9c --- /dev/null +++ b/packages/devtools_app/test/performance/frame_analysis/frame_analysis_test.dart @@ -0,0 +1,190 @@ +// Copyright 2022 The Chromium 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:devtools_app/src/config_specific/ide_theme/ide_theme.dart'; +import 'package:devtools_app/src/config_specific/import_export/import_export.dart'; +import 'package:devtools_app/src/screens/performance/panes/frame_analysis/frame_analysis.dart'; +import 'package:devtools_app/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart'; +import 'package:devtools_app/src/screens/performance/panes/frame_analysis/frame_hints.dart'; +import 'package:devtools_app/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart'; +import 'package:devtools_app/src/screens/performance/performance_controller.dart'; +import 'package:devtools_app/src/screens/performance/performance_model.dart'; +import 'package:devtools_app/src/service/service_manager.dart'; +import 'package:devtools_app/src/shared/globals.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../matchers/matchers.dart'; +import '../../test_data/performance.dart'; + +void main() { + const windowSize = Size(4000.0, 1000.0); + + group('FlutterFrameAnalysisView', () { + late FlutterFrame frame; + late FrameAnalysis frameAnalysis; + late MockEnhanceTracingController mockEnhanceTracingController; + + setUp(() { + frame = testFrame0.shallowCopy() + ..setEventFlow(goldenUiTimelineEvent) + ..setEventFlow(goldenRasterTimelineEvent); + frameAnalysis = FrameAnalysis(frame); + mockEnhanceTracingController = MockEnhanceTracingController(); + setGlobal(IdeTheme, IdeTheme()); + setGlobal(OfflineModeController, OfflineModeController()); + setGlobal(ServiceConnectionManager, FakeServiceManager()); + }); + + Future pumpAnalysisView( + WidgetTester tester, + FrameAnalysis? analysis, + ) async { + await tester.pumpWidget( + wrapWithControllers( + FlutterFrameAnalysisView( + frameAnalysis: analysis, + enhanceTracingController: mockEnhanceTracingController, + ), + performance: PerformanceController(), + ), + ); + expect(find.byType(FlutterFrameAnalysisView), findsOneWidget); + } + + testWidgetsWithWindowSize('builds with null data', windowSize, + (WidgetTester tester) async { + await pumpAnalysisView(tester, null); + + expect( + find.text('No analysis data available for this frame.'), + findsOneWidget, + ); + expect(find.byType(FrameHints), findsNothing); + expect(find.byType(FrameTimeVisualizer), findsNothing); + }); + + testWidgetsWithWindowSize('builds with non-null data', windowSize, + (WidgetTester tester) async { + await pumpAnalysisView(tester, frameAnalysis); + + expect( + find.text('No analysis data available for this frame.'), + findsNothing, + ); + expect(find.byType(FrameHints), findsOneWidget); + expect(find.byType(FrameTimeVisualizer), findsOneWidget); + }); + + group('FrameHints', () { + // TODO(kenz): write tests for FrameHints widget. + }); + + group('FrameTimeVisualizer', () { + Future pumpVisualizer( + WidgetTester tester, + FrameAnalysis frameAnalysis, + ) async { + await tester.pumpWidget( + wrap(FrameTimeVisualizer(frameAnalysis: frameAnalysis)), + ); + expect(find.byType(FrameTimeVisualizer), findsOneWidget); + } + + testWidgetsWithWindowSize('builds successfully', windowSize, + (WidgetTester tester) async { + await pumpVisualizer(tester, frameAnalysis); + + expect(find.text('UI phases:'), findsOneWidget); + expect(find.textContaining('Build - '), findsOneWidget); + expect(find.textContaining('Layout - '), findsOneWidget); + expect(find.textContaining('Paint - '), findsOneWidget); + expect(find.byIcon(Icons.build), findsOneWidget); + expect(find.byIcon(Icons.auto_awesome_mosaic), findsOneWidget); + expect(find.byIcon(Icons.format_paint), findsOneWidget); + + expect(find.text('Raster phase:'), findsOneWidget); + expect(find.textContaining('Raster - '), findsOneWidget); + expect(find.byIcon(Icons.grid_on), findsOneWidget); + + expect(find.text('Raster phases:'), findsNothing); + expect(find.textContaining('Shader compilation'), findsNothing); + expect(find.textContaining('Other raster'), findsNothing); + expect(find.byIcon(Icons.image_outlined), findsNothing); + + await expectLater( + find.byType(FrameTimeVisualizer), + matchesDevToolsGolden( + 'goldens/performance/frame_time_visualizer.png', + ), + ); + }); + + testWidgetsWithWindowSize( + 'builds with icons only for narrow screen', const Size(200.0, 1000.0), + (WidgetTester tester) async { + await pumpVisualizer(tester, frameAnalysis); + + expect(find.text('UI phases:'), findsOneWidget); + expect(find.textContaining('Build - '), findsNothing); + expect(find.textContaining('Layout - '), findsNothing); + expect(find.textContaining('Paint - '), findsNothing); + expect(find.byIcon(Icons.build), findsOneWidget); + expect(find.byIcon(Icons.auto_awesome_mosaic), findsOneWidget); + expect(find.byIcon(Icons.format_paint), findsOneWidget); + + expect(find.text('Raster phase:'), findsOneWidget); + expect(find.textContaining('Raster - '), findsNothing); + expect(find.byIcon(Icons.grid_on), findsOneWidget); + + expect(find.text('Raster phases:'), findsNothing); + expect(find.textContaining('Shader compilation'), findsNothing); + expect(find.textContaining('Other raster'), findsNothing); + expect(find.byIcon(Icons.image_outlined), findsNothing); + + await expectLater( + find.byType(FrameTimeVisualizer), + matchesDevToolsGolden( + 'goldens/performance/frame_time_visualizer_icons_only.png', + ), + ); + }); + + testWidgetsWithWindowSize( + 'builds for frame with shader compilation', windowSize, + (WidgetTester tester) async { + frame = testFrame0.shallowCopy() + ..setEventFlow(goldenUiTimelineEvent) + ..setEventFlow(rasterTimelineEventWithSubtleShaderJank); + frameAnalysis = FrameAnalysis(frame); + await pumpVisualizer(tester, frameAnalysis); + + expect(find.text('UI phases:'), findsOneWidget); + expect(find.textContaining('Build - '), findsOneWidget); + expect(find.textContaining('Layout - '), findsOneWidget); + expect(find.textContaining('Paint - '), findsOneWidget); + expect(find.byIcon(Icons.build), findsOneWidget); + expect(find.byIcon(Icons.auto_awesome_mosaic), findsOneWidget); + expect(find.byIcon(Icons.format_paint), findsOneWidget); + + expect(find.text('Raster phase:'), findsNothing); + expect(find.textContaining('Raster - '), findsNothing); + expect(find.byIcon(Icons.grid_on), findsOneWidget); + + expect(find.text('Raster phases:'), findsOneWidget); + expect(find.textContaining('Shader compilation - '), findsOneWidget); + expect(find.textContaining('Other raster - '), findsOneWidget); + expect(find.byIcon(Icons.image_outlined), findsOneWidget); + + await expectLater( + find.byType(FrameTimeVisualizer), + matchesDevToolsGolden( + 'goldens/performance/frame_time_visualizer_with_shader_compilation.png', + ), + ); + }); + }); + }); +} diff --git a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer.png b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer.png new file mode 100644 index 0000000000000000000000000000000000000000..0a563e632e125bcc30ca85cfecdc5bcdbb113840 GIT binary patch literal 18925 zcmeHPdr(tX8b65gXjMAe5$%Gp+i~c$Y>_%ro{0}yOKp)-Sg8w`N*A_tc`T_!2qdxY z4BfKAOtFR5KyY{3TI})=BNNkvY#B@~k5p5dqzOroH-UtLAv_}Ny*UAsd;7-@qwYF$ z&L6oaC->a@-QW5BzVAEd9{5{a?7Jn;wsU_;Ey(eVKI)B@l>;^l$0eE*{x6Fayu z;t%cz_1%7>*q618{V@q%SSj*)`vU-WfSBlg2`72uy}{XUj6T8bd!g(d@87(9D!MfK zz?rk}pV@ImdR%q=@uzM+_2lcX$Dee6a>s|?samR*LXLF49h*Jb)HQv1rgD0a_uE@7 zsYO$>zw~Eq&I&nKx23fG#~Zpz;S|j#m%Ha5uU(?cKT8)3t^ESgpHjOPu!)Zmum`*N(V0fP^*g5n4Rl!U-)O1Ao^R z?ptBjHA~mho8J)}eXV9;-eL?=;UGH|L#Tf=cX2|~k}4?*E68qECkT>e|A7yIQ{if! zZYEfGA#-kA-F*Fk(K6rAH+VZJjusVF9od`|`14@R(4sD-hT_y8tLAQv&Cbrg!U!T< zM{3)`2y#6YNZAh(qPbDUmqNk=1G-P(#csKLSceZ4ciY#$zxe&!W6(_7#v-a0R@J5p zjtZ(+-UQ=kPLoFA6(T95JBXyH-9#kCuE60`0Lr?0dJ=toeJj#+VwO^^R?{RI;#6iq zL0AEwLStX<&Hlr4U*1vE&I;>%gHmrB{(U-TH&e_PNpwwmd5YO?oQ=afMY)z77SSpf zhW}mPz^ld|s*%FmNwIDls&8P9TNR(~0t>jAEV`&yH(z7cw_1m~&RwUm?1Ts`%c~!o zn#!A^@yvxZX0bS}l`qn5#-Cgjm0^%^ei9jt{tI{B%g5O-HGlW!R3Gmx(|n_6Q7Q2CH#@(f8Ps~e!YLG7bJ8oXeeV`OAx(26HIDuW_) zP)(-t-MMxnvik$K0mCj#&j#N@%UF;0S$nfIsBgU=UgK&)4|#ylS4)MyfSbn1%ez#u z&3=YzPlmx>(O!G=c_8hZ-Y^1k<=x%g^OJpT+IQ_gNyQ8gyLXXh@pifcKPvWQejg|l ziW+KhctN%}^@t!jcA=T`G{d443p+YfOvCoUWhhx|@Cz)A#&yT}{IQ-_tshSIzh<`T z-^JUdP2p#Ex2MtCv=&R*9d`I;5Bjm{BHQd=SZp@cx*hM?Nf#VV94x|FkCigM+!IqV zSA(en6E9KF_fNcucC#X^o1h&lpPlA`p`n+EBHN3D>{{qFFQTWtK=QQfS4E+^PdK(; zWCQ1yMOittC~I$j(Pw*PRV35bXt_#TvN5K#=e6lg zC_XjcBu^A`mLw%Ul-L?kDJGa28i!h7iZczLk0gYX&@GA%%u%?MvXAbO54uNiwdI-C zUFb?zWC}iuv|5bHV~rbJ>;;peO||AzIeLj@YOv!v<6PGlb?x8!stIv&i8e?KxQV~x zN3lW*{v=La+Vc%>X5E;KdgMf5VR%7$Q_?_~R-fEE5$1X;so8|FCH+@eloM>V>%{5& zaZi%>AP3ofB<8Iy=6}Mc7JtSf3@@0GUK+F6icC>XX5rPU8<~R4V3HQ@nZ-%nV#gh& zRMFL|W4CS{zu75bn!_U^7G&11mB#0Sf&$Fb{j%~anyik&%jK-{;`*`nQs1bPlk}UN zDg3tjhT$-qw%)oHUT4;Sg-s@PzDR6|(zkNVUFTk?;LM~qf0fykRJA~|w zNjWBG%nBV541mdqm6X7C2;<2eO-@LwL}J3o6rpqYB`lPzBvOl?|5^mW3^C~+eAIx? z@An~=PBQcJXB0|F(cC3RP{U2)FRRsX%qeJKzu4mM`Y=^SKtMpmr_0_}253}ZPj}8beg(*FGVH5qjH2%d_(`ZV%Y059ZI7;3( zoG%W5J*8&D05+nU>FH^qb@^hBBmJd|J$3EcLxko=3dw`6E>|VXD6yliCvy)BK~8t= z9REKR@O=Yfb^ZjIvqGBtH^U;c77f-4>|BJ-3Kro0B@RgFD<~tOuRsAFO?$HY#^7zf)B zG7t#F8HmH)ok&e~B#@thgg_n`;s8)=f$B<{qT?^SC7ZvG%W80xNANl zGsFVP9OCehkQose%`WX6KodGB1vH_9IILnqCsnni;qS5ky3fe}S+?~Dcfj3D-t|D- zO%_Yukwokiy8OZsnuhHFd4<@?u$Fk;XGoopF%Un548#F(2I7F!YmEFy5 z=rxs2?rCmrE?xgP_V54JzWINj$80~p?fmDTcLce+;Cc@2whi17p>yoz%a==Lu3o)* z}{j_iSe9PSFdt-L*uI4@as!X1S-&p?i-|M}RCyieMec-*iuA)LhTwHv0#Ia+W z{ZFsW&JPnhaq;5C8~5-3e|cT3`>58m4~jr7*S4BVmmX>IRTki2YE*y{+CNR--@SYH z(*4By_wTPy;*@WyjmnFy-aN_S-1+n2AJ5lR0PQY)dv>?%#%z7s9=FIcawcmCc9<#8um978#_4<>Y+binoc)YCF R4%j7O@O1TaS?83{1OT+qgjoOp literal 0 HcmV?d00001 diff --git a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_with_shader_compilation.png b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_with_shader_compilation.png new file mode 100644 index 0000000000000000000000000000000000000000..26d3368d555055b50d19b7f2bb7d4ce4ecfefa30 GIT binary patch literal 19078 zcmeI4dr(tX9>))2DIyA^9T3YyXL)p#6)Rx$XhOLYMqKudYn&N2l>BrA_3gt+e3 zb=ig8rHU?r;B;KNP>D}(Au z#=K;cy|U^7rPhZl$Cgwe4xLy0hElS{?lnru1>j{}!g+(U*1ANnDf~t2lB#u8_SPlA zfr=!Vbl}vhH{)u%=sa4X*-m6#19(#9J&cBNiPv<=t|Heh3Hz&hA~y=((ERsZFu7n=#ITe_vV1l9^MOff_d<#>D)SKF<3 zg%ym>cYRjkhiz#M@eNm*tClvykG0DhxOGTOw7XJo{mZY5%oahF1BePAw_lK^GaCCnmfMCh=?~F*rftu(L6Ew0lsWDC|pR zwPoiJrX*lzU15pSuC}xd)k~w&{tc8{aazjrsvLu4Qa?YN!B1|lG^U*)ETX96s4zA$ zA>oMGMIjo(5|bwfCKYu$f&$3FJm$XP&8p`aoore~1;J}-d-fGpD79;2_?e-*dQst} z$@%$z9p@*h@_s_J2Td;v1*B2Td8c&&a4`b6blQUt4wW#bWS=CnRpaC7ozQ9plCut^ z7YKBrhaEuZk)mdIvWss&QfQ9P$^&bz5Qf^R9PTWssHiZ>`}Y*g-yS|qo?E64wI{t% z#^n0h15bY(r7=7xxsA(wfvhmMnR}je&Z#ir-|WEJ-nZ2+0NDVS%T>MuMI@)kBb2Nv z9S-yWvMK$VF_6`X&o~3uBXI`#m7s!d1%QQHe5h-6b#)lqm^UqEwAQ6JbG0WIqDRL| znDp{%BC+*cX?w48uX}eV{9;%8{pQ#Hgy|GJ3G%WNUftKes^_IzCUKShEq>&-prAVn zg@Qgw)DSqw)yBpOFOs7GvblVkBYw!j(i_r4bn9U0;-~V(CvdyHR6Ps#qv~0TtqSyY z$>s7Kwcp~o;K5aCS5MEMZCv}gDogB73r(~AUHheN1sN@p#)NG>CY}Q0;Q^%;k0$j= ze1qza)F1QeHtp6bD9UXZ9l=FN^w0e;4n4%xRNGwhm{|1q*1u+4Uu{i9aMUIiJUaz_ zlBql)Yy}O2r{ZtMl`8fWnvM32lsLeSTQL(hCBI*mbT=;~Kl9_kn1&N`2ep*TDy&YR zgxsncd1*kDzst@33EF#Qz3lVz^;m30Wo2aLn}KfmYc*J(iW-VMi%QGO%T?mT)}4dk z_6{+HBe>N= zWfQ_rvk9egO-M|fHR?4H+s{bbR7$1Nr#-DTDfjHzkbE9q(jduvYg@JH6~b)Ou*!3| z(MN2*IPE#!M3dHIeVIII9Y0s8qxUmckmH<^=3$d22^M;M=&)3t;B`;2b@B#b5lu{7 z(&qLyid4Dniiv)HE8E1F=CuxEF@wE#{EMXTi{Nl85&=nO(Pb zSaVtT8F5l^7%odkW=4C;+fD8mpU-E`j`tP+91aKobFt1yjxCrF5`V_b*ijen-E;T( ziHV7-yqC$ZaH-_wVJ_rxZ{TQ>rSQ>?V|tRD?5P4&O+4;E!1M1VKD3;i9Bo;$Vdm@J z;yaI=$b89q@cL=Fc1 zi=y-qbpQ;h&o&dYow4ze>FMlUHj(c?lOv;}uiHen=bENG-Hn3nCEDoT^Sp0C!&$WM z$K2H+I*1N<$`t`*JIHpB?JU{=ez08YHOR9TsV+y%GIzFlt;c)ZVhlRCL!~qHp;T;_cFDSU=_7rG#EJHhz#Takj+34$eSRCaHKO31X5tg zA=)N}ATo#yBw+bO`3wZ{J;~L8!$v$ggzo|n4UlIc`>_y!yYI0dq7t$h3o)cQ$Yw02 zK<>~DorMOvp@SfP@D1HbX)*DH+STD^VY<)BaPo=ihbyV?y+u;r_^4V(eR(I;j`BF1 z^9Hq%%%#Z3qIT5Rd~( zAuNu8QV0kFr4Sa!Kq&+Sfl>&_0i_U-147ki(DH9I_vzGY}`z8OQ Date: Mon, 1 Aug 2022 16:33:28 -0700 Subject: [PATCH 3/4] guard against naan --- .../performance/panes/frame_analysis/frame_analysis_model.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart index 43475e2f925..328c56693e7 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_analysis_model.dart @@ -228,6 +228,7 @@ class FrameAnalysis { } int _calculateFlex(int numeratorMicros, int denominatorMicros) { + if (numeratorMicros == 0 && denominatorMicros == 0) return 1; return ((numeratorMicros / denominatorMicros) * 100).round(); } } From a0fa2967b8feecc2b89658afdf4695ba94028506 Mon Sep 17 00:00:00 2001 From: Kenzie Schmoll Date: Tue, 2 Aug 2022 09:17:43 -0700 Subject: [PATCH 4/4] address review comments --- .../frame_analysis/frame_time_visualizer.dart | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart index b6776dc003f..9457acfb135 100644 --- a/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart +++ b/packages/devtools_app/lib/src/screens/performance/panes/frame_analysis/frame_time_visualizer.dart @@ -191,7 +191,7 @@ class _FrameBlockGroup extends StatelessWidget { /// /// The adjusted widths will ensure each block is at least /// [_FramePhaseBlock.minBlockWidth] wide, and will modify surrounding block - /// widths to accomodate. f + /// widths to accomodate. List adjustedWidthsForBlocks( BoxConstraints constraints, int totalFlex, @@ -215,16 +215,16 @@ class _FrameBlockGroup extends StatelessWidget { } } - final adjustedBlockWidths = []; - for (var i = 0; i < unadjustedBlockWidths.length; i++) { - final blockWidth = unadjustedBlockWidths[i]; - var adjustedWidth = blockWidth; - if (i == widestBlockIndex) { - adjustedWidth -= adjustment; - } - adjustedWidth = math.max(adjustedWidth, _FramePhaseBlock.minBlockWidth); - adjustedBlockWidths.add(adjustedWidth); - } + final adjustedBlockWidths = unadjustedBlockWidths + .map( + (blockWidth) => math.max(blockWidth, _FramePhaseBlock.minBlockWidth), + ) + .toList(); + final widest = adjustedBlockWidths[widestBlockIndex]; + adjustedBlockWidths[widestBlockIndex] = math.max( + widest - adjustment, + _FramePhaseBlock.minBlockWidth, + ); return adjustedBlockWidths; }