Skip to content

修复分时统计展示并释放托盘空闲内存 - #2

Merged
jinghero merged 3 commits into
mainfrom
feat/hourly-chart-tray-memory
Oct 16, 2026
Merged

jinghero merged 3 commits into
mainfrom
feat/hourly-chart-tray-memory

Conversation

@jinghero

@jinghero jinghero commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

面板展开后收回托盘仍会保留窗口与渲染资源,扫描大段日志也会留下较高的内存占用;分时统计的表格和日期控件存在显示问题,且包含尚未发生的小时。

这次改动:

  • 将分时统计改为加权 TPS / 中位数双折线图,提供小时详情与无样本断线,修正日期显示及日历按钮。当日隐藏未来小时,历史日保留 24 小时,图表、导出与快照均按时间正序排列。
  • 托盘独立于统计窗口。收起时停止计时器、取消扫描、销毁窗口,等待渲染队列释放引用后回收并归还闲置堆内存;再次打开时重建统计并保留日期、模型、设置及展开状态。
  • 限制日志读取缓冲池保留大小,使用流式 JSON 校验跳过大型无关记录,保留异常计数、UTF-8 尾行、重复属性与增量统计语义。
  • 取消请求覆盖目录枚举与 SQLite 查询执行;即使底层文件读取暂时阻塞,窗口也立即解除等待。模型暂时没有记录时保留筛选,并补充中途取消、坏记录计数和筛选恢复回归。测试工程使用独立中间目录,避免增量构建混用测试入口。
  • 保留平台入口:Windows 默认留在托盘,macOS 启动时显示面板,Linux 显示面板且禁用收起。CI 和发布工作流增加 Windows、macOS ARM64 / x64 的核心与 Avalonia 回归,Linux 由共享测试任务覆盖。
  • 将框架退出时的资源清理与主动退出请求分开,避免系统结束应用时重复触发退出事件。子进程测试使用真实桌面生命周期复现并验证系统退出、关闭窗口两条路径。

验证:

  • Windows:核心回归、49 项 Avalonia 无界面检查、旧版 WPF 隐藏托盘回归通过;Release 构建 0 警告,构建脚本语法及 git diff --check 通过。
  • 离屏使用真实日志反复开合 6 次,关闭窗口全部可回收,回收后 GC 已提交内存约 11 MiB。Windows 用户手动展开再收起后,同一进程私有工作集在约 1 分钟观察中为 60–61 MiB,日志读取增量为 0;这只是该日志集与短时窗口的测量。
  • 提交 7e8e899 的四平台 CI 全部通过:Windows x64、Linux x64、macOS ARM64 / x64 的核心回归、Avalonia 无界面回归及便携构建成功。macOS / Linux 的原生桌面托盘、窗口外观仍需实机手动验收,无界面检查和构建不覆盖这些交互。

Sourcery 摘要

修复分时统计展示问题并完善托盘窗口生命周期,以降低日志扫描和面板收起后的内存占用。

新功能:

  • 将分时统计升级为加权 TPS 与中位数双折线图,支持样本详情、空数据断线、按日期过滤和时间排序导出。

错误修复:

  • 修正分时统计的未来小时、日期控件和日历按钮显示问题。
  • 修复托盘收起后窗口、渲染资源和扫描缓存持续占用内存的问题,并避免应用退出流程重复触发。

改进:

  • 将托盘生命周期与统计窗口解耦,收起时暂停监控并销毁窗口,重新打开时恢复视图状态并重建统计。
  • 优化大型日志读取和 JSON 校验的内存使用,同时保留增量扫描、异常计数、尾行处理和重复属性语义。
  • 保留并明确 Windows、macOS 和 Linux 的平台启动及托盘行为差异。

持续集成(CI):

  • 在 Windows 与 macOS ARM64/x64 的 CI 和发布流程中增加核心及 Avalonia 回归测试,并覆盖四平台便携构建。

文档:

  • 更新 README,说明托盘暂停、内存释放、分时图表、日志读取行为及跨平台测试覆盖范围。

测试:

  • 扩展核心和 Avalonia 测试,覆盖分时统计展示、日志扫描取消与大记录处理、窗口回收、托盘生命周期、平台启动策略及框架退出路径。
Original summary in English

Summary by Sourcery

修复分时统计展示问题并完善托盘窗口生命周期,以降低日志扫描和面板收起后的内存占用。

New Features:

  • 将分时统计升级为加权 TPS 与中位数双折线图,支持样本详情、空数据断线、按日期过滤和时间排序导出。

Bug Fixes:

  • 修正分时统计的未来小时、日期控件和日历按钮显示问题。
  • 修复托盘收起后窗口、渲染资源和扫描缓存持续占用内存的问题,并避免应用退出流程重复触发。

Enhancements:

  • 将托盘生命周期与统计窗口解耦,收起时暂停监控并销毁窗口,重新打开时恢复视图状态并重建统计。
  • 优化大型日志读取和 JSON 校验的内存使用,同时保留增量扫描、异常计数、尾行处理和重复属性语义。
  • 保留并明确 Windows、macOS 和 Linux 的平台启动及托盘行为差异。

CI:

  • 在 Windows 与 macOS ARM64/x64 的 CI 和发布流程中增加核心及 Avalonia 回归测试,并覆盖四平台便携构建。

Documentation:

  • 更新 README,说明托盘暂停、内存释放、分时图表、日志读取行为及跨平台测试覆盖范围。

Tests:

  • 扩展核心和 Avalonia 测试,覆盖分时统计展示、日志扫描取消与大记录处理、窗口回收、托盘生命周期、平台启动策略及框架退出路径。

@sourcery-ai

sourcery-ai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

评审者指南

本 PR 将托盘提升为独立于统计窗口的生命周期宿主,收起时取消扫描并销毁 Avalonia 窗口、渲染引用和扫描缓存后主动回收内存,重新打开时恢复用户视图状态;同时重做分时统计的时间筛选、双指标图表和导出,并通过受控缓冲池、流式 JSON 校验和取消支持降低大日志扫描占用,最后扩展多平台 CI 与桌面生命周期回归验证。

托盘面板生命周期与内存回收时序图

sequenceDiagram
    participant User as User
    participant Tray as DesktopSession
    participant Panel as MainWindow
    participant Scan as UsageScanner
    participant Render as AvaloniaCompositor
    participant GC as GC

    User->>Tray: ShowPanel()
    Tray->>Panel: new MainWindow(..., viewState)
    Tray->>Panel: StartAsync()
    Panel->>Scan: Refresh(..., cancellationToken)
    User->>Tray: HidePanel()
    Tray->>Panel: RememberView()
    Tray->>Panel: Close()
    Panel->>Scan: CancellationTokenSource.Cancel()
    Tray->>Render: RequestCommitAsync()
    Tray->>GC: Collect and compact heap
    User->>Tray: ShowPanel()
    Tray->>Panel: new MainWindow(..., viewState)
    Panel-->>User: Restore date, model, settings and expansion state
Loading

分时 TPS 统计渲染流程图

flowchart LR
    Snapshot[UsageSnapshot] --> Hours[UsageScanner.Hours]
    Hours --> Filter[Filter hours by selected date and snapshot time]
    Filter --> Reverse[Reverse hours for export ordering]
    Filter --> Chart[HourlyChart.SetHours]
    Chart --> Weighted[Weighted TPS line]
    Chart --> Median[Median line]
    Chart --> Gaps[Break lines for missing samples]
    Chart --> Details[Hover sample details]
Loading

文件级变更

变更 详情 文件
重构托盘与统计面板生命周期,使面板收起时真正停止监控、销毁窗口和渲染资源,并在重新打开时恢复状态。
  • 新增独立的 DesktopSession 管理托盘、面板创建/销毁、退出和资源回收。
  • 增加扫描取消、渲染队列排空、GC/LOH 回收及窗口可回收性处理。
  • 保存并恢复日期、模型筛选、设置与分时统计展开状态,区分框架清理和主动退出。
