use parking_lot::RwLock;
use std::collections::HashMap;
use std::fmt;
use std::future::Future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant, SystemTime};
pub struct Profiler {
start_time: Instant,
queries: Vec<ProfiledQuery>,
is_active: bool,
}
#[derive(Debug, Clone)]
pub struct ProfiledQuery {
pub sql: String,
pub table: Option<String>,
pub duration: Duration,
pub rows: Option<u64>,
pub cached: bool,
pub operation: String,
pub timestamp: SystemTime,
}
impl ProfiledQuery {
pub fn new(sql: impl Into<String>, duration: Duration) -> Self {
let sql = sql.into();
let operation = detect_operation(&sql);
Self {
sql,
table: None,
duration,
rows: None,
cached: false,
operation,
timestamp: SystemTime::now(),
}
}
pub fn with_table(mut self, table: impl Into<String>) -> Self {
self.table = Some(table.into());
self
}
pub fn with_rows(mut self, rows: u64) -> Self {
self.rows = Some(rows);
self
}
pub fn cached(mut self) -> Self {
self.cached = true;
self
}
}
impl Profiler {
pub fn start() -> Self {
Self {
start_time: Instant::now(),
queries: Vec::new(),
is_active: true,
}
}
pub fn record(&mut self, sql: impl Into<String>, duration: Duration) {
if self.is_active {
self.queries.push(ProfiledQuery::new(sql, duration));
}
}
pub fn record_full(&mut self, query: ProfiledQuery) {
if self.is_active {
self.queries.push(query);
}
}
pub fn stop(mut self) -> ProfileReport {
self.is_active = false;
let total_duration = self.start_time.elapsed();
ProfileReport::from_queries(self.queries, total_duration)
}
pub fn query_count(&self) -> usize {
self.queries.len()
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
}
#[derive(Debug, Clone)]
pub struct ProfileReport {
pub total_duration: Duration,
pub query_duration: Duration,
pub queries: Vec<ProfiledQuery>,
pub operations: HashMap<String, u64>,
pub slowest: Vec<ProfiledQuery>,
pub tables: HashMap<String, u64>,
}
impl ProfileReport {
fn from_queries(queries: Vec<ProfiledQuery>, total_duration: Duration) -> Self {
let query_duration: Duration = queries.iter().map(|q| q.duration).sum();
let mut operations: HashMap<String, u64> = HashMap::new();
let mut tables: HashMap<String, u64> = HashMap::new();
for query in &queries {
*operations.entry(query.operation.clone()).or_insert(0) += 1;
if let Some(ref table) = query.table {
*tables.entry(table.clone()).or_insert(0) += 1;
}
}
let mut slowest: Vec<ProfiledQuery> = queries.clone();
slowest.sort_by(|a, b| b.duration.cmp(&a.duration));
slowest.truncate(10);
Self {
total_duration,
query_duration,
queries,
operations,
slowest,
tables,
}
}
pub fn query_count(&self) -> usize {
self.queries.len()
}
pub fn avg_query_time(&self) -> Duration {
if self.queries.is_empty() {
Duration::ZERO
} else {
self.query_duration / self.queries.len() as u32
}
}
pub fn query_time_percentage(&self) -> f64 {
if self.total_duration.as_nanos() == 0 {
0.0
} else {
(self.query_duration.as_nanos() as f64 / self.total_duration.as_nanos() as f64) * 100.0
}
}
pub fn queries_slower_than(&self, threshold: Duration) -> Vec<&ProfiledQuery> {
self.queries
.iter()
.filter(|q| q.duration >= threshold)
.collect()
}
pub fn suggestions(&self) -> Vec<String> {
let mut suggestions = Vec::new();
let mut table_counts: HashMap<&str, usize> = HashMap::new();
for query in &self.queries {
if let Some(ref table) = query.table {
*table_counts.entry(table.as_str()).or_insert(0) += 1;
}
}
for (table, count) in table_counts {
if count > 10 {
suggestions.push(format!(
"Potential N+1 query detected: {} queries on '{}' table. Consider using eager loading with `.with(\"{}\")` or batch queries.",
count, table, table
));
}
}
let slow_count = self
.queries
.iter()
.filter(|q| q.duration > Duration::from_millis(100))
.count();
if slow_count > 0 {
suggestions.push(format!(
"{} slow queries detected (>100ms). Review these queries and consider adding indexes.",
slow_count
));
}
let select_star = self
.queries
.iter()
.filter(|q| q.sql.contains("SELECT *") || q.sql.contains("select *"))
.count();
if select_star > 5 {
suggestions.push(
"Multiple SELECT * queries detected. Use `.select([\"col1\", \"col2\"])` to fetch only needed columns.".to_string()
);
}
if self.query_time_percentage() > 50.0 {
suggestions.push(
"More than 50% of time spent in database queries. Consider caching frequently accessed data.".to_string()
);
}
suggestions
}
}
impl fmt::Display for ProfileReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"╔═══════════════════════════════════════════════════════════╗"
)?;
writeln!(
f,
"║ TIDEORM PERFORMANCE PROFILE REPORT ║"
)?;
writeln!(
f,
"╠═══════════════════════════════════════════════════════════╣"
)?;
writeln!(
f,
"║ Total Duration: {:>10}ms ║",
self.total_duration.as_millis()
)?;
writeln!(
f,
"║ Query Duration: {:>10}ms ({:.1}% of total) ║",
self.query_duration.as_millis(),
self.query_time_percentage()
)?;
writeln!(
f,
"║ Total Queries: {:>10} ║",
self.query_count()
)?;
writeln!(
f,
"║ Avg Query Time: {:>10.2}ms ║",
self.avg_query_time().as_secs_f64() * 1000.0
)?;
writeln!(
f,
"╠═══════════════════════════════════════════════════════════╣"
)?;
writeln!(
f,
"║ Operations: ║"
)?;
for (op, count) in &self.operations {
writeln!(
f,
"║ {:10}: {:>6} ║",
op, count
)?;
}
if !self.slowest.is_empty() {
writeln!(
f,
"╠═══════════════════════════════════════════════════════════╣"
)?;
writeln!(
f,
"║ Slowest Queries: ║"
)?;
for (i, query) in self.slowest.iter().take(5).enumerate() {
let sql_preview: String = query.sql.chars().take(40).collect();
writeln!(
f,
"║ {}. {:>6}ms {}... ║",
i + 1,
query.duration.as_millis(),
sql_preview.replace('\n', " ")
)?;
}
}
let suggestions = self.suggestions();
if !suggestions.is_empty() {
writeln!(
f,
"╠═══════════════════════════════════════════════════════════╣"
)?;
writeln!(
f,
"║ 💡 Optimization Suggestions: ║"
)?;
for suggestion in suggestions.iter().take(3) {
let wrapped = textwrap_simple(suggestion, 55);
for line in wrapped {
writeln!(f, "║ {}{}║", line, " ".repeat(55 - line.len().min(55)))?;
}
}
}
writeln!(
f,
"╚═══════════════════════════════════════════════════════════╝"
)
}
}
static GLOBAL_PROFILING_ENABLED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Copy, Default)]
struct GlobalStatsState {
total_queries: u64,
total_time_ns: u64,
slow_queries: u64,
}
static GLOBAL_STATS: RwLock<GlobalStatsState> = RwLock::new(GlobalStatsState {
total_queries: 0,
total_time_ns: 0,
slow_queries: 0,
});
static GLOBAL_SLOW_THRESHOLD_MS: RwLock<u64> = RwLock::new(100);
pub struct GlobalProfiler;
impl GlobalProfiler {
pub fn enable() {
GLOBAL_PROFILING_ENABLED.store(true, Ordering::SeqCst);
}
pub fn disable() {
GLOBAL_PROFILING_ENABLED.store(false, Ordering::SeqCst);
}
pub fn is_enabled() -> bool {
GLOBAL_PROFILING_ENABLED.load(Ordering::SeqCst)
}
pub fn record(duration: Duration) {
if Self::is_enabled() {
let threshold_ms = *GLOBAL_SLOW_THRESHOLD_MS.read();
let mut stats = GLOBAL_STATS.write();
stats.total_queries += 1;
stats.total_time_ns += duration.as_nanos() as u64;
if duration.as_millis() as u64 >= threshold_ms {
stats.slow_queries += 1;
}
}
}
pub fn stats() -> GlobalStats {
let stats = *GLOBAL_STATS.read();
GlobalStats {
total_queries: stats.total_queries,
total_time_ns: stats.total_time_ns,
slow_queries: stats.slow_queries,
slow_threshold_ms: *GLOBAL_SLOW_THRESHOLD_MS.read(),
}
}
pub fn reset() {
*GLOBAL_STATS.write() = GlobalStatsState::default();
}
pub fn set_slow_threshold(ms: u64) {
*GLOBAL_SLOW_THRESHOLD_MS.write() = ms;
}
}
#[doc(hidden)]
pub async fn __profile_future<T, F>(future: F) -> T
where
F: Future<Output = T>,
{
if !GlobalProfiler::is_enabled() {
return future.await;
}
let start = Instant::now();
let output = future.await;
GlobalProfiler::record(start.elapsed());
output
}
#[derive(Debug, Clone, Copy)]
pub struct GlobalStats {
pub total_queries: u64,
pub total_time_ns: u64,
pub slow_queries: u64,
pub slow_threshold_ms: u64,
}
impl GlobalStats {
pub fn total_time(&self) -> Duration {
Duration::from_nanos(self.total_time_ns)
}
pub fn avg_query_time(&self) -> Duration {
if self.total_queries == 0 {
Duration::ZERO
} else {
Duration::from_nanos(self.total_time_ns / self.total_queries)
}
}
pub fn slow_percentage(&self) -> f64 {
if self.total_queries == 0 {
0.0
} else {
(self.slow_queries as f64 / self.total_queries as f64) * 100.0
}
}
}
impl fmt::Display for GlobalStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "TideORM Global Statistics:")?;
writeln!(f, " Total Queries: {}", self.total_queries)?;
writeln!(
f,
" Total Time: {:.2}ms",
self.total_time().as_secs_f64() * 1000.0
)?;
writeln!(
f,
" Avg Query Time: {:.2}ms",
self.avg_query_time().as_secs_f64() * 1000.0
)?;
writeln!(
f,
" Slow Queries: {} ({:.1}%)",
self.slow_queries,
self.slow_percentage()
)?;
write!(f, " Slow Threshold: {}ms", self.slow_threshold_ms)
}
}
pub struct QueryAnalyzer;
impl QueryAnalyzer {
pub fn analyze(sql: &str) -> Vec<QuerySuggestion> {
let mut suggestions = Vec::new();
let sql_upper = sql.to_uppercase();
if sql_upper.contains("SELECT *") {
suggestions.push(QuerySuggestion::new(
SuggestionLevel::Warning,
"Avoid SELECT *",
"Specify columns explicitly to reduce data transfer and improve performance.",
"Change to: .select([\"id\", \"name\", \"email\"])",
));
}
if (sql_upper.starts_with("UPDATE") || sql_upper.starts_with("DELETE"))
&& !sql_upper.contains("WHERE")
{
suggestions.push(QuerySuggestion::new(
SuggestionLevel::Critical,
"Missing WHERE clause",
"UPDATE/DELETE without WHERE will affect all rows!",
"Add a WHERE condition: .where_eq(\"id\", value)",
));
}
if sql_upper.contains("LIKE '%") || sql_upper.contains("LIKE '%") {
suggestions.push(QuerySuggestion::new(
SuggestionLevel::Warning,
"Leading wildcard in LIKE",
"LIKE '%pattern' cannot use indexes and will be slow on large tables.",
"Consider using full-text search or restructure the query.",
));
}
if sql_upper.contains(" OR ") {
suggestions.push(QuerySuggestion::new(
SuggestionLevel::Info,
"OR conditions detected",
"OR conditions may prevent index usage. Consider using UNION or restructuring.",
"Use .where_in(\"column\", values) instead of multiple OR conditions.",
));
}
if sql_upper.contains("ORDER BY") && !sql_upper.contains("LIMIT") {
suggestions.push(QuerySuggestion::new(
SuggestionLevel::Info,
"ORDER BY without LIMIT",
"Ordering all rows can be expensive. Consider adding a LIMIT.",
"Add .limit(100) to restrict result set.",
));
}
if sql_upper.contains("NOT IN") {
suggestions.push(QuerySuggestion::new(
SuggestionLevel::Info,
"NOT IN detected",
"NOT IN may have unexpected NULL handling. Consider using NOT EXISTS.",
"Use .where_not_exists(subquery) for more predictable behavior.",
));
}
let function_patterns = ["LOWER(", "UPPER(", "DATE(", "YEAR(", "MONTH("];
for pattern in function_patterns {
if sql_upper.contains(pattern) {
suggestions.push(QuerySuggestion::new(
SuggestionLevel::Warning,
"Function in WHERE clause",
"Functions in WHERE prevent index usage. Store computed values or use expression indexes.",
"Create a computed column or expression index."
));
break;
}
}
if sql_upper.contains("= '") && (sql_upper.contains("_id =") || sql_upper.contains("id ="))
{
suggestions.push(QuerySuggestion::new(
SuggestionLevel::Info,
"Possible type mismatch",
"Comparing numeric ID with string may cause implicit conversion.",
"Ensure parameter types match column types.",
));
}
suggestions
}
pub fn estimate_complexity(sql: &str) -> QueryComplexity {
let sql_upper = sql.to_uppercase();
let mut score = 0;
if sql_upper.starts_with("SELECT") {
score += 1;
} else if sql_upper.starts_with("INSERT") {
score += 2;
} else if sql_upper.starts_with("UPDATE") || sql_upper.starts_with("DELETE") {
score += 3;
}
score += sql_upper.matches("JOIN").count() * 2;
score += sql_upper.matches("SELECT").count().saturating_sub(1) * 3;
let agg_functions = ["COUNT(", "SUM(", "AVG(", "MAX(", "MIN(", "GROUP BY"];
for func in agg_functions {
if sql_upper.contains(func) {
score += 1;
}
}
if sql_upper.contains("ORDER BY") {
score += 1;
}
if sql_upper.contains("DISTINCT") {
score += 1;
}
QueryComplexity::from_score(score)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuggestionLevel {
Info,
Warning,
Critical,
}
impl SuggestionLevel {
pub fn emoji(&self) -> &'static str {
match self {
Self::Info => "ℹ️",
Self::Warning => "⚠️",
Self::Critical => "🚨",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Info => "INFO",
Self::Warning => "WARNING",
Self::Critical => "CRITICAL",
}
}
}
#[derive(Debug, Clone)]
pub struct QuerySuggestion {
pub level: SuggestionLevel,
pub title: String,
pub explanation: String,
pub suggestion: String,
}
impl QuerySuggestion {
pub fn new(
level: SuggestionLevel,
title: impl Into<String>,
explanation: impl Into<String>,
suggestion: impl Into<String>,
) -> Self {
Self {
level,
title: title.into(),
explanation: explanation.into(),
suggestion: suggestion.into(),
}
}
}
impl fmt::Display for QuerySuggestion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"{} [{}] {}",
self.level.emoji(),
self.level.label(),
self.title
)?;
writeln!(f, " {}", self.explanation)?;
write!(f, " 💡 {}", self.suggestion)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryComplexity {
Simple,
Moderate,
Complex,
VeryComplex,
}
impl QueryComplexity {
fn from_score(score: usize) -> Self {
match score {
0..=2 => Self::Simple,
3..=5 => Self::Moderate,
6..=10 => Self::Complex,
_ => Self::VeryComplex,
}
}
pub fn description(&self) -> &'static str {
match self {
Self::Simple => "Simple query, should be fast",
Self::Moderate => "Moderate complexity, ensure proper indexes",
Self::Complex => "Complex query, may benefit from optimization",
Self::VeryComplex => "Very complex query, review for N+1 issues and consider splitting",
}
}
}
impl fmt::Display for QueryComplexity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stars = match self {
Self::Simple => "★☆☆☆",
Self::Moderate => "★★☆☆",
Self::Complex => "★★★☆",
Self::VeryComplex => "★★★★",
};
write!(f, "{} {}", stars, self.description())
}
}
fn detect_operation(sql: &str) -> String {
let sql_upper = sql.trim().to_uppercase();
if sql_upper.starts_with("SELECT") {
"SELECT".to_string()
} else if sql_upper.starts_with("INSERT") {
"INSERT".to_string()
} else if sql_upper.starts_with("UPDATE") {
"UPDATE".to_string()
} else if sql_upper.starts_with("DELETE") {
"DELETE".to_string()
} else {
"OTHER".to_string()
}
}
fn textwrap_simple(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
if current_line.is_empty() {
current_line = word.to_string();
} else if current_line.len() + 1 + word.len() <= width {
current_line.push(' ');
current_line.push_str(word);
} else {
lines.push(current_line);
current_line = word.to_string();
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
if lines.is_empty() {
lines.push(String::new());
}
lines
}
#[cfg(test)]
#[path = "testing/profiling_tests.rs"]
mod tests;