Skip to content

优化百条以内待办的空闲刷新、列表重建与交互响应 #82

Description

@amazing-fish

背景

当前实际使用场景中,待办列表同时显示/管理的内容 少于 100 条。因此本次性能治理不需要迁移到 QListView + QAbstractListModel + Delegate,应优先消除现有 QWidget 架构中的无效主线程工作。

当前代码存在以下共同根因:

  1. master_timer 每秒执行 tick_update(),扫描全部任务后遍历全部卡片,并对每张卡片调用 update_timer_display()
  2. update_timer_display() 即使状态和显示文字没有变化,仍会重复设置完成图标、样式表、字体并调用 update_text_display() 重新测量布局。
  3. 每次 Tick 结束都会执行 _sync_todo_card_sizes()QListWidget.doItemsLayout()
  4. 创建、编辑、完成状态切换、删除和提醒处置都会保存完整 JSON,然后调用 update_list_widget() 清空并重建全部卡片。
  5. 单张卡片初始化过程中计时显示会被重复刷新;每次整表重建还会重新创建卡片控件、详情浮层、按钮、图标和信号连接。
  6. TaskEditDialog 以主窗口为 parent,关闭后没有显式释放,需要确认反复打开后是否积累隐藏子对象和主题信号连接。

此前对当前存储实现的微基准显示,在同一测试环境中:

任务数 JSON 大小 加载中位数 保存中位数
10 4 KB 0.04 ms 1.64 ms
100 40 KB 0.34 ms 2.50 ms

绝对数值不代表 Windows/杀毒软件环境,但足以说明:百条以内场景的首要瓶颈是 UI 重建和无效布局,而不是 JSON 解析。原子写入、备份和损坏保护不能为了提速而削弱。

相关代码:

  • todo_app/main_window.py::tick_update
  • todo_app/main_window.py::update_list_widget
  • todo_app/main_window.py::_sync_todo_card_sizes
  • todo_app/widgets.py::TodoItemWidget.update_timer_display
  • todo_app/dialogs.py::TaskEditDialog
  • todo_app/storage.py::save_todos

目标

在不改变界面、提醒、筛选排序和存储安全语义的前提下,针对 0~100 条任务

  • 降低空闲状态下每秒 Tick 的主线程开销;
  • 消除滑动过程中周期性的样式和布局抖动;
  • 缩短添加、编辑、完成、删除及提醒弹窗的响应时间;
  • 减少整表控件销毁和重建;
  • 建立可重复的 Windows 性能基线与回归门禁。

实施范围

1. 建立可观测的性能基线

在修改前后使用 0、10、50、100 条固定数据分别记录:

  • 应用初始化到首个完整列表可见;
  • 添加/编辑弹窗从点击到可交互;
  • 删除确认后到列表稳定;
  • 添加、编辑、完成切换、删除的处理时间;
  • tick_update() 的中位数与 P95;
  • 连续滑动期间是否出现与 1 秒 Tick 对齐的主线程长帧;
  • 反复打开并关闭编辑弹窗后的 TaskEditDialog 子对象数量。

要求:

  • 至少在 Windows 开发环境和正式单文件 EXE 各记录一组结果;
  • 性能日志默认关闭,只能由测试参数或环境变量显式启用;
  • CI 以调用次数和对象复用等确定性断言为主,不能仅依赖易抖动的毫秒阈值。

2. 拆分任务状态刷新与计时文字刷新

  • 将“完成状态/图标/字体/卡片配色”和“计时文字”拆分为不同更新路径。
  • 缓存上一轮渲染状态;状态、文字和颜色均未变化时直接返回。
  • 普通 Tick 不得重复调用 setIconsetStyleSheetsetFont 或重建操作图标。
  • 保留对全部 self.todos 的提醒扫描,不能因为筛选或不可见而漏提醒。
  • 可见卡片只在其最终显示文字变化时更新;屏幕外卡片不做无意义的 QWidget 刷新。
  • 从无变化的每秒 Tick 中移除 _sync_todo_card_sizes();布局同步只在以下情况发生:
    • viewport 宽度变化;
    • 任务正文、字体或主题变化;
    • 卡片加入、移除或逻辑行数变化;
    • 筛选/排序结果变化。
  • 缓存稳定的 SVG/QIcon 和程序化操作图标,避免每秒或每次全量刷新重复构造。

3. 用差量协调替代无条件整表重建

