blob: 8830f58387a65666049da2bc9ddbdd908f4b0c6b [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use trace_events::{Argument, ArgumentValue, Label, Point, Scope, Tracer};
use std::cell::Cell;
use std::fmt::Write as FmtWrite;
use std::fs::File;
use std::io::Write;
use std::os::unix::io::AsRawFd;
use libc::dup2;
thread_local!(static LOCAL_RECORDS: Cell<String> = Cell::new(String::with_capacity(4096 * 4)));
fn write_local_record<F: FnOnce(&mut String)>(
ph: char,
Label { name, category }: Label,
Point {
ts,
thread_ts,
tid,
pid,
}: Point,
args: &[Argument],
counters: &[(&str, u64)],
extra_props: F,
) -> (usize, usize) {
LOCAL_RECORDS.with(|cell| {
let mut s = cell.take();
s.push('{');
{
s.push_str(r#""name":""#);
s.push_str(name);
s.push_str(r#"","#);
s.push_str(r#""cat":""#);
s.push_str(category);
s.push_str(r#"","#);
s.push_str(r#""ph":""#);
s.push(ph);
s.push_str(r#"","#);
s.push_str(r#""ts":"#);
let _ = write!(s, "{}", ts);
s.push_str(",");
if let Some(tts) = thread_ts {
s.push_str(r#""tts":"#);
let _ = write!(s, "{}", tts);
s.push_str(",");
}
s.push_str(r#""pid":"#);
let _ = write!(s, "{}", pid);
s.push_str(",");
s.push_str(r#""tid":"#);
let _ = write!(s, "{}", tid);
s.push_str(",");
extra_props(&mut s);
s.push_str(r#""args":{"#);
{
for arg in args {
s.push('"');
s.push_str(arg.key);
s.push_str(r#"":"#);
match arg.value {
ArgumentValue::Bool(b) => s.push_str(if b { "true" } else { "false" }),
ArgumentValue::Sint(i) => {
let _ = write!(s, "{}", i);
}
ArgumentValue::Uint(i) => {
let _ = write!(s, "{}", i);
}
ArgumentValue::Float(f) => {
let _ = write!(s, "{}", f);
}
ArgumentValue::String(v) => {
let _ = write!(s, "{:?}", v);
}
}
s.push(',');
}
for (key, value) in counters {
s.push('"');
s.push_str(key);
s.push_str(r#"":"#);
let _ = write!(s, "{}", value);
s.push(',');
}
// Remove trailing comma
if !args.is_empty() || !counters.is_empty() {
s.pop();
}
}
s.push_str("}");
}
s.push_str("},\n");
let len_cap = (s.len(), s.capacity());
cell.set(s);
len_cap
})
}
fn flush_local_records<F: FnOnce(&str)>(f: F) {
LOCAL_RECORDS.with(|cell| {
let mut s = cell.take();
f(&s);
s.clear();
cell.set(s)
})
}
/// A tracer that outputs to a file in the [Json Trace Event Format](https://docs.google.com/document/u/1/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/view).
///
/// The most convenient viewer is the one builtin to Chrome: `chrome://tracing`.
#[derive(Default)]
pub struct JsonTracer {
out_file: Option<File>,
}
impl JsonTracer {
/// Creates a new `JsonTracer` with no output.
pub fn new() -> JsonTracer {
Default::default()
}
/// Sets the file that tracing output will be written to.
///
/// The given file will immediately start being written to in order to output prefix data.
pub fn set_out_file(&mut self, out_file: File) {
self.out_file = Some(out_file);
}
/// Closes the previously set file and outputs to the given `out_file`, returning `true` on
/// success.
///
/// This will fail if `set_out_file` was never called previously. Internally, this uses `dup2`
/// to atomically clone the given file to the already set file, allowing `JsonTracer` to be
/// lock-free.
///
/// This is most useful for closing the previously set file after tracing is finished while
/// still allowing any outstanding flushes to safely write to `out_file`, often `/dev/null`.
pub fn reset_out_file(&self, out_file: &File) -> bool {
match self.out_file.as_ref() {
Some(f) => unsafe { dup2(out_file.as_raw_fd(), f.as_raw_fd()) == 0 },
None => false,
}
}
/// Flushes thread-locally buffered output to the previously set output file. If a string
/// reference is provided, a copy of the flushed data will be appended to it. The `flushed`
/// parameter is most useful for unit testing.
pub fn flush(&self, flushed: Option<&mut String>) {
flush_local_records(|s| {
// There is no desire to propagate or report errors in tracing.
if let Some(mut f) = self.out_file.as_ref() {
let _ = f.write(&s.as_bytes());
}
if let Some(out) = flushed {
out.push_str(s);
}
});
}
fn maybe_flush(&self, (used, capacity): (usize, usize)) {
if used * 2 > capacity {
self.flush(None)
}
}
}
impl Tracer for JsonTracer {
fn duration_begin(&self, label: Label, begin: Point, args: &[Argument]) {
self.maybe_flush(write_local_record('B', label, begin, args, &[], |_| {}));
}
fn duration_end(&self, label: Label, end: Point, args: &[Argument]) {
self.maybe_flush(write_local_record('E', label, end, args, &[], |_| {}));
}
fn duration(&self, label: Label, begin: Point, duration: u64, args: &[Argument]) {
self.maybe_flush(write_local_record('X', label, begin, args, &[], |s| {
s.push_str(r#""duration":"#);
let _ = write!(s, "{}", duration);
s.push_str(r#","#);
}));
}
fn instant(&self, label: Label, instant: Point, scope: Scope, args: &[Argument]) {
let scope_ty = match scope {
Scope::Global => 'g',
Scope::Process => 'p',
Scope::Thread => 't',
};
self.maybe_flush(write_local_record('i', label, instant, args, &[], |s| {
s.push_str(r#""scope":""#);
s.push(scope_ty);
s.push_str(r#"","#);
}));
}
fn counter(&self, label: Label, instant: Point, counters: &[(&str, u64)]) {
self.maybe_flush(write_local_record(
'C',
label,
instant,
&[],
counters,
|_| {},
));
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{json, Value};
#[test]
fn duration() {
let t = JsonTracer::new();
let label = Label::new("test", "duration");
let begin = Point::from_timestamp(456);
let duration = 100;
t.duration(label, begin, duration, &[]);
let mut out = String::new();
t.flush(Some(&mut out));
println!("out = {}", out);
let out_val: Value = serde_json::from_str(out.trim().trim_matches(',')).unwrap();
assert_eq!(
out_val,
json!({
"name": label.name,
"cat": label.category,
"ph": "X",
"ts": begin.ts,
"duration": duration,
"pid": begin.pid,
"tid": begin.tid,
"args": {}
})
);
}
#[test]
fn instant() {
let t = JsonTracer::new();
let label = Label::new("test", "instant");
let point = Point::from_timestamp(456);
let scope = Scope::Global;
t.instant(
label,
point,
scope,
&[
Argument::new("apples", 2),
Argument::new("cost", 5.1),
Argument::new("paid", true),
Argument::new("invalid", false),
Argument::new("special", "spicy"),
],
);
let mut out = String::new();
t.flush(Some(&mut out));
println!("out = {}", out);
let out_val: Value = serde_json::from_str(out.trim().trim_matches(',')).unwrap();
assert_eq!(
out_val,
json!({
"name": label.name,
"cat": label.category,
"ph": "i",
"ts": point.ts,
"pid": point.pid,
"tid": point.tid,
"scope": "g",
"args": {
"apples": 2,
"cost": 5.1,
"paid": true,
"invalid": false,
"special": "spicy",
}
})
);
}
#[test]
fn counter() {
let t = JsonTracer::new();
let label = Label::new("test", "duration");
let instant = Point::from_timestamp(42);
t.counter(label, instant, &[("blocks", 123), ("ducks", 456)]);
let mut out = String::new();
t.flush(Some(&mut out));
println!("out = {}", out);
let out_val: Value = serde_json::from_str(out.trim().trim_matches(',')).unwrap();
assert_eq!(
out_val,
json!({
"name": label.name,
"cat": label.category,
"ph": "C",
"ts": instant.ts,
"pid": instant.pid,
"tid": instant.tid,
"args": {
"blocks": 123,
"ducks": 456,
}
})
);
}
}