Rust in Everything
基于 tracing 0.1.44

级别过滤的几道门

主线位置:事件主线从宏展开到 Subscriber::event 之间的全部判断。span 主线经过其中的前四道(第五道 event_enabled 只有事件有)。

flowchart TB
  entry["debug! 展开"] --> g1{"门一 静态级别"}
  g1 -->|挡住| none1["编译期删掉"]
  g1 --> g2{"门二 全局级别"}
  g2 -->|挡住| none2["约 0.33 ns"]
  g2 --> g3{"门三 Interest"}
  g3 -->|never| none3["约 0.6 ns"]
  g3 -->|always| vs["字段求值"]
  g3 -->|sometimes| g4{"门四 enabled"}
  g4 -->|false| none4["约 2.4 ns"]
  g4 -->|true| vs
  vs --> g5{"门五 event_enabled"}
  g5 -->|false| none5["约 4.2 ns"]
  g5 -->|true| ev["Subscriber::event"]

上一页 的展开里,事件的判断是一串 &&:

        let enabled = ::tracing::Level::INFO
            <= ::tracing::level_filters::STATIC_MAX_LEVEL
            && ::tracing::Level::INFO <= ::tracing::level_filters::LevelFilter::current()
            && {
                let interest = __CALLSITE.interest();
                !interest.is_never()
                    && ::tracing::__macro_support::__is_enabled(
                        __CALLSITE.metadata(),
                        interest,
                    )
            };

门都通过后,Event::dispatch 里还有一道 event_enabled。本页按顺序把五道门拆开,每道只回答三件事:它读的是什么、什么时候挡住、挡住时省掉了什么。后面用一个计数器验证字段表达式在哪道门之后才求值,再用 criterion 测每道门的开销。

五道门各自依赖的机制在别的页讲:LevelFilter 和全局 MAX_LEVEL 在 Metadata、Level 与 LevelFilter,Interest 缓存在 Callsite 与 Interest 缓存,找当前 subscriber 的 get_default 在 Dispatch:全局与线程局部。本页不重复。

门读什么在哪挡住的条件
一编译期常量 STATIC_MAX_LEVEL宏展开的代码里级别比 feature 定的上限详细
二全局原子变量 MAX_LEVEL宏展开的代码里级别比所有 subscriber 的 max_level_hint 都详细
三调用点自己的 Interest 字节宏展开的代码里never
四当前 subscriber 的 enabled(meta)__is_enabledInterest 是 sometimes 且 enabled 返回 false
五当前 subscriber 的 event_enabled(event)Dispatch::event返回 false

门一:STATIC_MAX_LEVEL

STATIC_MAX_LEVEL 是一个 const,值来自 get_max_level_inner:

pub const STATIC_MAX_LEVEL: LevelFilter = get_max_level_inner();

const fn get_max_level_inner() -> LevelFilter {
    if cfg!(all(
        not(debug_assertions),
        feature = "release_max_level_off"
    )) {
        LevelFilter::OFF
    } else if cfg!(all(
        not(debug_assertions),
        feature = "release_max_level_error"
    )) {
        LevelFilter::ERROR
    // ... warn、info、debug 同样写法
    } else if cfg!(all(
        not(debug_assertions),
        feature = "release_max_level_trace"
    )) {
        LevelFilter::TRACE
    } else if cfg!(feature = "max_level_off") {
        LevelFilter::OFF
    } else if cfg!(feature = "max_level_error") {
        LevelFilter::ERROR
    // ... warn、info 同样写法
    } else if cfg!(feature = "max_level_debug") {
        LevelFilter::DEBUG
    } else {
        // Same as branch cfg!(feature = "max_level_trace")
        LevelFilter::TRACE
    }
}

一条 if 链,第一个为真的分支决定结果,由此得到三条规则:

  1. release_max_level_* 只在没有 debug_assertions 时算数,也就是默认的 release 配置。dev 构建里它们像不存在一样。
  2. release 构建里,所有 release_max_level_* 排在所有 max_level_* 前面。只要开了任何一个 release_max_level_*,max_level_* 就不再起作用,哪怕后者更严。
  3. 同一组里从 off 往 trace 排,同时开了几个时最严的那个生效。

feature 是在整个依赖图里合并的。你的 Cargo.toml 没写,某个依赖写了 tracing = { features = ["release_max_level_info"] },整个程序 release 构建里的 debug! 和 trace! 一样会消失(怎么查是谁开的,放在「常见坑」一部分)。各个 feature 的列表见 源码地图。

