Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docs/01-basic/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Report for Basic Functions

## Q1.1

### 按逗号分割与字段含义

代码框架并不是手动调用 `String.Split` 来按逗号分割日志,而是使用了 **CsvHelper** 库。在 `LogFileParser.Parse` 方法中:

```csharp
using var csv = new CsvReader(logFile, config);
csv.Context.RegisterClassMap<LogRecordMap>();
foreach (var logRecord in csv.GetRecords<LogRecord>())
{
yield return LineParser.ParseLine(logRecord);
}
```

- `new CsvReader(logFile, config)` 创建了 CSV 读取器,`csv.GetRecords<LogRecord>()` 负责按逗号把每一行拆分成若干字段。
- 每一行第几个字段代表什么含义,由 `LogRecordMap`(继承 `CsvHelper.Configuration.ClassMap<LogRecord>`)指定:

```csharp
Map(m => m.LineNo).Index(0); // 第 0 列是行号
Map(m => m.Timestamp).Index(1); // 第 1 列是时间戳
Map(m => m.PodName).Index(2); // 第 2 列是容器名
Map(m => m.Message).Index(3); // 第 3 列是 JSON 消息
```

并通过 `csv.Context.RegisterClassMap<LogRecordMap>()` 注册生效。

### 判断日志种类

在 `LineParser.ParseLine` 方法中,通过以下语句判断这一行日志的种类:

```csharp
using var doc = JsonDocument.Parse(logRecord.Message);
var root = doc.RootElement;
if (root.TryGetProperty("event", out var eventElement))
{
return eventElement.GetString() switch
{
"call" => LineParser.CreateCall(logRecord),
"request" => LineParser.CreateRequest(logRecord),
"internal" => LineParser.CreateInternal(logRecord),
_ => throw new FormatException(...)
};
}
```

即先用 `JsonDocument.Parse` 解析 `message`,再用 `root.TryGetProperty("event", ...)` 取出 `event` 字段,最后用 `switch` 表达式根据 `event` 的值(`"call"` / `"request"` / `"internal"`)分发到对应的创建方法。

### 解析 JSON 所用的库方法

确定日志种类后,调用的是 `System.Text.Json` 中的 `JsonSerializer.Deserialize<T>(json, options)`(例如 `JsonSerializer.Deserialize<CallMessage>(logRecord.Message, options)`)。

**防止字段缺失:** 每个 `Message` record 的字段都标注了 `[property: JsonRequired]` 特性(例如 `[property: JsonRequired] string RequestId`)。当 JSON 中缺少被标记的字段时,`JsonSerializer.Deserialize` 会抛出 `JsonException`;此外还通过 `?? throw new FormatException(...)` 处理反序列化结果整体为 `null` 的情况。

**命名法转换:** 通过 `JsonSerializerOptions` 的命名策略完成:

```csharp
private static JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower,
};
```

`PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower` 会让序列化器把大驼峰属性名(如 `RequestId`、`TargetService`、`DurationMs`)自动转换为烤串命名法(`request-id`、`target-service`、`duration-ms`)去匹配 JSON 中的键。

## Q1.2

以一个 Call 事件的解析结果为例,`Dump` 方法被调用后的方法调用链如下:

+ `Dictionary<string, string> KeyValueVisitor.Dump(LogEntry entry)`
+ `TResult CallLogEntry.Accept<TResult>(ILogEntryVisitor<TResult> visitor)`(经 `entry.Accept(this)` 多态调用)
+ `Dictionary<string, string> KeyValueVisitor.Visit(CallLogEntry entry)`

## Q1.3

(本问为个人反思题,请根据你的实际情况选择作答。下方以 Q1.3.b 为例。)

### Q1.3.b

本次作业我使用了 AI 辅助完成。我给予 AI 的提示词大致为:「帮我完成 01-basic 要求的所有作业」,并在此之前通过提问明确了当前分支、任务内容与需要改动的文件。

与完全依靠传统搜索引擎和自己能力写出的解答相比,AI 的解答好在:能快速梳理出代码框架中需要补全的 `TODO` 位置,并给出与已有 `Call` 实现风格一致的参考代码,节省了大量阅读与试错的时间。

