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) = jetbrains_filter_line(node.source_file.as_deref(), node.source_line)
295                {
296                    lines.push(jb);
297                }
298            }
299        }
300
301        lines.push(style.divider());
302        lines
303    }
304
305    /// 输出调用栈耗时树到 stderr
306    pub fn print_report(
307        &self,
308        method: &Method,
309        uri: &Uri,
310        wall: Duration,
311        min_show: Duration,
312    ) {
313        HttpLogStyle::print_lines(&self.format_report(method, uri, wall, min_show));
314    }
315
316    /// 生成 `Server-Timing` 响应头(dev 前后端联调)
317    pub fn format_server_timing(&self, wall: Duration, min_show: Duration) -> Option<String> {
318        if !server_timing_enabled() {
319            return None;
320        }
321        let nodes = self.collect_print_nodes(wall, min_show);
322        if nodes.is_empty() {
323            return None;
324        }
325        let wall_ms = wall.as_secs_f64() * 1000.0;
326        let mut parts = vec![format!("total;dur={wall_ms:.2}")];
327
328        let hotspot_idx = nodes
329            .iter()
330            .enumerate()
331            .max_by(|(_, a), (_, b)| {
332                a.self_ms
333                    .partial_cmp(&b.self_ms)
334                    .unwrap_or(std::cmp::Ordering::Equal)
335            })
336            .filter(|(_, n)| n.self_ms > 0.0)
337            .map(|(i, _)| i);
338
339        if let Some(idx) = hotspot_idx {
340            let node = &nodes[idx];
341            parts.push(server_timing_metric("hot", node.self_ms, &node.label));
342        }
343
344        let mut ranked: Vec<_> = nodes
345            .iter()
346            .enumerate()
347            .filter(|(i, n)| Some(*i) != hotspot_idx && n.self_ms > 0.0)
348            .collect();
349        ranked.sort_by(|(_, a), (_, b)| {
350            b.self_ms
351                .partial_cmp(&a.self_ms)
352                .unwrap_or(std::cmp::Ordering::Equal)
353        });
354        for (seq, (_, node)) in ranked
355            .into_iter()
356            .take(SERVER_TIMING_EXTRA)
357            .enumerate()
358        {
359            parts.push(server_timing_metric(
360                &format!("s{}", seq + 1),
361                node.self_ms,
362                &node.label,
363            ));
364        }
365
366        Some(parts.join(", "))
367    }
368
369    /// 取根 span(父不在表内或无父)
370    fn root_ids(&self) -> Vec<tracing::Id> {
371        self.order
372            .iter()
373            .filter(|id| {
374                self.spans
375                    .get(*id)
376                    .and_then(|s| s.parent.as_ref())
377                    .map(|p| !self.spans.contains_key(p))
378                    .unwrap_or(true)
379            })
380            .cloned()
381            .collect()
382    }
383
384    /// 递归收集可见节点(跳过 http.request 容器)
385    fn collect_nodes(
386        &self,
387        id: &tracing::Id,
388        depth: usize,
389        wall_ms: f64,
390        min_show: Duration,
391        out: &mut Vec<PrintNode>,
392    ) {
393        let Some(record) = self.spans.get(id) else {
394            return;
395        };
396
397        if record.label.ends_with("::http.request") || record.label == "http.request" {
398            for child in self.direct_children(id) {
399                self.collect_nodes(&child, depth, wall_ms, min_show, out);
400            }
401            return;
402        }
403
404        let inclusive = record.duration();
405        let inclusive_ms = inclusive.as_secs_f64() * 1000.0;
406        let children_ms: f64 = self
407            .direct_children(id)
408            .iter()
409            .filter_map(|cid| self.spans.get(cid))
410            .map(|c| c.duration().as_secs_f64() * 1000.0)
411            .sum();
412        let self_ms = (inclusive_ms - children_ms).max(0.0);
413
414        if Duration::from_secs_f64(self_ms / 1000.0) < min_show {
415            for child in self.direct_children(id) {
416                self.collect_nodes(&child, depth + 1, wall_ms, min_show, out);
417            }
418            return;
419        }
420
421        let self_pct = if wall_ms > 0.0 {
422            self_ms / wall_ms * 100.0
423        } else {
424            0.0
425        };
426        out.push(PrintNode {
427            depth,
428            label: record.label.clone(),
429            self_ms,
430            inclusive_ms,
431            self_pct,
432            source_file: record.source_file.clone(),
433            source_line: record.source_line,
434        });
435
436        for child in self.direct_children(id) {
437            self.collect_nodes(&child, depth + 1, wall_ms, min_show, out);
438        }
439    }
440
441    /// 格式化单行节点
442    fn format_node(node: &PrintNode, is_hot: bool, style: &HttpLogStyle) -> Vec<String> {
443        let branch = tree_branch(node.depth);
444        let label_col = format_method_label(
445            style,
446            &node.label,
447            node.source_file.as_deref(),
448            node.source_line,
449        );
450
451        let hot_mark = if is_hot { " ◀ hot" } else { "" };
452        let line = format!(
453            "{branch}{label_col}  self {:>tw$}  incl {:>tw$}  {:>5.1}%{hot_mark}",
454            format_ms(node.self_ms),
455            format_ms(node.inclusive_ms),
456            node.self_pct,
457            tw = TIME_WIDTH,
458        );
459
460        let mut lines = vec![format!(
461            "{} {}",
462            style.label("prof │"),
463            style.prof_text(&line, is_hot),
464        )];
465        if let Some(jb) = jetbrains_filter_line(node.source_file.as_deref(), node.source_line) {
466            lines.push(jb);
467        }
468        lines
469    }
470
471    /// 直接子 span
472    fn direct_children(&self, id: &tracing::Id) -> Vec<tracing::Id> {
473        self.order
474            .iter()
475            .filter(|child| {
476                self.spans
477                    .get(*child)
478                    .and_then(|s| s.parent.as_ref())
479                    == Some(id)
480            })
481            .cloned()
482            .collect()
483    }
484}
485
486impl SpanRecord {
487    /// span inclusive 耗时
488    fn duration(&self) -> Duration {
489        self.ended
490            .map(|end| end.duration_since(self.started))
491            .unwrap_or_else(|| self.started.elapsed())
492    }
493}
494
495tokio::task_local! {
496    /// 当前请求的耗时采集器
497    static REQUEST_PROFILE: Arc<Mutex<RequestProfile>>;
498}
499
500/// 是否处于请求 profiling 上下文
501pub fn is_active() -> bool {
502    REQUEST_PROFILE.try_with(|_| ()).is_ok()
503}
504
505/// 在请求 profiling 作用域内执行 future
506pub async fn scope<F>(profile: Arc<Mutex<RequestProfile>>, fut: F) -> F::Output
507where
508    F: std::future::Future,
509{
510    REQUEST_PROFILE.scope(profile, fut).await
511}
512
513/// 写入 span 进入事件(Layer 调用)
514pub fn record_enter(
515    id: tracing::Id,
516    label: String,
517    parent: Option<tracing::Id>,
518    source_file: Option<&str>,
519    source_line: Option<u32>,
520) {
521    let _ = REQUEST_PROFILE.try_with(|cell| {
522        if let Ok(mut guard) = cell.lock() {
523            guard.on_enter(
524                id,
525                label,
526                parent,
527                source_file.map(str::to_string),
528                source_line,
529            );
530        }
531    });
532}
533
534/// 写入 span 关闭事件(Layer 调用)
535pub fn record_close(id: tracing::Id) {
536    let _ = REQUEST_PROFILE.try_with(|cell| {
537        if let Ok(mut guard) = cell.lock() {
538            guard.on_close(id);
539        }
540    });
541}
542
543/// 从 tracing metadata 组装展示名
544pub fn span_label(meta: &tracing::Metadata<'_>) -> String {
545    let target = meta.target();
546    let short = target.rsplit("::").take(2).collect::<Vec<_>>();
547    let module = short.into_iter().rev().collect::<Vec<_>>().join("::");
548    format!("{}::{}", module, meta.name())
549}
550
551/// 是否输出 Server-Timing 响应头(`DEV_SERVER_TIMING=0` 关闭)
552fn server_timing_enabled() -> bool {
553    std::env::var("DEV_SERVER_TIMING").ok().as_deref() != Some("0")
554}
555
556/// 组装单条 Server-Timing metric
557fn server_timing_metric(name: &str, dur_ms: f64, label: &str) -> String {
558    let desc = escape_server_timing_desc(&server_timing_desc(label));
559    format!("{name};dur={dur_ms:.2};desc=\"{desc}\"")
560}
561
562/// 缩短 span 展示名(取末尾两段 `::`)
563fn server_timing_desc(label: &str) -> String {
564    let parts: Vec<&str> = label.split("::").collect();
565    if parts.len() <= 2 {
566        label.to_string()
567    } else {
568        parts[parts.len() - 2..].join("::")
569    }
570}
571
572/// 转义 Server-Timing desc 中的引号与反斜杠
573fn escape_server_timing_desc(s: &str) -> String {
574    s.replace('\\', "\\\\").replace('"', "\\\"")
575}
576
577/// 读取最小展示阈值(毫秒),默认 0.1ms
578pub fn min_show_duration() -> Duration {
579    std::env::var("DEV_PROFILE_MIN_MS")
580        .ok()
581        .and_then(|v| v.parse::<f64>().ok())
582        .map(|ms| Duration::from_secs_f64(ms / 1000.0))
583        .unwrap_or_else(|| Duration::from_secs_f64(0.0001))
584}
585
586/// 将 profile 报告追加到 JSONL 文件(`DEV_PROFILE_JSONL` 环境变量)
587pub fn append_jsonl_report(report: &ProfileReport) {
588    let Ok(path) = std::env::var("DEV_PROFILE_JSONL") else {
589        return;
590    };
591    let Ok(line) = serde_json::to_string(report) else {
592        return;
593    };
594    HttpLogStyle::append_jsonl(&path, &line);
595}
596
597/// 格式化 URI(含 query)
598fn format_uri(uri: &Uri) -> String {
599    match uri.query() {
600        Some(q) => format!("{}?{q}", uri.path()),
601        None => uri.path().to_string(),
602    }
603}
604
605/// 创建 HTTP 根 span
606pub fn http_request_span(method: &Method, uri: &Uri) -> Span {
607    tracing::debug_span!(
608        "http.request",
609        method = %method,
610        uri = %uri,
611    )
612}
613
614/// 树形前缀
615fn tree_branch(depth: usize) -> String {
616    if depth == 0 {
617        "├─ ".to_string()
618    } else {
619        format!("{}└─ ", "│  ".repeat(depth.saturating_sub(1)))
620    }
621}
622
623/// 格式化毫秒
624fn format_ms(ms: f64) -> String {
625    if ms >= 100.0 {
626        format!("{ms:.0}ms")
627    } else if ms >= 10.0 {
628        format!("{ms:.1}ms")
629    } else {
630        format!("{ms:.2}ms")
631    }
632}
633
634/// 链接协议
635enum LinkScheme {
636    /// JetBrains / RustRover(独立一行 ` at path:line:col`)
637    JetBrains,
638    /// Cursor
639    Cursor,
640    /// VS Code
641    Vscode,
642    /// VS Code Insiders
643    VscodeInsiders,
644    /// file://
645    File,
646    /// 禁用
647    None,
648}
649
650impl LinkScheme {
651    /// 解析链接协议(环境变量 > 终端自动检测)
652    fn detect() -> Self {
653        if std::env::var("DEV_PROFILE_NO_LINKS").is_ok() {
654            return Self::None;
655        }
656        match std::env::var("DEV_PROFILE_LINK_SCHEME")
657            .unwrap_or_default()
658            .to_ascii_lowercase()
659            .as_str()
660        {
661            "none" => Self::None,
662            "jetbrains" | "rustrover" | "idea" | "goland" | "webstorm" => Self::JetBrains,
663            "vscode" => Self::Vscode,
664            "vscode-insiders" => Self::VscodeInsiders,
665            "file" => Self::File,
666            "cursor" => Self::Cursor,
667            "" => {
668                let emulator = std::env::var("TERMINAL_EMULATOR").unwrap_or_default();
669                if emulator.contains("JetBrains") {
670                    Self::JetBrains
671                } else if std::env::var("TERM_PROGRAM").unwrap_or_default() == "vscode" {
672                    Self::Cursor
673                } else {
674                    Self::JetBrains
675                }
676            }
677            _ => Self::Cursor,
678        }
679    }
680}
681
682/// 方法名(Cursor 等 IDE 可 OSC 8 跳转)
683fn format_method_label(
684    style: &HttpLogStyle,
685    method: &str,
686    file: Option<&str>,
687    line: Option<u32>,
688) -> String {
689    if matches!(LinkScheme::detect(), LinkScheme::JetBrains) {
690        return method.to_string();
691    }
692    let Some(uri) = build_osc8_uri(file, line) else {
693        return method.to_string();
694    };
695    style.link(&uri, method)
696}
697
698/// 生成 JetBrains 过滤器可识别的整行文本
699fn jetbrains_filter_line(file: Option<&str>, line: Option<u32>) -> Option<String> {
700    if !matches!(LinkScheme::detect(), LinkScheme::JetBrains) {
701        return None;
702    }
703    let file = file?;
704    let line = line?;
705    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
706    let path = resolve_source_path(&manifest, file);
707    let rel = project_relative_path(&path, &manifest);
708    let style = std::env::var("DEV_PROFILE_JB_STYLE").unwrap_or_else(|_| "rustc".into());
709    Some(match style.as_str() {
710        // RsBacktraceFilter: ^\s+at FILE:LINE(:COL)?$
711        "stack" | "at" => format!(" at {rel}:{line}:1"),
712        // RsConsoleFilter: ^(?:\s+--> )?FILE:LINE(:COL)?.*$
713        _ => format!("   --> {rel}:{line}:1"),
714    })
715}
716
717/// crate 内相对路径(正斜杠)
718fn project_relative_path(path: &Path, manifest: &Path) -> String {
719    path.strip_prefix(manifest)
720        .map(path_to_link)
721        .unwrap_or_else(|_| path_to_link(path))
722}
723
724/// 构建 OSC 8 URI(Cursor / VS Code)
725fn build_osc8_uri(file: Option<&str>, line: Option<u32>) -> Option<String> {
726    let file = file?;
727    let line = line?;
728    let scheme = LinkScheme::detect();
729    if matches!(scheme, LinkScheme::None | LinkScheme::JetBrains) {
730        return None;
731    }
732    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
733    let path = resolve_source_path(&manifest, file);
734    let abs = path_to_link(&path);
735    Some(match scheme {
736        LinkScheme::File => format!("file://{abs}:{line}:1"),
737        LinkScheme::Vscode => format!("vscode://file/{abs}:{line}:1"),
738        LinkScheme::VscodeInsiders => format!("vscode-insiders://file/{abs}:{line}:1"),
739        LinkScheme::Cursor => format!("cursor://file/{abs}:{line}:1"),
740        LinkScheme::JetBrains | LinkScheme::None => return None,
741    })
742}
743
744/// 路径转链接用字符串(去掉 Windows \\?\ 扩展前缀)
745fn path_to_link(path: &Path) -> String {
746    let raw = path.to_string_lossy();
747    let stripped = raw
748        .strip_prefix(r"\\?\")
749        .or_else(|| raw.strip_prefix("//?/"))
750        .unwrap_or(&raw);
751    stripped.replace('\\', "/")
752}
753
754/// 解析 instrument 返回的 file 路径
755fn resolve_source_path(manifest: &Path, file: &str) -> PathBuf {
756    let p = PathBuf::from(file);
757    if p.is_absolute() {
758        p
759    } else {
760        manifest.join(p)
761    }
762}