← back to iopsystems__llm-bench

Function bodies 96 total

All specs Real LLM only Function bodies
default_log_level function · rust · L209-L212 (4 LOC)
src/config.rs
fn default_log_level() -> LogLevel {
    LogLevel::Info
}
default_metrics_interval function · rust · L213-L216 (4 LOC)
src/config.rs
fn default_metrics_interval() -> String {
    "1m".to_string()
}
default_admin_listen function · rust · L217-L220 (4 LOC)
src/config.rs
fn default_admin_listen() -> String {
    "127.0.0.1:9090".to_string()
}
default_admin_enabled function · rust · L221-L224 (4 LOC)
src/config.rs
fn default_admin_enabled() -> bool {
    true
}
load function · rust · L227-L232 (6 LOC)
src/config.rs
    pub fn load(path: &PathBuf) -> anyhow::Result<Self> {
        let contents = std::fs::read_to_string(path)?;
        let config: Config = toml::from_str(&contents)?;
        config.validate()?;
        Ok(config)
    }
validate function · rust · L233-L259 (27 LOC)
src/config.rs
    pub fn validate(&self) -> anyhow::Result<()> {
        if self.load.total_requests.is_none() && self.load.duration_seconds.is_none() {
            anyhow::bail!("Either total_requests or duration_seconds must be specified");
        }

        if self.load.total_requests.is_some() && self.load.duration_seconds.is_some() {
            anyhow::bail!("Only one of total_requests or duration_seconds can be specified");
        }

        if self.load.concurrent_requests == 0 {
            anyhow::bail!("concurrent_requests must be greater than 0");
        }

        // If qps is specified, we're in fixed QPS mode
        if let Some(qps) = self.load.qps
            && qps <= 0.0
        {
            anyhow::bail!("qps must be greater than 0");
        }

        if self.runtime.worker_threads == 0 {
            anyhow::bail!("worker_threads must be greater than 0");
        }

        Ok(())
    }
new function · rust · L23-L41 (19 LOC)
src/distribution.rs
    pub fn new(arrival_dist: &ArrivalDistribution, qps: f64) -> Self {
        let dist_type = match arrival_dist {
            ArrivalDistribution::Uniform => {
                let interval_ms = (1000.0 / qps) as u64;
                DistributionType::Uniform {
                    interval: Duration::from_millis(interval_ms),
                }
            }
            ArrivalDistribution::Poisson => {
                // For Poisson arrivals, inter-arrival times follow exponential distribution
                // λ (lambda) = rate = qps
                let exp_dist =
                    Exp::new(qps).expect("QPS must be positive for exponential distribution");
                DistributionType::Exponential { exp_dist }
            }
        };

        Self { dist_type }
    }
Repobility analyzer · published findings · https://repobility.com
next_delay function · rust · L47-L57 (11 LOC)
src/distribution.rs
    pub fn next_delay(&self) -> Duration {
        match &self.dist_type {
            DistributionType::Uniform { interval } => *interval,
            DistributionType::Exponential { exp_dist } => {
                let mut rng = thread_rng();
                // Sample returns time in seconds
                let wait_secs = exp_dist.sample(&mut rng);
                Duration::from_secs_f64(wait_secs)
            }
        }
    }
distribution_name function · rust · L60-L65 (6 LOC)
src/distribution.rs
    pub fn distribution_name(&self) -> &str {
        match &self.dist_type {
            DistributionType::Uniform { .. } => "Uniform",
            DistributionType::Exponential { .. } => "Poisson",
        }
    }
test_uniform_distribution function · rust · L73-L83 (11 LOC)
src/distribution.rs
    fn test_uniform_distribution() {
        let dist = RequestDistribution::new(&ArrivalDistribution::Uniform, 10.0);

        // For 10 QPS, expect 100ms intervals
        let delay = dist.next_delay();
        assert_eq!(delay, Duration::from_millis(100));

        // Should be deterministic
        let delay2 = dist.next_delay();
        assert_eq!(delay, delay2);
    }
test_poisson_distribution_variability function · rust · L86-L110 (25 LOC)
src/distribution.rs
    fn test_poisson_distribution_variability() {
        let dist = RequestDistribution::new(&ArrivalDistribution::Poisson, 10.0);

        // Collect several samples
        let mut delays = Vec::new();
        for _ in 0..100 {
            delays.push(dist.next_delay());
        }

        // Poisson should produce variable delays
        let all_same = delays.iter().all(|d| *d == delays[0]);
        assert!(
            !all_same,
            "Poisson distribution should produce variable delays"
        );

        // Average should be roughly 1/rate = 1/10 = 0.1 seconds
        let avg_secs: f64 =
            delays.iter().map(|d| d.as_secs_f64()).sum::<f64>() / delays.len() as f64;
        assert!(
            (avg_secs - 0.1).abs() < 0.05,
            "Average delay should be close to 1/rate (0.1s), got {}",
            avg_secs
        );
    }
test_distribution_name function · rust · L113-L119 (7 LOC)
src/distribution.rs
    fn test_distribution_name() {
        let uniform = RequestDistribution::new(&ArrivalDistribution::Uniform, 10.0);
        assert_eq!(uniform.distribution_name(), "Uniform");

        let poisson = RequestDistribution::new(&ArrivalDistribution::Poisson, 10.0);
        assert_eq!(poisson.distribution_name(), "Poisson");
    }
parse_log_filters function · rust · L13-L30 (18 LOC)
src/main.rs
fn parse_log_filters(filters: &[String]) -> HashMap<String, LevelFilter> {
    let mut map = HashMap::new();
    for filter in filters {
        if let Some((module, level)) = filter.split_once('=') {
            let level_filter = match level.to_lowercase().as_str() {
                "error" => LevelFilter::Error,
                "warn" => LevelFilter::Warn,
                "info" => LevelFilter::Info,
                "debug" => LevelFilter::Debug,
                "trace" => LevelFilter::Trace,
                "off" => LevelFilter::Off,
                _ => continue,
            };
            map.insert(module.to_string(), level_filter);
        }
    }
    map
}
should_log function · rust · L33-L45 (13 LOC)
src/main.rs
fn should_log(metadata: &Metadata, filters: &HashMap<String, LevelFilter>) -> bool {
    let target = metadata.target();

    // Check each filter to see if it matches this target
    for (module_prefix, level_filter) in filters {
        if target.starts_with(module_prefix) {
            return metadata.level() <= *level_filter;
        }
    }

    // If no filter matched, allow the log (will be caught by global level filter)
    true
}
enabled function · rust · L55-L57 (3 LOC)
src/main.rs
    fn enabled(&self, metadata: &Metadata) -> bool {
        metadata.level() <= self.max_level && should_log(metadata, &self.filters)
    }
Source: Repobility analyzer · https://repobility.com
log function · rust · L58-L66 (9 LOC)
src/main.rs
    fn log(&self, record: &Record) {
        if self.enabled(record.metadata())
            && let Ok(mut output) = self.output.lock()
        {
            let message = format!("{}\n", record.args());
            let _ = output.write_all(message.as_bytes());
        }
    }
flush function · rust · L67-L72 (6 LOC)
src/main.rs
    fn flush(&self) {
        if let Ok(mut output) = self.output.lock() {
            let _ = output.flush();
        }
    }
main function · rust · L74-L154 (81 LOC)
src/main.rs
fn main() -> Result<()> {
    // Parse CLI arguments
    let cli = Cli::parse_args();

    // Load configuration first to check verbosity setting
    let config = Config::load(&cli.config)?;

    // Set up logging with ringlog and per-module filtering
    let log_level = config.log.level.to_level_filter();

    // Configure output destination
    let output: Box<dyn Output> = if let Some(ref log_file) = config.output.trace_log {
        // Log to file with rotation
        let backup_file = log_file.with_extension("old");
        Box::new(File::new(log_file.clone(), backup_file, LOG_FILE_MAX_SIZE)?)
    } else {
        // Log to stderr
        Box::new(Stderr::new())
    };

    // Parse per-module filters from config
    let filters = parse_log_filters(&config.log.filter);

    // Create logger with per-module filtering if configured
    if filters.is_empty() {
        // No filters - use ringlog directly
        let base_log = LogBuilder::new()
            .output(output)
         
run_benchmark function · rust · L155-L178 (24 LOC)
src/main.rs
async fn run_benchmark(config: Config) -> Result<()> {
    // Start admin server if configured
    if let Some(ref admin_config) = config.admin
        && admin_config.enabled
    {
        let addr: std::net::SocketAddr = admin_config
            .listen
            .parse()
            .expect("Invalid admin listen address");

        info!("Starting metrics server on {}", addr);
        tokio::spawn(async move {
            llm_bench::admin::start_server(addr).await;
        });
    }

    debug!("Initializing benchmark runner");
    let runner = llm_bench::BenchmarkRunner::new(config).await?;
    info!("Starting benchmark run");
    runner.run().await?;
    info!("Benchmark completed successfully");
    Ok(())
}
init function · rust · L217-L220 (4 LOC)
src/metrics.rs
    pub fn init() {
        // Metriken metrics are automatically registered via the #[metric] attribute
        // No explicit initialization needed
    }
record_request_sent function · rust · L221-L225 (5 LOC)
src/metrics.rs
    pub fn record_request_sent() {
        REQUESTS_SENT.increment();
        REQUESTS_INFLIGHT.increment();
    }
record_request_complete function · rust · L226-L249 (24 LOC)
src/metrics.rs
    pub fn record_request_complete(status: RequestStatus) {
        REQUESTS_INFLIGHT.decrement();
        match status {
            RequestStatus::Success => {
                REQUESTS_SUCCESS.increment();
            }
            RequestStatus::Failed(error_type) => {
                REQUESTS_FAILED.increment();
                match error_type {
                    ErrorType::Connection => ERRORS_CONNECTION.increment(),
                    ErrorType::Http4xx(_) => ERRORS_HTTP_4XX.increment(),
                    ErrorType::Http5xx(_) => ERRORS_HTTP_5XX.increment(),
                    ErrorType::Parse => ERRORS_PARSE.increment(),
                    ErrorType::Timeout => REQUESTS_TIMEOUT.increment(),
                    ErrorType::Other => ERRORS_OTHER.increment(),
                };
            }
            RequestStatus::Timeout => {
                REQUESTS_TIMEOUT.increment();
                ERRORS_OTHER.increment();
            }
        }
    }
record_tokens function · rust · L250-L254 (5 LOC)
src/metrics.rs
    pub fn record_tokens(input: u64, output: u64) {
        TOKENS_INPUT.add(input);
        TOKENS_OUTPUT.add(output);
    }
If a scraper extracted this row, it came from Repobility (https://repobility.com)
record_ttft_with_context function · rust · L255-L277 (23 LOC)
src/metrics.rs
    pub fn record_ttft_with_context(duration: Duration, input_tokens: u64) {
        let nanos = duration.as_nanos() as u64;

        // Record in context-specific histogram based on production patterns
        match input_tokens {
            0..=200 => {
                let _ = TTFT_SMALL.increment(nanos); // ~50% of production traffic
            }
            201..=500 => {
                let _ = TTFT_MEDIUM.increment(nanos); // ~30% of production traffic
            }
            501..=2000 => {
                let _ = TTFT_LARGE.increment(nanos); // ~15% of production traffic
            }
            2001..=8000 => {
                let _ = TTFT_XLARGE.increment(nanos); // ~4% of production traffic
            }
            _ => {
                let _ = TTFT_XXLARGE.increment(nanos); // ~1% of production traffic
            }
        }
    }
record_tpot function · rust · L278-L281 (4 LOC)
src/metrics.rs
    pub fn record_tpot(duration: Duration) {
        let _ = TPOT.increment(duration.as_nanos() as u64);
    }
record_itl_with_context function · rust · L282-L304 (23 LOC)
src/metrics.rs
    pub fn record_itl_with_context(duration: Duration, input_tokens: u64) {
        let nanos = duration.as_nanos() as u64;

        // Record in context-specific histogram
        match input_tokens {
            0..=200 => {
                let _ = ITL_SMALL.increment(nanos);
            }
            201..=500 => {
                let _ = ITL_MEDIUM.increment(nanos);
            }
            501..=1000 => {
                let _ = ITL_LARGE.increment(nanos);
            }
            1001..=2000 => {
                let _ = ITL_XLARGE.increment(nanos);
            }
            _ => {
                let _ = ITL_XXLARGE.increment(nanos);
            }
        }
    }
record_latency function · rust · L305-L308 (4 LOC)
src/metrics.rs
    pub fn record_latency(duration: Duration) {
        let _ = REQUEST_LATENCY.increment(duration.as_nanos() as u64);
    }
record_retry function · rust · L309-L312 (4 LOC)
src/metrics.rs
    pub fn record_retry() {
        REQUESTS_RETRIED.increment();
    }
default function · rust · L139-L141 (3 LOC)
src/report.rs
    fn default() -> Self {
        Self::new()
    }
new function · rust · L145-L151 (7 LOC)
src/report.rs
    pub fn new() -> Self {
        Self {
            start_time: SystemTime::now(),
            config: None,
            duration: None,
        }
    }
with_config function · rust · L152-L156 (5 LOC)
src/report.rs
    pub fn with_config(mut self, config: crate::config::Config) -> Self {
        self.config = Some(config);
        self
    }
Want this analysis on your repo? https://repobility.com/scan/
with_duration function · rust · L157-L161 (5 LOC)
src/report.rs
    pub fn with_duration(mut self, duration: Duration) -> Self {
        self.duration = Some(duration);
        self
    }
build function · rust · L162-L286 (125 LOC)
src/report.rs
    pub fn build(&self) -> Result<BenchmarkReport> {
        // Use provided duration if available, otherwise calculate from elapsed time
        let duration = if let Some(d) = self.duration {
            d
        } else {
            let end_time = SystemTime::now();
            end_time.duration_since(self.start_time)?
        };

        // Access metrics directly for now
        // In production, we'd use metriken-exposition properly
        use crate::metrics::{
            ERRORS_CONNECTION, ERRORS_HTTP_4XX, ERRORS_HTTP_5XX, ERRORS_OTHER, REQUESTS_FAILED,
            REQUESTS_RETRIED, REQUESTS_SENT, REQUESTS_SUCCESS, REQUESTS_TIMEOUT, TOKENS_INPUT,
            TOKENS_OUTPUT,
        };

        let requests_sent = REQUESTS_SENT.value();
        let requests_success = REQUESTS_SUCCESS.value();
        let requests_failed = REQUESTS_FAILED.value();
        let requests_timeout = REQUESTS_TIMEOUT.value();

        let input_tokens = TOKENS_INPUT.value();
        let output_tokens
calculate_histogram_mean function · rust · L289-L309 (21 LOC)
src/report.rs
    fn calculate_histogram_mean(histogram: &metriken::AtomicHistogram) -> f64 {
        if let Some(loaded) = histogram.load() {
            let mut sum = 0u64;
            let mut count = 0u64;

            // Iterate through all buckets
            for bucket in loaded.iter() {
                let bucket_count = bucket.count();
                if bucket_count > 0 {
                    // Use upper edge for consistency with percentile calculation
                    sum += bucket.end() * bucket_count;
                    count += bucket_count;
                }
            }

            if count > 0 {
                return (sum as f64 / count as f64) / 1_000_000.0; // Convert to ms
            }
        }
        0.0
    }
build_latency_stats function · rust · L310-L382 (73 LOC)
src/report.rs
    fn build_latency_stats(&self) -> Result<LatencyStats> {
        use crate::metrics::{REQUEST_LATENCY, TPOT};

        // Calculate means from histograms
        let tpot_mean = Self::calculate_histogram_mean(&TPOT);
        let request_mean = Self::calculate_histogram_mean(&REQUEST_LATENCY);

        // Extract TPOT percentiles
        let mut tpot_p50 = 0.0;
        let mut tpot_p90 = 0.0;
        let mut tpot_p95 = 0.0;
        let mut tpot_p99 = 0.0;

        if let Some(tpot_histogram) = TPOT.load()
            && let Ok(Some(percentiles)) = tpot_histogram.percentiles(&[50.0, 90.0, 95.0, 99.0])
        {
            for (percentile, bucket) in percentiles.iter() {
                let value_ms = bucket.end() as f64 / 1_000_000.0;
                match percentile.round() as u32 {
                    50 => tpot_p50 = value_ms,
                    90 => tpot_p90 = value_ms,
                    95 => tpot_p95 = value_ms,
                    99 => tpot_p99 = value_ms,
              
build_context_latency_stats function · rust · L383-L444 (62 LOC)
src/report.rs
    fn build_context_latency_stats(&self) -> Option<ContextLatencyStats> {
        use crate::metrics::{TTFT_LARGE, TTFT_MEDIUM, TTFT_SMALL, TTFT_XLARGE, TTFT_XXLARGE};

        let extract_percentiles =
            |histogram: &metriken::AtomicHistogram| -> Option<LatencyPercentiles> {
                if let Some(loaded) = histogram.load()
                    && let Ok(Some(percentiles)) = loaded.percentiles(&[50.0, 90.0, 95.0, 99.0])
                    && !percentiles.is_empty()
                {
                    let mut p50 = 0.0;
                    let mut p90 = 0.0;
                    let mut p95 = 0.0;
                    let mut p99 = 0.0;

                    for (percentile, bucket) in percentiles.iter() {
                        let value_ms = bucket.end() as f64 / 1_000_000.0;
                        match percentile.round() as u32 {
                            50 => p50 = value_ms,
                            90 => p90 = value_ms,
                            95 => p9
build_context_itl_stats function · rust · L445-L506 (62 LOC)
src/report.rs
    fn build_context_itl_stats(&self) -> Option<ContextITLStats> {
        use crate::metrics::{ITL_LARGE, ITL_MEDIUM, ITL_SMALL, ITL_XLARGE, ITL_XXLARGE};

        let extract_percentiles =
            |histogram: &metriken::AtomicHistogram| -> Option<ITLPercentiles> {
                if let Some(loaded) = histogram.load()
                    && let Ok(Some(percentiles)) = loaded.percentiles(&[50.0, 90.0, 95.0, 99.0])
                    && !percentiles.is_empty()
                {
                    let mut p50 = 0.0;
                    let mut p90 = 0.0;
                    let mut p95 = 0.0;
                    let mut p99 = 0.0;

                    for (percentile, bucket) in percentiles.iter() {
                        let value_ms = bucket.end() as f64 / 1_000_000.0;
                        match percentile.round() as u32 {
                            50 => p50 = value_ms,
                            90 => p90 = value_ms,
                            95 => p95 = value_ms,
   
print_console_report function · rust · L507-L596 (90 LOC)
src/report.rs
    pub fn print_console_report(&self) -> Result<()> {
        let report = self.build()?;

        use chrono::Utc;
        let now = Utc::now();
        let timestamp = now.to_rfc3339_opts(chrono::SecondsFormat::Millis, false);

        println!();
        println!("{}", timestamp);
        println!("{} -----", timestamp);
        println!("{} Benchmark Complete", timestamp);
        println!(
            "{} Duration: {:.1}s",
            timestamp,
            report.duration.as_secs_f64()
        );
        println!(
            "{} Requests: Sent: {} Retries: {}",
            timestamp, report.summary.total_requests, report.summary.retries
        );
        println!(
            "{} Responses: Received: {} Ok: {} Err: {} Success: {:.2}%",
            timestamp,
            report.summary.successful_requests + report.summary.failed_requests,
            report.summary.successful_requests,
            report.summary.failed_requests,
            report.summary.success_rate * 100.0
capture_snapshots function · rust · L17-L101 (85 LOC)
src/snapshot.rs
pub async fn capture_snapshots(config: Config) -> Result<()> {
    let metrics_config = match config.metrics.as_ref() {
        Some(cfg) => cfg,
        None => return Ok(()), // No metrics configured
    };

    // Parse interval
    let interval_duration = humantime::parse_duration(&metrics_config.interval)?;

    // Create a temporary file for msgpack data (will be converted to parquet)
    let temp_file = NamedTempFile::new()?;
    let file = File::from_std(temp_file.reopen()?);
    let mut writer = BufWriter::new(file);

    // Get an aligned start time (aligned to the second)
    let start = Instant::now() - Duration::from_nanos(Utc::now().nanosecond() as u64)
        + Duration::from_secs(1);

    // Create interval timer
    let mut interval = interval_at(start, interval_duration);

    // Build snapshotter with metadata
    let snapshotter = SnapshotterBuilder::new()
        .metadata("source".to_string(), env!("CARGO_PKG_NAME").to_string())
        .metadata("version".to_str
Repobility analyzer · published findings · https://repobility.com
new function · rust · L50-L66 (17 LOC)
src/stats.rs
    fn new() -> Self {
        Self {
            requests_sent: REQUESTS_SENT.value(),
            requests_success: REQUESTS_SUCCESS.value(),
            requests_failed: REQUESTS_FAILED.value(),
            requests_timeout: REQUESTS_TIMEOUT.value(),
            tokens_input: TOKENS_INPUT.value(),
            tokens_output: TOKENS_OUTPUT.value(),
            errors_connection: ERRORS_CONNECTION.value(),
            errors_4xx: ERRORS_HTTP_4XX.value(),
            errors_5xx: ERRORS_HTTP_5XX.value(),
            errors_parse: ERRORS_PARSE.value(),
            errors_other: ERRORS_OTHER.value(),
            tpot_histogram: TPOT.load(),
            request_histogram: REQUEST_LATENCY.load(),
        }
    }
update function · rust · L67-L82 (16 LOC)
src/stats.rs
    fn update(&mut self) {
        self.requests_sent = REQUESTS_SENT.value();
        self.requests_success = REQUESTS_SUCCESS.value();
        self.requests_failed = REQUESTS_FAILED.value();
        self.requests_timeout = REQUESTS_TIMEOUT.value();
        self.tokens_input = TOKENS_INPUT.value();
        self.tokens_output = TOKENS_OUTPUT.value();
        self.errors_connection = ERRORS_CONNECTION.value();
        self.errors_4xx = ERRORS_HTTP_4XX.value();
        self.errors_5xx = ERRORS_HTTP_5XX.value();
        self.errors_parse = ERRORS_PARSE.value();
        self.errors_other = ERRORS_OTHER.value();
        self.tpot_histogram = TPOT.load();
        self.request_histogram = REQUEST_LATENCY.load();
    }
periodic_stats function · rust · L84-L320 (237 LOC)
src/stats.rs
pub async fn periodic_stats(config: Config, warmup_complete: Arc<Notify>) {
    // Default to 1 minute interval if not specified
    let interval_duration = if let Some(metrics_config) = config.metrics.as_ref() {
        humantime::parse_duration(&metrics_config.interval).unwrap_or(Duration::from_secs(60))
    } else {
        Duration::from_secs(60)
    };

    // Build snapshotter for reading metrics (not used but kept for compatibility)
    let snapshotter = SnapshotterBuilder::new().build();

    // Wait for warmup to complete before starting stats intervals
    warmup_complete.notified().await;

    // Get an aligned start time (aligned to the second) AFTER warmup
    let start = Instant::now() - Duration::from_nanos(Utc::now().nanosecond() as u64)
        + Duration::from_secs(1);

    // Create interval timer
    let mut interval = interval_at(start, interval_duration);
    let mut window_id = 0;

    // Wait a bit for initial metrics
    tokio::time::sleep(Duration::from_secs(
new function · rust · L19-L31 (13 LOC)
src/tokenizer.rs
    pub fn new(model: &str) -> Result<Self> {
        let (encoder, model_type) = if model.contains("gpt-4o") {
            (Arc::new(o200k_base()?), ModelType::O200k)
        } else {
            // Default to cl100k for most models (GPT-3.5, GPT-4, etc.)
            (Arc::new(cl100k_base()?), ModelType::Cl100k)
        };

        Ok(Self {
            encoder,
            model_type,
        })
    }
count_tokens function · rust · L32-L41 (10 LOC)
src/tokenizer.rs
    pub fn count_tokens(&self, text: &str) -> usize {
        // Note: This counts tokens in the raw text content only.
        // When using chat APIs, additional tokens are added for:
        // - Chat format markers (e.g., <|im_start|>, <|im_end|>)
        // - Role indicators (e.g., "user", "assistant")
        // - Other protocol overhead
        // So the actual tokens sent to the API will be higher than this count.
        self.encoder.encode_with_special_tokens(text).len()
    }
model_type function · rust · L42-L45 (4 LOC)
src/tokenizer.rs
    pub fn model_type(&self) -> ModelType {
        self.model_type
    }
test_token_counting function · rust · L53-L66 (14 LOC)
src/tokenizer.rs
    fn test_token_counting() {
        let tokenizer = Tokenizer::new("gpt-3.5-turbo").unwrap();

        // Test basic text
        let count = tokenizer.count_tokens("Hello, world!");
        assert!(count > 0);

        // Test that whitespace counting differs from token counting
        let text = "This is a test";
        let word_count = text.split_whitespace().count();
        let token_count = tokenizer.count_tokens(text);
        // Tokens and words are usually different
        println!("Words: {}, Tokens: {}", word_count, token_count);
    }
‹ prevpage 2 / 2