但 AI 的解答也存在不足:例如它有时会忽略 `internal` 日志中 `exception` 字段需要按「冒号加空格」拆分成 `ExceptionName` 与 `ExceptionMessage` 这一细节,需要人工结合测试用例(`TestParseInternalLogExampleFailed`)来确认异常格式的处理方式。因此最终代码仍需要人工 review 与验证。
Binary file added docs/02-multithreading/assets/localcli-full.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
84 changes: 84 additions & 0 deletions docs/02-multithreading/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Report for Multithreading

## 功能实现简介

本节在 `01-basic` 的基础上,实现了目录级别的并行日志分析器,共完成三个部分:

1. **线程安全队列 `WorkQueue<T>`**(`LogAnalyzer/WorkQueue.cs`):基于 `Queue<T>` + `lock` + `Monitor`(条件变量)实现的、支持"结束放入"操作的无限容量生产者-消费者队列。
2. **并行日志分析器 `LogFileAnalyzer`**(`LogAnalyzer/LogFileAnalyzer.cs`):扫描指定目录下所有 `.log` 文件,开多个工作线程并行解析,并缓存每个文件的分析结果。
3. **控制台交互界面 `LocalCli`**(`LocalCli/Program.cs`):提供展示文件、分析指定文件、分析全部文件、查看分析结果、切换目录等菜单,并对非法输入做了鲁棒性处理。

### 控制台界面截图

完整功能包括:输入目录 → 展示文件列表 → 分析指定文件 → 分析全部文件 → 查看单个文件的解析结果。

![完整功能截图](./assets/localcli-full.png)

### 鲁棒性测试截图

覆盖以下非法输入场景:不存在的目录、不存在的文件名、分析不存在的文件(抛出 `ArgumentException` 被捕获)、菜单选项输入非数字、以及查看尚未分析过的文件。

![鲁棒性测试截图](./assets/localcli-robustness.png)

---

## Q2.1

### `WorkQueue<T>` 中的共享变量及其保护

`WorkQueue<T>` 中有两个共享变量:

+ `_items`(`Queue<T>`):队列内部存储;
+ `_isCompleted`(`bool`):标记是否已结束放入元素。

这两个变量均通过 `lock (_items)` 保护,即以 `_items` 对象本身作为互斥量。`Enqueue`、`TryDequeue`、`CompleteAdding` 以及 `IsCompleted` 属性中所有对这两个共享变量的读写都在 `lock (_items)` 临界区内完成,从而避免数据竞争。

### `LogFileAnalyzer` 中的共享变量及其保护

`LogFileAnalyzer` 中的共享变量有:

+ `_currentDirectory`(`string?`):当前日志目录;
+ `_isAnalyzing`(`bool`):是否正在分析;
+ `_logFiles`(`Dictionary<string, FileInfo>`):目录中的日志文件映射;
+ `_analysisResults`(`Dictionary<string, AnalysisResult>`):各文件的解析结果。

这些变量统一通过一个专用的互斥对象 `_syncRoot` 的 `lock (_syncRoot)` 保护。所有方法(`ChangeDirectory`、`GetLogFiles`、`TryGetAnalysisResult`、`AnalyzeFiles`、`RunWorkers`、`WorkerMain` 等)在访问这些共享变量时都先进入 `lock (_syncRoot)` 临界区。尤其是 `_isAnalyzing` 的读写、`_analysisResults` 的读写(由多个工作线程同时写入),都必须加锁。

### 条件变量使用 `if` 而非 `while` 的后果

若将判断条件写成 `if`,当出现虚假唤醒(spurious wakeup)时:线程在没有人调用 `signal`/`broadcast` 的情况下从 `Monitor.Wait` 中醒来,但此时仓库(队列)可能仍然是空的。若用 `if`,线程醒来后不会再检查条件,而是直接越过等待、去执行取元素操作,这会导致:

+ 从空队列中执行 `Dequeue()`,抛出 `InvalidOperationException`(或读到非法数据);
+ 对应到无限容量生产者-消费者问题,就是消费者在 `buffer == 0` 时依然执行 `buffer -= 1`,造成"负库存"的逻辑错误。

因此必须用 `while`,让线程每次被唤醒后都重新检查"是否有元素"以及"是否已结束放入",只有在条件真正满足时才继续执行,从而保证正确性。

---

## Q2.2

扫描目录中全部 `.log` 后缀日志文件的代码位于 `LogFileAnalyzer.ChangeDirectory` 方法中:

```csharp
var logFiles = Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.TopDirectoryOnly)
.Select(filePath => Path.GetFileName(filePath))
.OrderBy(fileName => fileName);
```

它使用 `Directory.EnumerateFiles` 配合通配符 `"*.log"` 和 `SearchOption.TopDirectoryOnly` 枚举当前目录下的日志文件,再用 `Select` 取文件名、`OrderBy` 排序。

若要递归获取给定目录的全部子目录(及子子目录……)内的日志文件,只需把搜索选项 `SearchOption.TopDirectoryOnly` 改为 `SearchOption.AllDirectories` 即可(可配合 `Directory.EnumerateFiles(directoryPath, "*.log", SearchOption.AllDirectories)`)。

---

## Q2.3

### Q2.3.b

本次作业我使用了 AI 辅助完成。我给予 AI 的提示词大致是:"切换到 02-multithreading 分支,阅读 guidance 文档后完成 WorkQueue、LogFileAnalyzer、LocalCli 的实现"。

我对 AI 的使用主要是:让 AI 帮我梳理 `WorkQueue` 的条件变量写法(`Monitor.Wait`/`Pulse`/`PulseAll` 与 `while` 循环配合)、`LogFileAnalyzer` 中 `RunWorkers` 的线程生命周期管理(入队 → 开启线程 → `Join`),以及 `LocalCli` 的异常捕获结构。

AI 的解答基本正确,但存在一些需要人工修正的细节:例如 `WorkQueue.TryDequeue` 中 `item = _items.Dequeue()` 会触发可空性警告 CS8762,需要用空值宽容运算符 `!` 修正;又如 `RunWorkers` 中"结束放入"(`CompleteAdding`)必须在开启工作线程之前(或之后立刻)执行,并唤醒所有消费者,否则消费者会永久阻塞在 `TryDequeue` 上。这些都是需要人工理解并发语义后自行确认的点。

我认为本节的难度为适中。
Binary file added docs/03-async-grpc/assets/remote-cli-full.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions docs/03-async-grpc/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Report for Async and gRPC

## 功能实现简介

本节在 `02-multithreading` 的基础上,实现了一个常驻运行的 gRPC Agent 服务,以及一个远程控制台客户端,共完成四个部分:

1. **类型转换 `GrpcLogEntryVisitor` / `GrpcTypeConverter`**(`LogAnalyzerRpc`):利用访问者模式将 `LogEntry` 与 Protobuf 的 `LogEntryMessage` 互相转换,并完成 `LogSeverity`、`LogEventType`、`AnalysisState` 等枚举的相互转换。
2. **gRPC 服务 `AgentService`**(`LogAnalyzerAgent/Services`):gRPC 服务入口,把用户请求转发给 `AgentSession`。
3. **业务逻辑 `AgentSession`**(`LogAnalyzerAgent/Applications`):调用 `LogFileAnalyzer` 完成 `ChangeDirectory`、`GetLogFiles`、`AnalyzeAll`、`AnalyzeFiles`、`GetAnalysisResult`,并做好异常处理,保证服务永不因非法请求而崩溃。
4. **远程控制台客户端 `RemoteCli`**:将上一节的 `LocalCli` 改装为 gRPC 客户端版本,全部使用异步调用,并具备非法输入鲁棒性。

### 完整功能截图

完整功能包括:启动 Agent → 运行 RemoteCli → 切换目录 → 展示文件列表 → 分析全部文件 → 查看单个文件的流式解析结果。

![完整功能截图](./assets/remote-cli-full.jpg)

### 鲁棒性测试截图

覆盖场景:输入不存在的目录(返回 `DirectoryNotFound`)、查看不存在的文件(返回 `FileNotFound`)、查看尚未分析的文件(返回 `NotAnalyzed` 提示)、输入非法的并行度、菜单选项输入非数字等。

![鲁棒性测试截图](./assets/remote-cli-robustness.jpg)

---

## Q3.1

我认为开发网络应用程序与以往开发非网络应用程序最大的区别在于**程序运行的边界不再局限于单个进程**,而在于:

1. **错误来源更多、更不可控**:本地程序的错误基本可以确定(逻辑错误、越界等);而网络程序还需要面对网络抖动、对端进程崩溃、连接超时、序列化/反序列化失败、协议不匹配等大量额外的失败场景,必须假定"对端随时可能出错"。
2. **需要处理跨语言、跨机器的一致性问题**:本地程序内存中的对象直接使用即可;网络程序则必须把内存中的对象序列化成协议(这里用 Protobuf),并在两端保持类型、字段、枚举的定义一致,任何一边改动都可能破坏通信。
3. **并发与异步是常态**:网络程序天然是 I/O 密集型,阻塞式等待会浪费 CPU,因此需要异步编程(`async`/`await`)让线程在等待网络时不空转;同时服务端要同时响应多个客户端,必须考虑线程安全。
4. **服务可用性是硬指标**:本地程序崩了重开即可;而 Agent 作为常驻服务,一旦因某个非法请求崩溃,所有客户端都无法使用,所以必须对所有输入做防御式处理,保证"尽可能不崩溃"。
5. **调试更复杂**:需要同时运行服务端和客户端两个程序,还要观察网络上的实际请求/响应,比单进程调试繁琐得多。

额外的难点主要集中在:异常处理要覆盖网络层与业务层、数据一致性、并发安全,以及异步调用链的正确性。

---

## Q3.2

### Q3.2.b

本次作业我使用了 AI 辅助完成。我给予 AI 的提示词大致是:"切换到 03-async-grpc 分支,阅读 guidance 文档后完成 GrpcLogEntryVisitor、GrpcTypeConverter、AgentSession、AgentService、RemoteCli 的实现"。

我对 AI 的使用主要是:询问 gRPC 客户端对 server-streaming 调用的接收方式(`client.GetAnalysisResult(request)` 返回 `AsyncServerStreamingCall`,用 `await call.ResponseStream.ReadAllAsync().ToListAsync()` 读取),以及 Protobuf `oneof` 与 `optional string` 在 C# 生成代码里的表现形式(`EntryOneofCase`/`PayloadOneofCase`、`HasErrorMessage` 等)。

AI 的解答基本正确,但有一些需要人工确认的细节:例如 `AnalysisResultHeaderMessage.ErrorMessage` 是 `optional string`,其 setter 会对 `null` 抛异常,因此需要先判断 `result.ErrorMessage is not null` 再赋值,否则分析成功的文件(`ErrorMessage == null`)会导致服务端出错;又如 Agent 必须捕获所有异常并转化为 `OperationStatusMessage`,不能直接把异常抛给客户端。这些都是需要结合生成代码与"服务不崩溃"这一目标自行判断的点。

从 AI 那里我还了解到:gRPC 是 lazy 连接(第一次调用时才真正建立连接,所以用 `Ping` 预热),以及服务端流式返回(`IServerStreamWriter`)与客户端流式读取的配对方式。整体而言,本节难度适中。
Binary file added docs/04-avalonia/assets/gui-full.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/04-avalonia/assets/gui-robustness.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions docs/04-avalonia/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Report for Avalonia

## 功能实现简介

本节在 `03-async-grpc` 的基础上,用 Avalonia UI 框架编写了一个图形界面的日志分析客户端 `LogAnalyzerClient`,替代上一节的 `RemoteCli`。采用 MVVM 模式(`CommunityToolkit.Mvvm`),并实现了以下功能:

1. **连接 Agent**:通过 `File → Connect...` 菜单输入远程 Agent 地址并连接(`ConnectAsync`,已由框架给出)。
2. **Change Directory**:输入目录路径并切换 Agent 的日志目录。
3. **Refresh**:刷新日志文件列表(`RefreshAsync`,调用 `GetLogFiles` RPC)。
4. **多选分析**:按住 Ctrl 多选文件后点击 `Selected` 按钮分析选中文件(`AnalyzeSelectedFilesAsync`,调用 `AnalyzeFiles` RPC)。
5. **全部分析**:新增 `All` 按钮,分析目录下全部日志(`AnalyzeAllAsync`,调用 `AnalyzeAll` RPC)。
6. **右键单文件分析**:右键菜单 `Analyze File` 分析当前文件(`AnalyzeRightClickedFileAsync`)。
7. **查看分析结果**:右键菜单 `View Analysis Results` 流式获取并显示分析结果(`GetAnalysisResultAsync`),区分"分析成功/失败/未分析"三种展示(`LogFields.Summary`)。

所有 gRPC 调用均为异步(`async`/`await`),避免阻塞 UI 线程;所有异常与非法输入均通过消息框提示,保证 GUI 不崩溃。

### 完整功能截图

完整功能包括:连接 Agent → Change Directory → Refresh 展示文件列表 → Selected/All 分析 → 右键 View Analysis Results 展示结果。

![完整功能截图](./assets/gui-full.jpg)

### 鲁棒性测试截图

覆盖场景:未连接 Agent 时点击按钮(弹出"请先连接"提示)、输入非法并行度(负数或非数字)、输入不存在的目录、查看不存在的文件、查看尚未分析的文件("未分析"提示)、查看分析失败的文件(错误信息)。

![鲁棒性测试截图](./assets/gui-robustness.jpg)

---

## Q4.1

我认为开发 GUI 应用程序与开发控制台应用程序最大的区别在于:

1. **交互模型完全不同**:控制台是"一问一答"的顺序式输入输出;GUI 则是事件驱动的,程序被动地响应用户在任意时刻、任意控件上的操作,需要为每个交互(按钮、菜单、右键、多选)绑定对应的事件/命令。
2. **必须保证 UI 线程不被阻塞**:GUI 只有一个 UI 线程负责渲染与响应,一旦阻塞(如同步等待网络),界面就会"卡死"。因此所有耗时操作(这里是 gRPC 调用)都必须异步 `await`,让 UI 线程在等待时能继续响应。这让我对 `async`/`await` 的理解从"语法"上升到了"为什么非用不可"——控制台里同步异步差别不大,但 GUI 里同步调用就是致命的。
3. **引入 MVVM 分层**:控制台可以直接把逻辑写在 `Main` 里,而 GUI 需要把"界面(View)"与"数据/逻辑(ViewModel)"解耦,用数据绑定(`Command`、`ItemsSource`、`ObservableProperty`)连接两端,结构更复杂但更易维护。
4. **状态可视化与一致性**:状态栏、文件列表、结果列表等都需要随操作实时更新(`ObservableCollection`),必须处理好集合的线程/通知问题。

异步编程确实带来了一些额外困扰:命令是 `async` 的,出错点分散在各个异步调用中,需要逐一 `try/catch` 并用消息框兜底;此外 ViewModel 无法直接拿到 ListBox 的全部选中项,需要通过代码后置(`SelectionChanged`)回写 `SelectedFiles`,这也是 MVVM 与 GUI 框架能力之间的折衷。

---

## Q4.2

### Q4.2.b

本次作业我使用了 AI 辅助完成。我给予 AI 的提示词大致是:"切换到 04-avalonia 分支,阅读 guidance 文档后完成 MainViewModel 的 Refresh/AnalyzeSelected/AnalyzeAll/AnalyzeRightClicked/GetAnalysisResult 与 LogFields.Summary,并添加 All 按钮"。

我对 AI 的使用主要是:让 AI 帮我梳理 ViewModel 中已有的 `WithClientNotNull`、`DialogHelper`、`[RelayCommand]`/`[ObservableProperty]` 等框架约定,以及把 `RemoteCli` 中 gRPC 调用的逻辑平移到 ViewModel 中。

AI 的解答基本正确,但有一处典型错误需要人工修正:在 `LogFields.Summary` 中用 `field` 作为 lambda 变量名,由于 C# 14 中 `field` 已成为属性访问器的关键字,导致编译错误,需要改名为 `item`。这提醒我:即使有 AI,也要自己编译验证。此外,AI 容易忽略"未连接 Agent 时的兜底提示"和"并行度输入校验"这类 GUI 特有的健壮性细节,需要结合作业要求自行补全。

从 AI 那里我进一步明确了 MVVM 中命令绑定与 `ObservableCollection` 的用法,以及 GUI 环境下"所有网络请求必须异步"的原因。整体而言,本节难度适中偏高(GUI + MVVM + 异步的组合较复杂)。
Binary file added docs/05-advanced/assets/advanced-full.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/05-advanced/assets/advanced-robustness.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading