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:
- Computes the hash using
fast_str_hash - Constructs
log::Metadatafor each possibleLevelFilter - Queries the
env_filterto determine the first matching level - Stores the resulting
LevelFilterinhot_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!orlog::debug!call triggersenv_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
u64hash calculation, a singleHashMaplookup, and an integer comparison. TheO(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 thefast_str_hashfunction that generates theu64identifiers used as hash table keys.src/logger/inner.rs: Defines theHotSelectstructure housing theHashMap, implements themove_events()cache population logic (lines 59-86), and contains theenabled()fast-path check (lines 71-80).src/logger/api.rs: Exposesinit_logger()andset_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>inHotSelectto cache filtering decisions per target. - The
fast_str_hashfunction insrc/logger/fast_hash.rsgenerates cheapu64hashes based on Java'sString.hashCode()algorithm. move_events()insrc/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, avoidingenv_filterparsing 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →