mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
at main 248 lines 7.4 kB view raw
1import 'package:flutter/material.dart'; 2import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 4import '../application/performance_monitor_notifier.dart'; 5 6/// A tab in the debug overlay that displays performance diagnostics. 7class PerformanceMetricsTab extends ConsumerWidget { 8 const PerformanceMetricsTab({super.key}); 9 10 @override 11 Widget build(BuildContext context, WidgetRef ref) { 12 final state = ref.watch(performanceMonitorProvider); 13 final theme = Theme.of(context); 14 15 return ListView( 16 padding: const EdgeInsets.all(16), 17 children: [ 18 _buildSection( 19 theme: theme, 20 title: 'Frame Rate', 21 child: _MetricCard( 22 label: 'Current FPS', 23 value: '${state.fps.toStringAsFixed(1)} fps', 24 color: _getFpsColor(state.fps, theme), 25 ), 26 ), 27 const SizedBox(height: 16), 28 _buildSection( 29 theme: theme, 30 title: 'Image Cache', 31 items: [ 32 _InfoItem(label: 'Live Images', value: '${state.imageCacheLiveCount}'), 33 _InfoItem(label: 'Pending Images', value: '${state.imageCachePendingCount}'), 34 _InfoItem(label: 'Cache Size', value: _formatBytes(state.imageCacheByteCount)), 35 ], 36 ), 37 const SizedBox(height: 16), 38 _buildSection( 39 theme: theme, 40 title: 'Widget Rebuilds', 41 trailing: IconButton( 42 icon: const Icon(Icons.refresh, size: 20), 43 onPressed: () => ref.read(performanceMonitorProvider.notifier).resetRebuildCounts(), 44 tooltip: 'Reset Counts', 45 ), 46 child: state.rebuildCounts.isEmpty 47 ? _buildEmptyState(theme, 'No rebuilds tracked yet.') 48 : Column( 49 children: state.rebuildCounts.entries.map((e) { 50 return _InfoRow( 51 label: e.key, 52 value: '${e.value}', 53 showDivider: e.key != state.rebuildCounts.keys.last, 54 theme: theme, 55 ); 56 }).toList(), 57 ), 58 ), 59 const SizedBox(height: 16), 60 _buildSection( 61 theme: theme, 62 title: 'Database Performance', 63 child: state.queryTimes.isEmpty 64 ? _buildEmptyState(theme, 'No queries logged.') 65 : Column( 66 children: [ 67 _MetricCard( 68 label: 'Avg Query Time', 69 value: '${_calculateAvg(state.queryTimes).toStringAsFixed(2)} ms', 70 ), 71 const SizedBox(height: 8), 72 _MetricCard( 73 label: 'Max Query Time', 74 value: '${_calculateMax(state.queryTimes).toStringAsFixed(2)} ms', 75 ), 76 ], 77 ), 78 ), 79 ], 80 ); 81 } 82 83 Widget _buildSection({ 84 required ThemeData theme, 85 required String title, 86 Widget? child, 87 List<_InfoItem>? items, 88 Widget? trailing, 89 }) { 90 return Column( 91 crossAxisAlignment: CrossAxisAlignment.start, 92 children: [ 93 Row( 94 mainAxisAlignment: MainAxisAlignment.spaceBetween, 95 children: [ 96 Text( 97 title, 98 style: theme.textTheme.labelMedium?.copyWith( 99 color: theme.colorScheme.primary, 100 fontWeight: FontWeight.w600, 101 ), 102 ), 103 if (trailing != null) trailing, 104 ], 105 ), 106 const SizedBox(height: 8), 107 Container( 108 decoration: BoxDecoration( 109 color: theme.colorScheme.surfaceContainerLow, 110 borderRadius: BorderRadius.circular(8), 111 ), 112 child: 113 child ?? 114 Column( 115 children: items!.map((item) { 116 final isLast = item == items.last; 117 return _InfoRow( 118 label: item.label, 119 value: item.value, 120 showDivider: !isLast, 121 theme: theme, 122 ); 123 }).toList(), 124 ), 125 ), 126 ], 127 ); 128 } 129 130 Widget _buildEmptyState(ThemeData theme, String message) { 131 return Padding( 132 padding: const EdgeInsets.all(12), 133 child: Text( 134 message, 135 style: theme.textTheme.bodySmall?.copyWith( 136 color: theme.colorScheme.onSurfaceVariant, 137 fontStyle: FontStyle.italic, 138 ), 139 ), 140 ); 141 } 142 143 Color? _getFpsColor(double fps, ThemeData theme) { 144 if (fps >= 55) return Colors.green; 145 if (fps >= 30) return Colors.orange; 146 return theme.colorScheme.error; 147 } 148 149 double _calculateAvg(List<double> values) { 150 if (values.isEmpty) return 0; 151 return values.reduce((a, b) => a + b) / values.length; 152 } 153 154 double _calculateMax(List<double> values) { 155 if (values.isEmpty) return 0; 156 return values.reduce((a, b) => a > b ? a : b); 157 } 158 159 String _formatBytes(int bytes) { 160 if (bytes < 1024) return '$bytes B'; 161 if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB'; 162 return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB'; 163 } 164} 165 166class _MetricCard extends StatelessWidget { 167 const _MetricCard({required this.label, required this.value, this.color}); 168 final String label; 169 final String value; 170 final Color? color; 171 172 @override 173 Widget build(BuildContext context) { 174 final theme = Theme.of(context); 175 return Padding( 176 padding: const EdgeInsets.all(12), 177 child: Row( 178 mainAxisAlignment: MainAxisAlignment.spaceBetween, 179 children: [ 180 Text(label, style: theme.textTheme.bodyMedium), 181 Text( 182 value, 183 style: theme.textTheme.titleMedium?.copyWith( 184 color: color ?? theme.colorScheme.onSurface, 185 fontWeight: FontWeight.bold, 186 fontFamily: 'monospace', 187 ), 188 ), 189 ], 190 ), 191 ); 192 } 193} 194 195class _InfoRow extends StatelessWidget { 196 const _InfoRow({ 197 required this.label, 198 required this.value, 199 required this.showDivider, 200 required this.theme, 201 }); 202 final String label; 203 final String value; 204 final bool showDivider; 205 final ThemeData theme; 206 207 @override 208 Widget build(BuildContext context) { 209 return Column( 210 children: [ 211 Padding( 212 padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), 213 child: Row( 214 mainAxisAlignment: MainAxisAlignment.spaceBetween, 215 children: [ 216 Text( 217 label, 218 style: theme.textTheme.bodyMedium?.copyWith( 219 color: theme.colorScheme.onSurfaceVariant, 220 ), 221 ), 222 Text( 223 value, 224 style: theme.textTheme.bodyMedium?.copyWith( 225 fontFamily: 'monospace', 226 fontWeight: FontWeight.w500, 227 ), 228 ), 229 ], 230 ), 231 ), 232 if (showDivider) 233 Divider( 234 height: 1, 235 indent: 12, 236 endIndent: 12, 237 color: theme.colorScheme.outline.withValues(alpha: 0.1), 238 ), 239 ], 240 ); 241 } 242} 243 244class _InfoItem { 245 const _InfoItem({required this.label, required this.value}); 246 final String label; 247 final String value; 248}