src/App/App.axaml.cs
src/App/DesktopSession.cs
src/App/MainWindow.axaml.cs
tests/AppTests.cs
修正分时统计的数据筛选、排序、图表展示和导出行为。
  • 按北京时间隐藏当日未来小时、为历史日期保留 24 小时并排除未来日期。
  • 以加权 TPS 和中位数绘制双折线,缺样本断线并提供样本与指标悬停详情。
  • 修正日期控件交互和显示,导出小时按时间倒序。
src/App/HourlyChart.cs
src/App/MainWindow.axaml
src/App/MainWindow.axaml.cs
src/Core/UsageScanner.cs
tests/AppTests.cs
tests/Program.cs
降低大日志扫描的常驻内存并支持可中断读取,同时保持原有增量统计语义。
  • 限制常规读取缓冲池保留大小,超长缓冲不返回池中长期驻留。
  • 对大型无关 JSONL 使用流式 UTF-8 校验,保留异常行、重复属性、尾行和去重语义。
  • 为日志扫描和 SQLite 诊断读取增加取消令牌,并在大批量缓冲释放后触发回收。
src/Core/UsageScanner.cs
src/Core/RequestClockReader.cs
tests/Program.cs
扩展平台启动策略、CI 回归覆盖和项目文档。
  • 保持 Windows 托盘启动、macOS 启动显示面板及 Linux 不可收起策略。
  • 在 Windows 与 macOS ARM64/x64 增加核心和 Avalonia 无界面回归,补充真实桌面生命周期子进程测试。
  • 更新托盘暂停、内存释放、分时图表、数据读取和发布验证说明。
.github/workflows/ci.yml
.github/workflows/release.yml
README.md
src/App/CodexTpsFloat.App.csproj
src/App/packages.lock.json
tests/AppTests.cs

提示与命令

与 Sourcery 交互

  • 触发新的评审: 在拉取请求中评论 @sourcery-ai review。
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 根据评审评论创建 GitHub issue: 回复某条评审评论,请求 Sourcery 根据该评论创建 issue。也可以回复 @sourcery-ai issue,根据该评论创建 issue。
  • 生成拉取请求标题: 在拉取请求标题中的任意位置写入 @sourcery-ai,即可随时生成标题。也可以在拉取请求中评论 @sourcery-ai title,随时重新生成标题。
  • 生成拉取请求摘要: 在拉取请求正文中希望生成摘要的位置写入 @sourcery-ai summary,即可随时在该位置生成 PR 摘要。也可以在拉取请求中评论 @sourcery-ai summary,随时重新生成摘要。
  • 生成评审者指南: 在拉取请求中评论 @sourcery-ai guide,即可随时重新生成评审者指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已处理完所有评论且不想再看到它们,这会很有用。
  • 关闭所有 Sourcery 评审: 在拉取请求中评论 @sourcery-ai dismiss,即可关闭所有现有的 Sourcery 评审。如果你想从头开始进行新的评审,这尤其有用——别忘了评论 @sourcery-ai review 来触发新的评审!

自定义使用体验

访问你的控制面板以:

  • 启用或禁用评审功能,例如 Sourcery 生成的拉取请求摘要、评审者指南等。
  • 更改评审语言。
  • 添加、移除或编辑自定义评审说明。
  • 调整其他评审设置。

获取帮助

Original review guide in English

Reviewer's Guide

本 PR 将托盘提升为独立于统计窗口的生命周期宿主,收起时取消扫描并销毁 Avalonia 窗口、渲染引用和扫描缓存后主动回收内存,重新打开时恢复用户视图状态;同时重做分时统计的时间筛选、双指标图表和导出,并通过受控缓冲池、流式 JSON 校验和取消支持降低大日志扫描占用,最后扩展多平台 CI 与桌面生命周期回归验证。

Sequence diagram for tray panel lifecycle and memory reclamation

