///|
/// 文本报告:调用关系查询、差异排行、栈过滤。
///
/// 这些都是**文本形态**的分析能力,与出图互补:
/// 图适合「一眼看出大头在哪」,文本适合「精确回答某个函数的上下游是谁」。
/// 把它们做成子命令而不是渲染选项,顶层参数因此不会随功能增长而膨胀。
///|
/// 一条调用关系条目。
pub struct Relation {
/// 可读的标签:callers 是祖先链,callees 是子函数名
label : String
/// 该条目的权重
value : Double
/// 占全图的比例
ratio : Double
} derive(Eq)
///|
/// 显式声明 `Eq` 的方法可被当作常规方法调用。
///
/// 新版编译器不再自动完成这个提升,不声明会报 `implicit_impl_as_method`。
pub extend Relation with Eq::{not_equal, equal}
///|
/// 按权重降序、同值按标签升序,保证输出确定。
fn compare_relations(a : Relation, b : Relation) -> Int {
if a.value > b.value {
-1
} else if a.value < b.value {
1
} else {
String::compare(a.label, b.label)
}
}
///|
/// 查询匹配规则:两侧都先归一化名字,再按**子串**比对。
///
/// 用子串而不是全等,是因为真实数据里的节点名是 `moonflame::demo::walk__tree`
/// 这样的完整路径,要求用户一字不差地打全既累赘又容易错;
/// 归一化则让 `walk_tree` 和 `walk__tree` 两种写法都能命中。
fn matches_query(name : String, query : String) -> Bool {
if query == "" {
return false
}
normalize_name(name).contains(normalize_name(query))
}
///|
/// 深度优先遍历,按**直接调用者**聚合权重。
///
/// 只记直属父节点的名字、并把同一父节点的多次出现合并,而不是把每条完整
/// 调用链都列一行——递归函数会产生大量几乎相同的长链,全列出来反而看不出
/// 「到底是谁在调用它」。
fn collect_callers(
node : Node,
parent : String,
wanted : String,
acc : Map[String, Double],
) -> Unit {
if matches_query(node.name, wanted) {
// 自递归产生的边要被排除:那条边承载的权重就是本函数自己的子树,
// 逐层累加会让同一段时间被重复计入,占比算出 369% 这种荒谬值。
// 递归深度不改变「谁在调用它」这个问题的答案。
let self_edge = parent != "" && matches_query(parent, wanted)
if !self_edge {
let label = if parent == "" { "(根帧)" } else { parent }
let current = match acc.get(label) {
Some(value) => value
None => 0.0
}
acc.set(label, current + node.value)
}
}
for child in node.children {
collect_callers(child, node.name, wanted, acc)
}
}
///|
/// 深度优先遍历,按**直接子函数名**聚合权重。
fn collect_callees(
node : Node,
wanted : String,
acc : Map[String, Double],
) -> Unit {
if matches_query(node.name, wanted) {
for child in node.children {
let current = match acc.get(child.name) {
Some(value) => value
None => 0.0
}
acc.set(child.name, current + child.value)
}
}
for child in node.children {
collect_callees(child, wanted, acc)
}
}
///|
/// 把「名字 → 权重」的累加表转成排序好的条目列表。
fn relations_of(acc : Map[String, Double], total : Double) -> Array[Relation] {
let out : Array[Relation] = []
for pair in acc {
out.push({
label: pair.0,
value: pair.1,
ratio: if total > 0.0 {
pair.1 / total
} else {
0.0
},
})
}
out.sort_by(compare_relations)
out
}
///|
/// 查询「谁调用了这个函数」。按直接调用者聚合。
///
/// 同一个函数会在多条路径下被调用(例如 `array::Array::at` 既在冒泡排序里
/// 也在插入排序里被调用),因此返回的是一个列表而不是单条路径。
pub fn find_callers(tree : CallTree, wanted : String) -> Array[Relation] {
let acc : Map[String, Double] = Map([])
for root in tree.roots {
collect_callers(root, "", wanted, acc)
}
relations_of(acc, tree.total)
}
///|
/// 查询「这个函数调用了谁」。按被调用者聚合。
pub fn find_callees(tree : CallTree, wanted : String) -> Array[Relation] {
let acc : Map[String, Double] = Map([])
for root in tree.roots {
collect_callees(root, wanted, acc)
}
relations_of(acc, tree.total)
}
///|
/// 把调用关系渲染成文本。
pub fn render_relations(
relations : Array[Relation],
wanted : String,
kind : String,
unit : WeightUnit,
) -> String {
let sb = StringBuilder()
if relations.length() == 0 {
sb.write_string("没有找到与「\{wanted}」匹配的帧。\n")
return sb.to_string()
}
sb.write_string("\{kind}「\{wanted}」— 共 \{relations.length()} 项\n\n")
sb.write_string(" 占比 权重 调用链\n")
for r in relations {
sb.write_string(
" " +
String::pad_start(format_percent(r.ratio), 6, ' ') +
" " +
String::pad_start(format_weight(r.value, unit), 12, ' ') +
" " +
r.label +
"\n",
)
}
sb.to_string()
}
///|
/// 一个函数在两份剖面之间的自身耗时变化。
pub struct DeltaEntry {
name : String
before : Double
after : Double
/// `after - before`,正数表示变慢了
change : Double
/// 变化量占基线总量的比例
ratio : Double
} derive(Eq)
///|
/// 显式声明 `Eq` 的方法可被当作常规方法调用。
///
/// 新版编译器不再自动完成这个提升,不声明会报 `implicit_impl_as_method`。
pub extend DeltaEntry with Eq::{not_equal, equal}
///|
/// 按变化量降序(变慢最多的在前),同值按名字升序。
fn compare_deltas(a : DeltaEntry, b : DeltaEntry) -> Int {
if a.change > b.change {
-1
} else if a.change < b.change {
1
} else {
String::compare(a.name, b.name)
}
}
///|
/// 对比两份剖面的**自身耗时**,按函数名对齐。
///
/// 只出现在其中一侧的函数也会被列出(另一侧记 0),因为「新增的开销」
/// 和「消失的开销」恰恰是差异分析最关心的两件事。
///
/// `top <= 0` 表示不截断。
pub fn compute_delta(
before : CallTree,
after : CallTree,
top : Int,
) -> Array[DeltaEntry] {
let before_map : Map[String, Double] = Map([])
for hotspot in top_hotspots(before, 0) {
before_map.set(hotspot.name, hotspot.self_time)
}
let after_map : Map[String, Double] = Map([])
for hotspot in top_hotspots(after, 0) {
after_map.set(hotspot.name, hotspot.self_time)
}
let all : Array[DeltaEntry] = []
let seen : Map[String, Bool] = Map([])
for hotspot in top_hotspots(after, 0) {
let name = hotspot.name
seen.set(name, true)
let previous = match before_map.get(name) {
Some(value) => value
None => 0.0
}
all.push({
name,
before: previous,
after: hotspot.self_time,
change: hotspot.self_time - previous,
ratio: if before.total > 0.0 {
(hotspot.self_time - previous) / before.total
} else {
0.0
},
})
}
for hotspot in top_hotspots(before, 0) {
if seen.get(hotspot.name) is None {
all.push({
name: hotspot.name,
before: hotspot.self_time,
after: 0.0,
change: -hotspot.self_time,
ratio: if before.total > 0.0 {
-hotspot.self_time / before.total
} else {
0.0
},
})
}
}
all.sort_by(compare_deltas)
let count = if top > 0 && top < all.length() { top } else { all.length() }
let kept : Array[DeltaEntry] = []
for i = 0; i < count; i = i + 1 {
kept.push(all[i])
}
kept
}
///|
/// 把差异排行渲染成文本。
///
/// 与差异火焰图的分工:图看**结构**(哪条路径变宽了),
/// 这张表看**函数**(哪个函数的自身耗时变了多少)。
pub fn render_delta(
entries : Array[DeltaEntry],
before_total : Double,
after_total : Double,
unit : WeightUnit,
) -> String {
let sb = StringBuilder()
sb.write_string(
"自身耗时变化(基线共 \{format_weight(before_total, unit)}," +
"当前共 \{format_weight(after_total, unit)})\n",
)
sb.write_string("\n")
if entries.length() == 0 {
sb.write_string("两份剖面没有可比对的函数。\n")
return sb.to_string()
}
sb.write_string(" 变化 变化% 基线 当前 函数\n")
for e in entries {
sb.write_string(
" " +
String::pad_start(format_signed_weights(e.change, unit), 12, ' ') +
" " +
String::pad_start(format_percent(e.ratio), 7, ' ') +
" " +
String::pad_start(format_weight(e.before, unit), 11, ' ') +
" " +
String::pad_start(format_weight(e.after, unit), 11, ' ') +
" " +
e.name +
"\n",
)
}
sb.to_string()
}
///|
/// 取出折叠栈行里的「栈」部分(丢掉末尾的权重)。
///
/// 不用「找最后一个空格再切」的写法:名字里可能含多字节字符,
/// 按字符下标切片容易切错字节边界。按空格拆开再拼回去没有这个问题。
fn stack_part(line : String) -> String {
let parts : Array[String] = []
for piece in line.split(" ") {
parts.push(piece.to_owned())
}
if parts.length() <= 1 {
return line
}
let head : Array[String] = []
for i = 0; i < parts.length() - 1; i = i + 1 {
head.push(parts[i])
}
head.join(" ")
}
///|
/// 对整条栈逐帧做名字归一化。
///
/// 不能把整串丢给 `normalize_name`:那个函数是按**单个帧名**设计的,
/// 而栈串以 `____moonbit__main` 开头,会命中「下划线开头不折叠」的保护规则,
/// 结果整条栈一个下划线都折不了。
fn normalized_stack(stack : String) -> String {
let parts : Array[String] = []
for frame in stack.split(";") {
parts.push(normalize_name(frame.to_owned()))
}
parts.join(";")
}
///|
/// 按子串过滤折叠栈:只保留调用链里含该子串的行。
///
/// 这是经典的 `grep funcA input | flamegraph.pl` 用法,但省掉了管道——
/// 过滤后的结果仍是折叠栈格式,可以直接再喂给出图命令。
///
/// 匹配前做名字归一化,否则用户按可读名 `build_strings` 去筛,
/// 会一条都筛不到(数据里存的是 `build__strings`)。
///
/// 权重原样保留、不做归一化:过滤后的图回答的是「这个子系统内部怎么分配时间」,
/// 而它占全局多少,靠保留原始权重才能在两张图之间对照。
pub fn filter_folded(text : String, pattern : String) -> String {
let wanted = normalize_name(pattern)
let sb = StringBuilder()
for raw in text.split("\n") {
let line = raw.trim().to_owned()
if line == "" {
continue
}
// 只匹配栈部分,免得像 "42" 这样的模式误命中权重数字
if wanted != "" && normalized_stack(stack_part(line)).contains(wanted) {
sb.write_string(line)
sb.write_string("\n")
}
}
sb.to_string()
}