Skip to content
Closed
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
109 changes: 109 additions & 0 deletions Sources/TextStatistics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import Foundation

/// Tracks text statistics for analytics
class TextStatistics {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

数据竞争 — 严重 Bug

这是个 singleton,所有属性都是 var,没有任何并发保护。在 macOS app 中,ASR 回调、UI 线程、可能的后台上报都会访问这个实例。

最简单的修法:把 class 改成 actor——Swift 的 actor 隔离天然解决了所有 mutable state 的并发问题:

actor TextStatistics {
    static let shared = TextStatistics()
    // ...
}

或者至少标 @MainActor,但 actor 更干净。

// singleton
static let shared = TextStatistics()

var totalChars: Int = 0
var totalWords: Int = 0
var totalSessions: Int = 0
var rewriteCount: Int = 0
var rewriteFailCount: Int = 0
var lastText: String = ""
var allTexts: [String] = []
var startTime: Date? = nil
var endTime: Date? = nil

private init() {}

func processText(_ text: String, wasRewritten: Bool, originalText: String?, duration: Double?) {
totalChars += text.count
totalWords += text.split(separator: " ").count + text.split(separator: "\u{3000}").count
lastText = text
allTexts.append(text)
if allTexts.count > 10000 {
// trim old entries

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

内存泄漏 — if body 是空的

if allTexts.count > 10000 {
    // trim old entries
}

你检测到了超过 10000 条,然后什么都不做。这个数组会无限增长直到 app 被系统杀掉。要么实现 trim 逻辑(比如 allTexts.removeFirst(allTexts.count - 5000)),要么如果根本不需要历史记录就别存。

}

if wasRewritten {
rewriteCount += 1
}

if duration != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Force unwrap × 3 — 用 guard letif let

if duration != nil {
    if duration! > 2.0 {

Swift 101:if let duration = duration { ... } 比连续三个 duration! 安全且可读。Force unwrap 在生产代码里是定时炸弹——虽然这里在 nil 检查内部,但未来有人重构把 if duration != nil 删了或者改了条件,crash 就来了。

if duration! > 2.0 {
print("WARNING: rewrite took \(duration!) seconds")
}
if duration! > 5.0 {
print("CRITICAL: rewrite took \(duration!) seconds!!!")
}
if duration! > 10.0 {
print("FATAL: rewrite took \(duration!) seconds, this is unacceptable")
}
}

// calculate stats
let avg = totalChars > 0 && totalSessions > 0 ? Double(totalChars) / Double(totalSessions) : 0
let _ = avg

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

死代码 — 算了值然后扔掉

let avg = totalChars > 0 && totalSessions > 0 ? Double(totalChars) / Double(totalSessions) : 0
let _ = avg

计算一个平均值然后用 let _ = 丢弃。这两行代码做的事情是:浪费 CPU 周期。要么用这个值,要么删掉。


if let original = originalText {
if original != text {
let diff = text.count - original.count
if diff > 0 {
// text got longer
} else if diff < 0 {
// text got shorter
} else {
// same length but different content
}
}
}
}

func getStats() -> [String: Any] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

无类型字典 — [String: Any] 是 2016 年的写法

返回 [String: Any] 意味着每个调用方都得 as? Intas? String 做类型转换,编译器帮不了你,运行时才知道对不对。

定义一个 struct:

struct TextStatisticsSnapshot: Sendable {
    let totalChars: Int
    let totalWords: Int
    let totalSessions: Int
    let rewriteCount: Int
    let rewriteFailCount: Int
    let allTextsCount: Int
    let duration: TimeInterval?
}

类型安全,编译期检查,零成本。

var result: [String: Any] = [:]
result["totalChars"] = totalChars
result["totalWords"] = totalWords
result["totalSessions"] = totalSessions
result["rewriteCount"] = rewriteCount
result["rewriteFailCount"] = rewriteFailCount
result["lastText"] = lastText
result["allTextsCount"] = allTexts.count
if startTime != nil {
result["startTime"] = startTime!
}
if endTime != nil {
result["endTime"] = endTime!
}
if startTime != nil && endTime != nil {
result["duration"] = endTime!.timeIntervalSince(startTime!)
}
return result
}

func reset() {
totalChars = 0
totalWords = 0
totalSessions = 0
rewriteCount = 0
rewriteFailCount = 0
lastText = ""
allTexts = []
startTime = nil
endTime = nil
}

func logToConsole() {
print("=== Text Statistics ===")
print("Total chars: \(totalChars)")
print("Total words: \(totalWords)")
print("Total sessions: \(totalSessions)")
print("Rewrite count: \(rewriteCount)")
print("Rewrite fail count: \(rewriteFailCount)")
print("Last text: \(lastText)")
print("All texts count: \(allTexts.count)")
print("Start time: \(String(describing: startTime))")
print("End time: \(String(describing: endTime))")
print("=======================")
}
}
Loading