sequenceDiagram
    participant User as User
    participant Tray as DesktopSession
    participant Panel as MainWindow
    participant Scan as UsageScanner
    participant Render as AvaloniaCompositor
    participant GC as GC

    User->>Tray: ShowPanel()
    Tray->>Panel: new MainWindow(..., viewState)
    Tray->>Panel: StartAsync()
    Panel->>Scan: Refresh(..., cancellationToken)
    User->>Tray: HidePanel()
    Tray->>Panel: RememberView()
    Tray->>Panel: Close()
    Panel->>Scan: CancellationTokenSource.Cancel()
    Tray->>Render: RequestCommitAsync()
    Tray->>GC: Collect and compact heap
    User->>Tray: ShowPanel()
    Tray->>Panel: new MainWindow(..., viewState)
    Panel-->>User: Restore date, model, settings and expansion state
Loading

Flow diagram for hourly TPS statistics rendering

flowchart LR
    Snapshot[UsageSnapshot] --> Hours[UsageScanner.Hours]
    Hours --> Filter[Filter hours by selected date and snapshot time]
    Filter --> Reverse[Reverse hours for export ordering]
    Filter --> Chart[HourlyChart.SetHours]
    Chart --> Weighted[Weighted TPS line]
    Chart --> Median[Median line]
    Chart --> Gaps[Break lines for missing samples]
    Chart --> Details[Hover sample details]
Loading

File-Level Changes

Change Details Files
重构托盘与统计面板生命周期,使面板收起时真正停止监控、销毁窗口和渲染资源,并在重新打开时恢复状态。
  • 新增独立的 DesktopSession 管理托盘、面板创建/销毁、退出和资源回收。
  • 增加扫描取消、渲染队列排空、GC/LOH 回收及窗口可回收性处理。
  • 保存并恢复日期、模型筛选、设置与分时统计展开状态,区分框架清理和主动退出。
src/App/App.axaml.cs
src/App/DesktopSession.cs
src/App/MainWindow.axaml.cs
tests/AppTests.cs
修正分时统计的数据筛选、排序、图表展示和导出行为。
  • 按北京时间隐藏当日未来小时、为历史日期保留 24 小时并排除未来日期。
  • 以加权 TPS 和中位数绘制双折线,缺样本断线并提供样本与指标悬停详情。
  • 修正日期控件交互和显示,导出小时按时间倒序。
src/App/HourlyChart.cs
src/App/MainWindow.axaml
src/App/MainWindow.axaml.cs
src/Core/UsageScanner.cs
tests/AppTests.cs
tests/Program.cs
降低大日志扫描的常驻内存并支持可中断读取,同时保持原有增量统计语义。
  • 限制常规读取缓冲池保留大小,超长缓冲不返回池中长期驻留。
  • 对大型无关 JSONL 使用流式 UTF-8 校验,保留异常行、重复属性、尾行和去重语义。
  • 为日志扫描和 SQLite 诊断读取增加取消令牌,并在大批量缓冲释放后触发回收。
src/Core/UsageScanner.cs
src/Core/RequestClockReader.cs
tests/Program.cs
扩展平台启动策略、CI 回归覆盖和项目文档。
  • 保持 Windows 托盘启动、macOS 启动显示面板及 Linux 不可收起策略。
  • 在 Windows 与 macOS ARM64/x64 增加核心和 Avalonia 无界面回归,补充真实桌面生命周期子进程测试。
  • 更新托盘暂停、内存释放、分时图表、数据读取和发布验证说明。
.github/workflows/ci.yml
.github/workflows/release.yml
README.md
src/App/CodexTpsFloat.App.csproj
src/App/packages.lock.json
tests/AppTests.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

嗨——我发现了 7 个问题

给 AI 智能体的提示
请处理这次代码审查中的评论:

## 个别评论

### 评论 1
<location path="src/Core/UsageScanner.cs" line_range="287" />
<code_context>
-    public IReadOnlyDictionary<string, double[]> Refresh()
+    public IReadOnlyDictionary<string, double[]> Refresh(CancellationToken cancellationToken = default)
     {
+        cancellationToken.ThrowIfCancellationRequested();
         Warning = null;
         try
</code_context>
<issue_to_address>
**Refresh 的取消响应得太迟**

当会话目录异常庞大,或者面板隐藏时诊断数据库查询耗时很长,`UsageScanner.Refresh` 会在检查令牌前先物化递归文件列表,而 `RequestClockReader.Refresh` 则会等到 `ExecuteReader` 返回行后才检查;因此,`ReclaimAsync` 会一直持有已关闭窗口的扫描状态,直到枚举或数据库操作完成。

请使文件枚举和诊断数据库读取在执行过程中响应刷新取消令牌。

另见 `src/Core/RequestClockReader.cs:47`、`src/App/MainWindow.axaml.cs:119`。
</issue_to_address>

### 评论 2
<location path="src/Core/RequestClockReader.cs" line_range="22" />
<code_context>
     public string? Warning { get; private set; }
     public RequestClockReader(string home) => this.home = home;
-    public IReadOnlyDictionary<string, double[]> Refresh()
+    public IReadOnlyDictionary<string, double[]> Refresh(CancellationToken cancellationToken = default)
     {
+        cancellationToken.ThrowIfCancellationRequested();
</code_context>
<issue_to_address>
**已编译的调用方将无法运行**

如果预编译的外部使用方在不重新编译的情况下使用更新后的 Core 程序集,可选取消参数会改变 CLR 签名,而不是保留旧重载。针对旧版 Core 程序集编译的使用方仍会请求旧方法签名,并在使用更新后的程序集运行时收到 `MissingMethodException`。

请保留旧的公共重载,并让它们使用 `CancellationToken.None` 委托给新的、支持取消的重载。

另见 `src/Core/UsageScanner.cs:272`、`src/Core/UsageScanner.cs:339`。
</issue_to_address>

### 评论 3
<location path="src/Core/UsageScanner.cs" line_range="133" />
<code_context>
+            if (reader.TokenType == JsonTokenType.EndObject)
+            {
+                reader.Read(); // Validate that no trailing JSON or garbage follows the object.
+                return relevant;
+            }
+            bool isType = reader.ValueTextEquals("type"u8);
</code_context>
<issue_to_address>
**格式错误的大型日志未被报告**

当 JSONL 行长度至少为 64 KiB、声明了无关的根 `type`,并且在结束对象前被截断或格式错误时,`IsTelemetryLine` 会在未看到根 `EndObject` 的情况下到达 EOF 并返回 `false`;随后 `Parse` 会跳过该行,因此 `ReadCompleteLines` 不会增加 `invalidLines`,调用方会误以为不完整的日志没有问题。

只有在确认读取器到达有效的根对象结尾后才跳过该行;否则应让 `JsonDocument.Parse` 验证内容,并统计解析失败。
</issue_to_address>

### 评论 4
<location path="src/App/MainWindow.axaml.cs" line_range="60-61" />
<code_context>
+        {
+            lastSnapshotDate = viewState.LastSnapshotDate;
+            DateBox.SelectedDate = viewState.Date;
+            if (!models.Contains(viewState.Model)) models.Add(viewState.Model);
+            ModelBox.SelectedItem = viewState.Model;
+            HistoryPanel.IsVisible = viewState.History;
+            HistoryButton.Content = viewState.History ? "分时统计  ⌄" : "今日分时  ›";
</code_context>
<issue_to_address>
**恢复的模型筛选条件会消失**

如果隐藏或重新打开后执行的扫描中没有先前选定模型的记录,`RefreshCoreAsync` 会根据新扫描结果重新生成模型列表;当列表中没有记住的模型时,它会选中“全部模型”,因此恢复的筛选条件会丢失,而且即使该模型的记录重新出现,筛选条件也不会恢复。

即使当前扫描中没有该模型的记录,也请在刷新期间保留记住的模型选择。
</issue_to_address>

### 评论 5
<location path="src/Core/UsageScanner.cs" line_range="406" />
<code_context>
             && r.StreamSeconds is > 0 && (model == null || r.Model == model)).ToArray();
-        return Enumerable.Range(0, 24).Select(hour =>
+        long asOf = snapshot.GeneratedAt.ToUnixTimeSeconds();
+        return Enumerable.Range(0, 24).Where(hour => start + hour * 3600 <= asOf).Reverse().Select(hour =>
         {
             var a = rows.Where(r => DateTimeOffset.FromUnixTimeMilliseconds((long)(r.StreamStart!.Value * 1000))
</code_context>
<issue_to_address>
**每小时结果按最新到最旧排列**

当现有 WPF 前端或快照 JSON 使用共享的每小时结果时,`UsageScanner.Hours` 会在返回前反转小时顺序,因此 WPF 网格和快照 JSON 输出的行会按最新到最旧排列,而不是之前的升序;依赖该顺序的调用方会看到结果发生变化。

请按时间升序返回每小时结果。
</issue_to_address>

### 评论 6
<location path="src/App/DesktopSession.cs" line_range="77" />
<code_context>
+
+    async Task ReclaimAsync(Task pendingRefresh, Compositor? compositor, int version)
+    {
+        await pendingRefresh;
+        pendingRefresh = Task.CompletedTask; // Do not root the closed window through its scan task.
+        // Detaching the control tree queues composition changes after the native
</code_context>
<issue_to_address>
**托盘模式下内存仍被占用**

当托盘隐藏期间配置的 Codex 目录位于速度缓慢或停滞的文件系统上时,`ReclaimAsync` 会无限期等待正在进行的刷新,没有超时限制;因此,停滞的文件系统操作会使已关闭窗口及其扫描状态在应用仍处于托盘模式时一直被引用。

请让枚举和文件读取及时响应取消,并限制回收过程等待已取消刷新的时长。
</issue_to_address>

### 评论 7
<location path="tests/Program.cs" line_range="105-107" />
<code_context>
+    {
+        canceled.Cancel();
+        bool scanCanceled = false, clocksCanceled = false;
+        try { inc.Refresh(now, canceled.Token); }
+        catch (OperationCanceledException) { scanCanceled = true; }
+        try { new RequestClockReader(directory).Refresh(canceled.Token); }
+        catch (OperationCanceledException) { clocksCanceled = true; }
+        Check(scanCanceled && clocksCanceled, "扫描器和诊断库读取响应取消请求");
</code_context>
<issue_to_address>
**未测试扫描过程中的取消**

如果在任一读取开始后才发出取消请求,`UsageScanner.Refresh` 和 `RequestClockReader.Refresh` 可能会在开始后忽略取消,但测试仍然通过:预先取消的令牌会使它们在入口检查时抛出异常,扫描尚未开始,因此 `scanCanceled` 和 `clocksCanceled` 都会变为 true,测试便报告成功。

请在每项操作开始处理测试夹具后再取消令牌,并断言正在进行的操作会停止。
</issue_to_address>

Sourcery 评估

需要人工审查。 需要先处理 7 个发现;每小时聚合或刷新生命周期中的缺陷可能会导致统计数据不完整,而导出的报告可能会在回滚后仍保留错误数值。源日志未受影响,可以重新扫描,因此统计数据和报告都可以重新生成。

阻塞性发现:src/Core/UsageScanner.cs:287、src/Core/RequestClockReader.cs:22、src/Core/UsageScanner.cs:133、src/App/MainWindow.axaml.cs:61、src/Core/UsageScanner.cs:406,以及另外 2 项


Sourcery 对开源项目免费——如果您喜欢我们的审查,请考虑分享 ✨
Original comment in English

Hey - I've found 7 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/Core/UsageScanner.cs" line_range="287" />
<code_context>
-    public IReadOnlyDictionary<string, double[]> Refresh()
+    public IReadOnlyDictionary<string, double[]> Refresh(CancellationToken cancellationToken = default)
     {
+        cancellationToken.ThrowIfCancellationRequested();
         Warning = null;
         try
</code_context>
<issue_to_address>
**Refresh cancellation arrives too late**

When a session directory is unusually large or a diagnostic-database query takes a long time while the panel is hidden, `UsageScanner.Refresh` materializes the recursive file list before checking its token, and `RequestClockReader.Refresh` waits for `ExecuteReader` to return rows before checking; `ReclaimAsync` therefore keeps the closed window's scan state reachable until the enumeration or database work finishes.

Make file enumeration and diagnostic-database reads respond to the refresh cancellation token while the work is in progress.

Also at `src/Core/RequestClockReader.cs:47`, `src/App/MainWindow.axaml.cs:119`.
</issue_to_address>

### Comment 2
<location path="src/Core/RequestClockReader.cs" line_range="22" />
<code_context>
     public string? Warning { get; private set; }
     public RequestClockReader(string home) => this.home = home;
-    public IReadOnlyDictionary<string, double[]> Refresh()
+    public IReadOnlyDictionary<string, double[]> Refresh(CancellationToken cancellationToken = default)
     {
+        cancellationToken.ThrowIfCancellationRequested();
</code_context>
<issue_to_address>
**Previously compiled callers fail**

When a precompiled external consumer uses the updated Core assembly without recompiling, the optional cancellation parameters change the CLR signatures rather than preserving the old overloads. A consumer compiled against the previous Core assembly still requests the old method signatures and gets `MissingMethodException` when run with the updated assembly.

Retain the old public overloads and delegate them to the new cancellation-aware overloads using `CancellationToken.None`.

Also at `src/Core/UsageScanner.cs:272`, `src/Core/UsageScanner.cs:339`.
</issue_to_address>

### Comment 3
<location path="src/Core/UsageScanner.cs" line_range="133" />
<code_context>
+            if (reader.TokenType == JsonTokenType.EndObject)
+            {
+                reader.Read(); // Validate that no trailing JSON or garbage follows the object.
+                return relevant;
+            }
+            bool isType = reader.ValueTextEquals("type"u8);
</code_context>
<issue_to_address>
**Malformed large logs go unreported**

When a JSONL line is at least 64 KiB, declares an irrelevant root `type`, and is truncated or malformed before its closing object, `IsTelemetryLine` reaches EOF without seeing the root `EndObject` and returns `false`; `Parse` then skips the line, so `ReadCompleteLines` never increments `invalidLines` and the caller sees an incomplete log reported as clean.

Only skip the line after confirming the reader reached a valid root end; otherwise let `JsonDocument.Parse` validate it and count the parse failure.
</issue_to_address>

### Comment 4
<location path="src/App/MainWindow.axaml.cs" line_range="60-61" />
<code_context>
+        {
+            lastSnapshotDate = viewState.LastSnapshotDate;
+            DateBox.SelectedDate = viewState.Date;
+            if (!models.Contains(viewState.Model)) models.Add(viewState.Model);
+            ModelBox.SelectedItem = viewState.Model;
+            HistoryPanel.IsVisible = viewState.History;
+            HistoryButton.Content = viewState.History ? "分时统计  ⌄" : "今日分时  ›";
</code_context>
<issue_to_address>
**Restored model filter disappears**

When a scan after hiding or reopening has no records for the previously selected model, `RefreshCoreAsync` rebuilds the model list from the new scan and selects “全部模型” when the remembered model is absent, so the restored filter is lost and does not return when its records reappear.

Preserve the remembered model selection across refreshes even when the current scan has no records for that model.
</issue_to_address>

### Comment 5
<location path="src/Core/UsageScanner.cs" line_range="406" />
<code_context>
             && r.StreamSeconds is > 0 && (model == null || r.Model == model)).ToArray();
-        return Enumerable.Range(0, 24).Select(hour =>
+        long asOf = snapshot.GeneratedAt.ToUnixTimeSeconds();
+        return Enumerable.Range(0, 24).Where(hour => start + hour * 3600 <= asOf).Reverse().Select(hour =>
         {
             var a = rows.Where(r => DateTimeOffset.FromUnixTimeMilliseconds((long)(r.StreamStart!.Value * 1000))
</code_context>
<issue_to_address>
**Hourly results appear newest first**

When the existing WPF frontend or a snapshot JSON consumer uses the shared hourly results, `UsageScanner.Hours` reverses the hours before returning them, so the WPF grid and snapshot JSON emit newest-first rows instead of the previous ascending order; callers that rely on that order see changed results.

Return the hourly results in chronological ascending order.
</issue_to_address>

### Comment 6
<location path="src/App/DesktopSession.cs" line_range="77" />
<code_context>
+
+    async Task ReclaimAsync(Task pendingRefresh, Compositor? compositor, int version)
+    {
+        await pendingRefresh;
+        pendingRefresh = Task.CompletedTask; // Do not root the closed window through its scan task.
+        // Detaching the control tree queues composition changes after the native
</code_context>
<issue_to_address>
**Tray memory remains allocated**

When the configured Codex directory is on a slow or stalled filesystem during a tray hide, `ReclaimAsync` awaits the in-flight refresh without a timeout, so a stalled filesystem operation keeps the closed window and its scan state reachable while the app remains in the tray.

Make enumeration and file reads respond promptly to cancellation, and bound how long reclamation waits for a canceled refresh.
</issue_to_address>

### Comment 7
<location path="tests/Program.cs" line_range="105-107" />
<code_context>
+    {
+        canceled.Cancel();
+        bool scanCanceled = false, clocksCanceled = false;
+        try { inc.Refresh(now, canceled.Token); }
+        catch (OperationCanceledException) { scanCanceled = true; }
+        try { new RequestClockReader(directory).Refresh(canceled.Token); }
+        catch (OperationCanceledException) { clocksCanceled = true; }
+        Check(scanCanceled && clocksCanceled, "扫描器和诊断库读取响应取消请求");
</code_context>
<issue_to_address>
**Mid-scan cancellation goes untested**

When cancellation arrives after either read has started, `UsageScanner.Refresh` and `RequestClockReader.Refresh` can ignore cancellation after starting and still pass: the pre-canceled token makes their entry checks throw before either scan begins, so `scanCanceled` and `clocksCanceled` become true and the test reports success.

Cancel the token after each operation begins processing a fixture and assert that the in-progress operation stops.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 7 findings to address first, and a defect in the hourly aggregation or refresh lifecycle could produce incomplete statistics, and an exported report could preserve those wrong values after a revert. The source logs remain untouched and can be rescanned, so the statistics and report can be regenerated.

Blocking findings: src/Core/UsageScanner.cs:287, src/Core/RequestClockReader.cs:22, src/Core/UsageScanner.cs:133, src/App/MainWindow.axaml.cs:61, src/Core/UsageScanner.cs:406, and 2 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/Core/UsageScanner.cs Resolved
Comment thread src/Core/RequestClockReader.cs Resolved
Comment thread src/Core/UsageScanner.cs Resolved
Comment thread src/App/MainWindow.axaml.cs Resolved
Comment thread src/Core/UsageScanner.cs Resolved
Comment thread src/App/DesktopSession.cs Resolved
Comment thread tests/Program.cs Resolved
@jinghero

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

请针对当前提交 7e8e899203ad450704e72a090cbb2f6aa10be5eb 重新评审。原 7 条意见已逐条回复:已改进目录与 SQLite 执行中的取消、取消时解除窗口等待、保留空快照时的模型筛选,并补齐中途取消回归;分时统计按维护者最新决定统一使用时间正序。

两条经核查不适用:Core 随应用整体重编译发布,没有独立外部 DLL 热替换兼容承诺;默认 final-block 的 Utf8JsonReader 已会对截断对象/字符串抛 JsonException,新增 >64 KiB 样例验证了异常计数。另修复测试工程共用 obj 导致增量构建可能混用 apphost 入口的问题,各测试现在使用独立中间目录。

Windows 本地核心、49 项 Avalonia、旧版 WPF 隐藏托盘回归与无警告构建已通过,当前四平台 CI 正在运行。请检查本次修复是否有实际遗漏。

@jinghero
jinghero merged commit a9f8e2b into main Oct 16, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant