Description
Feature Request
Crates
tracing-core
Motivation
The Field
type is used in two primary ways:
- formatting the string value of the
Field
's name, - comparing for equality, either between two
Field
s or between aField
and an&str
.
We would like both of these use cases to be as efficient as possible.
In an attempt to enable this, we currently represent Field
s as an &'static
reference to a FieldSet
(a static array of strings generated by the macros for each callsite) and an integer index into that array. This allows us fast comparisons between two Fields
by comparing the callsite pointers, and if they are equal, comparing the integer index, avoiding the string comparison. The static string can be accessed by indexing the FieldSet
array with the integer index, and is used when formatting the field or when comparing a Field
with a &str
.
This approach is very nice in theory, as it gives subscribers a nice way to do tests for field equality without having to compare strings. However, in practice, this is almost impossible for subscriber implementations to actually use the fast comparison. Here's why:
-
In order to use fast integer comparisons rather than slow string comparisons, you need to be comparing two
Field
s; otherwise, it is necessary to fall back to string comparisons. The only way to get a field is with code like this:// where `meta` is the metadata for some span or event that I have access to let field = meta.field_set().field("some_field_i_care_about")?;
This means that if a
Subscriber
or aVisit
implementation wants to do fast comparisons for a particular field it cares about, it needs to implement theregister_callsite
method and wait for a span or event that has a field matching the name that it cares about to be registered. Once a span or event has been registered, it can store theField
returned by theMetadata
and use it for fast comparisons in the future...in theory. However, becauseregister_callsite
is called with a&self
reference, the subscriber needs some way to mutate itself to actually store the field. This means locking, both when the field is stored initially, and every time the field is used (although the latter could be a read lock). This overhead may be enough to make the fast comparison no longer "fast", and complicates the implementation. -
Field
keys are only equal if they came from the same callsite. Because they are represented as an index into a static array that's created per callsite, they are only considered equal if the callsite pointers are the same --- if two spans from separate callsites have a field named "foo", it might be at index 1 in the first callsite, but at index 4 in the other callsite. This means that if I want to do the fast comparison for a field that exists on multiple spans, I need to store all those fields.When this code was initially written, this seemed like an acceptable limitation --- I thought that we would generally only care about field keys from a particular span. This, it turns out, is definitely not the case. It turns out that perhaps the most common use case for field key comparisons is implementing special formatting or handling for fields named
message
(and, secondarily, other fields with conventional names that occur on multiple spans/events). For example:
tracing/tracing-subscriber/src/field/display.rs
Lines 60 to 64 in ee1e530
If we wanted to use "fast" field comparisons here, we would need to have a
register_callsite
hook that stores every field key named "message" that's registered by any callsite (probably in a giantRwLock<HashSet<Field>>
or something), and test if it contains the visited field every time we visit a field. This is, once again, no longer fast --- the string comparison is more efficient at this point.
Proposal
I think we can solve all of these problems by changing how we represent Field
keys internally. Rather than representing them as indices into a static array that's registered per-callsite, we could instead represent them with a form of string interning. We would have a global registry that stores a reference to a static string for each field name that exists. Field
s would be represented internally by&'static str
s in that registry. We would provide a new function, tracing::field::register
(or something), taking an &'static str
and returning a Field
. If a string whose character content is equal to the provided string exists in the registry, this function would return a Field
with that string in it, and if it doesn't, we would add it to the registry.
This way, comparing two Field
s could be implemented with ptr::eq
on the two &'static str
s, since requiring the registry to provide all Field
s ensures that there is a single canonical address for each name. The implementation would look something like this:
pub struct Field {
name: &'static str,
}
impl Field {
pub fn register(name: &'static str) -> Field {
lazy_static! {
static ref FIELD_REGISTRY: RwLock<HashSet<&'static str>> = RwLock::new(HashSet::new());
}
if let Some(name) = FIELD_REGISTRY.read().unwrap().get(name) {
Self { name }
} else {
FIELD_REGISTRY.write().unwrap().insert(name);
Self { name }
}
}
}
impl fmt::Display for Field {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.name)
}
}
impl PartialEq<Self> for Field {
fn eq(&self, other: &Self) -> bool {
// since the registry ensures that all fields created from equivalent strings have
// the same address, we can just do this.
std::ptr::eq(self.name, other.name)
}
}
impl PartialEq<str> for Field {
fn eq(&self, other: &str) -> bool {
if std::ptr::eq(self.name as *const _ as *const (), other.name as *const _ as *const ()) {
// if the string has the same address as we do, it must've been dereferenced from
// another `Field` with our value, so we don't need to compare characters..
return true;
}
// fall back to comparing the strings' actual contents
self.name.eq(other)
}
}
// ... and so on ...
Because the register
function is globally available, Subscriber
implementations (or Visit
implementations, or tracing_subscriber::Layer
implementations...) that care about specific field names can call it when they are constructed, rather than having to wait for a particular span or event which has that field name to be registered. A subscriber could write code like this:
struct MySubscriber {
// let's say I care about messages.
message,
// ...
}
impl MySubscriber {
fn new() -> Self {
// add the field to the registry if it does not already exist, or
// a `Field` with its canonical addr if it does.
let message = Field::register("message");
// ...
Self {
message,
// ...
}
}
}
impl Subscriber for MySubscriber {
// or whichever method, it doesn't matter
fn new_span(&self, span: span::Attributes<'_>) -> span::Id {
// we get to use fast pointer compares rather than slow string compares
// here, no matter which callsite the fields came from!
if span.fields().contains(self.message) {
// ... do stuff ...
}
}
// ...
}
The callsite macros would be changed to register their fields the first time they are hit, and use the registered keys when recording spans/events. Because this only happens in the initialization path, the lock shouldn't be contended in the hot path.
Also, this approach has the side benefit of making access to the Field
's string value a little tiny bit faster: in the current implementation, accessing the string means dereferencing the FieldSet
reference and indexing the FieldSet
array by the integer key (including a bounds check) in order to access the &'static str
which is then itself dereferenced to format it; in this design, the Field
just stores the &'static str
directly, so there's one dereference.
If we are very smart, we may even be able to make this change without breaking stuff.
Alternatives
- We could, of course, not do this, and continue with the way things are. That has all the flaws discussed above.
- The very simple registry design proposed here only works with
&'static str
s. There is no way to intern a dynamic string. This is fine with the current state oftracing
, since we don't allow that anyway. However, depending on how nested map-like value structures are represented for inValue::from_serde
? #819, we might need a way to have dynamic field values. We could do that in a few ways:- We could change the registry to be pointers, rather than
&'static
refs, and "leak" newString
s into the registry the first time they are registered, but this would require unsafe code that might look scary to readers. - Alternatively, we could make the internal repr of
Field
similar to aCow
that is either an ownedString
or a&'static str
, giving us interning performance for static strings, but falling back to passingString
s around for dynamic ones. - A third option is to do both: since some dynamic strings might occur frequently, but others are one-shots that will only exist once, we could provide the user implementing
Value
a way to generate aField
that owns aString
and a way to intern a dynamicString
into the registry...
- We could change the registry to be pointers, rather than