stat page and cache working
This commit is contained in:
@@ -25,6 +25,8 @@ enum Commands {
|
||||
},
|
||||
/// Watch for changes in the posts directory
|
||||
Watch,
|
||||
/// Show Rust parser statistics
|
||||
Rsparseinfo,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -65,5 +67,8 @@ fn main() {
|
||||
std::thread::sleep(std::time::Duration::from_secs(60));
|
||||
}
|
||||
}
|
||||
Commands::Rsparseinfo => {
|
||||
println!("{}", markdown::rsparseinfo());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,14 @@ use ammonia::clean;
|
||||
use slug::slugify;
|
||||
use notify::{RecursiveMode, RecommendedWatcher, Watcher, Config};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
use syntect::highlighting::{ThemeSet, Style};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
use syntect::html::{highlighted_html_for_string, IncludeBackground};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use serde_json;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, serde::Serialize)]
|
||||
pub struct PostFrontmatter {
|
||||
@@ -37,6 +40,19 @@ pub struct Post {
|
||||
pub author: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, Default)]
|
||||
pub struct PostStats {
|
||||
pub slug: String,
|
||||
pub cache_hits: u64,
|
||||
pub cache_misses: u64,
|
||||
pub last_interpret_time_ms: u128,
|
||||
pub last_compile_time_ms: u128,
|
||||
}
|
||||
|
||||
static POST_CACHE: Lazy<RwLock<HashMap<String, Post>>> = Lazy::new(|| RwLock::new(HashMap::new()));
|
||||
static ALL_POSTS_CACHE: Lazy<RwLock<Option<Vec<Post>>>> = Lazy::new(|| RwLock::new(None));
|
||||
static POST_STATS: Lazy<RwLock<HashMap<String, PostStats>>> = Lazy::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
fn get_posts_directory() -> PathBuf {
|
||||
let candidates = [
|
||||
"./posts",
|
||||
@@ -111,7 +127,27 @@ static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
|
||||
builder
|
||||
});
|
||||
|
||||
pub fn rsparseinfo() -> String {
|
||||
let stats = POST_STATS.read().unwrap();
|
||||
serde_json::to_string(&stats.values().collect::<Vec<_>>()).unwrap_or_else(|_| "[]".to_string())
|
||||
}
|
||||
|
||||
pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>> {
|
||||
let start = Instant::now();
|
||||
let mut stats = POST_STATS.write().unwrap();
|
||||
let entry = stats.entry(slug.to_string()).or_insert_with(|| PostStats {
|
||||
slug: slug.to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
// Try cache first
|
||||
if let Some(post) = POST_CACHE.read().unwrap().get(slug).cloned() {
|
||||
entry.cache_hits += 1;
|
||||
entry.last_interpret_time_ms = 0;
|
||||
entry.last_compile_time_ms = 0;
|
||||
return Ok(post);
|
||||
}
|
||||
entry.cache_misses += 1;
|
||||
drop(stats); // Release lock before heavy work
|
||||
let posts_dir = get_posts_directory();
|
||||
let file_path = posts_dir.join(format!("{}.md", slug));
|
||||
let file_content = fs::read_to_string(&file_path)?;
|
||||
@@ -202,7 +238,9 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
|
||||
let sanitized_html = AMMONIA.clean(&html_output).to_string();
|
||||
|
||||
Ok(Post {
|
||||
let interpret_time = start.elapsed();
|
||||
let compile_start = Instant::now();
|
||||
let post = Post {
|
||||
slug: slug.to_string(),
|
||||
title: front.title,
|
||||
date: front.date,
|
||||
@@ -211,10 +249,26 @@ pub fn get_post_by_slug(slug: &str) -> Result<Post, Box<dyn std::error::Error>>
|
||||
content: sanitized_html,
|
||||
created_at: created_at.to_rfc3339(),
|
||||
author: std::env::var("BLOG_OWNER").unwrap_or_else(|_| "Anonymous".to_string()),
|
||||
})
|
||||
};
|
||||
let compile_time = compile_start.elapsed();
|
||||
// Insert into cache
|
||||
POST_CACHE.write().unwrap().insert(slug.to_string(), post.clone());
|
||||
// Update stats
|
||||
let mut stats = POST_STATS.write().unwrap();
|
||||
let entry = stats.entry(slug.to_string()).or_insert_with(|| PostStats {
|
||||
slug: slug.to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
entry.last_interpret_time_ms = interpret_time.as_millis();
|
||||
entry.last_compile_time_ms = compile_time.as_millis();
|
||||
Ok(post)
|
||||
}
|
||||
|
||||
pub fn get_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Error>> {
|
||||
// Try cache first
|
||||
if let Some(posts) = ALL_POSTS_CACHE.read().unwrap().clone() {
|
||||
return Ok(posts);
|
||||
}
|
||||
let posts_dir = get_posts_directory();
|
||||
let mut posts = Vec::new();
|
||||
for entry in fs::read_dir(posts_dir)? {
|
||||
@@ -228,6 +282,8 @@ pub fn get_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
posts.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
// Cache the result
|
||||
*ALL_POSTS_CACHE.write().unwrap() = Some(posts.clone());
|
||||
Ok(posts)
|
||||
}
|
||||
|
||||
@@ -240,11 +296,13 @@ pub fn watch_posts<F: Fn() + Send + 'static>(on_change: F) -> notify::Result<Rec
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
|
||||
watcher.watch(get_posts_directory().as_path(), RecursiveMode::Recursive)?;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(_event) => {
|
||||
// Invalidate caches on any change
|
||||
POST_CACHE.write().unwrap().clear();
|
||||
*ALL_POSTS_CACHE.write().unwrap() = None;
|
||||
on_change();
|
||||
},
|
||||
Err(e) => {
|
||||
@@ -254,6 +312,5 @@ pub fn watch_posts<F: Fn() + Send + 'static>(on_change: F) -> notify::Result<Rec
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(watcher)
|
||||
}
|
||||
Reference in New Issue
Block a user