用一个只打印常量的程序验证。依赖:

[features]
max_info = ["tracing/max_level_info"]
max_warn = ["tracing/max_level_warn"]
rel_info = ["tracing/release_max_level_info"]
rel_debug = ["tracing/release_max_level_debug"]

[dependencies]
tracing = "=0.1.44"
use tracing::level_filters::STATIC_MAX_LEVEL;

fn main() {
  let profile = if cfg!(debug_assertions) { "dev" } else { "release" };
  println!("{profile:<7} STATIC_MAX_LEVEL = {STATIC_MAX_LEVEL}");
}

用 cargo run --profile dev|release --features ... 把几种组合各跑一遍,行首的方括号是 shell 循环打印的 feature 组合。M2 Max、rustc 1.99.0,三轮结果相同(删掉了下面动脑筋要问的那两行):

[]                   dev     STATIC_MAX_LEVEL = trace
[]                   release STATIC_MAX_LEVEL = trace
[rel_info]           dev     STATIC_MAX_LEVEL = trace
[rel_info]           release STATIC_MAX_LEVEL = info
[max_info]           dev     STATIC_MAX_LEVEL = info
[max_info]           release STATIC_MAX_LEVEL = info
// ...
[rel_info,rel_debug] dev     STATIC_MAX_LEVEL = trace
[rel_info,rel_debug] release STATIC_MAX_LEVEL = info
  • 什么都不开,两种构建都是 TRACE:默认不关任何级别。
  • release_max_level_info 在 dev 里不起作用。
  • max_level_info 两种构建都起作用。
  • 同时开 release_max_level_info 和 release_max_level_debug,取更严的 INFO。
📝NOTE
冷知识:有两年多,只开 max_level_* 在 release 里不起作用

现在的写法是每个分支都把 not(debug_assertions) 和 feature 写进同一个 cfg!(all(...))。#2553(「Remove dep cfg-if from tracing」,tracing 0.1.38)把原来的 cfg_if! 改写成 const fn 时,写成了外层 if cfg!(not(debug_assertions)) { ... }、里面再按 release_max_level_* 分,内层最后一个 else 返回 LevelFilter::TRACE。这样一来,release 构建只要没开 release_max_level_*,就进了外层分支、拿到 TRACE,根本走不到后面的 max_level_*。

#3373(「fix perf regression when release_max_level_* not set」)把它改回现在的平铺写法,说明是:「Regression was introduced in #2553, where logic was unintentionally changed: before pr, in case if release_max_level_* wasn't set, log level from max_level_* was used, but after it was unconditionally set to trace level」。标题里叫「perf regression」,因为后果不是报错,而是本该在编译期删掉的日志代码又回来了(PR 说的是「binary size and instruction counts」)。修复随 tracing 0.1.42 发布,0.1.42 因为另一处破坏性改动被撤回,可用的第一个版本是 0.1.43;受影响的是 0.1.38 到 0.1.41。

门一挡住时省掉了什么?Level::DEBUG <= STATIC_MAX_LEVEL 两边都是常量,整个 && 在编译期就是 false,if enabled { ... } 成了死代码,连 static META 也一起被删掉。用下面「字段什么时候求值」一节的程序(release_max_level_info、release 构建)看二进制里的字符串:没开 feature 时能找到 event src/bin/side_effects.rs:18(那条 debug! 的元数据名字),开了之后 strings 一条都搜不到。后面的 criterion 测量也会撞上这一点。

文档还提醒一件事:静态级别 feature 不管 tracing 的 log feature 发出的 log 记录,原文是「tracing's static max level features do not control the [log] records that may be emitted」。开了 log feature 时,被门一挡住的事件仍然可能生成转给 log 的代码,要用 log 自己的静态级别 feature 关掉。见 和 log 互通。

门二:LevelFilter::current()

LevelFilter::current() 用 Relaxed 读全局的 MAX_LEVEL,release 构建里编译成一次原子读加一次比较。MAX_LEVEL 是所有 dispatcher 的 max_level_hint 取最大值,在 Dispatch::new 和 rebuild_interest_cache 时重算,细节在 Metadata、Level 与 LevelFilter。

和门一一起写在 level_enabled! 里。它挡住两类事件:

  • 程序里还没有创建过任何 Dispatch:MAX_LEVEL 是初值 OFF,所有事件都停在这里。
  • 级别比所有 subscriber 声明的上限都详细。比如 fmt().with_env_filter("info"),EnvFilter 的 max_level_hint 报 INFO,所有 debug! 都停在门二。

门二挡住时,调用点不注册(interest() 没被调用),不找 subscriber,字段不求值。没给 max_level_hint 的 subscriber 按 TRACE 算,门二就挡不住任何东西,全靠后面的门。

门三:调用点的 Interest

DefaultCallsite::interest 读调用点自己的一个 AtomicU8,never 就停。第一次经过时没有缓存,要注册调用点、问每个 dispatcher 的 register_callsite,这一次的代价不算在常态开销里(Callsite 与 Interest 缓存)。

门二和门三的分工:门二是一个全局的数,只能按级别一刀切;门三是每个调用点一份,subscriber 可以按 target、模块、名字逐个回答。用 EnvFilter 的指令 envhint::db=debug,info 看一下:envhint::db 模块里和 crate 根上各有一条 debug!,各经过两次,外面包一层只打印 register_callsite、enabled、event 的 Spy(写法同 一条日志穿过 tracing 里的那个):

tracing = "=0.1.44"
tracing-subscriber = { version = "=0.3.23", default-features = false, features = ["env-filter", "registry"] }
mod db {
  pub fn work() {
    tracing::debug!("db debug");
  }
}

fn other() {
  tracing::debug!("other debug");
}

fn main() {
  let sub = Spy(Registry::default().with(EnvFilter::new("envhint::db=debug,info")));
  println!("max_level_hint = {:?}", sub.max_level_hint());
  tracing::subscriber::with_default(sub, || {
    for _ in 0..2 {
      db::work();
      other();
    }
  });
}

跑 3 次输出相同:

max_level_hint = Some(LevelFilter::DEBUG)
register_callsite envhint::db (DEBUG) -> Interest(Always)
event envhint::db
register_callsite envhint (DEBUG) -> Interest(Never)
event envhint::db

max_level_hint 只能报全程序最详细的那一级 DEBUG,所以两条 debug! 都过了门二。other() 里那条在注册时得到 Never,第二遍经过时连 register_callsite 都不再打印,停在门三;enabled 一次也没被调用,因为两个调用点的回答都不是 sometimes。

门四:Subscriber::enabled

只有 Interest 是 sometimes 时才有这一道。__is_enabled:

    pub fn __is_enabled(meta: &Metadata<'static>, interest: Interest) -> bool {
        interest.is_always() || crate::dispatcher::get_default(|default| default.enabled(meta))
    }

always 直接短路;sometimes 要先用 get_default 找到当前 dispatcher(没有线程局部默认时是两次原子读:SCOPED_COUNT 为 0,再确认 GLOBAL_INIT 已初始化),再通过 dyn Subscriber 虚调用 enabled(meta)。这一道看得到的只有元数据,看不到字段值,也还没有构造事件。会回答 sometimes 的典型是 EnvFilter 的动态指令(要看当前在哪个 span 里,见 EnvFilter)。

门五:event_enabled

前四道都过了,宏才对字段表达式求值、构造 ValueSet,调用 Event::dispatch。它再找一次当前 dispatcher,调用 Dispatch::event:

    pub fn event(&self, event: &Event<'_>) {
        let subscriber = self.subscriber();
        if subscriber.event_enabled(event) {
            subscriber.event(event);
        }
    }

Subscriber::event_enabled 默认返回 true。它和 enabled 的区别是参数:这里拿到的是完整的 Event,可以按字段值过滤。它是 #2008(「add event_enabled to filter events on fields」)加的,首次发布在 tracing-core 0.1.28。代价也由此而来:要看字段,字段就得先求值。

tracing-subscriber 里,Layered::event_enabled 先问外层 layer,外层说 true 才问内层,任何一层说 false 整条事件就不交给任何一层的 on_event。每层过滤器也有自己的 event_enabled,见 过滤:全局与每层。

span 没有这一道:span! 的判断到 __is_enabled 为止,通过了就调用 Span::new。

字段什么时候求值

一个可以切换行为的 subscriber,MODE 决定事件停在哪道门;每次切换后调用 rebuild_interest_cache,让新的 max_level_hint 和 register_callsite 生效:

tracing = "=0.1.44"
use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering::Relaxed};

use tracing::level_filters::LevelFilter;
use tracing::span::{Attributes, Id, Record};
use tracing::subscriber::Interest;
use tracing::{Event, Metadata, Subscriber};

pub const GATE2_LEVEL: u8 = 2; // max_level_hint = INFO,debug! 停在 LevelFilter::current()
pub const GATE3_NEVER: u8 = 3; // register_callsite 回答 never
pub const GATE4_ENABLED: u8 = 4; // 回答 sometimes,enabled 返回 false
pub const GATE5_EVENT_ENABLED: u8 = 5; // 回答 always,event_enabled 返回 false
pub const PASS: u8 = 6; // 全部放行,event 什么也不做

static MODE: AtomicU8 = AtomicU8::new(PASS);
pub static EVENTS: AtomicUsize = AtomicUsize::new(0);

pub struct Gates;

impl Subscriber for Gates {
  fn max_level_hint(&self) -> Option<LevelFilter> {
    if MODE.load(Relaxed) == GATE2_LEVEL {
      Some(LevelFilter::INFO)
    } else {
      None
    }
  }
  fn register_callsite(&self, _: &'static Metadata<'static>) -> Interest {
    match MODE.load(Relaxed) {
      GATE3_NEVER => Interest::never(),
      GATE4_ENABLED => Interest::sometimes(),
      _ => Interest::always(),
    }
  }
  fn enabled(&self, _: &Metadata<'_>) -> bool {
    MODE.load(Relaxed) != GATE4_ENABLED
  }
  fn event_enabled(&self, _: &Event<'_>) -> bool {
    MODE.load(Relaxed) != GATE5_EVENT_ENABLED
  }
  fn event(&self, _: &Event<'_>) {
    EVENTS.fetch_add(1, Relaxed);
  }
  fn new_span(&self, _: &Attributes<'_>) -> Id {
    Id::from_u64(1)
  }
  fn record(&self, _: &Id, _: &Record<'_>) {}
  fn record_follows_from(&self, _: &Id, _: &Id) {}
  fn enter(&self, _: &Id) {}
  fn exit(&self, _: &Id) {}
}

/// 换模式后重建所有调用点的 Interest 缓存,并重算 MAX_LEVEL
pub fn set_mode(mode: u8) {
  MODE.store(mode, Relaxed);
  tracing::callsite::rebuild_interest_cache();
}

(「event 什么也不做」是指不输出,计数用的 fetch_add 还在。)主程序在每种模式下打 1000 条 debug!,两个字段表达式都调用一个带计数器的函数:

static EVALS: AtomicUsize = AtomicUsize::new(0);

// 字段表达式:每求值一次计数加一
fn side_effect() -> u64 {
  EVALS.fetch_add(1, Relaxed);
  42
}

fn run(label: &str) {
  EVALS.store(0, Relaxed);
  EVENTS.store(0, Relaxed);
  for _ in 0..1000 {
    tracing::debug!(v = side_effect(), "msg {}", side_effect());
  }
  println!(
    "{label:<26} current={:<5} evals={:<4} events={}",
    LevelFilter::current().to_string(),
    EVALS.load(Relaxed),
    EVENTS.load(Relaxed),
  );
}

fn main() {
  println!("STATIC_MAX_LEVEL = {STATIC_MAX_LEVEL}");
  run("no subscriber");
  tracing::subscriber::set_global_default(Gates).unwrap();
  for (label, mode) in [
    ("gate2 LevelFilter", GATE2_LEVEL),
    ("gate3 Interest never", GATE3_NEVER),
    ("gate4 enabled=false", GATE4_ENABLED),
    ("gate5 event_enabled=false", GATE5_EVENT_ENABLED),
    ("pass", PASS),
  ] {
    set_mode(mode);
    run(label);
  }
}

M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:

STATIC_MAX_LEVEL = trace
no subscriber              current=off   evals=0    events=0
gate2 LevelFilter          current=info  evals=0    events=0
gate3 Interest never       current=trace evals=0    events=0
gate4 enabled=false        current=trace evals=0    events=0
gate5 event_enabled=false  current=trace evals=2000 events=0
pass                       current=trace evals=2000 events=1000
  • 前四道门挡住时 evals=0:两个字段表达式一次都没执行。
  • 门五挡住时 evals=2000:每条事件两个表达式都求值了,event 却一次没被调用。到 event_enabled 时 ValueSet 已经构造好了。
  • 第一行 no subscriber:set_global_default 之前还没有任何 Dispatch,current 是 off,停在门二。

同一个程序放进依赖为 tracing = { version = "=0.1.44", features = ["release_max_level_info"] } 的 crate、release 构建,跑 3 次输出相同:

STATIC_MAX_LEVEL = info
no subscriber              current=off   evals=0    events=0
gate2 LevelFilter          current=info  evals=0    events=0
gate3 Interest never       current=trace evals=0    events=0
gate4 enabled=false        current=trace evals=0    events=0
gate5 event_enabled=false  current=trace evals=0    events=0
pass                       current=trace evals=0    events=0

运行时的 current 已经是 trace、subscriber 全部放行,debug! 仍然一条都不出:门一在编译期就把它删了。

所以字段里写一个昂贵的表达式(序列化一个大结构、查一次锁),只要事件会被前四道门挡住就不花钱;如果你的过滤靠 event_enabled(或者每层过滤器的 event_enabled),那就每次都要花。这里说的是没开 tracing 的 log feature 的情况,开了以后被挡住的事件还可能为 log 求值,见 宏展开 最后一节。

每道门花多少

用 criterion 0.5 测同一条事件在各道门停下时的单次耗时。还是上面的 Gates,装成全局默认(get_default 走不碰线程局部的快路径),每种模式下测一次:

[dependencies]
tracing = "=0.1.44"

[dev-dependencies]
criterion = "0.5"
use std::hint::black_box;

use criterion::{Criterion, criterion_group, criterion_main};
use exp_tracing_macros::*;

fn gates(c: &mut Criterion) {
  tracing::subscriber::set_global_default(Gates).unwrap();
  c.bench_function("baseline", |b| b.iter(|| black_box(1u64)));
  for (name, mode) in [
    ("gate2_level_filter", GATE2_LEVEL),
    ("gate3_interest_never", GATE3_NEVER),
    ("gate4_enabled_false", GATE4_ENABLED),
    ("gate5_event_enabled_false", GATE5_EVENT_ENABLED),
    ("pass_noop_event", PASS),
  ] {
    set_mode(mode);
    c.bench_function(name, |b| {
      b.iter(|| tracing::debug!(v = black_box(1u64), "msg"))
    });
  }
}

criterion_group!(benches, gates);
criterion_main!(benches);

cargo bench --bench gates -- --noplot --warm-up-time 1 --measurement-time 3,M2 Max、rustc 1.99.0。第三轮的原始输出(只留 time: 行):

baseline                time:   [356.26 ps 363.82 ps 375.47 ps]
gate2_level_filter      time:   [327.15 ps 328.55 ps 329.88 ps]
gate3_interest_never    time:   [604.09 ps 605.09 ps 606.16 ps]
gate4_enabled_false     time:   [2.3842 ns 2.3910 ns 2.3991 ns]
gate5_event_enabled_false
                        time:   [4.2392 ns 4.2524 ns 4.2662 ns]
pass_noop_event         time:   [4.5356 ns 4.5503 ns 4.5662 ns]

三轮的中间估计值:

停在第 1 轮第 2 轮第 3 轮
baseline(只有 black_box(1u64))444.33 ps487.95 ps363.82 ps
门二 LevelFilter::current()326.23 ps331.02 ps328.55 ps
门三 Interest 为 never604.59 ps604.99 ps605.09 ps
门四 enabled 返回 false2.5948 ns2.4463 ns2.3910 ns
门五 event_enabled 返回 false4.5822 ns4.2432 ns4.2524 ns
全部通过,event 只计数4.8714 ns4.5733 ns4.5503 ns

门二、门三三轮几乎一样,门四、门五和全部通过的波动在 10% 以内,baseline 本身最不稳定。逐级看:

