Skip to content

Commit 48c55c4

Browse files
committed
Add named "additional data" support
This allows representing KV pairs of data in e.g. chrome profiles more faithfully.
1 parent 88522bb commit 48c55c4

File tree

6 files changed

+100
-50
lines changed

6 files changed

+100
-50
lines changed

analyzeme/src/event.rs

+72-37
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
use crate::timestamp::Timestamp;
2-
use memchr::memchr;
32
use std::borrow::Cow;
43
use std::time::Duration;
54

5+
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
6+
pub struct Argument<'a> {
7+
pub name: Option<Cow<'a, str>>,
8+
pub value: Cow<'a, str>,
9+
}
10+
11+
impl<'a> Argument<'a> {
12+
pub fn new(value: &'a str) -> Self {
13+
Self { name: None, value: Cow::from(value) }
14+
}
15+
16+
pub fn new_named(name: &'a str, value: &'a str) -> Self {
17+
Self { name: Some(Cow::from(name)), value: Cow::from(value) }
18+
}
19+
}
20+
621
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
722
pub struct Event<'a> {
823
pub event_kind: Cow<'a, str>,
924
pub label: Cow<'a, str>,
10-
pub additional_data: Vec<Cow<'a, str>>,
25+
pub additional_data: Vec<Argument<'a>>,
1126
pub timestamp: Timestamp,
1227
pub thread_id: u32,
1328
}
@@ -38,7 +53,7 @@ impl<'a> Event<'a> {
3853
}
3954
}
4055

41-
pub(crate) fn parse_event_id(event_id: Cow<'a, str>) -> (Cow<'a, str>, Vec<Cow<'a, str>>) {
56+
pub(crate) fn parse_event_id(event_id: Cow<'a, str>) -> (Cow<'a, str>, Vec<Argument<'a>>) {
4257
let event_id = match event_id {
4358
Cow::Owned(s) => Cow::Owned(s.into_bytes()),
4459
Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()),
@@ -75,52 +90,58 @@ struct Parser<'a> {
7590
pos: usize,
7691
}
7792

78-
const SEPARATOR_BYTE: u8 = measureme::event_id::SEPARATOR_BYTE.as_bytes()[0];
93+
const ARGUMENT_VALUE_TAG_BYTE: u8 = measureme::event_id::ARGUMENT_VALUE_TAG_BYTE.as_bytes()[0];
94+
const ARGUMENT_NAME_TAG_BYTE: u8 = measureme::event_id::ARGUMENT_NAME_TAG_BYTE.as_bytes()[0];
7995

8096
impl<'a> Parser<'a> {
8197
fn new(full_text: Cow<'a, [u8]>) -> Parser<'a> {
8298
Parser { full_text, pos: 0 }
8399
}
84100

85-
fn peek(&self) -> u8 {
86-
self.full_text[self.pos]
87-
}
88-
89101
fn parse_label(&mut self) -> Result<Cow<'a, str>, String> {
90102
assert!(self.pos == 0);
91-
self.parse_separator_terminated_text()
103+
let text = self.parse_text()?;
104+
if text.is_empty() {
105+
return self.err("<label> is empty");
106+
} else {
107+
Ok(text)
108+
}
92109
}
93110

94-
fn parse_separator_terminated_text(&mut self) -> Result<Cow<'a, str>, String> {
111+
fn parse_text(&mut self) -> Result<Cow<'a, str>, String> {
95112
let start = self.pos;
96-
97-
let end = memchr(SEPARATOR_BYTE, &self.full_text[start..])
98-
.map(|pos| pos + start)
99-
.unwrap_or(self.full_text.len());
100-
101-
if start == end {
102-
return self.err("Zero-length <text>");
103-
}
104-
105-
self.pos = end;
106-
107-
if self.full_text[start..end].iter().any(u8::is_ascii_control) {
108-
return self.err("Found ASCII control character in <text>");
109-
}
110-
111-
Ok(self.substring(start, end))
113+
self.pos += self.full_text[start..]
114+
.iter()
115+
.take_while(|c| !u8::is_ascii_control(c))
116+
.count();
117+
Ok(self.substring(start, self.pos))
112118
}
113119

114-
fn parse_arg(&mut self) -> Result<Cow<'a, str>, String> {
115-
if self.peek() != SEPARATOR_BYTE {
116-
return self.err(&format!(
117-
"Expected '\\x{:x}' char at start of <argument>",
118-
SEPARATOR_BYTE
119-
));
120+
fn parse_arg(&mut self) -> Result<Argument<'a>, String> {
121+
let name = if let Some(&byte) = self.full_text.get(self.pos) {
122+
if byte == ARGUMENT_NAME_TAG_BYTE {
123+
self.pos += 1;
124+
Some(self.parse_text()?)
125+
} else {
126+
None
127+
}
128+
} else {
129+
None
130+
};
131+
let value = if let Some(&byte) = self.full_text.get(self.pos) {
132+
if byte == ARGUMENT_VALUE_TAG_BYTE {
133+
self.pos += 1;
134+
Some(self.parse_text()?)
135+
} else {
136+
None
137+
}
138+
} else {
139+
None
140+
};
141+
match (name, value) {
142+
(name, Some(value)) => Ok(Argument { name, value }),
143+
(_, None) => self.err("Unable to parse required <argument_value>"),
120144
}
121-
122-
self.pos += 1;
123-
self.parse_separator_terminated_text()
124145
}
125146

126147
fn err<T>(&self, message: &str) -> Result<T, String> {
@@ -161,7 +182,7 @@ mod tests {
161182
let (label, args) = Event::parse_event_id(Cow::from("foo\x1emy_arg"));
162183

163184
assert_eq!(label, "foo");
164-
assert_eq!(args, vec![Cow::from("my_arg")]);
185+
assert_eq!(args, vec![Argument::new("my_arg")]);
165186
}
166187

167188
#[test]
@@ -171,7 +192,21 @@ mod tests {
171192
assert_eq!(label, "foo");
172193
assert_eq!(
173194
args,
174-
vec![Cow::from("arg1"), Cow::from("arg2"), Cow::from("arg3")]
195+
vec![Argument::new("arg1"), Argument::new("arg2"), Argument::new("arg3")]
196+
);
197+
}
198+
199+
#[test]
200+
fn parse_event_id_n_named_args() {
201+
let (label, args) = Event::parse_event_id(Cow::from("foo\x1darg1\x1eval1\x1darg2\x1eval2"));
202+
203+
assert_eq!(label, "foo");
204+
assert_eq!(
205+
args,
206+
vec![
207+
Argument::new_named("arg1", "val1"),
208+
Argument::new_named("arg2", "val2"),
209+
]
175210
);
176211
}
177212
}

analyzeme/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ mod stringtable;
2121
pub mod testing_common;
2222
mod timestamp;
2323

24-
pub use crate::event::Event;
24+
pub use crate::event::{Event, Argument};
2525
pub use crate::lightweight_event::LightweightEvent;
2626
pub use crate::profiling_data::{ProfilingData, ProfilingDataBuilder};
2727
pub use crate::stack_collapse::collapse_stacks;

analyzeme/src/testing_common.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::timestamp::Timestamp;
2-
use crate::{Event, ProfilingData};
2+
use crate::{Event, Argument, ProfilingData};
33
use measureme::{EventId, EventIdBuilder, Profiler, SerializationSink, StringId};
44
use rustc_hash::FxHashMap;
55
use std::borrow::Cow;
@@ -22,15 +22,16 @@ fn mk_filestem(file_name_stem: &str) -> PathBuf {
2222
struct ExpectedEvent {
2323
kind: Cow<'static, str>,
2424
label: Cow<'static, str>,
25-
args: Vec<Cow<'static, str>>,
25+
args: Vec<Argument<'static>>,
2626
}
2727

2828
impl ExpectedEvent {
29-
fn new(kind: &'static str, label: &'static str, args: &[&'static str]) -> ExpectedEvent {
29+
fn new(kind: &'static str, label: &'static str, args: &[Argument<'static>])
30+
-> ExpectedEvent {
3031
ExpectedEvent {
3132
kind: Cow::from(kind),
3233
label: Cow::from(label),
33-
args: args.iter().map(|&x| Cow::from(x)).collect(),
34+
args: args.to_vec(),
3435
}
3536
}
3637
}
@@ -65,7 +66,7 @@ fn generate_profiling_data<S: SerializationSink>(
6566
let expected_events_templates = vec![
6667
ExpectedEvent::new("Generic", "SomeGenericActivity", &[]),
6768
ExpectedEvent::new("Query", "SomeQuery", &[]),
68-
ExpectedEvent::new("QueryWithArg", "AQueryWithArg", &["some_arg"]),
69+
ExpectedEvent::new("QueryWithArg", "AQueryWithArg", &[Argument::new("some_arg")]),
6970
];
7071

7172
let threads: Vec<_> = (0..num_threads)

crox/src/main.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@ fn get_args(full_event: &analyzeme::Event) -> Option<FxHashMap<String, String>>
123123
.additional_data
124124
.iter()
125125
.enumerate()
126-
.map(|(i, arg)| (format!("arg{}", i).to_string(), arg.to_string()))
126+
.map(|(i, arg)| (if let Some(name) = &arg.name {
127+
name.to_string()
128+
} else {
129+
format!("arg{}", i).to_string()
130+
}, arg.value.to_string()))
127131
.collect(),
128132
)
129133
} else {

measureme/src/event_id.rs

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
use crate::{Profiler, SerializationSink, StringComponent, StringId};
22

3-
/// Event IDs are strings conforming to the following grammar:
3+
/// Event IDs are strings conforming to the following (eBNF) grammar:
44
///
55
/// ```ignore
66
/// <event_id> = <label> {<argument>}
77
/// <label> = <text>
8-
/// <argument> = '\x1E' <text>
8+
/// <argument> = ['\x1D' <argument_name>] '\x1E' <argument_value>
9+
/// <argument_name> = <text>
10+
/// <argument_value> = <text>
911
/// <text> = regex([^[[:cntrl:]]]+) // Anything but ASCII control characters
1012
/// ```
1113
///
1214
/// This means there's always a "label", followed by an optional list of
1315
/// arguments. Future versions my support other optional suffixes (with a tag
1416
/// other than '\x11' after the '\x1E' separator), such as a "category".
1517
16-
/// The byte used to separate arguments from the label and each other.
17-
pub const SEPARATOR_BYTE: &str = "\x1E";
18+
/// The byte used to denote the following text is an argument value.
19+
pub const ARGUMENT_VALUE_TAG_BYTE: &str = "\x1E";
20+
/// The byte used to denote the following text is an argument name.
21+
pub const ARGUMENT_NAME_TAG_BYTE: &str = "\x1D";
1822

1923
/// An `EventId` is a `StringId` with the additional guarantee that the
2024
/// corresponding string conforms to the event_id grammar.
@@ -73,7 +77,7 @@ impl<'p, S: SerializationSink> EventIdBuilder<'p, S> {
7377
// Label
7478
StringComponent::Ref(label),
7579
// Seperator and start tag for arg
76-
StringComponent::Value(SEPARATOR_BYTE),
80+
StringComponent::Value(ARGUMENT_VALUE_TAG_BYTE),
7781
// Arg string id
7882
StringComponent::Ref(arg),
7983
]))

mmview/src/main.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ fn system_time_to_micros_since(t: SystemTime, since: SystemTime) -> u128 {
4141
}
4242

4343
fn print_event(event: &Event<'_>, global_start_time: SystemTime) {
44-
let additional_data = event.additional_data.join(",");
44+
let additional_data = event.additional_data.iter().map(|arg| {
45+
if let Some(name) = &arg.name {
46+
format!("{} = {}", name, arg.value)
47+
} else {
48+
arg.value.to_string()
49+
}
50+
}).collect::<Vec<_>>().join(", ");
4551

4652
let timestamp = match event.timestamp {
4753
Timestamp::Instant(t) => {

0 commit comments

Comments
 (0)