Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add frame numbers to the flutter frames chart in the performance page #3526

Merged
merged 1 commit into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/devtools_app/lib/src/common_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:flutter/material.dart';

import 'analytics/analytics.dart' as ga;
import 'config_specific/launch_url/launch_url.dart';
import 'flutter_widgets/linked_scroll_controller.dart';
import 'globals.dart';
import 'scaffold.dart';
import 'theme.dart';
Expand Down Expand Up @@ -1196,6 +1197,39 @@ extension ScrollControllerAutoScroll on ScrollController {
}
}

/// An extension on [LinkedScrollControllerGroup] to facilitate having the
/// scrolling widgets auto scroll to the bottom on new content.
///
/// This extension serves the same function as the [ScrollControllerAutoScroll]
/// extension above, but we need to implement these methods again as an
/// extension on [LinkedScrollControllerGroup] because individual
/// [ScrollController]s are intentionally inaccessible from
/// [LinkedScrollControllerGroup].
extension LinkedScrollControllerGroupExtension on LinkedScrollControllerGroup {
bool get atScrollBottom {
final pos = position;
return pos.pixels == pos.maxScrollExtent;
}

/// Scroll the content to the bottom using the app's default animation
/// duration and curve..
void autoScrollToBottom() async {
await animateTo(
position.maxScrollExtent,
duration: rapidDuration,
curve: defaultCurve,
);

// Scroll again if we've received new content in the interim.
if (hasAttachedControllers) {
final pos = position;
if (pos.pixels != pos.maxScrollExtent) {
jumpTo(pos.maxScrollExtent);
}
}
}
}

/// Utility extension methods to the [Color] class.
extension ColorExtension on Color {
/// Return a slightly darker color than the current color.
Expand Down
109 changes: 74 additions & 35 deletions packages/devtools_app/lib/src/performance/flutter_frames_chart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../analytics/constants.dart' as analytics_constants;
import '../auto_dispose_mixin.dart';
import '../banner_messages.dart';
import '../common_widgets.dart';
import '../flutter_widgets/linked_scroll_controller.dart';
import '../globals.dart';
import '../scaffold.dart';
import '../theme.dart';
Expand Down Expand Up @@ -51,31 +52,35 @@ class _FlutterFramesChartState extends State<FlutterFramesChart>

static const outlineBorderWidth = 1.0;

double get frameNumberSectionHeight => scaleByFontFactor(20.0);

double get frameChartScrollbarOffset =>
defaultScrollBarOffset + frameNumberSectionHeight;

PerformanceController _controller;

ScrollController scrollController;
LinkedScrollControllerGroup linkedScrollControllerGroup;

FlutterFrame _selectedFrame;
ScrollController framesScrollController;

double horizontalScrollOffset = 0.0;
ScrollController frameNumbersScrollController;

double get availableChartHeight => defaultChartHeight - defaultSpacing;
FlutterFrame _selectedFrame;

/// Milliseconds per pixel value for the y-axis.
///
/// This value will result in a y-axis time range spanning two times the
/// target frame time for a single frame (e.g. 16.6 * 2 for a 60 FPS device).
double get msPerPx =>
// Multiply by two to reach two times the target frame time.
1 / widget.displayRefreshRate * 1000 * 2 / availableChartHeight;
1 / widget.displayRefreshRate * 1000 * 2 / defaultChartHeight;

@override
void initState() {
super.initState();
scrollController = ScrollController()
..addListener(() {
horizontalScrollOffset = scrollController.offset;
});
linkedScrollControllerGroup = LinkedScrollControllerGroup();
framesScrollController = linkedScrollControllerGroup.addAndGet();
frameNumbersScrollController = linkedScrollControllerGroup.addAndGet();
}

@override
Expand All @@ -99,8 +104,9 @@ class _FlutterFramesChartState extends State<FlutterFramesChart>
@override
void didUpdateWidget(FlutterFramesChart oldWidget) {
super.didUpdateWidget(oldWidget);
if (scrollController.hasClients && scrollController.atScrollBottom) {
scrollController.autoScrollToBottom();
if (linkedScrollControllerGroup.hasAttachedControllers &&
linkedScrollControllerGroup.atScrollBottom) {
linkedScrollControllerGroup.autoScrollToBottom();
}

if (!collectionEquals(oldWidget.frames, widget.frames)) {
Expand Down Expand Up @@ -131,25 +137,26 @@ class _FlutterFramesChartState extends State<FlutterFramesChart>

@override
void dispose() {
scrollController.dispose();
framesScrollController.dispose();
frameNumbersScrollController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(
margin: const EdgeInsets.only(
left: denseSpacing,
right: denseSpacing,
bottom: defaultSpacing,
bottom: denseSpacing,
),
height: defaultChartHeight + defaultScrollBarOffset,
height: defaultChartHeight + frameChartScrollbarOffset,
child: Row(
children: [
Expanded(child: _buildChart()),
const SizedBox(width: defaultSpacing),
Padding(
padding: const EdgeInsets.only(bottom: defaultScrollBarOffset),
padding: EdgeInsets.only(bottom: frameChartScrollbarOffset),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down Expand Up @@ -185,12 +192,12 @@ class _FlutterFramesChartState extends State<FlutterFramesChart>
final themeData = Theme.of(context);
final chart = Scrollbar(
isAlwaysShown: true,
controller: scrollController,
controller: framesScrollController,
child: Padding(
padding: const EdgeInsets.only(bottom: defaultScrollBarOffset),
padding: EdgeInsets.only(bottom: frameChartScrollbarOffset),
child: RoundedOutlinedBorder(
child: ListView.builder(
controller: scrollController,
controller: framesScrollController,
scrollDirection: Axis.horizontal,
itemCount: widget.frames.length,
itemExtent: defaultFrameWidthWithPadding,
Expand All @@ -200,20 +207,45 @@ class _FlutterFramesChartState extends State<FlutterFramesChart>
selected: widget.frames[index] == _selectedFrame,
msPerPx: msPerPx,
availableChartHeight:
availableChartHeight - 2 * outlineBorderWidth,
defaultChartHeight - 2 * outlineBorderWidth,
displayRefreshRate: widget.displayRefreshRate,
),
),
),
),
);
final frameNumbers = Container(
height: frameNumberSectionHeight,
padding: EdgeInsets.only(left: yAxisUnitsSpace),
child: ListView.builder(
controller: frameNumbersScrollController,
scrollDirection: Axis.horizontal,
itemCount: widget.frames.length,
itemExtent: defaultFrameWidthWithPadding,
shrinkWrap: true,
itemBuilder: (context, index) {
if (index % 2 == 1) {
return const SizedBox(width: defaultFrameWidthWithPadding);
}
return Center(
child: Text(
// TODO(https://github.com/flutter/flutter/issues/94896): stop
// dividing by 2 to get the proper id.
'${widget.frames[index].id ~/ 2}',
style: themeData.subtleChartTextStyle,
),
);
},
),
);
final chartAxisPainter = CustomPaint(
painter: ChartAxisPainter(
constraints: constraints,
yAxisUnitsSpace: yAxisUnitsSpace,
displayRefreshRate: widget.displayRefreshRate,
msPerPx: msPerPx,
themeData: themeData,
bottomMargin: frameChartScrollbarOffset,
),
);
final fpsLinePainter = CustomPaint(
Expand All @@ -223,11 +255,16 @@ class _FlutterFramesChartState extends State<FlutterFramesChart>
displayRefreshRate: widget.displayRefreshRate,
msPerPx: msPerPx,
themeData: themeData,
bottomMargin: frameChartScrollbarOffset,
),
);
return Stack(
children: [
chartAxisPainter,
Positioned(
top: defaultChartHeight,
child: frameNumbers,
),
Padding(
padding: EdgeInsets.only(left: yAxisUnitsSpace),
child: chart,
Expand Down Expand Up @@ -575,6 +612,7 @@ class ChartAxisPainter extends CustomPainter {
@required this.displayRefreshRate,
@required this.msPerPx,
@required this.themeData,
@required this.bottomMargin,
});

static const yAxisTickWidth = 8.0;
Expand All @@ -589,7 +627,7 @@ class ChartAxisPainter extends CustomPainter {

final ThemeData themeData;

ColorScheme get colorScheme => themeData.colorScheme;
final double bottomMargin;

@override
void paint(Canvas canvas, Size size) {
Expand All @@ -598,7 +636,7 @@ class ChartAxisPainter extends CustomPainter {
yAxisUnitsSpace,
0.0,
constraints.maxWidth - yAxisUnitsSpace,
constraints.maxHeight - defaultScrollBarOffset,
constraints.maxHeight - bottomMargin,
);

_paintYAxisLabels(canvas, chartArea);
Expand All @@ -608,7 +646,7 @@ class ChartAxisPainter extends CustomPainter {
Canvas canvas,
Rect chartArea,
) {
const yAxisLabelCount = 6;
const yAxisLabelCount = 5;
final totalMs = msPerPx * constraints.maxHeight;

// Subtract 1 because one of the labels will be 0.0 ms.
Expand Down Expand Up @@ -651,20 +689,23 @@ class ChartAxisPainter extends CustomPainter {

// Paint a tick on the axis.
final tickY = chartArea.height - timeMs / msPerPx;

// Do not draw the y axis label if it will collide with the 0.0 label or if
// it will go beyond the uper bound of the chart.
if (timeMs != 0 && (tickY > chartArea.height - 10.0 || tickY < 10.0))
return;

canvas.drawLine(
Offset(chartArea.left - yAxisTickWidth / 2, tickY),
Offset(chartArea.left + yAxisTickWidth / 2, tickY),
Paint()..color = colorScheme.chartAccentColor,
Paint()..color = themeData.colorScheme.chartAccentColor,
);

// Paint the axis label.
final textPainter = TextPainter(
text: TextSpan(
text: labelText,
style: TextStyle(
color: colorScheme.chartSubtleColor,
fontSize: chartFontSizeSmall,
),
style: themeData.subtleChartTextStyle,
),
textAlign: TextAlign.end,
textDirection: TextDirection.ltr,
Expand Down Expand Up @@ -700,6 +741,7 @@ class FPSLinePainter extends CustomPainter {
@required this.displayRefreshRate,
@required this.msPerPx,
@required this.themeData,
@required this.bottomMargin,
});

double get fpsTextSpace => scaleByFontFactor(45.0);
Expand All @@ -714,7 +756,7 @@ class FPSLinePainter extends CustomPainter {

final ThemeData themeData;

ColorScheme get colorScheme => themeData.colorScheme;
final double bottomMargin;

@override
void paint(Canvas canvas, Size size) {
Expand All @@ -723,7 +765,7 @@ class FPSLinePainter extends CustomPainter {
yAxisUnitsSpace,
0.0,
constraints.maxWidth - yAxisUnitsSpace,
constraints.maxHeight - defaultScrollBarOffset,
constraints.maxHeight - bottomMargin,
);

// Max FPS non-jank value in ms. E.g., 16.6 for 60 FPS, 8.3 for 120 FPS.
Expand All @@ -733,16 +775,13 @@ class FPSLinePainter extends CustomPainter {
canvas.drawLine(
Offset(chartArea.left, targetLineY),
Offset(chartArea.right, targetLineY),
Paint()..color = colorScheme.chartAccentColor,
Paint()..color = themeData.colorScheme.chartAccentColor,
);

final textPainter = TextPainter(
text: TextSpan(
text: '${displayRefreshRate.toStringAsFixed(0)} FPS',
style: TextStyle(
color: colorScheme.chartSubtleColor,
fontSize: chartFontSizeSmall,
),
style: themeData.subtleChartTextStyle,
),
textAlign: TextAlign.right,
textDirection: TextDirection.ltr,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ class PerformanceController extends DisposableController
if (existingTabForFrame != null) {
_selectedAnalysisTab.value = existingTabForFrame;
} else {
final newTab = FlutterFrameAnalysisTabData('Frame ${frame.id}', frame);
// TODO(https://github.com/flutter/flutter/issues/94896): stop dividing by
// 2 to get the proper id.
final newTab = FlutterFrameAnalysisTabData(
'Frame ${frame.id ~/ 2}',
frame,
);
_analysisTabs.add(newTab);
_selectedAnalysisTab.value = newTab;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/devtools_app/lib/src/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,11 @@ extension ThemeDataExtension on ThemeData {
decoration: TextDecoration.underline,
fontSize: defaultFontSize,
);

TextStyle get subtleChartTextStyle => TextStyle(
color: colorScheme.chartSubtleColor,
fontSize: chartFontSizeSmall,
);
}

const extraWideSearchTextWidth = 600.0;
Expand Down