///|
/// 折叠栈(folded stack)格式的解析。
///
/// 输入格式为每行一条样本:
///
/// <帧1>;<帧2>;<帧3>;<...> <权重>
///
/// 约定与实现要点:
///
/// - 帧之间用分号分隔,**最后一个空格**之后是权重,因此帧名内部允许含空格;
/// - 权重按浮点解析:上游 moon-pprof 输出的是**纳秒时间**,不是整数采样计数;
/// - 空帧名统一兜底为 `(root)`——真实数据里出现过 `:ackermann;...` 这类以空帧
/// 开头的栈;
/// - 空白行直接忽略,非空但无法解析的行计入 `skipped_lines`。
///|
/// 一个采样样本:一条调用栈 + 权重。
///
/// `frames` 自顶向下排列,`frames[0]` 为根帧。
pub struct Sample {
frames : Array[String]
weight : Double
} derive(Eq)
///|
/// 显式声明 `Eq` 的方法可被当作常规方法调用。
///
/// 新版编译器不再自动完成这个提升,不声明会报 `implicit_impl_as_method`。
pub extend Sample with Eq::{not_equal, equal}
///|
/// 解析结果。`skipped_lines` 记录非空但无法解析的行数,便于调用方提示用户。
pub struct ParseResult {
samples : Array[Sample]
skipped_lines : Int
} derive(Eq)
///|
/// 显式声明 `Eq` 的方法可被当作常规方法调用。
///
/// 新版编译器不再自动完成这个提升,不声明会报 `implicit_impl_as_method`。
pub extend ParseResult with Eq::{not_equal, equal}
///|
/// 解析权重。无法解析或为非有限值(NaN / Inf)时返回 `None`——
/// 这类值会让后续布局的比例计算整体失效,必须在入口拦掉。
fn parse_weight(view : StringView) -> Double? {
let parsed : Result[Double, Error] = Ok(@string.parse_double(view)) catch {
error => Err(error)
}
match parsed {
Ok(value) =>
if value.is_nan() || value.is_inf() {
None
} else {
Some(value)
}
Err(_) => None
}
}
///|
/// 解析一行折叠栈。
///
/// 返回 `None` 表示该行不是有效样本,由调用方决定是忽略还是计入跳过数。
pub fn parse_line(line : StringView) -> Sample? {
let trimmed = line.trim()
if trimmed.is_empty() {
return None
}
// 最后一个空格分隔「栈」与「权重」
guard trimmed.rev_find(" ") is Some(sep) else { return None }
let stack_part = trimmed.view(end_offset=sep)
let weight_part = trimmed.view(start_offset=sep + 1)
guard parse_weight(weight_part) is Some(weight) else { return None }
let frames : Array[String] = []
for raw in stack_part.split(";") {
let name = raw.trim()
frames.push(if name.is_empty() { "(root)" } else { name.to_owned() })
}
guard frames.length() > 0 else { return None }
Some({ frames, weight, })
}
///|
/// 解析整个折叠栈文本。
///
/// 空白行被直接忽略;非空但无法解析的行计入 `skipped_lines`。
pub fn parse_folded(text : String) -> ParseResult {
let samples : Array[Sample] = []
let mut skipped = 0
for line in text.split("\n") {
match parse_line(line) {
Some(sample) => samples.push(sample)
None => if !line.is_blank() { skipped = skipped + 1 }
}
}
{ samples, skipped_lines: skipped, }
}