Skip to content

Commit 5098b5e

Browse files
committed
feat(hostname): add benchmark with large /etc/hosts
Adds comprehensive benchmarks for the hostname utility, including: - Realistic hosts file generation with various hostname patterns - Benchmarks for all flags: -s, -d, -f, -i - Parameterized tests with 1K, 10K, 100K entries - NSS_WRAPPER support for testing with generated hosts files - CI integration for automated benchmark tracking Fixes #10949
1 parent 2993148 commit 5098b5e

File tree

4 files changed

+290
-0
lines changed

4 files changed

+290
-0
lines changed

.github/workflows/benchmarks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
uu_du,
3838
uu_expand,
3939
uu_fold,
40+
uu_hostname,
4041
uu_join,
4142
uu_ls,
4243
uu_mv,

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/hostname/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,12 @@ path = "src/main.rs"
3939

4040
[package.metadata.cargo-udeps.ignore]
4141
normal = ["uucore_procs"]
42+
43+
[[bench]]
44+
name = "hostname_bench"
45+
harness = false
46+
47+
[dev-dependencies]
48+
divan = { workspace = true }
49+
tempfile = { workspace = true }
50+
uucore = { workspace = true, features = ["benchmark"] }
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
//! Benchmarks for hostname utility
7+
//!
8+
//! This benchmark tests the performance of hostname with large /etc/hosts files,
9+
//! specifically targeting the DNS resolution functionality (`-i` flag).
10+
//!
11+
//! # Important Note on Large Hosts File Testing
12+
//!
13+
//! To properly test with large /etc/hosts files, NSS_WRAPPER library must be used:
14+
//!
15+
//! ```bash
16+
//! LD_PRELOAD=/usr/lib/libnss_wrapper.so NSS_WRAPPER_HOSTS=/tmp/large_hosts \
17+
//! cargo bench --package uu_hostname hostname_ip_lookup
18+
//! ```
19+
//!
20+
//! Without NSS_WRAPPER, the benchmark tests with the system's real /etc/hosts.
21+
22+
use divan::{Bencher, black_box};
23+
use std::io::Write;
24+
use std::path::PathBuf;
25+
use uu_hostname::uumain;
26+
use uucore::benchmark::run_util_function;
27+
28+
/// Generate a realistic large hosts file with the specified number of entries
29+
///
30+
/// Creates a hosts file that mimics real enterprise environments:
31+
/// - Includes comments and blank lines
32+
/// - Multiple aliases per IP (short name, FQDN, service names)
33+
/// - Various IP ranges (127.x, 10.x, 192.168.x, 172.16.x)
34+
/// - Realistic hostname patterns (datacenter, region, environment, role)
35+
fn generate_realistic_hosts_file(entries: usize) -> Vec<u8> {
36+
let avg_line_size = 80; // Average bytes per line with comments/aliases
37+
let mut data = Vec::with_capacity(entries * avg_line_size);
38+
let mut writer = std::io::BufWriter::new(&mut data);
39+
40+
// File header comment
41+
writeln!(writer, "# Generated hosts file for benchmarking").unwrap();
42+
writeln!(writer, "# Total entries: {}", entries).unwrap();
43+
writeln!(writer, "#").unwrap();
44+
writeln!(writer).unwrap();
45+
46+
// Standard localhost entries
47+
writeln!(writer, "# Localhost entries").unwrap();
48+
writeln!(
49+
writer,
50+
"127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4"
51+
)
52+
.unwrap();
53+
writeln!(
54+
writer,
55+
"::1 localhost localhost.localdomain localhost6 localhost6.localdomain6"
56+
)
57+
.unwrap();
58+
writeln!(writer).unwrap();
59+
60+
// Generate realistic enterprise host entries
61+
let environments = ["prod", "staging", "dev", "test"];
62+
let regions = ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"];
63+
let roles = [
64+
"web", "db", "cache", "api", "worker", "lb", "monitor", "backup",
65+
];
66+
67+
for i in 2..=entries {
68+
// Add occasional section comments (every 1000 entries)
69+
if i % 1000 == 2 && i > 2 {
70+
writeln!(writer).unwrap();
71+
writeln!(
72+
writer,
73+
"# {} environment hosts (entry {}-{})",
74+
environments[(i / 1000) % environments.len()],
75+
i,
76+
i + 998
77+
)
78+
.unwrap();
79+
}
80+
81+
// Add occasional blank lines (every 50 entries)
82+
if i % 50 == 0 {
83+
writeln!(writer).unwrap();
84+
}
85+
86+
// Generate realistic IP
87+
let ip = match i % 5 {
88+
0 => format!("127.0.0.{}", i % 256),
89+
1 => format!("10.{}.{}.{}", (i / 256) % 256, (i / 16) % 16, i % 256),
90+
2 => format!("192.168.{}.{}", (i / 256) % 256, i % 256),
91+
3 => format!("172.16.{}.{}", (i / 256) % 256, i % 256),
92+
_ => format!("10.0.{}.{}", (i / 256) % 256, i % 256),
93+
};
94+
95+
// Generate realistic hostname patterns
96+
let role = roles[i % roles.len()];
97+
let env = environments[i % environments.len()];
98+
let region = regions[i % regions.len()];
99+
let index = i;
100+
101+
// Multiple aliases: FQDN, short name, service alias
102+
let fqdn = format!("{}-{:03}.{}.{}.example.com", role, index, region, env);
103+
let short = format!("{}-{:03}", role, index);
104+
let service_alias = format!("{}-{}-{}", role, env, region.replace("-", ""));
105+
106+
// Write entry with realistic spacing
107+
writeln!(writer, "{:<15} {} {} {}", ip, fqdn, short, service_alias).unwrap();
108+
}
109+
110+
// Add footer comment
111+
writeln!(writer).unwrap();
112+
writeln!(writer, "# End of hosts file").unwrap();
113+
114+
writer.flush().unwrap();
115+
drop(writer);
116+
data
117+
}
118+
119+
/// Setup function that prepares the environment with a large hosts file
120+
///
121+
/// Uses NSS_WRAPPER if available to redirect /etc/hosts resolution
122+
/// Falls back to testing with the system's real /etc/hosts
123+
fn setup_hosts_environment(entries: usize) -> Option<HostsEnvironment> {
124+
let temp_dir = tempfile::tempdir().ok()?;
125+
let hosts_file = temp_dir.path().join("hosts");
126+
127+
// Generate realistic hosts file
128+
let hosts_data = generate_realistic_hosts_file(entries);
129+
std::fs::write(&hosts_file, &hosts_data).ok()?;
130+
131+
Some(HostsEnvironment {
132+
_temp_dir: temp_dir,
133+
hosts_file,
134+
})
135+
}
136+
137+
/// Environment guard for hosts file benchmarks
138+
struct HostsEnvironment {
139+
_temp_dir: tempfile::TempDir,
140+
hosts_file: PathBuf,
141+
}
142+
143+
/// Benchmark hostname -i (IP address resolution) with large hosts files
144+
///
145+
/// This tests the DNS lookup performance which scans /etc/hosts linearly
146+
/// when resolving the hostname to IP addresses.
147+
///
148+
/// # Important
149+
/// To test with the generated large hosts file, run with NSS_WRAPPER:
150+
/// `LD_PRELOAD=/usr/lib/libnss_wrapper.so NSS_WRAPPER_HOSTS=/path/to/hosts cargo bench`
151+
#[divan::bench(
152+
args = [1_000, 10_000, 100_000],
153+
name = "hostname_ip_lookup"
154+
)]
155+
fn bench_hostname_ip(bencher: Bencher, entries: usize) {
156+
// Setup hosts environment
157+
let _env = match setup_hosts_environment(entries) {
158+
Some(e) => e,
159+
None => {
160+
// Skip benchmark if we can't setup environment
161+
return;
162+
}
163+
};
164+
165+
// Check if NSS_WRAPPER is available
166+
let nss_wrapper_env = std::env::var("LD_PRELOAD").unwrap_or_default();
167+
let has_nss_wrapper = nss_wrapper_env.contains("nss_wrapper");
168+
169+
if has_nss_wrapper {
170+
unsafe {
171+
std::env::set_var("NSS_WRAPPER_HOSTS", &_env.hosts_file);
172+
}
173+
}
174+
175+
bencher.bench(|| {
176+
// Benchmark the IP address lookup
177+
// Note: Without NSS_WRAPPER, this uses the real /etc/hosts
178+
let result = black_box(run_util_function(uumain, &["-i"]));
179+
// Ensure the command succeeded (0 = success)
180+
assert_eq!(result, 0, "hostname -i should succeed");
181+
});
182+
183+
// Cleanup environment
184+
if has_nss_wrapper {
185+
unsafe {
186+
std::env::remove_var("NSS_WRAPPER_HOSTS");
187+
}
188+
}
189+
}
190+
191+
/// Benchmark basic hostname display (no DNS lookup)
192+
///
193+
/// This serves as a baseline to compare against DNS resolution benchmarks
194+
#[divan::bench(name = "hostname_basic")]
195+
fn bench_hostname_basic(bencher: Bencher) {
196+
bencher.bench(|| {
197+
let result = black_box(run_util_function(uumain, &[]));
198+
assert_eq!(result, 0, "hostname should succeed");
199+
});
200+
}
201+
202+
/// Benchmark hostname -s (short hostname)
203+
#[divan::bench(name = "hostname_short")]
204+
fn bench_hostname_short(bencher: Bencher) {
205+
bencher.bench(|| {
206+
let result = black_box(run_util_function(uumain, &["-s"]));
207+
assert_eq!(result, 0, "hostname -s should succeed");
208+
});
209+
}
210+
211+
/// Benchmark hostname -f (FQDN)
212+
#[divan::bench(name = "hostname_fqdn")]
213+
fn bench_hostname_fqdn(bencher: Bencher) {
214+
bencher.bench(|| {
215+
let result = black_box(run_util_function(uumain, &["-f"]));
216+
assert_eq!(result, 0, "hostname -f should succeed");
217+
});
218+
}
219+
220+
/// Benchmark hostname -d (domain)
221+
#[divan::bench(name = "hostname_domain")]
222+
fn bench_hostname_domain(bencher: Bencher) {
223+
bencher.bench(|| {
224+
let result = black_box(run_util_function(uumain, &["-d"]));
225+
assert_eq!(result, 0, "hostname -d should succeed");
226+
});
227+
}
228+
229+
/// Benchmark direct dns-lookup crate performance (BSD path)
230+
///
231+
/// This benchmarks the underlying DNS lookup without the full hostname command overhead
232+
#[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
233+
#[divan::bench(
234+
args = [1_000, 10_000, 100_000],
235+
name = "dns_lookup_direct"
236+
)]
237+
fn bench_dns_lookup_direct(bencher: Bencher, _entries: usize) {
238+
use dns_lookup::lookup_host;
239+
240+
// Get current hostname for lookup
241+
let hostname = hostname::get()
242+
.unwrap_or_default()
243+
.to_string_lossy()
244+
.into_owned();
245+
246+
bencher.bench(|| {
247+
let result = lookup_host(&hostname);
248+
let _ = black_box(result);
249+
});
250+
}
251+
252+
/// Benchmark std::net::ToSocketAddrs performance (Linux/macOS path)
253+
///
254+
/// This benchmarks the underlying DNS lookup without the full hostname command overhead
255+
#[cfg(not(any(target_os = "freebsd", target_os = "openbsd")))]
256+
#[divan::bench(
257+
args = [1_000, 10_000, 100_000],
258+
name = "socket_addrs_direct"
259+
)]
260+
fn bench_socket_addrs_direct(bencher: Bencher, _entries: usize) {
261+
use std::net::ToSocketAddrs;
262+
263+
// Get current hostname for lookup
264+
let hostname = hostname::get()
265+
.unwrap_or_default()
266+
.to_string_lossy()
267+
.into_owned();
268+
let hostname_with_port = format!("{}:1", hostname);
269+
270+
bencher.bench(|| {
271+
let result: Result<Vec<_>, _> = hostname_with_port.to_socket_addrs().map(|i| i.collect());
272+
let _ = black_box(result);
273+
});
274+
}
275+
276+
fn main() {
277+
divan::main();
278+
}

0 commit comments

Comments
 (0)