How Hash Table-Based Enable/Disable Detection Optimizes tui-logger Performance

tui-logger hashes each log target once and caches the resolved LevelFilter in a HashMap, reducing per-record overhead from complex filter parsing to a single integer lookup.

The gin66/tui-logger crate implements a high-performance logging backend for Terminal User Interface (TUI) applications. Its hash table-based enable/disable detection mechanism eliminates redundant filter evaluations by caching target-to-level mappings after the first encounter, keeping UI threads responsive under heavy logging loads.

How Hash Table-Based Enable/Disable Detection Works

The optimization centers on a HotSelect structure that maintains a HashMap<u64, LevelFilter>, mapping pre-computed hash values to their effective log levels.

Fast Target Hashing with fast_str_hash

In src/logger/fast_hash.rs (lines 1-7), the fast_str_hash function implements a lightweight, deterministic hashing algorithm based on Java's String.hashCode(). This converts log target strings into u64 values with minimal computational cost:

// Located in src/logger/fast_hash.rs
pub fn fast_str_hash(s: &str) -> u64 {
    // Java String.hashCode() implementation: h = 31*h + c
    let mut hash: u64 = 0;
    for c in s.chars() {
        hash = hash.wrapping_mul(31).wrapping_add(c as u64);
    }
    hash
}

This hash function ensures consistent u64 identifiers for log targets without the overhead of cryptographic hashing.

Cache Population in move_events()

When the logger processes events in src/logger/inner.rs (lines 59-86), the move_events() method populates the hash table lazily. Upon encountering a new target for the first time, the code:

  1. Computes the hash using fast_str_hash
  2. Constructs log::Metadata for each possible LevelFilter
  3. Queries the env_filter to determine the first matching level
  4. Stores the resulting LevelFilter in hot_select.hashtable

This one-time evaluation captures the effective filtering decision for reuse across all subsequent log records from that target.

Fast-Path Enabled Check

The Log::enabled implementation in src/logger/inner.rs (lines 71-80) leverages the cached values:

// Simplified from src/logger/inner.rs
fn enabled(&self, metadata: &Metadata) -> bool {
    let h = fast_str_hash(metadata.target());
    if let Some(&levelfilter) = self.hot_select.hashtable.get(&h) {
        // Fast path: integer comparison only
        metadata.level() <= levelfilter
    } else {
        // Slow path: full env_filter evaluation
        self.env_filter.enabled(metadata)
    }
}

When a hash exists in the table, the logger avoids the env_filter entirely, comparing only integer values to determine if the record should be processed.

Performance Impact: With vs. Without Hash Table

The hash table-based enable/disable detection fundamentally changes the overhead profile of logging operations:

  • Without caching: Every log::info! or log::debug! call triggers env_filter.enabled(metadata), which parses filter directives and performs string comparisons against the target name. This cost repeats for every log line, even those sharing identical targets.
  • With caching: After the first encounter, subsequent calls perform only a u64 hash calculation, a single HashMap lookup, and an integer comparison. The O(1) lookup eliminates per-record parsing overhead, making logging effectively free for known targets.

This distinction proves critical in TUI applications where the main thread must process UI refreshes alongside logging operations.

Source Code Implementation Details

The optimization spans three critical source files in the gin66/tui-logger repository:

  • src/logger/fast_hash.rs: Contains the fast_str_hash function that generates the u64 identifiers used as hash table keys.
  • src/logger/inner.rs: Defines the HotSelect structure housing the HashMap, implements the move_events() cache population logic (lines 59-86), and contains the enabled() fast-path check (lines 71-80).
  • src/logger/api.rs: Exposes init_logger() and set_default_level(), which configure the underlying filter that the hash table caches.

Practical Usage Example

When using tui-logger in a Rust application, the hash table populates automatically during the first log emission for each unique target:

use log::{info, debug, warn};
use tui_logger::init_logger;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize with trace level filtering
    init_logger(log::LevelFilter::Trace)?;
    
    // First call: cache miss, evaluates env_filter
    info!("Application starting");
    
    // Second call: cache hit, O(1) integer lookup
    info!("Processing configuration");
    
    // Different target (warn): new cache entry created
    warn!("Low disk space detected");
    
    // Same target as above: immediate hash table lookup
    warn!("Closing files to free resources");
    
    Ok(())
}

In this example, the second info! call and second warn! call execute the fast path, reading directly from the HotSelect hash table without re-evaluating filter expressions.

Summary

  • tui-logger uses a HashMap<u64, LevelFilter> in HotSelect to cache filtering decisions per target.
  • The fast_str_hash function in src/logger/fast_hash.rs generates cheap u64 hashes based on Java's String.hashCode() algorithm.
  • move_events() in src/logger/inner.rs (lines 59-86) populates the cache lazily on first target encounter.
  • The enabled() method (lines 71-80) performs a single integer lookup and comparison for cached targets, avoiding env_filter parsing overhead.
  • This optimization reduces per-log-call overhead from string parsing to constant-time integer operations, maintaining UI responsiveness.

Frequently Asked Questions

What is the hash table used for in tui-logger?

The hash table stores a mapping between log target hashes (u64) and their resolved LevelFilter values. According to the gin66/tui-logger source code in src/logger/inner.rs, this allows the logger to check enabled() status via a single integer lookup rather than parsing filter expressions on every log call.

How does the fast_str_hash function work?

Located in src/logger/fast_hash.rs (lines 1-7), fast_str_hash implements the Java String.hashCode() algorithm by iterating through characters and calculating hash = hash * 31 + c. This produces a deterministic u64 value suitable for HashMap keys while remaining computationally inexpensive compared to standard library hashing.

What happens when a new log target is encountered?

When move_events() in src/logger/inner.rs (lines 59-86) processes a record from a previously unseen target, it computes the hash, evaluates the env_filter to determine the effective level, and stores the result in the hash table. Subsequent calls with the same target use the cached value immediately.

Does this optimization affect all log levels equally?

Yes. The hash table stores LevelFilter values, which represent the maximum enabled level for a target. The enabled() method performs an integer comparison metadata.level() <= levelfilter, making the fast path equally efficient regardless of whether the stored level is Trace, Debug, Info, Warn, or Error.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →