mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-05-24 04:22:18 +00:00
Fixes #543. The statistics screen was crashing on Android with a populated library (~75+ favourites). Repro: open the More -> Statistics screen on an account with a real-sized library; app hangs and is killed. Cause is in `getStatistics()` in `lib/modules/more/statistics/ statistics_provider.dart`. The provider materialised every favourite manga *and* every chapter of every favourite into a Dart list, then walked the lists in memory: final items = await isar.mangas.filter()...findAll(); final chapters = await isar.chapters.filter() .manga((q) => q.favoriteEqualTo(true)...).findAll(); For a library with 75 favourites at ~100 chapters each that is 7,500+ heavy Isar objects materialised at once just to compute six counts. The "completed items" loop then re-fetched chapters per completed manga via the link relation (`item.chapters.toList()`), and the reading-time aggregation loaded every History row to sum a single int field. This commit rewrites the function to do all aggregation inside Isar: * total / read / completed-items / downloads => `count()` queries on indexed filter chains. No object hydration. * completed favourites => projects only the IDs (`idProperty()`), then runs two cheap `count()` queries per item to check "has-chapters && unread == 0". Logic is unchanged from the original `every(isRead) && isNotEmpty` check. * reading time => projects the `readingTimeSeconds` field (`readingTimeSecondsProperty()`) and folds the resulting List<int?>. Avoids hydrating full History rows. Memory: peak in-memory list shrinks from ~7,500 fully-hydrated chapters plus all favourite mangas plus all history rows, to (at most) the IDs of completed favourites (typically <50 ints) and the projected reading- time list (one int per history row, decoded as a flat list). DB round-trips: the completed-items branch now does 2 round-trips per completed favourite instead of 1 link-traversal per completed favourite. On indexed `mangaId` + `isRead` filters this is sub-millisecond per query and well below the cost of materialising thousands of rows. The overall function is faster end-to-end on any non-trivial library. Behaviour preserved: same six fields, same definitions, same edge cases (manga with no chapters is not counted as completed). Verified - `flutter analyze` clean on the touched file - `flutter build macos --release` succeeds - Manual smoke test on macOS with the local-all-fixes build (the bug reproduces only with a large mobile library; macOS desktop with a small test library returns the same numbers as before)
113 lines
3.3 KiB
Dart
113 lines
3.3 KiB
Dart
import 'package:isar_community/isar.dart';
|
|
import 'package:mangayomi/main.dart';
|
|
import 'package:mangayomi/models/chapter.dart';
|
|
import 'package:mangayomi/models/download.dart';
|
|
import 'package:mangayomi/models/history.dart';
|
|
import 'package:mangayomi/models/manga.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
part 'statistics_provider.g.dart';
|
|
|
|
class StatisticsData {
|
|
final int totalItems;
|
|
final int totalChapters;
|
|
final int readChapters;
|
|
final int completedItems;
|
|
final int downloadedItems;
|
|
final int totalReadingTimeSeconds;
|
|
|
|
const StatisticsData({
|
|
required this.totalItems,
|
|
required this.totalChapters,
|
|
required this.readChapters,
|
|
required this.completedItems,
|
|
required this.downloadedItems,
|
|
required this.totalReadingTimeSeconds,
|
|
});
|
|
}
|
|
|
|
@riverpod
|
|
Future<StatisticsData> getStatistics(
|
|
Ref ref, {
|
|
required ItemType itemType,
|
|
}) async {
|
|
// Aggregate inside Isar instead of materialising every favourite manga
|
|
// and every one of their chapters in Dart. Loading all chapters of a
|
|
// 75+ manga library exhausts heap on Android and reliably crashes the
|
|
// statistics screen — see issue #543.
|
|
|
|
final totalItems = await isar.mangas
|
|
.filter()
|
|
.favoriteEqualTo(true)
|
|
.itemTypeEqualTo(itemType)
|
|
.count();
|
|
|
|
final totalChapters = await isar.chapters
|
|
.filter()
|
|
.manga((q) => q.favoriteEqualTo(true).itemTypeEqualTo(itemType))
|
|
.count();
|
|
|
|
final readChapters = await isar.chapters
|
|
.filter()
|
|
.manga((q) => q.favoriteEqualTo(true).itemTypeEqualTo(itemType))
|
|
.isReadEqualTo(true)
|
|
.count();
|
|
|
|
final downloadedCount = await isar.downloads
|
|
.filter()
|
|
.chapter(
|
|
(q) => q.manga(
|
|
(m) => m.favoriteEqualTo(true).itemTypeEqualTo(itemType),
|
|
),
|
|
)
|
|
.isDownloadEqualTo(true)
|
|
.count();
|
|
|
|
// Completed items: a favourite manga whose source-reported status is
|
|
// Completed AND every chapter is read AND there is at least one chapter.
|
|
// Pull only the IDs of completed favourites, then run two cheap count()
|
|
// queries per item — never materialise the chapter rows.
|
|
final completedFavouriteIds = await isar.mangas
|
|
.filter()
|
|
.favoriteEqualTo(true)
|
|
.itemTypeEqualTo(itemType)
|
|
.statusEqualTo(Status.completed)
|
|
.idProperty()
|
|
.findAll();
|
|
|
|
int completedItems = 0;
|
|
for (final id in completedFavouriteIds) {
|
|
final total = await isar.chapters
|
|
.filter()
|
|
.mangaIdEqualTo(id)
|
|
.count();
|
|
if (total == 0) continue;
|
|
final unread = await isar.chapters
|
|
.filter()
|
|
.mangaIdEqualTo(id)
|
|
.isReadEqualTo(false)
|
|
.count();
|
|
if (unread == 0) completedItems++;
|
|
}
|
|
|
|
// Sum reading time without loading full History rows. Project just the
|
|
// int field — Isar returns a List<int?> of plain ints, far cheaper than
|
|
// hydrating every history record.
|
|
final readingTimes = await isar.historys
|
|
.filter()
|
|
.itemTypeEqualTo(itemType)
|
|
.readingTimeSecondsProperty()
|
|
.findAll();
|
|
final totalReadingTimeSeconds = readingTimes.fold<int>(
|
|
0,
|
|
(sum, v) => sum + (v ?? 0),
|
|
);
|
|
|
|
return StatisticsData(
|
|
totalItems: totalItems,
|
|
totalChapters: totalChapters,
|
|
readChapters: readChapters,
|
|
completedItems: completedItems,
|
|
downloadedItems: downloadedCount,
|
|
totalReadingTimeSeconds: totalReadingTimeSeconds,
|
|
);
|
|
}
|