Repository navigation
修复分时统计展示并释放托盘空闲内存 - #2
Conversation
评审者指南本 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
分时 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]
文件级变更
提示与命令与 Sourcery 交互
自定义使用体验访问你的控制面板以:
获取帮助Original review guide in EnglishReviewer's Guide本 PR 将托盘提升为独立于统计窗口的生命周期宿主,收起时取消扫描并销毁 Avalonia 窗口、渲染引用和扫描缓存后主动回收内存,重新打开时恢复用户视图状态;同时重做分时统计的时间筛选、双指标图表和导出,并通过受控缓冲池、流式 JSON 校验和取消支持降低大日志扫描占用,最后扩展多平台 CI 与桌面生命周期回归验证。 Sequence diagram for tray panel lifecycle and memory reclamationsequenceDiagram
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
Flow diagram for hourly TPS statistics renderingflowchart 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]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
嗨——我发现了 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 项
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
…lts chronological
|
@sourcery-ai review 请针对当前提交 两条经核查不适用:Core 随应用整体重编译发布,没有独立外部 DLL 热替换兼容承诺;默认 final-block 的 Utf8JsonReader 已会对截断对象/字符串抛 JsonException,新增 >64 KiB 样例验证了异常计数。另修复测试工程共用 obj 导致增量构建可能混用 apphost 入口的问题,各测试现在使用独立中间目录。 Windows 本地核心、49 项 Avalonia、旧版 WPF 隐藏托盘回归与无警告构建已通过,当前四平台 CI 正在运行。请检查本次修复是否有实际遗漏。 |
面板展开后收回托盘仍会保留窗口与渲染资源,扫描大段日志也会留下较高的内存占用;分时统计的表格和日期控件存在显示问题,且包含尚未发生的小时。
这次改动:
验证:
git diff --check通过。7e8e899的四平台 CI 全部通过:Windows x64、Linux x64、macOS ARM64 / x64 的核心回归、Avalonia 无界面回归及便携构建成功。macOS / Linux 的原生桌面托盘、窗口外观仍需实机手动验收,无界面检查和构建不覆盖这些交互。Sourcery 摘要
修复分时统计展示问题并完善托盘窗口生命周期,以降低日志扫描和面板收起后的内存占用。
新功能:
错误修复:
改进:
持续集成(CI):
文档:
测试:
Original summary in English
Summary by Sourcery
修复分时统计展示问题并完善托盘窗口生命周期,以降低日志扫描和面板收起后的内存占用。
New Features:
Bug Fixes:
Enhancements:
CI:
Documentation:
Tests: