收藏页 Komikku 对齐迁移 — Phase 0 基线记录(2026-09)
状态:Phase 0 完成(测试落地并通过),Phase 1 进行中 计划:
favourites-komikku-alignment-implementation-plan-2026-09.md测量设备:Xiaomi M332BF(Android 16 / API 36,arm64),debug 变体 + in-memory Room,USB 连接 测量日期:2026-09-01
1. 已落地的基线测试
| 测试 | 位置 | 覆盖 |
|---|---|---|
FavouriteCardFieldContractTest(JVM,10 用例) | app/src/test/kotlin/org/skepsun/kototoro/favourites/ui/ | 三种卡片模式(GRID/COMPACT_GRID/LIST/DETAILED_LIST)真实读取字段契约:title+override、coverUrl+override、altTitle→subtitle(grid/detailed)、tags→compact subtitle & detailed chips、contentRating→NSFW badge、counter/progress/projectionCount/isPinned/isSaved/metadataTrackingService 透传。验证了 description/sourceData/publicUrl/largeCoverUrl 对卡片渲染不可见(等价性断言) |
FavouriteLibrarySemanticsCharacterizationTest(instrumented,24 用例) | app/src/androidTest/kotlin/org/skepsun/kototoro/favourites/data/ | 收藏 paging SQL 全语义:12 种排序(含 pinned 恒定优先 + entity_id tie-break)、代表 membership 选择(pinned→createdAt→updatedAt→categoryId)、分类 slice 独立 membership 属性、preferred/anchor display 选择、tracks 聚合(SUM/MAX)、history join、9 种 SQL 过滤(nsfw/downloaded/new-chapters/source/tag/publication/content-type/space/dangling-category) |
FavouriteLibraryAggregateChainCharacterizationTest(instrumented,12 用例) | app/src/androidTest/kotlin/org/skepsun/kototoro/favourites/domain/ | 聚合链语义(observeFavouriteLibraryAggregates):display = preferred→anchor(窄路径不查 binding)、localMangaIds = preferred+anchor+bindings(与 WorkResolver 的 binding-only 语义不同!)、COMPLETED/NEW_CHAPTERS/MULTI_PROJECTION/PublicationState/ReadingStatus(显式 entity prefs 优先,history percent 兜底)/Tag/Downloaded 过滤、分类 slice |
FavouriteLibraryBaselineBenchmarkTest(instrumented,3 用例) | 同上 | 6.5k 合成库性能基线 + EXPLAIN QUERY PLAN 记录 |
共享种子助手:FavouriteLibrarySeed.kt(androidTest)。
2. 性能基线(6.5k entities / ~7.1k memberships,in-memory DB)
| 指标 | 数值(warm,单次) |
|---|---|
findLibraryRepresentatives(-1)(窄身份查询) | 22 ms |
| 宽 paging 行全量读取(500/页 ×13 页,含 work_history 嵌入 + tracks 聚合 join) | 360 ms |
| (参考)计划第 11 节新快照构建预算(6.3k warm P95) | ≤ 250 ms |
结论:宽行 SQL 本身 360ms 只占链路第一步;其后 buildFavouritePagingAggregates(per-page bindings+categories+projections 批查)、mapFavouritePage(metadata authority + override + ContentListMapper)与 Paging 状态机才是滚动/返回卡顿的主成本。窄 read model(只取卡片契约字段)有充分余量满足 250ms 预算。
3. characterization 发现的真实语义(迁移必须保留或显式决策)
isPinned在所有排序中恒定最优先(ORDER BY selected.pinned DESC打头,12 种排序无一例外)。- 代表 membership 选择规则:
pinned DESC, created_at DESC, updated_at DESC, category_id ASC;All 分类用相关子查询逐行判定,分类 slice 各自独立。 - 窄聚合路径与宽聚合路径的
localMangaIds语义不一致:- 窄路径(
findFavouriteLibraryAggregates,无 space/groupTab/非 SFW filter 时启用)=preferred + anchor + bindings并集; - 宽路径(
buildFavouritePagingAggregates/WorkResolver.resolveManyByEntityIds)= bindings only。 - 影响
MULTI_PROJECTION过滤结果。新 snapshot store 必须统一为 binding-based(与WorkPagingDaoTest断言的语义一致:anchor 不得膨胀计数)。
- 窄路径(
- display projection 在窄路径 = preferred → anchor(不验证 binding);SQL 层
COALESCE(preferred, anchor)的 dangling preferred 产生 NULL display(broken row),聚合层宽路径会 fallback 到 binding。新 store 需保留 broken row 并确定性放置。 - SQL NULL 语义:
- NULL 标题(missing display manga)在 ALPHABETIC ASC 排最前 / DESC 排最后;
- NULL nsfw 既不匹配 NSFW 也不匹配 SFW 模式;
- space 过滤的
sm.content_type IN (...)不接受 NULL(binding 的 content_type 为 NULL → 排除),而 display 侧 content-type 过滤对 NULL 放行。两者不一致,新实现按"binding NULL → 排除"保守处理并记录。
- tag filter 的 tag ID 是确定性 hash:
"${key}_${source.name}".longHashCode()(ContentTag.toEntity());种子/测试必须用同一 ID 派生。 tracks聚合:一个 entity 多条 track(每个 manga 一条,owner_id/manga_id均 UNIQUE),summary =SUM(chapters_new),MAX(last_chapter_date),MAX(last_check_time)。- dangling category membership(软删除分类下的收藏)当前仍可见(SQL 不 join
favourite_categories)——保留现状,与repairActiveDanglingCategoryRefs维护路径一致。 - rating 是纯排序键:收藏卡片从不渲染 score(
scoreText仅 Discover feed 使用)——新 row 保留 rating 用于 RATING 排序即可。 largeCoverUrl不被收藏卡片消费(只有 Home 封面请求 / 下载器用)——新 row 不需要它。
4. 未提交 Paging 实验(保留决定)
工作区有用户未提交的 Paging 实验:
BatchMappingPagingSource.kt:FavouriteLibraryPagingConfig.enablePlaceholders = trueContentListViewModel.createRetainedPagingSnapshot/RetainedPagingSnapshotController:K510G debug 日志- 另有 details UI / backup / AppModule / docs 的无关改动
决定:全部保留不动。这些属于旧 Paging 路径,随 Phase 7(删除收藏 Paging 专用代码)一并消失;debug 日志按计划第 11.3 节在 Phase 8 清理。Phase 1 只新增文件 + 新 DAO,不触碰上述文件。
5. Phase 1 输入(字段预算定稿)
Phase 0 字段契约 → Phase 1 窄 row 实际需要的列(全部有真实消费者):
- 身份:
entity_id,display_manga_id,local_manga_ids(binding facets),preferred_local_manga_id - 展示:
title,alt_title(→subtitle),cover_url,author - 来源:
source(名称, →source chip/cache key/group suffix/筛选),content_type(→type chips 兼容 display),state(→PublicationState 过滤 + info text),nsfw(→NSFW 过滤/badge) - 排序键:
rating,created_at/updated_at/pinned(membership),percent/updated_at(history),new_chapters/last_chapter_date(tracks) - 卡片状态:
new_chapters(counter),percent/chapters_count(progress),projection_count(multi badge/filter),is_pinned(badge),downloaded(saved badge/filter),reading_status(entity prefs → filter) - 过滤 facets:
tag_ids(Tag 过滤 + compact/detailed 展示),source names(Source 过滤 + quick filter chips) - Override:
title_override,cover_override(entity prefs + legacy preferences 兜底) - 丢弃:
description,source_data,large_cover_url,public_url, 完整 chapters/tags 对象图