pub mod multi;
pub use multi::MultiMonitor;
#[cfg(all(feature = "tui_monitor", feature = "std"))]
#[allow(missing_docs)]
pub mod tui;
#[cfg(all(feature = "prometheus_monitor", feature = "std"))]
#[allow(missing_docs)]
pub mod prometheus;
#[cfg(all(feature = "prometheus_monitor", feature = "std"))]
pub use prometheus::PrometheusMonitor;
#[cfg(feature = "std")]
pub mod disk;
use alloc::{fmt::Debug, string::String, vec::Vec};
use core::{fmt, fmt::Write, time::Duration};
#[cfg(feature = "std")]
pub use disk::{OnDiskJSONMonitor, OnDiskTOMLMonitor};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use crate::bolts::{current_time, format_duration_hms};
#[cfg(feature = "afl_exec_sec")]
const CLIENT_STATS_TIME_WINDOW_SECS: u64 = 5;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum UserStats {
Number(u64),
Float(f64),
String(String),
Ratio(u64, u64),
}
impl fmt::Display for UserStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UserStats::Number(n) => write!(f, "{n}"),
UserStats::Float(n) => write!(f, "{n}"),
UserStats::String(s) => write!(f, "{s}"),
UserStats::Ratio(a, b) => {
if *b == 0 {
write!(f, "{a}/{b}")
} else {
write!(f, "{a}/{b} ({}%)", a * 100 / b)
}
}
}
}
}
fn prettify_float(value: f64) -> String {
let (value, suffix) = match value {
value if value >= 1000000.0 => (value / 1000000.0, "M"),
value if value >= 1000.0 => (value / 1000.0, "k"),
value => (value, ""),
};
match value {
value if value >= 1000.0 => {
format!("{value}{suffix}")
}
value if value >= 100.0 => {
format!("{value:.1}{suffix}")
}
value if value >= 10.0 => {
format!("{value:.2}{suffix}")
}
value => {
format!("{value:.3}{suffix}")
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ClientStats {
pub corpus_size: u64,
pub executions: u64,
pub objective_size: u64,
#[cfg(feature = "afl_exec_sec")]
pub last_window_executions: u64,
#[cfg(feature = "afl_exec_sec")]
pub last_execs_per_sec: f64,
pub last_window_time: Duration,
pub user_monitor: HashMap<String, UserStats>,
#[cfg(feature = "introspection")]
pub introspection_monitor: ClientPerfMonitor,
}
impl ClientStats {
#[cfg(feature = "afl_exec_sec")]
pub fn update_executions(&mut self, executions: u64, cur_time: Duration) {
let diff = cur_time
.checked_sub(self.last_window_time)
.map_or(0, |d| d.as_secs());
if diff > CLIENT_STATS_TIME_WINDOW_SECS {
let _ = self.execs_per_sec(cur_time);
self.last_window_time = cur_time;
self.last_window_executions = self.executions;
}
self.executions = executions;
}
#[cfg(not(feature = "afl_exec_sec"))]
pub fn update_executions(&mut self, executions: u64, _cur_time: Duration) {
self.executions = executions;
}
pub fn update_corpus_size(&mut self, corpus_size: u64) {
self.corpus_size = corpus_size;
}
pub fn update_objective_size(&mut self, objective_size: u64) {
self.objective_size = objective_size;
}
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
#[cfg(feature = "afl_exec_sec")]
pub fn execs_per_sec(&mut self, cur_time: Duration) -> f64 {
if self.executions == 0 {
return 0.0;
}
let elapsed = cur_time
.checked_sub(self.last_window_time)
.map_or(0.0, |d| d.as_secs_f64());
if elapsed as u64 == 0 {
return self.last_execs_per_sec;
}
let cur_avg = ((self.executions - self.last_window_executions) as f64) / elapsed;
if self.last_window_executions == 0 {
self.last_execs_per_sec = cur_avg;
return self.last_execs_per_sec;
}
if cur_avg * 5.0 < self.last_execs_per_sec || cur_avg / 5.0 > self.last_execs_per_sec {
self.last_execs_per_sec = cur_avg;
}
self.last_execs_per_sec =
self.last_execs_per_sec * (1.0 - 1.0 / 16.0) + cur_avg * (1.0 / 16.0);
self.last_execs_per_sec
}
#[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
#[cfg(not(feature = "afl_exec_sec"))]
pub fn execs_per_sec(&mut self, cur_time: Duration) -> f64 {
if self.executions == 0 {
return 0.0;
}
let elapsed = cur_time
.checked_sub(self.last_window_time)
.map_or(0.0, |d| d.as_secs_f64());
if elapsed as u64 == 0 {
return 0.0;
}
(self.executions as f64) / elapsed
}
fn execs_per_sec_pretty(&mut self, cur_time: Duration) -> String {
prettify_float(self.execs_per_sec(cur_time))
}
pub fn update_user_stats(&mut self, name: String, value: UserStats) {
self.user_monitor.insert(name, value);
}
pub fn get_user_stats(&mut self, name: &str) -> Option<&UserStats> {
self.user_monitor.get(name)
}
#[cfg(feature = "introspection")]
pub fn update_introspection_monitor(&mut self, introspection_monitor: ClientPerfMonitor) {
self.introspection_monitor = introspection_monitor;
}
}
pub trait Monitor {
fn client_stats_mut(&mut self) -> &mut Vec<ClientStats>;
fn client_stats(&self) -> &[ClientStats];
fn start_time(&mut self) -> Duration;
fn display(&mut self, event_msg: String, sender_id: u32);
fn corpus_size(&self) -> u64 {
self.client_stats()
.iter()
.fold(0_u64, |acc, x| acc + x.corpus_size)
}
fn objective_size(&self) -> u64 {
self.client_stats()
.iter()
.fold(0_u64, |acc, x| acc + x.objective_size)
}
#[inline]
fn total_execs(&mut self) -> u64 {
self.client_stats()
.iter()
.fold(0_u64, |acc, x| acc + x.executions)
}
#[allow(clippy::cast_sign_loss)]
#[inline]
fn execs_per_sec(&mut self) -> f64 {
let cur_time = current_time();
self.client_stats_mut()
.iter_mut()
.fold(0.0, |acc, x| acc + x.execs_per_sec(cur_time))
}
fn execs_per_sec_pretty(&mut self) -> String {
prettify_float(self.execs_per_sec())
}
fn client_stats_mut_for(&mut self, client_id: u32) -> &mut ClientStats {
let client_stat_count = self.client_stats().len();
for _ in client_stat_count..(client_id + 1) as usize {
self.client_stats_mut().push(ClientStats {
last_window_time: current_time(),
..ClientStats::default()
});
}
&mut self.client_stats_mut()[client_id as usize]
}
}
#[derive(Debug, Clone)]
pub struct NopMonitor {
start_time: Duration,
client_stats: Vec<ClientStats>,
}
impl Monitor for NopMonitor {
fn client_stats_mut(&mut self) -> &mut Vec<ClientStats> {
&mut self.client_stats
}
fn client_stats(&self) -> &[ClientStats] {
&self.client_stats
}
fn start_time(&mut self) -> Duration {
self.start_time
}
fn display(&mut self, _event_msg: String, _sender_id: u32) {}
}
impl NopMonitor {
#[must_use]
pub fn new() -> Self {
Self {
start_time: current_time(),
client_stats: vec![],
}
}
}
impl Default for NopMonitor {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "std")]
#[derive(Debug, Clone, Default)]
pub struct SimplePrintingMonitor {
start_time: Duration,
client_stats: Vec<ClientStats>,
}
#[cfg(feature = "std")]
impl SimplePrintingMonitor {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[cfg(feature = "std")]
impl Monitor for SimplePrintingMonitor {
fn client_stats_mut(&mut self) -> &mut Vec<ClientStats> {
&mut self.client_stats
}
fn client_stats(&self) -> &[ClientStats] {
&self.client_stats
}
fn start_time(&mut self) -> Duration {
self.start_time
}
fn display(&mut self, event_msg: String, sender_id: u32) {
println!(
"[{} #{}] run time: {}, clients: {}, corpus: {}, objectives: {}, executions: {}, exec/sec: {}",
event_msg,
sender_id,
format_duration_hms(&(current_time() - self.start_time)),
self.client_stats().len(),
self.corpus_size(),
self.objective_size(),
self.total_execs(),
self.execs_per_sec_pretty()
);
#[cfg(feature = "introspection")]
{
println!(
"Client {:03}:\n{}",
sender_id, self.client_stats[sender_id as usize].introspection_monitor
);
println!();
}
}
}
#[derive(Clone)]
pub struct SimpleMonitor<F>
where
F: FnMut(String),
{
print_fn: F,
start_time: Duration,
print_user_monitor: bool,
client_stats: Vec<ClientStats>,
}
impl<F> Debug for SimpleMonitor<F>
where
F: FnMut(String),
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SimpleMonitor")
.field("start_time", &self.start_time)
.field("client_stats", &self.client_stats)
.finish()
}
}
impl<F> Monitor for SimpleMonitor<F>
where
F: FnMut(String),
{
fn client_stats_mut(&mut self) -> &mut Vec<ClientStats> {
&mut self.client_stats
}
fn client_stats(&self) -> &[ClientStats] {
&self.client_stats
}
fn start_time(&mut self) -> Duration {
self.start_time
}
fn display(&mut self, event_msg: String, sender_id: u32) {
let mut fmt = format!(
"[{} #{}] run time: {}, clients: {}, corpus: {}, objectives: {}, executions: {}, exec/sec: {}",
event_msg,
sender_id,
format_duration_hms(&(current_time() - self.start_time)),
self.client_stats().len(),
self.corpus_size(),
self.objective_size(),
self.total_execs(),
self.execs_per_sec_pretty()
);
if self.print_user_monitor {
let client = self.client_stats_mut_for(sender_id);
for (key, val) in &client.user_monitor {
write!(fmt, ", {key}: {val}").unwrap();
}
}
(self.print_fn)(fmt);
#[cfg(feature = "introspection")]
{
let fmt = format!(
"Client {:03}:\n{}",
sender_id, self.client_stats[sender_id as usize].introspection_monitor
);
(self.print_fn)(fmt);
(self.print_fn)(String::new());
}
}
}
impl<F> SimpleMonitor<F>
where
F: FnMut(String),
{
pub fn new(print_fn: F) -> Self {
Self {
print_fn,
start_time: current_time(),
print_user_monitor: false,
client_stats: vec![],
}
}
pub fn with_time(print_fn: F, start_time: Duration) -> Self {
Self {
print_fn,
start_time,
print_user_monitor: false,
client_stats: vec![],
}
}
pub fn with_user_monitor(print_fn: F, print_user_monitor: bool) -> Self {
Self {
print_fn,
start_time: current_time(),
print_user_monitor,
client_stats: vec![],
}
}
}
#[macro_export]
macro_rules! start_timer {
($state:expr) => {{
#[cfg(feature = "introspection")]
$state.introspection_monitor_mut().start_timer();
}};
}
#[macro_export]
macro_rules! mark_feature_time {
($state:expr, $feature:expr) => {{
#[cfg(feature = "introspection")]
$state
.introspection_monitor_mut()
.mark_feature_time($feature);
}};
}
#[macro_export]
macro_rules! mark_feedback_time {
($state:expr) => {{
#[cfg(feature = "introspection")]
$state.introspection_monitor_mut().mark_feedback_time();
}};
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ClientPerfMonitor {
start_time: u64,
current_time: u64,
scheduler: u64,
manager: u64,
curr_stage: u8,
stages_used: Vec<bool>,
stages: Vec<[u64; PerfFeature::Count as usize]>,
feedbacks: HashMap<String, u64>,
timer_start: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[repr(u8)]
pub enum PerfFeature {
GetInputFromCorpus = 0,
Mutate = 1,
MutatePostExec = 2,
TargetExecution = 3,
PreExec = 4,
PostExec = 5,
PreExecObservers = 6,
PostExecObservers = 7,
GetFeedbackInterestingAll = 8,
GetObjectivesInterestingAll = 9,
Count, }
impl From<PerfFeature> for usize {
fn from(val: PerfFeature) -> usize {
match val {
PerfFeature::GetInputFromCorpus => PerfFeature::GetInputFromCorpus as usize,
PerfFeature::Mutate => PerfFeature::Mutate as usize,
PerfFeature::MutatePostExec => PerfFeature::MutatePostExec as usize,
PerfFeature::TargetExecution => PerfFeature::TargetExecution as usize,
PerfFeature::PreExec => PerfFeature::PreExec as usize,
PerfFeature::PostExec => PerfFeature::PostExec as usize,
PerfFeature::PreExecObservers => PerfFeature::PreExecObservers as usize,
PerfFeature::PostExecObservers => PerfFeature::PostExecObservers as usize,
PerfFeature::GetFeedbackInterestingAll => {
PerfFeature::GetFeedbackInterestingAll as usize
}
PerfFeature::GetObjectivesInterestingAll => {
PerfFeature::GetObjectivesInterestingAll as usize
}
PerfFeature::Count => PerfFeature::Count as usize,
}
}
}
impl From<usize> for PerfFeature {
fn from(val: usize) -> PerfFeature {
match val {
0 => PerfFeature::GetInputFromCorpus,
1 => PerfFeature::Mutate,
2 => PerfFeature::MutatePostExec,
3 => PerfFeature::TargetExecution,
4 => PerfFeature::PreExec,
5 => PerfFeature::PostExec,
6 => PerfFeature::PreExecObservers,
7 => PerfFeature::PostExecObservers,
8 => PerfFeature::GetFeedbackInterestingAll,
9 => PerfFeature::GetObjectivesInterestingAll,
_ => panic!("Unknown PerfFeature: {val}"),
}
}
}
#[cfg(feature = "introspection")]
pub const NUM_PERF_FEATURES: usize = PerfFeature::Count as usize;
#[cfg(feature = "introspection")]
impl ClientPerfMonitor {
#[must_use]
pub fn new() -> Self {
let start_time = crate::bolts::cpu::read_time_counter();
Self {
start_time,
current_time: start_time,
scheduler: 0,
manager: 0,
curr_stage: 0,
stages: vec![],
stages_used: vec![],
feedbacks: HashMap::new(),
timer_start: None,
}
}
#[inline]
pub fn set_current_time(&mut self, time: u64) {
self.current_time = time;
}
#[inline]
pub fn start_timer(&mut self) {
self.timer_start = Some(crate::bolts::cpu::read_time_counter());
}
pub fn update(&mut self, monitor: &ClientPerfMonitor) {
self.set_current_time(monitor.current_time);
self.update_scheduler(monitor.scheduler);
self.update_manager(monitor.manager);
self.update_stages(&monitor.stages);
self.update_feedbacks(&monitor.feedbacks);
}
#[inline]
fn mark_time(&mut self) -> u64 {
match self.timer_start {
None => {
#[cfg(feature = "std")]
eprint!("Attempted to `mark_time` without starting timer first.");
0
}
Some(timer_start) => {
let elapsed = crate::bolts::cpu::read_time_counter() - timer_start;
self.timer_start = None;
elapsed
}
}
}
#[inline]
pub fn mark_scheduler_time(&mut self) {
let elapsed = self.mark_time();
self.update_scheduler(elapsed);
}
#[inline]
pub fn mark_manager_time(&mut self) {
let elapsed = self.mark_time();
self.update_manager(elapsed);
}
#[inline]
pub fn mark_feature_time(&mut self, feature: PerfFeature) {
let elapsed = self.mark_time();
self.update_feature(feature, elapsed);
}
#[inline]
pub fn update_scheduler(&mut self, time: u64) {
self.scheduler = self
.scheduler
.checked_add(time)
.expect("update_scheduler overflow");
}
#[inline]
pub fn update_manager(&mut self, time: u64) {
self.manager = self
.manager
.checked_add(time)
.expect("update_manager overflow");
}
#[inline]
pub fn finish_stage(&mut self) {
self.curr_stage += 1;
}
#[inline]
pub fn reset_stage_index(&mut self) {
self.curr_stage = 0;
}
pub fn update_feedback(&mut self, name: &str, time: u64) {
self.feedbacks.insert(
name.into(),
self.feedbacks
.get(name)
.unwrap_or(&0)
.checked_add(time)
.expect("update_feedback overflow"),
);
}
pub fn update_feedbacks(&mut self, feedbacks: &HashMap<String, u64>) {
for (key, value) in feedbacks {
self.update_feedback(key, *value);
}
}
pub fn update_stages(&mut self, stages: &[[u64; PerfFeature::Count as usize]]) {
if self.stages.len() < stages.len() {
self.stages
.resize(stages.len(), [0; PerfFeature::Count as usize]);
self.stages_used.resize(stages.len(), false);
}
for (stage_index, features) in stages.iter().enumerate() {
for (feature_index, feature) in features.iter().enumerate() {
self.stages[stage_index][feature_index] = self.stages[stage_index][feature_index]
.checked_add(*feature)
.expect("Stage overflow");
}
}
}
pub fn update_feature(&mut self, feature: PerfFeature, time: u64) {
let stage_index: usize = self.curr_stage.try_into().unwrap();
let feature_index: usize = feature.try_into().unwrap();
if stage_index >= self.stages.len() {
self.stages
.resize(stage_index + 1, [0; PerfFeature::Count as usize]);
self.stages_used.resize(stage_index + 1, false);
}
self.stages[stage_index][feature_index] = self.stages[stage_index][feature_index]
.checked_add(time)
.expect("Stage overflow");
self.stages_used[stage_index] = true;
}
#[must_use]
pub fn elapsed_cycles(&self) -> u64 {
self.current_time - self.start_time
}
#[must_use]
pub fn manager_cycles(&self) -> u64 {
self.manager
}
#[must_use]
pub fn scheduler_cycles(&self) -> u64 {
self.scheduler
}
pub fn used_stages(
&self,
) -> impl Iterator<Item = (usize, &[u64; PerfFeature::Count as usize])> {
let used = self.stages_used.clone();
self.stages
.iter()
.enumerate()
.filter(move |(stage_index, _)| used[*stage_index])
}
#[must_use]
pub fn feedbacks(&self) -> &HashMap<String, u64> {
&self.feedbacks
}
}
#[cfg(feature = "introspection")]
impl core::fmt::Display for ClientPerfMonitor {
#[allow(clippy::cast_precision_loss)]
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
let elapsed: f64 = self.elapsed_cycles() as f64;
let scheduler_percent = self.scheduler as f64 / elapsed;
let manager_percent = self.manager as f64 / elapsed;
let mut other_percent = 1.0;
other_percent -= scheduler_percent;
other_percent -= manager_percent;
writeln!(
f,
" {scheduler_percent:6.4}: Scheduler\n {manager_percent:6.4}: Manager"
)?;
for (stage_index, features) in self.used_stages() {
writeln!(f, " Stage {stage_index}:")?;
for (feature_index, feature) in features.iter().enumerate() {
let feature_percent = *feature as f64 / elapsed;
if feature_percent == 0.0 {
continue;
}
other_percent -= feature_percent;
let feature: PerfFeature = feature_index.into();
writeln!(f, " {feature_percent:6.4}: {feature:?}")?;
}
}
writeln!(f, " Feedbacks:")?;
for (feedback_name, feedback_time) in self.feedbacks() {
let feedback_percent = *feedback_time as f64 / elapsed;
if feedback_percent == 0.0 {
continue;
}
other_percent -= feedback_percent;
writeln!(f, " {feedback_percent:6.4}: {feedback_name}")?;
}
write!(f, " {other_percent:6.4}: Not Measured")?;
Ok(())
}
}
#[cfg(feature = "introspection")]
impl Default for ClientPerfMonitor {
#[must_use]
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "python")]
#[allow(missing_docs)]
pub mod pybind {
use alloc::{boxed::Box, string::String, vec::Vec};
use core::time::Duration;
use pyo3::{prelude::*, types::PyUnicode};
use super::ClientStats;
use crate::monitors::{Monitor, SimpleMonitor};
#[pyclass(unsendable, name = "SimpleMonitor")]
pub struct PythonSimpleMonitor {
pub inner: SimpleMonitor<Box<dyn FnMut(String)>>,
print_fn: PyObject,
}
impl std::fmt::Debug for PythonSimpleMonitor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PythonSimpleMonitor")
.field("print_fn", &self.print_fn)
.finish()
}
}
impl Clone for PythonSimpleMonitor {
fn clone(&self) -> PythonSimpleMonitor {
let py_print_fn = self.print_fn.clone();
let closure = move |s: String| {
Python::with_gil(|py| -> PyResult<()> {
py_print_fn.call1(py, (PyUnicode::new(py, &s),))?;
Ok(())
})
.unwrap();
};
PythonSimpleMonitor {
inner: SimpleMonitor {
print_fn: Box::new(closure),
start_time: self.inner.start_time,
print_user_monitor: false,
client_stats: self.inner.client_stats.clone(),
},
print_fn: self.print_fn.clone(),
}
}
}
#[pymethods]
impl PythonSimpleMonitor {
#[new]
fn new(py_print_fn: PyObject) -> Self {
let py_print_fn1 = py_print_fn.clone();
let closure = move |s: String| {
Python::with_gil(|py| -> PyResult<()> {
py_print_fn1.call1(py, (PyUnicode::new(py, &s),))?;
Ok(())
})
.unwrap();
};
Self {
inner: SimpleMonitor::new(Box::new(closure)),
print_fn: py_print_fn,
}
}
#[must_use]
pub fn as_monitor(slf: Py<Self>) -> PythonMonitor {
PythonMonitor::new_simple(slf)
}
}
#[derive(Clone, Debug)]
enum PythonMonitorWrapper {
Simple(Py<PythonSimpleMonitor>),
}
#[pyclass(unsendable, name = "Monitor")]
#[derive(Clone, Debug)]
pub struct PythonMonitor {
wrapper: PythonMonitorWrapper,
}
macro_rules! unwrap_me {
($wrapper:expr, $name:ident, $body:block) => {
crate::unwrap_me_body!($wrapper, $name, $body, PythonMonitorWrapper, { Simple })
};
}
macro_rules! unwrap_me_mut {
($wrapper:expr, $name:ident, $body:block) => {
crate::unwrap_me_mut_body!($wrapper, $name, $body, PythonMonitorWrapper, { Simple })
};
}
#[pymethods]
impl PythonMonitor {
#[staticmethod]
#[must_use]
pub fn new_simple(simple_monitor: Py<PythonSimpleMonitor>) -> Self {
Self {
wrapper: PythonMonitorWrapper::Simple(simple_monitor),
}
}
}
impl Monitor for PythonMonitor {
fn client_stats_mut(&mut self) -> &mut Vec<ClientStats> {
let ptr = unwrap_me_mut!(self.wrapper, m, {
m.client_stats_mut() as *mut Vec<ClientStats>
});
unsafe { ptr.as_mut().unwrap() }
}
fn client_stats(&self) -> &[ClientStats] {
let ptr = unwrap_me!(self.wrapper, m, {
m.client_stats() as *const [ClientStats]
});
unsafe { ptr.as_ref().unwrap() }
}
fn start_time(&mut self) -> Duration {
unwrap_me_mut!(self.wrapper, m, { m.start_time() })
}
fn display(&mut self, event_msg: String, sender_id: u32) {
unwrap_me_mut!(self.wrapper, m, { m.display(event_msg, sender_id) });
}
}
pub fn register(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PythonSimpleMonitor>()?;
m.add_class::<PythonMonitor>()?;
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::monitors::prettify_float;
#[test]
fn test_prettify_float() {
assert_eq!(prettify_float(123423123.0), "123.4M");
assert_eq!(prettify_float(12342312.3), "12.34M");
assert_eq!(prettify_float(1234231.23), "1.234M");
assert_eq!(prettify_float(123423.123), "123.4k");
assert_eq!(prettify_float(12342.3123), "12.34k");
assert_eq!(prettify_float(1234.23123), "1.234k");
assert_eq!(prettify_float(123.423123), "123.4");
assert_eq!(prettify_float(12.3423123), "12.34");
assert_eq!(prettify_float(1.23423123), "1.234");
assert_eq!(prettify_float(0.123423123), "0.123");
assert_eq!(prettify_float(0.0123423123), "0.012");
}
}