2019-09-30 03:58:53 -05:00
//! FIXME: write short doc here
2019-06-30 05:30:17 -05:00
mod memory_usage ;
2019-08-17 07:29:57 -05:00
#[ cfg(feature = " cpu_profiler " ) ]
mod google_cpu_profiler ;
2019-06-30 05:30:17 -05:00
2019-05-07 12:54:33 -05:00
use std ::{
cell ::RefCell ,
2020-01-19 09:07:15 -06:00
collections ::BTreeMap ,
2019-07-04 15:05:17 -05:00
collections ::HashSet ,
2019-05-07 12:54:33 -05:00
io ::{ stderr , Write } ,
2019-07-04 15:05:17 -05:00
sync ::{
atomic ::{ AtomicBool , Ordering } ,
RwLock ,
} ,
time ::{ Duration , Instant } ,
2019-05-07 12:54:33 -05:00
} ;
2019-07-04 15:05:17 -05:00
use once_cell ::sync ::Lazy ;
2019-03-27 10:09:51 -05:00
2019-07-04 15:05:17 -05:00
pub use crate ::memory_usage ::{ Bytes , MemoryUsage } ;
2019-06-30 05:30:17 -05:00
// We use jemalloc mainly to get heap usage statistics, actual performance
// difference is not measures.
2019-11-14 10:47:18 -06:00
#[ cfg(all(feature = " jemalloc " , not(target_env = " msvc " ))) ]
2019-06-30 05:30:17 -05:00
#[ global_allocator ]
static ALLOC : jemallocator ::Jemalloc = jemallocator ::Jemalloc ;
2020-04-25 07:52:23 -05:00
/// Filtering syntax
/// env RA_PROFILE=* // dump everything
/// env RA_PROFILE=foo|bar|baz // enabled only selected entries
/// env RA_PROFILE=*@3>10 // dump everything, up to depth 3, if it takes more than 10 ms
2020-02-16 11:04:08 -06:00
pub fn init ( ) {
2020-04-25 07:52:23 -05:00
let spec = std ::env ::var ( " RA_PROFILE " ) . unwrap_or_default ( ) ;
init_from ( & spec ) ;
}
pub fn init_from ( spec : & str ) {
let filter = if spec . is_empty ( ) { Filter ::disabled ( ) } else { Filter ::from_spec ( spec ) } ;
set_filter ( filter ) ;
2020-02-16 11:04:08 -06:00
}
2019-04-03 08:05:55 -05:00
/// Set profiling filter. It specifies descriptions allowed to profile.
/// This is helpful when call stack has too many nested profiling scopes.
/// Additionally filter can specify maximum depth of profiling scopes nesting.
///
/// #Example
/// ```
2019-04-14 17:03:11 -05:00
/// use ra_prof::{set_filter, Filter};
/// let f = Filter::from_spec("profile1|profile2@2");
2019-04-03 08:05:55 -05:00
/// set_filter(f);
/// ```
2020-04-25 07:52:23 -05:00
fn set_filter ( f : Filter ) {
2019-04-14 14:54:57 -05:00
PROFILING_ENABLED . store ( f . depth > 0 , Ordering ::SeqCst ) ;
2019-05-07 12:54:33 -05:00
let set : HashSet < _ > = f . allowed . iter ( ) . cloned ( ) . collect ( ) ;
2019-03-29 07:34:05 -05:00
let mut old = FILTER . write ( ) . unwrap ( ) ;
2019-04-14 15:20:12 -05:00
let filter_data = FilterData {
depth : f . depth ,
allowed : set ,
longer_than : f . longer_than ,
version : old . version + 1 ,
} ;
2019-03-29 07:34:05 -05:00
* old = filter_data ;
}
2020-02-02 11:54:26 -06:00
pub type Label = & 'static str ;
2019-04-03 08:05:55 -05:00
/// This function starts a profiling scope in the current execution stack with a given description.
/// It returns a Profile structure and measure elapsed time between this method invocation and Profile structure drop.
/// It supports nested profiling scopes in case when this function invoked multiple times at the execution stack. In this case the profiling information will be nested at the output.
/// Profiling information is being printed in the stderr.
///
2019-07-06 06:04:56 -05:00
/// # Example
2019-04-03 08:05:55 -05:00
/// ```
2019-04-14 17:03:11 -05:00
/// use ra_prof::{profile, set_filter, Filter};
2019-04-03 08:05:55 -05:00
///
2019-04-14 17:03:11 -05:00
/// let f = Filter::from_spec("profile1|profile2@2");
2019-04-03 08:05:55 -05:00
/// set_filter(f);
/// profiling_function1();
///
/// fn profiling_function1() {
/// let _p = profile("profile1");
/// profiling_function2();
/// }
///
/// fn profiling_function2() {
/// let _p = profile("profile2");
/// }
/// ```
/// This will print in the stderr the following:
/// ```text
/// 0ms - profile
/// 0ms - profile2
/// ```
2020-02-02 11:54:26 -06:00
pub fn profile ( label : Label ) -> Profiler {
assert! ( ! label . is_empty ( ) ) ;
2019-04-14 14:54:57 -05:00
if ! PROFILING_ENABLED . load ( Ordering ::Relaxed ) {
2020-03-06 10:23:08 -06:00
return Profiler { label : None , detail : None } ;
2019-04-14 14:54:57 -05:00
}
2019-03-29 07:34:05 -05:00
PROFILE_STACK . with ( | stack | {
let mut stack = stack . borrow_mut ( ) ;
2019-04-26 10:42:56 -05:00
if stack . starts . is_empty ( ) {
if let Ok ( f ) = FILTER . try_read ( ) {
if f . version > stack . filter_data . version {
stack . filter_data = f . clone ( ) ;
2019-03-29 07:34:05 -05:00
}
} ;
}
2019-04-14 14:54:57 -05:00
if stack . starts . len ( ) > stack . filter_data . depth {
2020-03-06 10:23:08 -06:00
return Profiler { label : None , detail : None } ;
2019-04-14 14:54:57 -05:00
}
2019-06-01 02:41:47 -05:00
let allowed = & stack . filter_data . allowed ;
2020-02-02 11:54:26 -06:00
if stack . starts . is_empty ( ) & & ! allowed . is_empty ( ) & & ! allowed . contains ( label ) {
2020-03-06 10:23:08 -06:00
return Profiler { label : None , detail : None } ;
2019-03-29 07:34:05 -05:00
}
2019-06-01 02:41:47 -05:00
stack . starts . push ( Instant ::now ( ) ) ;
2020-03-06 10:23:08 -06:00
Profiler { label : Some ( label ) , detail : None }
2019-03-29 07:34:05 -05:00
} )
}
2019-03-27 10:09:51 -05:00
pub struct Profiler {
2020-02-02 11:54:26 -06:00
label : Option < Label > ,
2020-03-06 10:23:08 -06:00
detail : Option < String > ,
}
impl Profiler {
pub fn detail ( mut self , detail : impl FnOnce ( ) -> String ) -> Profiler {
if self . label . is_some ( ) {
self . detail = Some ( detail ( ) )
}
self
}
2019-03-27 10:09:51 -05:00
}
2020-04-25 07:52:23 -05:00
struct Filter {
2019-03-27 10:09:51 -05:00
depth : usize ,
allowed : Vec < String > ,
2019-04-14 15:20:12 -05:00
longer_than : Duration ,
2019-03-27 10:09:51 -05:00
}
2019-04-02 09:52:04 -05:00
impl Filter {
2020-04-25 07:52:23 -05:00
fn from_spec ( mut spec : & str ) -> Filter {
2019-04-26 10:41:58 -05:00
let longer_than = if let Some ( idx ) = spec . rfind ( '>' ) {
2019-04-14 15:20:12 -05:00
let longer_than = spec [ idx + 1 .. ] . parse ( ) . expect ( " invalid profile longer_than " ) ;
2019-04-14 15:18:58 -05:00
spec = & spec [ .. idx ] ;
2019-04-14 15:20:12 -05:00
Duration ::from_millis ( longer_than )
2019-04-14 15:18:58 -05:00
} else {
Duration ::new ( 0 , 0 )
} ;
2019-04-26 10:41:58 -05:00
let depth = if let Some ( idx ) = spec . rfind ( '@' ) {
2019-04-14 15:18:58 -05:00
let depth : usize = spec [ idx + 1 .. ] . parse ( ) . expect ( " invalid profile depth " ) ;
spec = & spec [ .. idx ] ;
depth
} else {
999
} ;
let allowed =
2019-04-26 10:41:58 -05:00
if spec = = " * " { Vec ::new ( ) } else { spec . split ( '|' ) . map ( String ::from ) . collect ( ) } ;
2019-04-14 15:20:12 -05:00
Filter ::new ( depth , allowed , longer_than )
2019-04-14 15:18:58 -05:00
}
2019-04-14 15:04:08 -05:00
pub fn disabled ( ) -> Filter {
2019-04-14 15:18:58 -05:00
Filter ::new ( 0 , Vec ::new ( ) , Duration ::new ( 0 , 0 ) )
2019-04-14 15:04:08 -05:00
}
2019-04-14 15:20:12 -05:00
pub fn new ( depth : usize , allowed : Vec < String > , longer_than : Duration ) -> Filter {
Filter { depth , allowed , longer_than }
2019-04-02 09:52:04 -05:00
}
}
2019-03-27 10:09:51 -05:00
struct ProfileStack {
2019-03-29 07:34:05 -05:00
starts : Vec < Instant > ,
2019-03-27 10:09:51 -05:00
messages : Vec < Message > ,
filter_data : FilterData ,
}
2019-03-29 07:34:05 -05:00
struct Message {
level : usize ,
duration : Duration ,
2020-02-02 11:54:26 -06:00
label : Label ,
2020-03-06 10:23:08 -06:00
detail : Option < String > ,
2019-03-29 07:34:05 -05:00
}
2019-03-27 10:09:51 -05:00
impl ProfileStack {
fn new ( ) -> ProfileStack {
ProfileStack { starts : Vec ::new ( ) , messages : Vec ::new ( ) , filter_data : Default ::default ( ) }
}
}
2019-03-29 07:34:05 -05:00
#[ derive(Default, Clone) ]
2019-03-27 10:09:51 -05:00
struct FilterData {
depth : usize ,
version : usize ,
allowed : HashSet < String > ,
2019-04-14 15:20:12 -05:00
longer_than : Duration ,
2019-03-27 10:09:51 -05:00
}
2019-04-14 14:54:57 -05:00
static PROFILING_ENABLED : AtomicBool = AtomicBool ::new ( false ) ;
2019-05-07 12:54:33 -05:00
static FILTER : Lazy < RwLock < FilterData > > = Lazy ::new ( Default ::default ) ;
2019-03-27 10:09:51 -05:00
thread_local! ( static PROFILE_STACK : RefCell < ProfileStack > = RefCell ::new ( ProfileStack ::new ( ) ) ) ;
impl Drop for Profiler {
fn drop ( & mut self ) {
2019-03-29 07:34:05 -05:00
match self {
2020-03-06 10:23:08 -06:00
Profiler { label : Some ( label ) , detail } = > {
2019-03-29 07:34:05 -05:00
PROFILE_STACK . with ( | stack | {
let mut stack = stack . borrow_mut ( ) ;
let start = stack . starts . pop ( ) . unwrap ( ) ;
let duration = start . elapsed ( ) ;
let level = stack . starts . len ( ) ;
2020-03-06 10:23:08 -06:00
stack . messages . push ( Message { level , duration , label , detail : detail . take ( ) } ) ;
2019-03-29 07:34:05 -05:00
if level = = 0 {
let stdout = stderr ( ) ;
2019-05-21 16:00:58 -05:00
let longer_than = stack . filter_data . longer_than ;
2020-01-02 10:21:40 -06:00
// Convert to millis for comparison to avoid problems with rounding
// (otherwise we could print `0ms` despite user's `>0` filter when
// `duration` is just a few nanos).
if duration . as_millis ( ) > longer_than . as_millis ( ) {
2020-01-19 09:07:15 -06:00
print ( & stack . messages , longer_than , & mut stdout . lock ( ) ) ;
2019-04-14 15:18:58 -05:00
}
2019-03-29 07:34:05 -05:00
stack . messages . clear ( ) ;
}
} ) ;
2019-03-27 10:09:51 -05:00
}
2020-03-06 10:23:08 -06:00
Profiler { label : None , .. } = > ( ) ,
2019-03-29 07:34:05 -05:00
}
2019-03-27 10:09:51 -05:00
}
}
2020-01-19 09:07:15 -06:00
fn print ( msgs : & [ Message ] , longer_than : Duration , out : & mut impl Write ) {
2020-01-11 12:09:31 -06:00
if msgs . is_empty ( ) {
return ;
}
2020-01-19 09:07:15 -06:00
let children_map = idx_to_children ( msgs ) ;
let root_idx = msgs . len ( ) - 1 ;
print_for_idx ( root_idx , & children_map , msgs , longer_than , out ) ;
}
fn print_for_idx (
current_idx : usize ,
children_map : & [ Vec < usize > ] ,
msgs : & [ Message ] ,
longer_than : Duration ,
out : & mut impl Write ,
) {
let current = & msgs [ current_idx ] ;
let current_indent = " " . repeat ( current . level ) ;
2020-03-06 10:23:08 -06:00
let detail = current . detail . as_ref ( ) . map ( | it | format! ( " @ {} " , it ) ) . unwrap_or_default ( ) ;
writeln! (
out ,
" {}{:5}ms - {}{} " ,
current_indent ,
current . duration . as_millis ( ) ,
current . label ,
detail ,
)
. expect ( " printing profiling info " ) ;
2020-01-19 09:07:15 -06:00
let longer_than_millis = longer_than . as_millis ( ) ;
let children_indices = & children_map [ current_idx ] ;
2019-10-24 01:05:11 -05:00
let mut accounted_for = Duration ::default ( ) ;
2020-01-19 09:07:15 -06:00
let mut short_children = BTreeMap ::new ( ) ; // Use `BTreeMap` to get deterministic output.
2019-05-21 08:24:53 -05:00
2020-01-19 09:07:15 -06:00
for child_idx in children_indices . iter ( ) {
let child = & msgs [ * child_idx ] ;
if child . duration . as_millis ( ) > longer_than_millis {
print_for_idx ( * child_idx , children_map , msgs , longer_than , out ) ;
2019-05-21 16:00:58 -05:00
} else {
2020-02-02 11:54:26 -06:00
let pair = short_children . entry ( child . label ) . or_insert ( ( Duration ::default ( ) , 0 ) ) ;
2020-01-19 09:07:15 -06:00
pair . 0 + = child . duration ;
pair . 1 + = 1 ;
2019-05-21 16:00:58 -05:00
}
2020-01-19 09:07:15 -06:00
accounted_for + = child . duration ;
}
2019-03-29 07:34:05 -05:00
2020-01-19 09:07:15 -06:00
for ( child_msg , ( duration , count ) ) in short_children . iter ( ) {
let millis = duration . as_millis ( ) ;
writeln! ( out , " {}{:5}ms - {} ({} calls) " , current_indent , millis , child_msg , count )
. expect ( " printing profiling info " ) ;
2019-03-27 10:09:51 -05:00
}
2020-01-19 09:07:15 -06:00
let unaccounted_millis = ( current . duration - accounted_for ) . as_millis ( ) ;
if ! children_indices . is_empty ( )
& & unaccounted_millis > 0
& & unaccounted_millis > longer_than_millis
{
writeln! ( out , " {}{:5}ms - ??? " , current_indent , unaccounted_millis )
. expect ( " printing profiling info " ) ;
2019-05-21 16:00:58 -05:00
}
2020-01-19 09:07:15 -06:00
}
2019-10-24 01:05:11 -05:00
2020-01-19 09:07:15 -06:00
/// Returns a mapping from an index in the `msgs` to the vector with the indices of its children.
///
/// This assumes that the entries in `msgs` are in the order of when the calls to `profile` finish.
/// In other words, a postorder of the call graph. In particular, the root is the last element of
/// `msgs`.
fn idx_to_children ( msgs : & [ Message ] ) -> Vec < Vec < usize > > {
2020-01-29 13:55:35 -06:00
// Initialize with the index of the root; `msgs` and `ancestors` should be never empty.
assert! ( ! msgs . is_empty ( ) ) ;
2020-01-19 09:07:15 -06:00
let mut ancestors = vec! [ msgs . len ( ) - 1 ] ;
2020-01-29 13:55:35 -06:00
let mut result : Vec < Vec < usize > > = vec! [ vec! [ ] ; msgs . len ( ) ] ;
2020-01-19 09:07:15 -06:00
for ( idx , msg ) in msgs [ .. msgs . len ( ) - 1 ] . iter ( ) . enumerate ( ) . rev ( ) {
// We need to find the parent of the current message, i.e., the last ancestor that has a
// level lower than the current message.
while msgs [ * ancestors . last ( ) . unwrap ( ) ] . level > = msg . level {
ancestors . pop ( ) ;
2019-10-24 01:05:11 -05:00
}
2020-01-19 09:07:15 -06:00
result [ * ancestors . last ( ) . unwrap ( ) ] . push ( idx ) ;
ancestors . push ( idx ) ;
}
// Note that above we visited all children from the last to the first one. Let's reverse vectors
// to get the more natural order where the first element is the first child.
for vec in result . iter_mut ( ) {
vec . reverse ( ) ;
2019-10-24 01:05:11 -05:00
}
2020-01-19 09:07:15 -06:00
result
2019-03-27 10:09:51 -05:00
}
2019-06-03 16:25:43 -05:00
/// Prints backtrace to stderr, useful for debugging.
2020-02-19 09:28:25 -06:00
#[ cfg(feature = " backtrace " ) ]
2019-06-03 16:25:43 -05:00
pub fn print_backtrace ( ) {
let bt = backtrace ::Backtrace ::new ( ) ;
eprintln! ( " {:?} " , bt ) ;
}
2020-03-28 07:19:05 -05:00
#[ cfg(not(feature = " backtrace " )) ]
pub fn print_backtrace ( ) {
eprintln! (
r #" enable the backtrace feature:
ra_prof = { { path = " ../ra_prof " , features = [ " backtrace " ] } }
" #
) ;
}
2019-06-03 16:25:43 -05:00
thread_local! ( static IN_SCOPE : RefCell < bool > = RefCell ::new ( false ) ) ;
/// Allows to check if the current code is withing some dynamic scope, can be
/// useful during debugging to figure out why a function is called.
pub struct Scope {
2019-06-04 06:46:22 -05:00
prev : bool ,
2019-06-03 16:25:43 -05:00
}
impl Scope {
pub fn enter ( ) -> Scope {
2019-06-04 06:46:22 -05:00
let prev = IN_SCOPE . with ( | slot | std ::mem ::replace ( & mut * slot . borrow_mut ( ) , true ) ) ;
Scope { prev }
2019-06-03 16:25:43 -05:00
}
pub fn is_active ( ) -> bool {
IN_SCOPE . with ( | slot | * slot . borrow ( ) )
}
}
impl Drop for Scope {
fn drop ( & mut self ) {
2019-06-04 06:46:22 -05:00
IN_SCOPE . with ( | slot | * slot . borrow_mut ( ) = self . prev ) ;
2019-06-03 16:25:43 -05:00
}
}
2019-08-17 07:29:57 -05:00
/// A wrapper around google_cpu_profiler.
2019-06-26 03:11:28 -05:00
///
2019-08-17 07:29:57 -05:00
/// Usage:
/// 1. Install gpref_tools (https://github.com/gperftools/gperftools), probably packaged with your Linux distro.
/// 2. Build with `cpu_profiler` feature.
/// 3. Tun the code, the *raw* output would be in the `./out.profile` file.
/// 4. Install pprof for visualization (https://github.com/google/pprof).
2020-02-18 05:33:16 -06:00
/// 5. Use something like `pprof -svg target/release/rust-analyzer ./out.profile` to see the results.
2019-08-17 07:29:57 -05:00
///
/// For example, here's how I run profiling on NixOS:
///
/// ```bash
/// $ nix-shell -p gperftools --run \
2020-02-18 05:33:16 -06:00
/// 'cargo run --release -p rust-analyzer -- parse < ~/projects/rustbench/parser.rs > /dev/null'
2019-08-17 07:29:57 -05:00
/// ```
2019-06-26 03:11:28 -05:00
#[ derive(Debug) ]
pub struct CpuProfiler {
_private : ( ) ,
}
pub fn cpu_profiler ( ) -> CpuProfiler {
2019-08-17 07:29:57 -05:00
#[ cfg(feature = " cpu_profiler " ) ]
2019-06-26 03:11:28 -05:00
{
2019-08-17 07:29:57 -05:00
google_cpu_profiler ::start ( " ./out.profile " . as_ref ( ) )
2019-06-26 03:11:28 -05:00
}
2019-08-17 07:29:57 -05:00
#[ cfg(not(feature = " cpu_profiler " )) ]
2019-06-26 03:11:28 -05:00
{
2019-08-17 07:29:57 -05:00
eprintln! ( " cpu_profiler feature is disabled " )
2019-06-26 03:11:28 -05:00
}
CpuProfiler { _private : ( ) }
}
impl Drop for CpuProfiler {
fn drop ( & mut self ) {
2019-08-17 07:29:57 -05:00
#[ cfg(feature = " cpu_profiler " ) ]
2019-06-26 03:11:28 -05:00
{
2019-08-17 07:29:57 -05:00
google_cpu_profiler ::stop ( )
2019-06-26 03:11:28 -05:00
}
}
}
2019-06-30 05:30:17 -05:00
pub fn memory_usage ( ) -> MemoryUsage {
MemoryUsage ::current ( )
}
2019-03-27 10:09:51 -05:00
#[ cfg(test) ]
mod tests {
2019-04-14 17:03:11 -05:00
use super ::* ;
2019-03-27 10:09:51 -05:00
#[ test ]
fn test_basic_profile ( ) {
let s = vec! [ " profile1 " . to_string ( ) , " profile2 " . to_string ( ) ] ;
2019-04-14 17:03:11 -05:00
let f = Filter ::new ( 2 , s , Duration ::new ( 0 , 0 ) ) ;
2019-03-27 10:09:51 -05:00
set_filter ( f ) ;
profiling_function1 ( ) ;
}
fn profiling_function1 ( ) {
let _p = profile ( " profile1 " ) ;
profiling_function2 ( ) ;
}
fn profiling_function2 ( ) {
let _p = profile ( " profile2 " ) ;
}
2020-01-02 10:21:40 -06:00
#[ test ]
fn test_longer_than ( ) {
let mut result = vec! [ ] ;
let msgs = vec! [
2020-03-06 10:23:08 -06:00
Message { level : 1 , duration : Duration ::from_nanos ( 3 ) , label : " bar " , detail : None } ,
Message { level : 1 , duration : Duration ::from_nanos ( 2 ) , label : " bar " , detail : None } ,
Message { level : 0 , duration : Duration ::from_millis ( 1 ) , label : " foo " , detail : None } ,
2020-01-02 10:21:40 -06:00
] ;
2020-01-19 09:07:15 -06:00
print ( & msgs , Duration ::from_millis ( 0 ) , & mut result ) ;
2020-01-02 10:21:40 -06:00
// The calls to `bar` are so short that they'll be rounded to 0ms and should get collapsed
// when printing.
assert_eq! (
std ::str ::from_utf8 ( & result ) . unwrap ( ) ,
" 1ms - foo \n 0ms - bar (2 calls) \n "
) ;
}
2020-01-11 12:09:31 -06:00
#[ test ]
fn test_unaccounted_for_topmost ( ) {
let mut result = vec! [ ] ;
let msgs = vec! [
2020-03-06 10:23:08 -06:00
Message { level : 1 , duration : Duration ::from_millis ( 2 ) , label : " bar " , detail : None } ,
Message { level : 0 , duration : Duration ::from_millis ( 5 ) , label : " foo " , detail : None } ,
2020-01-11 12:09:31 -06:00
] ;
2020-01-19 09:07:15 -06:00
print ( & msgs , Duration ::from_millis ( 0 ) , & mut result ) ;
2020-01-11 12:09:31 -06:00
assert_eq! (
std ::str ::from_utf8 ( & result ) . unwrap ( ) . lines ( ) . collect ::< Vec < _ > > ( ) ,
vec! [
" 5ms - foo " ,
" 2ms - bar " ,
" 3ms - ??? " ,
// Dummy comment to improve formatting
]
) ;
}
#[ test ]
fn test_unaccounted_for_multiple_levels ( ) {
let mut result = vec! [ ] ;
let msgs = vec! [
2020-03-06 10:23:08 -06:00
Message { level : 2 , duration : Duration ::from_millis ( 3 ) , label : " baz " , detail : None } ,
Message { level : 1 , duration : Duration ::from_millis ( 5 ) , label : " bar " , detail : None } ,
Message { level : 2 , duration : Duration ::from_millis ( 2 ) , label : " baz " , detail : None } ,
Message { level : 1 , duration : Duration ::from_millis ( 4 ) , label : " bar " , detail : None } ,
Message { level : 0 , duration : Duration ::from_millis ( 9 ) , label : " foo " , detail : None } ,
2020-01-11 12:09:31 -06:00
] ;
2020-01-19 09:07:15 -06:00
print ( & msgs , Duration ::from_millis ( 0 ) , & mut result ) ;
2020-01-11 12:09:31 -06:00
assert_eq! (
std ::str ::from_utf8 ( & result ) . unwrap ( ) . lines ( ) . collect ::< Vec < _ > > ( ) ,
vec! [
" 9ms - foo " ,
" 5ms - bar " ,
" 3ms - baz " ,
" 2ms - ??? " ,
" 4ms - bar " ,
" 2ms - baz " ,
" 2ms - ??? " ,
]
) ;
}
2019-03-27 10:09:51 -05:00
}