console_plus 2.0.2
console_plus: ^2.0.2 copied to clipboard
A Flutter plugin that provides an elegant in-app developer console with floating button.
example/lib/main.dart
import 'dart:async';
import 'package:console_plus/console_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
Future<void> main() async {
await ConsolePlus.initApp(
const ConsolePlusExampleApp(),
interceptPrints: true,
captureFlutterErrors: true,
capturePlatformErrors: true,
);
}
class ConsolePlusExampleApp extends StatelessWidget {
const ConsolePlusExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ConsolePlus Benchmark',
theme: ThemeData.dark(useMaterial3: true),
home: const BenchmarkPage(),
);
}
}
class BenchmarkPage extends StatefulWidget {
const BenchmarkPage({super.key});
@override
State<BenchmarkPage> createState() => _BenchmarkPageState();
}
class _BenchmarkPageState extends State<BenchmarkPage> {
static const String _payload = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_';
bool _running = false;
BenchmarkResult? _result1k;
BenchmarkResult? _result5k;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await DebugLogConsole.setSavedQueryProfile('benchmark-example');
DebugLogConsole.registerCustomAction(
ConsoleCustomAction(
id: 'benchmark_note',
label: 'Add Benchmark Note',
icon: Icons.note_add_outlined,
onExecute: (context, filteredLogs) async {
DebugLogConsole.addLog(
"Benchmark note: filtered log count = ${filteredLogs.length}",
type: LogType.info,
);
},
),
);
if (!mounted) return;
FloatingDebugButton.show(context);
});
}
Future<void> _runBenchmark(int totalLogs) async {
if (_running) return;
setState(() => _running = true);
final timings = <FrameTiming>[];
void frameCollector(List<FrameTiming> incoming) {
timings.addAll(incoming);
}
SchedulerBinding.instance.addTimingsCallback(frameCollector);
try {
final firstUpdate = Completer<int>();
late final VoidCallback listener;
final expectedVisible = totalLogs > DebugLogConsole.maxLogCount
? DebugLogConsole.maxLogCount
: totalLogs;
final startedAt = DateTime.now();
var firstSeen = false;
listener = () {
final current = DebugLogConsole.logs.length;
if (!firstSeen && current > 0) {
firstSeen = true;
firstUpdate.complete(
DateTime.now().difference(startedAt).inMilliseconds,
);
}
};
DebugLogConsole.clear();
DebugLogConsole.logsNotifier.addListener(listener);
final burstWatch = Stopwatch()..start();
for (int i = 0; i < totalLogs; i++) {
DebugLogConsole.addLog(
"benchmark-$totalLogs-$i $_payload $_payload $_payload",
type: i % 17 == 0
? LogType.error
: (i % 7 == 0 ? LogType.warning : LogType.info),
);
}
burstWatch.stop();
final firstUpdateMs = await firstUpdate.future.timeout(
const Duration(seconds: 5),
onTimeout: () => -1,
);
final settleWatch = Stopwatch()..start();
while (DebugLogConsole.logs.length < expectedVisible &&
settleWatch.elapsed < const Duration(seconds: 6)) {
await Future<void>.delayed(const Duration(milliseconds: 16));
}
settleWatch.stop();
DebugLogConsole.logsNotifier.removeListener(listener);
await Future<void>.delayed(const Duration(milliseconds: 600));
final fps = _estimateFps(timings);
final result = BenchmarkResult(
totalLogs: totalLogs,
mode: DebugLogConsole.highPerformanceMode
? 'High-performance'
: 'Full-text',
enqueueMs: burstWatch.elapsedMilliseconds,
firstUiUpdateMs: firstUpdateMs,
settleMs: settleWatch.elapsedMilliseconds,
estimatedFps: fps,
);
setState(() {
if (totalLogs == 1000) {
_result1k = result;
} else {
_result5k = result;
}
});
} finally {
SchedulerBinding.instance.removeTimingsCallback(frameCollector);
setState(() => _running = false);
}
}
double _estimateFps(List<FrameTiming> timings) {
if (timings.isEmpty) return 0;
final totalMicros = timings
.map((t) => t.totalSpan.inMicroseconds)
.reduce((a, b) => a + b);
final avgMicros = totalMicros / timings.length;
if (avgMicros <= 0) return 0;
return 1000000 / avgMicros;
}
@override
Widget build(BuildContext context) {
final highPerformanceMode = DebugLogConsole.highPerformanceMode;
return Scaffold(
appBar: AppBar(title: const Text('ConsolePlus Benchmark')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('High Performance Mode'),
subtitle: Text(
highPerformanceMode
? 'Virtualized log list (faster, per-line selection)'
: 'Full-text mode (single selectable block)',
),
value: highPerformanceMode,
onChanged: _running
? null
: (value) {
setState(() {
DebugLogConsole.highPerformanceMode = value;
});
},
),
const SizedBox(height: 8),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
ElevatedButton(
onPressed: _running ? null : () => _runBenchmark(1000),
child: const Text('Run 1k Benchmark'),
),
ElevatedButton(
onPressed: _running ? null : () => _runBenchmark(5000),
child: const Text('Run 5k Benchmark'),
),
OutlinedButton(
onPressed: _running
? null
: () {
setState(() {
_result1k = null;
_result5k = null;
});
DebugLogConsole.clear();
},
child: const Text('Reset'),
),
],
),
const SizedBox(height: 12),
if (_running) const LinearProgressIndicator(),
const SizedBox(height: 16),
_ResultCard(title: '1k Result', result: _result1k),
const SizedBox(height: 10),
_ResultCard(title: '5k Result', result: _result5k),
const SizedBox(height: 12),
const Text(
'Tip: open the floating console while running to see real UI impact.',
style: TextStyle(color: Colors.white70),
),
],
),
),
);
}
}
class _ResultCard extends StatelessWidget {
final String title;
final BenchmarkResult? result;
const _ResultCard({required this.title, required this.result});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: result == null
? Text('$title: not run yet')
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$title (${result!.mode})'),
const SizedBox(height: 6),
Text('Logs: ${result!.totalLogs}'),
Text('Enqueue time: ${result!.enqueueMs} ms'),
Text('First UI update: ${result!.firstUiUpdateMs} ms'),
Text('Settle time: ${result!.settleMs} ms'),
Text(
'Estimated FPS: ${result!.estimatedFps.toStringAsFixed(1)}',
),
],
),
),
);
}
}
class BenchmarkResult {
final int totalLogs;
final String mode;
final int enqueueMs;
final int firstUiUpdateMs;
final int settleMs;
final double estimatedFps;
const BenchmarkResult({
required this.totalLogs,
required this.mode,
required this.enqueueMs,
required this.firstUiUpdateMs,
required this.settleMs,
required this.estimatedFps,
});
}