保留筛选和排序结果的重新计算,但复用未变化的 QListWidgetItemTodoItemWidget

  • 添加:只创建新任务对应卡片,并插入正确位置;
  • 编辑/完成切换:更新目标卡片;若筛选归属或排序位置变化,仅移动/移除该卡片;
  • 删除:只删除目标卡片;
  • 提醒完成、推迟、忽略:只更新受影响任务;
  • 仅在无法安全协调的异常恢复路径允许完整重建。

差量更新必须:

  • 维护稳定的 task_id -> item/widget 映射;
  • 避免重复连接信号;
  • 保持当前筛选、排序和空列表占位语义;
  • 尽量保留滚动位置及当前可见区域;
  • 继续在用户操作后调用现有 save_todos(),不修改原子保存、备份和损坏保护规则。

4. 收敛启动与弹窗生命周期

  • 单张卡片在初次插入时只执行一次完整状态初始化,避免构造、应用主题、插入列表后重复刷新。
  • 启动阶段先记录各阶段耗时,再决定是否延迟非必要对象;不得为了“视觉上先显示”引入明显空白闪烁。
  • 检查 TaskEditDialogexec() 返回后的释放方式,确保重复添加/编辑不会持续增加主窗口子对象或残留主题信号。
  • 通知弹窗仍需先写入提醒去重状态;优化后应尽快显示,不再被无关卡片的整表重建阻塞。
  • 提醒窗口的逐项完成、推迟、忽略及唯一非模态窗口语义保持不变。

验收标准

  1. 100 条任务、数据和界面均无变化时:
    • 每秒 Tick 不调用 _sync_todo_card_sizes()QListWidget.doItemsLayout()
    • 不重复设置卡片图标、静态样式或字体;
    • 不调用 update_list_widget()
    • tick_update() P95 相比修改前下降至少 80%。
  2. 添加、编辑、完成切换和删除一个任务时:
    • 不调用 QListWidget.clear()
    • 未变化任务的 TodoItemWidget 对象身份保持不变;
    • 仅目标任务及必要的排序/筛选位置发生变化;
    • 同机 P95 相比修改前下降至少 50%。
  3. 100 条任务连续滑动 60 秒,不出现与每秒 Tick 同步的明显卡顿;主线程不得出现由全列表布局导致的周期性长帧。
  4. 初始构建中,每张卡片只进行一次完整状态/计时初始化;首个完整列表可见时间不得回退超过 10%,并记录实际优化结果。
  5. 连续打开并关闭添加/编辑弹窗 100 次后,不持续累积已关闭的 TaskEditDialog、内部控件或主题信号接收者。
  6. 筛选、排序、提醒扫描、完成、推迟、忽略、托盘恢复、详情浮层和深浅主题行为保持一致。
  7. save_todos() 的原子替换、单份备份、损坏文件只读恢复和拒绝覆盖语义零变化。
  8. 增加针对无变化 Tick、差量 CRUD、对象复用、滚动位置和弹窗释放的自动化测试。
  9. 完整执行:
    • python -m compileall todo_app
    • python -m unittest discover -s tests -v
    • git diff --check
  10. PR 中附修改前后 Windows 开发版与单文件 EXE 的性能表,并说明无法稳定量化的剩余风险。

风险与防护

  • 漏提醒:只减少 QWidget 刷新,不减少对全部任务数据的提醒扫描。
  • 筛选/排序错误:字段变化后必须重新判断目标任务是否仍应显示以及应处位置。
  • 控件状态陈旧:状态刷新和计时刷新拆分后,要覆盖完成、取消完成、推迟、到期、忽略和主题切换。
  • 信号重复:复用卡片时禁止重复 connect;删除卡片时同步断开和释放相关浮层。
  • 数据可靠性回退:不通过延迟或异步保存削弱现有崩溃安全保证。

与现有 Issue 的关系

非目标

  • 不迁移到 QAbstractListModel、Delegate 或其他列表虚拟化架构。
  • 不以数千条任务作为设计容量。
  • 不修改 JSON 字段、存储位置或文件格式。
  • 不把同步持久化改成可能丢失最后一次操作的后台延迟保存。
  • 不修改 UI 视觉、提醒语义、版本规则或正式发布流程。
  • 不处理 pip、PyInstaller 缓存和 Actions 构建耗时,相关工作继续由 优化 Windows 自动打包耗时并保留正式发布的干净构建 #79 跟踪。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions