454 lines
16 KiB
Dart
454 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../core/config/app_shape.dart';
|
|
import '../../core/storage/app_database.dart';
|
|
import '../widgets/glossy_pill.dart';
|
|
import '../widgets/small_spinner.dart';
|
|
import 'load_simulator.dart';
|
|
import 'render_stress_screen.dart';
|
|
|
|
class DebugLoadSimulationSection extends StatefulWidget {
|
|
const DebugLoadSimulationSection({super.key});
|
|
|
|
@override
|
|
State<DebugLoadSimulationSection> createState() =>
|
|
_DebugLoadSimulationSectionState();
|
|
}
|
|
|
|
class _DebugLoadSimulationSectionState
|
|
extends State<DebugLoadSimulationSection> {
|
|
final _chatIdController = TextEditingController();
|
|
bool _busy = false;
|
|
bool _loadingChats = false;
|
|
String? _result;
|
|
List<({int id, String title})> _myChats = const [];
|
|
final Set<int> _selectedForParallel = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadMyChats();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_chatIdController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<int?> _activeAccountId() async {
|
|
final profile = await AppDatabase.loadActiveProfile();
|
|
return profile?.id;
|
|
}
|
|
|
|
Future<void> _loadMyChats() async {
|
|
setState(() => _loadingChats = true);
|
|
try {
|
|
final accountId = await _activeAccountId();
|
|
if (accountId == null) throw Exception('нет активного профиля');
|
|
final rows = await AppDatabase.loadChats(accountId, includeHidden: true);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_loadingChats = false;
|
|
_myChats = rows
|
|
.map(
|
|
(r) => (
|
|
id: r['id'] as int,
|
|
title: (r['title'] as String?)?.trim().isNotEmpty == true
|
|
? r['title'] as String
|
|
: '(без названия)',
|
|
),
|
|
)
|
|
.toList();
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_loadingChats = false;
|
|
_result = 'Ошибка загрузки списка чатов: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<int?> _resolveChatId({required int accountId}) async {
|
|
final chatId = int.tryParse(_chatIdController.text.trim());
|
|
if (chatId == null) {
|
|
setState(() => _result = 'Введите числовой ID чата');
|
|
return null;
|
|
}
|
|
final exists = (await AppDatabase.loadChat(accountId, chatId)).isNotEmpty;
|
|
if (!exists) {
|
|
setState(
|
|
() => _result =
|
|
'Чат $chatId не найден в локальном кэше — выбери из списка '
|
|
'ниже или открой этот чат хотя бы раз, чтобы он закэшировался.',
|
|
);
|
|
return null;
|
|
}
|
|
return chatId;
|
|
}
|
|
|
|
Future<void> _generate(int count) async {
|
|
if (!await _confirmGeneration() || !mounted) return;
|
|
setState(() {
|
|
_busy = true;
|
|
_result = null;
|
|
});
|
|
try {
|
|
final accountId = await _activeAccountId();
|
|
if (accountId == null) throw Exception('нет активного профиля');
|
|
final chatId = await _resolveChatId(accountId: accountId);
|
|
if (chatId == null) {
|
|
setState(() => _busy = false);
|
|
return;
|
|
}
|
|
await LoadSimulator.generateMessages(
|
|
accountId: accountId,
|
|
chatId: chatId,
|
|
count: count,
|
|
);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_result =
|
|
'Добавлено $count синтетических сообщений в чат $chatId.\n'
|
|
'Переоткрой чат, чтобы увидеть их.';
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_result = 'Ошибка: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _clear() async {
|
|
setState(() {
|
|
_busy = true;
|
|
_result = null;
|
|
});
|
|
try {
|
|
final accountId = await _activeAccountId();
|
|
if (accountId == null) throw Exception('нет активного профиля');
|
|
final chatId = await _resolveChatId(accountId: accountId);
|
|
if (chatId == null) {
|
|
setState(() => _busy = false);
|
|
return;
|
|
}
|
|
await LoadSimulator.clear(accountId: accountId, chatId: chatId);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_result = 'Синтетические сообщения в чате $chatId удалены.';
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_result = 'Ошибка: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _generateParallel(int perChat) async {
|
|
if (!await _confirmGeneration() || !mounted) return;
|
|
if (_selectedForParallel.isEmpty) {
|
|
setState(() => _result = 'Выбери хотя бы один чат из списка ниже');
|
|
return;
|
|
}
|
|
setState(() {
|
|
_busy = true;
|
|
_result = null;
|
|
});
|
|
try {
|
|
final accountId = await _activeAccountId();
|
|
if (accountId == null) throw Exception('нет активного профиля');
|
|
final chatIds = _selectedForParallel.toList();
|
|
await LoadSimulator.generateAcrossChats(
|
|
accountId: accountId,
|
|
chatIds: chatIds,
|
|
perChat: perChat,
|
|
);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_result =
|
|
'Параллельно добавлено по $perChat сообщений в '
|
|
'${chatIds.length} чатов: $chatIds.';
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_result = 'Ошибка: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _clearParallel() async {
|
|
if (_selectedForParallel.isEmpty) {
|
|
setState(() => _result = 'Выбери хотя бы один чат из списка ниже');
|
|
return;
|
|
}
|
|
setState(() {
|
|
_busy = true;
|
|
_result = null;
|
|
});
|
|
try {
|
|
final accountId = await _activeAccountId();
|
|
if (accountId == null) throw Exception('нет активного профиля');
|
|
final chatIds = _selectedForParallel.toList();
|
|
await LoadSimulator.clearAcrossChats(
|
|
accountId: accountId,
|
|
chatIds: chatIds,
|
|
);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_result = 'Синтетические сообщения удалены из ${chatIds.length} чатов.';
|
|
});
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_result = 'Ошибка: $e';
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<bool> _confirmGeneration() async =>
|
|
await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Добавить тестовые сообщения?'),
|
|
content: const Text(
|
|
'Синтетические сообщения появятся в локальной истории выбранных чатов. Используйте тестовый аккаунт. После проверки нажмите очистку тестовых сообщений.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Отмена'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: const Text('Добавить'),
|
|
),
|
|
],
|
|
),
|
|
) ??
|
|
false;
|
|
|
|
void _openRenderStress() {
|
|
Navigator.of(context).push(
|
|
MaterialPageRoute(
|
|
settings: const RouteSettings(name: 'RenderStressScreen'),
|
|
builder: (_) => const RenderStressScreen(),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
return GlossyPill(
|
|
color: cs.surfaceContainerHigh,
|
|
borderRadius: BorderRadius.circular(20),
|
|
depth: 6,
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Симуляция нагрузки',
|
|
style: TextStyle(
|
|
color: cs.onSurface,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Набивает локальную БД фейковыми сообщениями в указанном чате '
|
|
'для стресс-теста скролла и рендера. Пишет напрямую в БД — '
|
|
'чат нужно переоткрыть, чтобы увидеть изменения. Чат должен '
|
|
'уже быть в локальном кэше (открывался хотя бы раз).',
|
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: _loadingChats
|
|
? const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 12),
|
|
child: SmallSpinner(size: 16),
|
|
)
|
|
: DropdownButtonFormField<int>(
|
|
initialValue: int.tryParse(_chatIdController.text),
|
|
isExpanded: true,
|
|
decoration: InputDecoration(
|
|
hintText: _myChats.isEmpty
|
|
? 'Нет закэшированных чатов'
|
|
: 'Выбери чат',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 14,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
items: [
|
|
for (final chat in _myChats)
|
|
DropdownMenuItem(
|
|
value: chat.id,
|
|
child: Text(
|
|
'${chat.title} (${chat.id})',
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
onChanged: _busy || _myChats.isEmpty
|
|
? null
|
|
: (id) => setState(
|
|
() => _chatIdController.text = id.toString(),
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: _loadingChats ? null : _loadMyChats,
|
|
icon: const Icon(Icons.refresh),
|
|
tooltip: 'Обновить список чатов',
|
|
),
|
|
],
|
|
),
|
|
if (_myChats.isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Отметь чипы ниже, чтобы добавить чаты в параллельный тест '
|
|
'дальше по странице.',
|
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 11),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Wrap(
|
|
spacing: 6,
|
|
runSpacing: 6,
|
|
children: [
|
|
for (final chat in _myChats)
|
|
FilterChip(
|
|
label: Text('${chat.title} (${chat.id})'),
|
|
selected: _selectedForParallel.contains(chat.id),
|
|
onSelected: _busy
|
|
? null
|
|
: (selected) => setState(() {
|
|
if (selected) {
|
|
_selectedForParallel.add(chat.id);
|
|
} else {
|
|
_selectedForParallel.remove(chat.id);
|
|
}
|
|
_chatIdController.text = chat.id.toString();
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
for (final count in [100, 1000, 5000])
|
|
FilledButton(
|
|
onPressed: _busy ? null : () => _generate(count),
|
|
style: FilledButton.styleFrom(shape: AppShape.buttonBorder),
|
|
child: Text('+$count'),
|
|
),
|
|
OutlinedButton(
|
|
onPressed: _busy ? null : _clear,
|
|
style: OutlinedButton.styleFrom(shape: AppShape.buttonBorder),
|
|
child: const Text('Очистить'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Параллельная нагрузка на несколько чатов',
|
|
style: TextStyle(
|
|
color: cs.onSurface,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Отметь чаты выше (FilterChip), затем пиши сразу во все — '
|
|
'имитация нагрузки от нескольких чатов одновременно.',
|
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
for (final count in [200, 1000])
|
|
FilledButton(
|
|
onPressed: _busy ? null : () => _generateParallel(count),
|
|
style: FilledButton.styleFrom(shape: AppShape.buttonBorder),
|
|
child: Text('Выбранным +$count каждому'),
|
|
),
|
|
OutlinedButton(
|
|
onPressed: _busy ? null : _clearParallel,
|
|
style: OutlinedButton.styleFrom(shape: AppShape.buttonBorder),
|
|
child: const Text('Очистить у выбранных'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Рендер-стресс-тест',
|
|
style: TextStyle(
|
|
color: cs.onSurface,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Открывает экран с сотнями синтетических "пузырей" и сам '
|
|
'автоматически быстро скроллит его, снимая FPS/джанк через '
|
|
'встроенный APM. Результат — воспроизводимая цифра, не на глаз.',
|
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12),
|
|
),
|
|
const SizedBox(height: 8),
|
|
FilledButton.tonal(
|
|
onPressed: _openRenderStress,
|
|
style: FilledButton.styleFrom(shape: AppShape.buttonBorder),
|
|
child: const Text('Запустить рендер-стресс-тест'),
|
|
),
|
|
if (_busy) ...[
|
|
const SizedBox(height: 12),
|
|
const Center(child: SmallSpinner(size: 20)),
|
|
],
|
|
if (_result != null) ...[
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: cs.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
_result!,
|
|
style: TextStyle(color: cs.onSurface, fontSize: 13),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|