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..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 @@ -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,46 @@ 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) { + if (numeratorMicros == 0 && denominatorMicros == 0) return 1; + return ((numeratorMicros / denominatorMicros) * 100).round(); + } } enum FramePhaseType { @@ -253,7 +268,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..9457acfb135 --- /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. + 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 = unadjustedBlockWidths + .map( + (blockWidth) => math.max(blockWidth, _FramePhaseBlock.minBlockWidth), + ) + .toList(); + final widest = adjustedBlockWidths[widestBlockIndex]; + adjustedBlockWidths[widestBlockIndex] = math.max( + widest - adjustment, + _FramePhaseBlock.minBlockWidth, + ); + + 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 00000000000..0a563e632e1 Binary files /dev/null and b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer.png differ diff --git a/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_icons_only.png b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_icons_only.png new file mode 100644 index 00000000000..57107461e10 Binary files /dev/null and b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_icons_only.png differ 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 00000000000..26d3368d555 Binary files /dev/null and b/packages/devtools_app/test/performance/frame_analysis/goldens/performance/frame_time_visualizer_with_shader_compilation.png differ diff --git a/packages/devtools_test/lib/src/mocks/generated.dart b/packages/devtools_test/lib/src/mocks/generated.dart index 002f3f2a1ef..4ae6b947847 100644 --- a/packages/devtools_test/lib/src/mocks/generated.dart +++ b/packages/devtools_test/lib/src/mocks/generated.dart @@ -13,6 +13,7 @@ import 'package:vm_service/vm_service.dart'; @GenerateMocks([ ConnectedApp, DebuggerController, + EnhanceTracingController, ErrorBadgeManager, HeapSnapshotGraph, PerformanceController,