级别过滤的几道门
主线位置:事件主线从宏展开到
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_enabled | Interest 是 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 链,第一个为真的分支决定结果,由此得到三条规则:
release_max_level_*只在没有debug_assertions时算数,也就是默认的 release 配置。dev 构建里它们像不存在一样。- release 构建里,所有
release_max_level_*排在所有max_level_*前面。只要开了任何一个release_max_level_*,max_level_*就不再起作用,哪怕后者更严。 - 同一组里从
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。
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 ps | 487.95 ps | 363.82 ps |
门二 LevelFilter::current() | 326.23 ps | 331.02 ps | 328.55 ps |
门三 Interest 为 never | 604.59 ps | 604.99 ps | 605.09 ps |
门四 enabled 返回 false | 2.5948 ns | 2.4463 ns | 2.3910 ns |
门五 event_enabled 返回 false | 4.5822 ns | 4.2432 ns | 4.2524 ns |
全部通过,event 只计数 | 4.8714 ns | 4.5733 ns | 4.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 失败,正好就是门一的结论。
问: 门二(约 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 里。
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。