perf: reduce allocations and deserialization in handlers and models

This commit is contained in:
NetworkCats
2026-03-02 23:40:06 +08:00
parent da2f847faf
commit 0834a05c84
7 changed files with 150 additions and 113 deletions
Generated
-48
View File
@@ -582,15 +582,6 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
@@ -679,29 +670,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -846,15 +814,6 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
@@ -997,12 +956,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
@@ -1222,7 +1175,6 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2024"
[dependencies]
axum = "0.8.8"
tokio = { version = "1.49.0", features = ["full"] }
tokio = { version = "1.49.0", features = ["rt-multi-thread", "macros", "net", "signal", "time", "fs", "io-util"] }
askama = "0.15.4"
askama_web = { version = "0.15.1", features = ["axum-0.8"] }
maxminddb = "0.27.3"
+69 -1
View File
@@ -4,8 +4,11 @@ use std::sync::Arc;
use arc_swap::ArcSwap;
use maxminddb::Reader;
use serde::Deserialize;
use crate::models::{from_mmdb_record, IpInfo, MmdbRecord};
use crate::models::{
from_mmdb_record, IpInfo, MmdbAsn, MmdbCity, MmdbCountry, MmdbProxy, MmdbRecord, ProxyInfo,
};
pub type DbReader = Reader<Vec<u8>>;
pub type SharedDb = Arc<ArcSwap<DbReader>>;
@@ -23,3 +26,68 @@ pub fn lookup(db: &DbReader, ip: IpAddr) -> Option<IpInfo> {
let record: MmdbRecord<'_> = result.decode().ok()??;
Some(from_mmdb_record(ip, &record))
}
// Lightweight deserialization structs for single-field endpoints.
// These avoid deserializing the full MMDB record when only one field is needed.
#[derive(Deserialize, Default)]
struct ProxyOnly {
#[serde(default)]
proxy: MmdbProxy,
}
#[derive(Deserialize, Default)]
struct AsnOnly<'a> {
#[serde(borrow, default)]
asn: MmdbAsn<'a>,
}
#[derive(Deserialize, Default)]
struct CountryOnly<'a> {
#[serde(borrow, default)]
country: MmdbCountry<'a>,
}
#[derive(Deserialize, Default)]
struct CityOnly<'a> {
#[serde(borrow, default)]
city: MmdbCity<'a>,
}
pub fn lookup_proxy(db: &DbReader, ip: IpAddr) -> Option<ProxyInfo> {
let result = db.lookup(ip).ok()?;
let record: ProxyOnly = result.decode().ok()??;
Some(ProxyInfo {
is_proxy: record.proxy.is_proxy,
is_vpn: record.proxy.is_vpn,
is_tor: record.proxy.is_tor,
is_hosting: record.proxy.is_hosting,
is_cdn: record.proxy.is_cdn,
is_school: record.proxy.is_school,
is_anonymous: record.proxy.is_anonymous,
})
}
pub fn lookup_asn_number(db: &DbReader, ip: IpAddr) -> Option<u32> {
let result = db.lookup(ip).ok()?;
let record: AsnOnly<'_> = result.decode().ok()??;
record.asn.autonomous_system_number
}
pub fn lookup_asn_org(db: &DbReader, ip: IpAddr) -> Option<String> {
let result = db.lookup(ip).ok()?;
let record: AsnOnly<'_> = result.decode().ok()??;
record.asn.autonomous_system_organization.map(str::to_owned)
}
pub fn lookup_country_name(db: &DbReader, ip: IpAddr) -> Option<String> {
let result = db.lookup(ip).ok()?;
let record: CountryOnly<'_> = result.decode().ok()??;
record.country.names.english.map(str::to_owned)
}
pub fn lookup_city_name(db: &DbReader, ip: IpAddr) -> Option<String> {
let result = db.lookup(ip).ok()?;
let record: CityOnly<'_> = result.decode().ok()??;
record.city.names.english.map(str::to_owned)
}
+36 -25
View File
@@ -1,4 +1,5 @@
use std::net::IpAddr;
use std::sync::Arc;
use askama::Template;
use axum::extract::{Query, State};
@@ -18,7 +19,7 @@ pub struct IpQuery {
#[derive(Clone)]
pub struct AppState {
pub db: SharedDb,
pub site_domain: String,
pub site_domain: Arc<str>,
}
#[derive(Template)]
@@ -26,7 +27,7 @@ pub struct AppState {
struct IndexTemplate {
ip: String,
query: String,
site_domain: String,
site_domain: Arc<str>,
asn: String,
org: String,
country: String,
@@ -75,21 +76,21 @@ fn format_asn(info: &IpInfo) -> String {
.unwrap_or_default()
}
fn format_org(info: &IpInfo) -> String {
fn format_org(info: &IpInfo) -> &str {
info.asn
.as_ref()
.and_then(|a| a.autonomous_system_organization.clone())
.and_then(|a| a.autonomous_system_organization.as_deref())
.unwrap_or_default()
}
fn format_country(info: &IpInfo) -> String {
fn format_country(info: &IpInfo) -> &str {
info.country
.as_ref()
.map(|c| get_en_name(&c.names))
.unwrap_or_default()
}
fn format_city(info: &IpInfo) -> String {
fn format_city(info: &IpInfo) -> &str {
info.city
.as_ref()
.map(|c| get_en_name(&c.names))
@@ -125,9 +126,9 @@ pub async fn root(
query: query.ip.unwrap_or_default(),
site_domain: state.site_domain.clone(),
asn: format_asn(&info),
org: format_org(&info),
country: format_country(&info),
city: format_city(&info),
org: format_org(&info).to_owned(),
country: format_country(&info).to_owned(),
city: format_city(&info).to_owned(),
is_proxy: info.proxy.is_proxy,
is_vpn: info.proxy.is_vpn,
is_hosting: info.proxy.is_hosting,
@@ -165,8 +166,11 @@ pub async fn asn_handler(
Query(query): Query<IpQuery>,
) -> Result<String, AppError> {
let ip = resolve_ip(&headers, &query)?;
let info = lookup_ip(&state.db, ip)?;
Ok(format!("{}\n", format_asn(&info)))
let reader = state.db.load();
let text = db::lookup_asn_number(&reader, ip)
.map(|n| format!("AS{n}"))
.unwrap_or_default();
Ok(format!("{text}\n"))
}
pub async fn org_handler(
@@ -175,8 +179,9 @@ pub async fn org_handler(
Query(query): Query<IpQuery>,
) -> Result<String, AppError> {
let ip = resolve_ip(&headers, &query)?;
let info = lookup_ip(&state.db, ip)?;
Ok(format!("{}\n", format_org(&info)))
let reader = state.db.load();
let org = db::lookup_asn_org(&reader, ip).unwrap_or_default();
Ok(format!("{org}\n"))
}
pub async fn country_handler(
@@ -185,8 +190,9 @@ pub async fn country_handler(
Query(query): Query<IpQuery>,
) -> Result<String, AppError> {
let ip = resolve_ip(&headers, &query)?;
let info = lookup_ip(&state.db, ip)?;
Ok(format!("{}\n", format_country(&info)))
let reader = state.db.load();
let country = db::lookup_country_name(&reader, ip).unwrap_or_default();
Ok(format!("{country}\n"))
}
pub async fn city_handler(
@@ -195,8 +201,9 @@ pub async fn city_handler(
Query(query): Query<IpQuery>,
) -> Result<String, AppError> {
let ip = resolve_ip(&headers, &query)?;
let info = lookup_ip(&state.db, ip)?;
Ok(format!("{}\n", format_city(&info)))
let reader = state.db.load();
let city = db::lookup_city_name(&reader, ip).unwrap_or_default();
Ok(format!("{city}\n"))
}
pub async fn proxy_handler(
@@ -205,8 +212,9 @@ pub async fn proxy_handler(
Query(query): Query<IpQuery>,
) -> Result<String, AppError> {
let ip = resolve_ip(&headers, &query)?;
let info = lookup_ip(&state.db, ip)?;
Ok(format!("{}\n", info.proxy.is_proxy))
let reader = state.db.load();
let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?;
Ok(format!("{}\n", proxy.is_proxy))
}
pub async fn vpn_handler(
@@ -215,8 +223,9 @@ pub async fn vpn_handler(
Query(query): Query<IpQuery>,
) -> Result<String, AppError> {
let ip = resolve_ip(&headers, &query)?;
let info = lookup_ip(&state.db, ip)?;
Ok(format!("{}\n", info.proxy.is_vpn))
let reader = state.db.load();
let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?;
Ok(format!("{}\n", proxy.is_vpn))
}
pub async fn hosting_handler(
@@ -225,8 +234,9 @@ pub async fn hosting_handler(
Query(query): Query<IpQuery>,
) -> Result<String, AppError> {
let ip = resolve_ip(&headers, &query)?;
let info = lookup_ip(&state.db, ip)?;
Ok(format!("{}\n", info.proxy.is_hosting))
let reader = state.db.load();
let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?;
Ok(format!("{}\n", proxy.is_hosting))
}
pub async fn tor_handler(
@@ -235,6 +245,7 @@ pub async fn tor_handler(
Query(query): Query<IpQuery>,
) -> Result<String, AppError> {
let ip = resolve_ip(&headers, &query)?;
let info = lookup_ip(&state.db, ip)?;
Ok(format!("{}\n", info.proxy.is_tor))
let reader = state.db.load();
let proxy = db::lookup_proxy(&reader, ip).ok_or(AppError::DbLookupFailed)?;
Ok(format!("{}\n", proxy.is_tor))
}
+1 -1
View File
@@ -40,7 +40,7 @@ async fn main() {
let state = handlers::AppState {
db: shared_db,
site_domain: config.site_domain,
site_domain: config.site_domain.into(),
};
let app = routes::build_router(state);
+42 -36
View File
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::fmt::Write;
use std::net::IpAddr;
use serde::{Deserialize, Serialize};
@@ -154,12 +154,32 @@ pub struct IpInfo {
pub proxy: ProxyInfo,
}
#[derive(Debug, Serialize)]
pub struct Names {
#[serde(rename = "de", skip_serializing_if = "Option::is_none")]
pub german: Option<String>,
#[serde(rename = "en", skip_serializing_if = "Option::is_none")]
pub english: Option<String>,
#[serde(rename = "es", skip_serializing_if = "Option::is_none")]
pub spanish: Option<String>,
#[serde(rename = "fr", skip_serializing_if = "Option::is_none")]
pub french: Option<String>,
#[serde(rename = "ja", skip_serializing_if = "Option::is_none")]
pub japanese: Option<String>,
#[serde(rename = "pt-BR", skip_serializing_if = "Option::is_none")]
pub brazilian_portuguese: Option<String>,
#[serde(rename = "ru", skip_serializing_if = "Option::is_none")]
pub russian: Option<String>,
#[serde(rename = "zh-CN", skip_serializing_if = "Option::is_none")]
pub simplified_chinese: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CityInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<BTreeMap<String, String>>,
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
@@ -169,7 +189,7 @@ pub struct ContinentInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<BTreeMap<String, String>>,
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
@@ -179,7 +199,7 @@ pub struct CountryInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub iso_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<BTreeMap<String, String>>,
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
@@ -209,7 +229,7 @@ pub struct SubdivisionInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub iso_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<BTreeMap<String, String>>,
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
@@ -235,36 +255,20 @@ pub struct ProxyInfo {
// --- Conversion from MMDB record to API output ---
fn convert_names(names: &MmdbNames<'_>) -> Option<BTreeMap<String, String>> {
fn convert_names(names: &MmdbNames<'_>) -> Option<Names> {
if names.is_empty() {
return None;
}
let mut map = BTreeMap::new();
if let Some(v) = names.german {
map.insert("de".to_string(), v.to_string());
}
if let Some(v) = names.english {
map.insert("en".to_string(), v.to_string());
}
if let Some(v) = names.spanish {
map.insert("es".to_string(), v.to_string());
}
if let Some(v) = names.french {
map.insert("fr".to_string(), v.to_string());
}
if let Some(v) = names.japanese {
map.insert("ja".to_string(), v.to_string());
}
if let Some(v) = names.brazilian_portuguese {
map.insert("pt-BR".to_string(), v.to_string());
}
if let Some(v) = names.russian {
map.insert("ru".to_string(), v.to_string());
}
if let Some(v) = names.simplified_chinese {
map.insert("zh-CN".to_string(), v.to_string());
}
Some(map)
Some(Names {
german: names.german.map(str::to_owned),
english: names.english.map(str::to_owned),
spanish: names.spanish.map(str::to_owned),
french: names.french.map(str::to_owned),
japanese: names.japanese.map(str::to_owned),
brazilian_portuguese: names.brazilian_portuguese.map(str::to_owned),
russian: names.russian.map(str::to_owned),
simplified_chinese: names.simplified_chinese.map(str::to_owned),
})
}
fn has_city(c: &MmdbCity<'_>) -> bool {
@@ -396,8 +400,11 @@ pub fn from_mmdb_record(ip: IpAddr, record: &MmdbRecord<'_>) -> IpInfo {
is_anonymous: record.proxy.is_anonymous,
};
let mut ip_str = String::with_capacity(45);
write!(ip_str, "{ip}").expect("IP address formatting cannot fail");
IpInfo {
ip: ip.to_string(),
ip: ip_str,
city,
continent,
country,
@@ -410,10 +417,9 @@ pub fn from_mmdb_record(ip: IpAddr, record: &MmdbRecord<'_>) -> IpInfo {
}
}
pub fn get_en_name(names: &Option<BTreeMap<String, String>>) -> String {
pub fn get_en_name(names: &Option<Names>) -> &str {
names
.as_ref()
.and_then(|m| m.get("en"))
.cloned()
.and_then(|n| n.english.as_deref())
.unwrap_or_default()
}
+1 -1
View File
@@ -22,7 +22,7 @@ fn build_test_app() -> axum::Router {
let shared_db = db::new_shared(reader);
let state = AppState {
db: shared_db,
site_domain: "test.example.com".to_string(),
site_domain: "test.example.com".into(),
};
build_router(state)
}