Skip to content

Commit

Permalink
Add frame numbers to the flutter frames chart in the performance page (
Browse files Browse the repository at this point in the history
  • Loading branch information
kenzieschmoll authored Dec 9, 2021
1 parent 5c3493d commit b4e4043
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 36 deletions.
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

0 comments on commit b4e4043

Please sign in to comment.