  • 门二到门三多约 0.28 ns:多了一次读调用点的 interest 字节和一次比较。
  • 门三到门四多约 1.8 ns:get_default 读 SCOPED_COUNT 和 GLOBAL_INIT 两个原子变量,然后是一次 dyn Subscriber 虚调用 enabled(Gates 在里面还读了一次 MODE)。
  • 门四到门五多约 1.9 ns:门五的调用点是 always,省了 enabled,但要构造 ValueSet(这里两个字段:message 和 v)、构造 Event、在 Event::dispatch 里再 get_default 一次、虚调用 event_enabled。
  • 门五到全部通过多约 0.3 ns:只多了一次 event 虚调用和里面的 fetch_add。真实的 subscriber 在 event 里要格式化、写 I/O,那才是大头(见 fmt)。

门一单独测:把同一个 bench 放进依赖 release_max_level_info 的 crate,运行时 Gates 设成全部放行:

fn gate1(c: &mut Criterion) {
  tracing::subscriber::set_global_default(Gates).unwrap();
  set_mode(PASS); // 运行时全部放行,只剩编译期的门
  c.bench_function("baseline", |b| b.iter(|| black_box(1u64)));
  c.bench_function("gate1_static_max_level", |b| {
    b.iter(|| tracing::debug!(v = black_box(1u64), "msg"))
  });
}

跑 3 次,每次都是:

Benchmarking gate1_static_max_level: Collecting 100 samples in estimated 3.0000 s (18446744074B iterations)
Criterion.rs ERROR: At least one measurement of benchmark gate1_static_max_level took zero time per iteration. This should not be possible. If using iter_custom, please verify that your routine is correctly measured.

criterion 量出了 0:整个闭包体在编译期就没了,连 black_box(1u64) 都没有执行,它在被删掉的 if 分支里。这个 bench 失败,正好就是门一的结论。

📝NOTE
没有蠢问题

问: 门二(约 0.33 ns)比 baseline(0.36–0.49 ns)还快。一条被挡住的日志,怎么会比「什么都不做」还便宜?

答: baseline 不是「什么都不做」,它做的是 black_box(1u64):black_box 的作用就是不让编译器把这个值优化掉,它本身有一点代价。门二那条 debug! 里的 black_box(1u64) 写在字段表达式里,门二挡住后根本不执行,剩下的只是一次 Relaxed 原子读和一次比较。两者都在 1 ns 以下,又都包在 criterion 的循环里,这个量级上只能得出「门二的开销和一次 black_box 同一量级」,不能说它是负的。

把这几个数放在一起:被级别挡住的日志(门一、门二)几乎不花钱,靠 Interest 缓存挡住的也不到 1 ns;从门四开始要找 subscriber、做虚调用,到 2 ns 以上;到门五为止已经付了构造事件的全部代价,只省下 subscriber 自己的 event。所以过滤条件能在 max_level_hint 和 register_callsite 里表达,就别放到 enabled 和 event_enabled 里。

❗IMPORTANT
动脑筋:max_level_warn 加 release_max_level_info

同一个打印 STATIC_MAX_LEVEL 的程序,开两个 feature:

tracing = { version = "=0.1.44", features = ["max_level_warn", "release_max_level_info"] }

dev 构建和 release 构建分别打印什么?release 构建里 max_level_warn 比 release_max_level_info 更严,结果会是更严的那个吗?

动脑筋答案

上面那次三轮运行里被删掉的两行:

[max_warn,rel_info]  dev     STATIC_MAX_LEVEL = warn
[max_warn,rel_info]  release STATIC_MAX_LEVEL = info
  • dev 构建是 WARN:有 debug_assertions,所有 release_max_level_* 分支都是假,落到 max_level_warn。
  • release 构建是 INFO,不是更严的 WARN。get_max_level_inner 的 if 链里,六个 release_max_level_* 分支全部排在 max_level_* 前面,release_max_level_info 先命中,max_level_warn 根本没机会。「取最严的」只在同一组内成立,两组之间是 release 组优先。

想在 release 里用 WARN,就得写 release_max_level_warn;只要依赖图里有人开了任何一个 release_max_level_*,你写的 max_level_* 在 release 构建里就不起作用了。

小结

  • 事件依次过五道门:STATIC_MAX_LEVEL(编译期常量)、LevelFilter::current()(全局原子变量)、调用点的 Interest(每个调用点一个字节)、Subscriber::enabled(只对 sometimes)、event_enabled(在 Dispatch::event 里)。span 只有前四道。
  • STATIC_MAX_LEVEL 由一条 if cfg! 链决定:release_max_level_* 只在无 debug_assertions 时生效,并且整组排在 max_level_* 前面;同组内最严的生效;feature 在依赖图里合并。0.1.38 到 0.1.41 里只开 max_level_* 在 release 中不生效(#3373 修复)。
  • 门一挡住的事件连元数据都被编译期删掉,criterion 量出 0;门二约 0.33 ns,门三约 0.6 ns,门四约 2.4 ns,门五约 4.2 ns,全部通过到一个只计数的 event 约 4.6 ns(M2 Max)。
  • 前四道门挡住时字段表达式不求值;event_enabled 需要看字段,所以到它时字段已经求值、ValueSet 已经构造。
  • 能用级别和 register_callsite 表达的过滤,就不要放进 enabled 或 event_enabled。

💬 关联讨论

发起讨论
加载中...