Skip to main content

tdm_server_rust/profile/
mod.rs

1//! 开发模式请求级调用栈耗时采集 (Request Profile)
2//!
3//! 在 dev 环境下自动采集每个 `#[tracing::instrument]` 的 self/inclusive 耗时,
4//! 支持终端树形输出、热点高亮、JetBrains/Cursor 源码跳转链接。
5//!
6//! ## 功能特性
7//!
8//! - **Self/Inclusive 耗时**: 区分自身耗时和包含子调用的总耗时
9//! - **热点高亮**: 自动标记 self 耗时最大的 span
10//! - **源码跳转**: JetBrains 过滤器格式 / Cursor/VS Code OSC 8 链接
11//! - **Server-Timing**: 生成 `Server-Timing` 响应头供前端联调
12//! - **JSONL 导出**: `DEV_PROFILE_JSONL` 环境变量控制 JSONL 文件输出
13//!
14//! ## 环境变量
15//!
16//! | 变量 | 默认值 | 说明 |
17//! |------|--------|------|
18//! | `DEV_PROFILE_MIN_MS` | 0.1 | 最小展示阈值 (ms) |
19//! | `DEV_PROFILE_LINK_SCHEME` | 自动检测 | 源码跳转协议 |
20//! | `DEV_PROFILE_NO_LINKS` | - | 禁用源码链接 |
21//! | `DEV_PROFILE_JSONL` | - | JSONL 输出文件路径 |
22//! | `DEV_SERVER_TIMING` | 1 | 是否输出 Server-Timing 头 |
23
24mod layer;
25
26/// dev 模式 tracing Layer,自动采集 span 耗时并生成调用栈树
27pub use layer::DevProfileLayer;
28
29use crate::middleware::HttpLogStyle;
30use axum::http::{Method, StatusCode, Uri};
31use serde::Serialize;
32use std::collections::HashMap;
33use std::path::{Path, PathBuf};
34use std::sync::{Arc, Mutex};
35use std::time::{Duration, Instant};
36use tracing::Span;
37
38/// 耗时列宽
39const TIME_WIDTH: usize = 8;
40
41/// Server-Timing 除 total/hot 外最多附加 span 数
42const SERVER_TIMING_EXTRA: usize = 4;
43
44/// 单次 span 记录
45#[derive(Debug, Clone)]
46struct SpanRecord {
47    /// 展示名(module::name)
48    label: String,
49    /// 开始时刻
50    started: Instant,
51    /// 结束时刻
52    ended: Option<Instant>,
53    /// 父 span id
54    parent: Option<tracing::Id>,
55    /// 源码文件(instrument 位置)
56    source_file: Option<String>,
57    /// 源码行号
58    source_line: Option<u32>,
59}
60
61/// 热点 span 摘要(JSONL 用)
62#[derive(Debug, Clone, Serialize)]
63pub struct ProfileHotspot {
64    /// 展示名
65    pub label: String,
66    /// self 毫秒
67    pub self_ms: f64,
68    /// self 占墙钟百分比
69    pub self_pct: f64,
70    /// 源码文件
71    pub file: Option<String>,
72    /// 源码行号
73    pub line: Option<u32>,
74}
75
76/// 单个 span 节点(JSONL 用)
77#[derive(Debug, Clone, Serialize)]
78pub struct ProfileSpanNode {
79    /// 展示名
80    pub label: String,
81    /// self 毫秒
82    pub self_ms: f64,
83    /// inclusive 毫秒
84    pub inclusive_ms: f64,
85    /// self 占墙钟百分比
86    pub self_pct: f64,
87    /// 源码文件
88    pub file: Option<String>,
89    /// 源码行号
90    pub line: Option<u32>,
91}
92
93/// 单次 HTTP 请求 profile 报告(JSONL 一行)
94#[derive(Debug, Clone, Serialize)]
95pub struct ProfileReport {
96    /// HTTP 方法
97    pub method: String,
98    /// 完整 URI(含 query)
99    pub uri: String,
100    /// 墙钟毫秒
101    pub wall_ms: f64,
102    /// HTTP 状态码
103    pub status: u16,
104    /// self 最大的 span
105    pub hotspot: Option<ProfileHotspot>,
106    /// 可见 span 列表
107    pub spans: Vec<ProfileSpanNode>,
108}
109
110/// 待打印节点(扁平收集后统一排版)
111struct PrintNode {
112    /// 树深度
113    depth: usize,
114    /// 展示名
115    label: String,
116    /// self 毫秒
117    self_ms: f64,
118    /// inclusive 毫秒
119    inclusive_ms: f64,
120    /// self 占墙钟时间百分比
121    self_pct: f64,
122    /// 源码文件
123    source_file: Option<String>,
124    /// 源码行号
125    source_line: Option<u32>,
126}
127
128/// 单个 HTTP 请求内的 span 聚合
129#[derive(Debug, Default)]
130pub struct RequestProfile {
131    /// span id → 记录
132    spans: HashMap<tracing::Id, SpanRecord>,
133    /// 创建顺序
134    order: Vec<tracing::Id>,
135}
136
137impl RequestProfile {
138    /// 创建空采集器
139    pub fn new() -> Self {
140        Self::default()
141    }
142
143    /// span 进入时登记
144    pub fn on_enter(
145        &mut self,
146        id: tracing::Id,
147        label: String,
148        parent: Option<tracing::Id>,
149        source_file: Option<String>,
150        source_line: Option<u32>,
151    ) {
152        if self.spans.contains_key(&id) {
153            return;
154        }
155        self.order.push(id.clone());
156        self.spans.insert(
157            id,
158            SpanRecord {
159                label,
160                started: Instant::now(),
161                ended: None,
162                parent,
163                source_file,
164                source_line,
165            },
166        );
167    }
168
169    /// span 关闭时记录结束时刻
170    pub fn on_close(&mut self, id: tracing::Id) {
171        if let Some(record) = self.spans.get_mut(&id) {
172            record.ended = Some(Instant::now());
173        }
174    }
175
176    /// 收集可见 span 节点
177    fn collect_print_nodes(&self, wall: Duration, min_show: Duration) -> Vec<PrintNode> {
178        if self.spans.is_empty() {
179            return Vec::new();
180        }
181        let wall_ms = wall.as_secs_f64() * 1000.0;
182        let roots = self.root_ids();
183        let mut nodes = Vec::new();
184        for root in &roots {
185            self.collect_nodes(root, 0, wall_ms, min_show, &mut nodes);
186        }
187        nodes
188    }
189
190    /// 构建结构化 profile 报告
191    pub fn build_report(
192        &self,
193        method: &Method,
194        uri: &Uri,
195        status: StatusCode,
196        wall: Duration,
197        min_show: Duration,
198    ) -> Option<ProfileReport> {
199        let nodes = self.collect_print_nodes(wall, min_show);
200        if nodes.is_empty() {
201            return None;
202        }
203        let wall_ms = wall.as_secs_f64() * 1000.0;
204        let endpoint = format_uri(uri);
205        let hotspot = nodes
206            .iter()
207            .max_by(|a, b| {
208                a.self_ms
209                    .partial_cmp(&b.self_ms)
210                    .unwrap_or(std::cmp::Ordering::Equal)
211            })
212            .filter(|n| n.self_ms > 0.0)
213            .map(|n| ProfileHotspot {
214                label: n.label.clone(),
215                self_ms: n.self_ms,
216                self_pct: n.self_pct,
217                file: n.source_file.clone(),
218                line: n.source_line,
219            });
220        Some(ProfileReport {
221            method: method.to_string(),
222            uri: endpoint,
223            wall_ms,
224            status: status.as_u16(),
225            hotspot,
226            spans: nodes
227                .iter()
228                .map(|n| ProfileSpanNode {
229                    label: n.label.clone(),
230                    self_ms: n.self_ms,
231                    inclusive_ms: n.inclusive_ms,
232                    self_pct: n.self_pct,
233                    file: n.source_file.clone(),
234                    line: n.source_line,
235                })
236                .collect(),
237        })
238    }
239
240    /// 格式化调用栈耗时树(不直接写 stderr,供整段原子输出)
241    pub fn format_report(
242        &self,
243        method: &Method,
244        uri: &Uri,
245        wall: Duration,
246        min_show: Duration,
247    ) -> Vec<String> {
248        let nodes = self.collect_print_nodes(wall, min_show);
249        if nodes.is_empty() {
250            return Vec::new();
251        }
252        HttpLogStyle::enable_ansi_support();
253        let style = HttpLogStyle::detect();
254
255        let hotspot_idx = nodes
256            .iter()
257            .enumerate()
258            .max_by(|(_, a), (_, b)| {
259                a.self_ms
260                    .partial_cmp(&b.self_ms)
261                    .unwrap_or(std::cmp::Ordering::Equal)
262            })
263            .filter(|(_, n)| n.self_ms > 0.0)
264            .map(|(i, _)| i);
265
266        let mut lines = Vec::new();
267        lines.push(style.divider());
268        lines.push(format!(
269            "{} {} {}  {}",
270            style.label("prof │"),
271            style.method(method),
272            style.endpoint(uri),
273            style.elapsed(wall),
274        ));
275
276        for (i, node) in nodes.iter().enumerate() {
277            let is_hot = hotspot_idx == Some(i);
278            lines.extend(Self::format_node(node, is_hot, &style));
279        }
280
281        if let Some(idx) = hotspot_idx {
282            if let Some(node) = nodes.get(idx) {
283                let hint = format!(
284                    "hotspot {}  self {}  ({:.1}%)",
285                    node.label,
286                    format_ms(node.self_ms),
287                    node.self_pct,
288                );
289                lines.push(format!(
290                    "{} {}",
291                    style.label("prof │"),
292                    style.hotspot(&hint),
293                ));
294                if let Some(jb) =
295                    jetbrains_filter_line(node.source_file.as_deref(), node.source_line)
296                {
297                    lines.push(jb);
298                }
299            }
300        }
301
302        lines.push(style.divider());
303        lines
304    }
305
306    /// 输出调用栈耗时树到 stderr
307    pub fn print_report(&self, method: &Method, uri: &Uri, wall: Duration, min_show: Duration) {
308        HttpLogStyle::print_lines(&self.format_report(method, uri, wall, min_show));
309    }
310
311    /// 生成 `Server-Timing` 响应头(dev 前后端联调)
312    pub fn format_server_timing(&self, wall: Duration, min_show: Duration) -> Option<String> {
313        if !server_timing_enabled() {
314            return None;
315        }
316        let nodes = self.collect_print_nodes(wall, min_show);
317        if nodes.is_empty() {
318            return None;
319        }
320        let wall_ms = wall.as_secs_f64() * 1000.0;
321        let mut parts = vec![format!("total;dur={wall_ms:.2}")];
322
323        let hotspot_idx = nodes
324            .iter()
325            .enumerate()
326            .max_by(|(_, a), (_, b)| {
327                a.self_ms
328                    .partial_cmp(&b.self_ms)
329                    .unwrap_or(std::cmp::Ordering::Equal)
330            })
331            .filter(|(_, n)| n.self_ms > 0.0)
332            .map(|(i, _)| i);
333
334        if let Some(idx) = hotspot_idx {
335            let node = &nodes[idx];
336            parts.push(server_timing_metric("hot", node.self_ms, &node.label));
337        }
338
339        let mut ranked: Vec<_> = nodes
340            .iter()
341            .enumerate()
342            .filter(|(i, n)| Some(*i) != hotspot_idx && n.self_ms > 0.0)
343            .collect();
344        ranked.sort_by(|(_, a), (_, b)| {
345            b.self_ms
346                .partial_cmp(&a.self_ms)
347                .unwrap_or(std::cmp::Ordering::Equal)
348        });
349        for (seq, (_, node)) in ranked.into_iter().take(SERVER_TIMING_EXTRA).enumerate() {
350            parts.push(server_timing_metric(
351                &format!("s{}", seq + 1),
352                node.self_ms,
353                &node.label,
354            ));
355        }
356
357        Some(parts.join(", "))
358    }
359
360    /// 取根 span(父不在表内或无父)
361    fn root_ids(&self) -> Vec<tracing::Id> {
362        self.order
363            .iter()
364            .filter(|id| {
365                self.spans
366                    .get(*id)
367                    .and_then(|s| s.parent.as_ref())
368                    .map(|p| !self.spans.contains_key(p))
369                    .unwrap_or(true)
370            })
371            .cloned()
372            .collect()
373    }
374
375    /// 递归收集可见节点(跳过 http.request 容器)
376    fn collect_nodes(
377        &self,
378        id: &tracing::Id,
379        depth: usize,
380        wall_ms: f64,
381        min_show: Duration,
382        out: &mut Vec<PrintNode>,
383    ) {
384        let Some(record) = self.spans.get(id) else {
385            return;
386        };
387
388        if record.label.ends_with("::http.request") || record.label == "http.request" {
389            for child in self.direct_children(id) {
390                self.collect_nodes(&child, depth, wall_ms, min_show, out);
391            }
392            return;
393        }
394
395        let inclusive = record.duration();
396        let inclusive_ms = inclusive.as_secs_f64() * 1000.0;
397        let children_ms: f64 = self
398            .direct_children(id)
399            .iter()
400            .filter_map(|cid| self.spans.get(cid))
401            .map(|c| c.duration().as_secs_f64() * 1000.0)
402            .sum();
403        let self_ms = (inclusive_ms - children_ms).max(0.0);
404
405        if Duration::from_secs_f64(self_ms / 1000.0) < min_show {
406            for child in self.direct_children(id) {
407                self.collect_nodes(&child, depth + 1, wall_ms, min_show, out);
408            }
409            return;
410        }
411
412        let self_pct = if wall_ms > 0.0 {
413            self_ms / wall_ms * 100.0
414        } else {
415            0.0
416        };
417        out.push(PrintNode {
418            depth,
419            label: record.label.clone(),
420            self_ms,
421            inclusive_ms,
422            self_pct,
423            source_file: record.source_file.clone(),
424            source_line: record.source_line,
425        });
426
427        for child in self.direct_children(id) {
428            self.collect_nodes(&child, depth + 1, wall_ms, min_show, out);
429        }
430    }
431
432    /// 格式化单行节点
433    fn format_node(node: &PrintNode, is_hot: bool, style: &HttpLogStyle) -> Vec<String> {
434        let branch = tree_branch(node.depth);
435        let label_col = format_method_label(
436            style,
437            &node.label,
438            node.source_file.as_deref(),
439            node.source_line,
440        );
441
442        let hot_mark = if is_hot { " ◀ hot" } else { "" };
443        let line = format!(
444            "{branch}{label_col}  self {:>tw$}  incl {:>tw$}  {:>5.1}%{hot_mark}",
445            format_ms(node.self_ms),
446            format_ms(node.inclusive_ms),
447            node.self_pct,
448            tw = TIME_WIDTH,
449        );
450
451        let mut lines = vec![format!(
452            "{} {}",
453            style.label("prof │"),
454            style.prof_text(&line, is_hot),
455        )];
456        if let Some(jb) = jetbrains_filter_line(node.source_file.as_deref(), node.source_line) {
457            lines.push(jb);
458        }
459        lines
460    }
461
462    /// 直接子 span
463    fn direct_children(&self, id: &tracing::Id) -> Vec<tracing::Id> {
464        self.order
465            .iter()
466            .filter(|child| self.spans.get(*child).and_then(|s| s.parent.as_ref()) == Some(id))
467            .cloned()
468            .collect()
469    }
470}
471
472impl SpanRecord {
473    /// span inclusive 耗时
474    fn duration(&self) -> Duration {
475        self.ended
476            .map(|end| end.duration_since(self.started))
477            .unwrap_or_else(|| self.started.elapsed())
478    }
479}
480
481tokio::task_local! {
482    /// 当前请求的耗时采集器
483    static REQUEST_PROFILE: Arc<Mutex<RequestProfile>>;
484}
485
486/// 是否处于请求 profiling 上下文
487pub fn is_active() -> bool {
488    REQUEST_PROFILE.try_with(|_| ()).is_ok()
489}
490
491/// 在请求 profiling 作用域内执行 future
492pub async fn scope<F>(profile: Arc<Mutex<RequestProfile>>, fut: F) -> F::Output
493where
494    F: std::future::Future,
495{
496    REQUEST_PROFILE.scope(profile, fut).await
497}
498
499/// 写入 span 进入事件(Layer 调用)
500pub fn record_enter(
501    id: tracing::Id,
502    label: String,
503    parent: Option<tracing::Id>,
504    source_file: Option<&str>,
505    source_line: Option<u32>,
506) {
507    let _ = REQUEST_PROFILE.try_with(|cell| {
508        if let Ok(mut guard) = cell.lock() {
509            guard.on_enter(
510                id,
511                label,
512                parent,
513                source_file.map(str::to_string),
514                source_line,
515            );
516        }
517    });
518}
519
520/// 写入 span 关闭事件(Layer 调用)
521pub fn record_close(id: tracing::Id) {
522    let _ = REQUEST_PROFILE.try_with(|cell| {
523        if let Ok(mut guard) = cell.lock() {
524            guard.on_close(id);
525        }
526    });
527}
528
529/// 从 tracing metadata 组装展示名
530pub fn span_label(meta: &tracing::Metadata<'_>) -> String {
531    let target = meta.target();
532    let short = target.rsplit("::").take(2).collect::<Vec<_>>();
533    let module = short.into_iter().rev().collect::<Vec<_>>().join("::");
534    format!("{}::{}", module, meta.name())
535}
536
537/// 是否输出 Server-Timing 响应头(`DEV_SERVER_TIMING=0` 关闭)
538fn server_timing_enabled() -> bool {
539    std::env::var("DEV_SERVER_TIMING").ok().as_deref() != Some("0")
540}
541
542/// 组装单条 Server-Timing metric
543fn server_timing_metric(name: &str, dur_ms: f64, label: &str) -> String {
544    let desc = escape_server_timing_desc(&server_timing_desc(label));
545    format!("{name};dur={dur_ms:.2};desc=\"{desc}\"")
546}
547
548/// 缩短 span 展示名(取末尾两段 `::`)
549fn server_timing_desc(label: &str) -> String {
550    let parts: Vec<&str> = label.split("::").collect();
551    if parts.len() <= 2 {
552        label.to_string()
553    } else {
554        parts[parts.len() - 2..].join("::")
555    }
556}
557
558/// 转义 Server-Timing desc 中的引号与反斜杠
559fn escape_server_timing_desc(s: &str) -> String {
560    s.replace('\\', "\\\\").replace('"', "\\\"")
561}
562
563/// 读取最小展示阈值(毫秒),默认 0.1ms
564pub fn min_show_duration() -> Duration {
565    std::env::var("DEV_PROFILE_MIN_MS")
566        .ok()
567        .and_then(|v| v.parse::<f64>().ok())
568        .map(|ms| Duration::from_secs_f64(ms / 1000.0))
569        .unwrap_or_else(|| Duration::from_secs_f64(0.0001))
570}
571
572/// 将 profile 报告追加到 JSONL 文件(`DEV_PROFILE_JSONL` 环境变量)
573pub fn append_jsonl_report(report: &ProfileReport) {
574    let Ok(path) = std::env::var("DEV_PROFILE_JSONL") else {
575        return;
576    };
577    let Ok(line) = serde_json::to_string(report) else {
578        return;
579    };
580    HttpLogStyle::append_jsonl(&path, &line);
581}
582
583/// 格式化 URI(含 query)
584fn format_uri(uri: &Uri) -> String {
585    match uri.query() {
586        Some(q) => format!("{}?{q}", uri.path()),
587        None => uri.path().to_string(),
588    }
589}
590
591/// 创建 HTTP 根 span
592pub fn http_request_span(method: &Method, uri: &Uri) -> Span {
593    tracing::debug_span!(
594        "http.request",
595        method = %method,
596        uri = %uri,
597    )
598}
599
600/// 树形前缀
601fn tree_branch(depth: usize) -> String {
602    if depth == 0 {
603        "├─ ".to_string()
604    } else {
605        format!("{}└─ ", "│  ".repeat(depth.saturating_sub(1)))
606    }
607}
608
609/// 格式化毫秒
610fn format_ms(ms: f64) -> String {
611    if ms >= 100.0 {
612        format!("{ms:.0}ms")
613    } else if ms >= 10.0 {
614        format!("{ms:.1}ms")
615    } else {
616        format!("{ms:.2}ms")
617    }
618}
619
620/// 链接协议
621enum LinkScheme {
622    /// JetBrains / RustRover(独立一行 ` at path:line:col`)
623    JetBrains,
624    /// Cursor
625    Cursor,
626    /// VS Code
627    Vscode,
628    /// VS Code Insiders
629    VscodeInsiders,
630    /// file://
631    File,
632    /// 禁用
633    None,
634}
635
636impl LinkScheme {
637    /// 解析链接协议(环境变量 > 终端自动检测)
638    fn detect() -> Self {
639        if std::env::var("DEV_PROFILE_NO_LINKS").is_ok() {
640            return Self::None;
641        }
642        match std::env::var("DEV_PROFILE_LINK_SCHEME")
643            .unwrap_or_default()
644            .to_ascii_lowercase()
645            .as_str()
646        {
647            "none" => Self::None,
648            "jetbrains" | "rustrover" | "idea" | "goland" | "webstorm" => Self::JetBrains,
649            "vscode" => Self::Vscode,
650            "vscode-insiders" => Self::VscodeInsiders,
651            "file" => Self::File,
652            "cursor" => Self::Cursor,
653            "" => {
654                let emulator = std::env::var("TERMINAL_EMULATOR").unwrap_or_default();
655                if emulator.contains("JetBrains") {
656                    Self::JetBrains
657                } else if std::env::var("TERM_PROGRAM").unwrap_or_default() == "vscode" {
658                    Self::Cursor
659                } else {
660                    Self::JetBrains
661                }
662            }
663            _ => Self::Cursor,
664        }
665    }
666}
667
668/// 方法名(Cursor 等 IDE 可 OSC 8 跳转)
669fn format_method_label(
670    style: &HttpLogStyle,
671    method: &str,
672    file: Option<&str>,
673    line: Option<u32>,
674) -> String {
675    if matches!(LinkScheme::detect(), LinkScheme::JetBrains) {
676        return method.to_string();
677    }
678    let Some(uri) = build_osc8_uri(file, line) else {
679        return method.to_string();
680    };
681    style.link(&uri, method)
682}
683
684/// 生成 JetBrains 过滤器可识别的整行文本
685fn jetbrains_filter_line(file: Option<&str>, line: Option<u32>) -> Option<String> {
686    if !matches!(LinkScheme::detect(), LinkScheme::JetBrains) {
687        return None;
688    }
689    let file = file?;
690    let line = line?;
691    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
692    let path = resolve_source_path(&manifest, file);
693    let rel = project_relative_path(&path, &manifest);
694    let style = std::env::var("DEV_PROFILE_JB_STYLE").unwrap_or_else(|_| "rustc".into());
695    Some(match style.as_str() {
696        // RsBacktraceFilter: ^\s+at FILE:LINE(:COL)?$
697        "stack" | "at" => format!(" at {rel}:{line}:1"),
698        // RsConsoleFilter: ^(?:\s+--> )?FILE:LINE(:COL)?.*$
699        _ => format!("   --> {rel}:{line}:1"),
700    })
701}
702
703/// crate 内相对路径(正斜杠)
704fn project_relative_path(path: &Path, manifest: &Path) -> String {
705    path.strip_prefix(manifest)
706        .map(path_to_link)
707        .unwrap_or_else(|_| path_to_link(path))
708}
709
710/// 构建 OSC 8 URI(Cursor / VS Code)
711fn build_osc8_uri(file: Option<&str>, line: Option<u32>) -> Option<String> {
712    let file = file?;
713    let line = line?;
714    let scheme = LinkScheme::detect();
715    if matches!(scheme, LinkScheme::None | LinkScheme::JetBrains) {
716        return None;
717    }
718    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
719    let path = resolve_source_path(&manifest, file);
720    let abs = path_to_link(&path);
721    Some(match scheme {
722        LinkScheme::File => format!("file://{abs}:{line}:1"),
723        LinkScheme::Vscode => format!("vscode://file/{abs}:{line}:1"),
724        LinkScheme::VscodeInsiders => format!("vscode-insiders://file/{abs}:{line}:1"),
725        LinkScheme::Cursor => format!("cursor://file/{abs}:{line}:1"),
726        LinkScheme::JetBrains | LinkScheme::None => return None,
727    })
728}
729
730/// 路径转链接用字符串(去掉 Windows \\?\ 扩展前缀)
731fn path_to_link(path: &Path) -> String {
732    let raw = path.to_string_lossy();
733    let stripped = raw
734        .strip_prefix(r"\\?\")
735        .or_else(|| raw.strip_prefix("//?/"))
736        .unwrap_or(&raw);
737    stripped.replace('\\', "/")
738}
739
740/// 解析 instrument 返回的 file 路径
741fn resolve_source_path(manifest: &Path, file: &str) -> PathBuf {
742    let p = PathBuf::from(file);
743    if p.is_absolute() {
744        p
745    } else {
746        manifest.join(p)
747    }
748}