fancy caching works now

This commit is contained in:
2025-06-25 19:57:28 +02:00
parent 784dcbf91c
commit e4c6a7e0a8
5 changed files with 226 additions and 36 deletions

View File

@@ -2,6 +2,7 @@ use clap::{Parser, Subcommand};
mod markdown;
use markdown::{get_all_posts, get_post_by_slug, get_posts_by_tag, watch_posts};
use serde_json;
use std::fs;
#[derive(Parser)]
#[command(name = "Markdown Backend")]
@@ -30,6 +31,7 @@ enum Commands {
}
fn main() {
markdown::load_post_cache_from_disk();
let cli = Cli::parse();
match &cli.command {
Commands::List => {
@@ -43,6 +45,7 @@ fn main() {
match get_post_by_slug(slug) {
Ok(post) => {
println!("{}", serde_json::to_string(&post).unwrap());
markdown::save_post_cache_to_disk();
}
Err(e) => {
eprintln!("{}", e);

View File

@@ -30,6 +30,9 @@ use std::sync::RwLock;
use serde_json;
use sysinfo::{System, Pid, RefreshKind, CpuRefreshKind, ProcessRefreshKind};
const POSTS_CACHE_PATH: &str = "./cache/posts_cache.json";
const POST_STATS_PATH: &str = "./cache/post_stats.json";
#[derive(Debug, Deserialize, Clone, serde::Serialize)]
pub struct PostFrontmatter {
pub title: String,
@@ -38,7 +41,7 @@ pub struct PostFrontmatter {
pub summary: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Post {
pub slug: String,
pub title: String,
@@ -50,7 +53,7 @@ pub struct Post {
pub author: String,
}
#[derive(Debug, Clone, serde::Serialize, Default)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct PostStats {
pub slug: String,
pub cache_hits: u64,
@@ -306,6 +309,8 @@ pub fn get_all_posts() -> Result<Vec<Post>, Box<dyn std::error::Error>> {
if path.extension().map(|e| e == "md").unwrap_or(false) {
let file_stem = path.file_stem().unwrap().to_string_lossy();
if let Ok(post) = get_post_by_slug(&file_stem) {
// Insert each post into the individual post cache as well
POST_CACHE.write().unwrap().insert(file_stem.to_string(), post.clone());
posts.push(post);
}
}
@@ -342,4 +347,28 @@ pub fn watch_posts<F: Fn() + Send + 'static>(on_change: F) -> notify::Result<Rec
}
});
Ok(watcher)
}
pub fn load_post_cache_from_disk() {
if let Ok(data) = fs::read_to_string(POSTS_CACHE_PATH) {
if let Ok(map) = serde_json::from_str::<HashMap<String, Post>>(&data) {
*POST_CACHE.write().unwrap() = map;
}
}
if let Ok(data) = fs::read_to_string(POST_STATS_PATH) {
if let Ok(map) = serde_json::from_str::<HashMap<String, PostStats>>(&data) {
*POST_STATS.write().unwrap() = map;
}
}
}
pub fn save_post_cache_to_disk() {
if let Ok(map) = serde_json::to_string(&*POST_CACHE.read().unwrap()) {
let _ = fs::create_dir_all("./cache");
let _ = fs::write(POSTS_CACHE_PATH, map);
}
if let Ok(map) = serde_json::to_string(&*POST_STATS.read().unwrap()) {
let _ = fs::create_dir_all("./cache");
let _ = fs::write(POST_STATS_PATH, map);
}
}

30
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"autoprefixer": "^10.4.17",
"bcrypt": "^5.0.2",
"bcryptjs": "^2.4.3",
"chart.js": "^4.5.0",
"chokidar": "^3.6.0",
"date-fns": "^3.6.0",
"dompurify": "^3.0.9",
@@ -28,6 +29,7 @@
"pm2": "^6.0.8",
"postcss": "^8.4.35",
"react": "^18.2.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
@@ -502,6 +504,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -2214,6 +2222,18 @@
"integrity": "sha512-syedaZ9cPe7r3hoQA9twWYKu5AIyCswN5+szkmPBe9ccdLrj4bYaCnLVPTLd2kgVRc7+zoX4tyPgRnFKCj5YjQ==",
"license": "MIT/X11"
},
"node_modules/chart.js": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -6805,6 +6825,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-chartjs-2": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
"license": "MIT",
"peerDependencies": {
"chart.js": "^4.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",

View File

@@ -18,6 +18,7 @@
"autoprefixer": "^10.4.17",
"bcrypt": "^5.0.2",
"bcryptjs": "^2.4.3",
"chart.js": "^4.5.0",
"chokidar": "^3.6.0",
"date-fns": "^3.6.0",
"dompurify": "^3.0.9",
@@ -31,6 +32,7 @@
"pm2": "^6.0.8",
"postcss": "^8.4.35",
"react": "^18.2.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"

View File

@@ -1,5 +1,18 @@
'use client';
import React, { useEffect, useState } from 'react';
import { Bar } from 'react-chartjs-2';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ChartOptions,
} from 'chart.js';
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
interface PostStats {
slug: string;
@@ -13,6 +26,8 @@ export default function RustStatusPage() {
const [stats, setStats] = useState<PostStats[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [autoRefresh, setAutoRefresh] = useState(false);
const autoRefreshRef = React.useRef<NodeJS.Timeout | null>(null);
const fetchStats = async () => {
setLoading(true);
@@ -31,45 +46,156 @@ export default function RustStatusPage() {
React.useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 5000);
return () => clearInterval(interval);
// Listen for post changes via BroadcastChannel
let bc: BroadcastChannel | null = null;
if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
bc = new BroadcastChannel('posts-changed');
bc.onmessage = (event) => {
if (event.data === 'changed') {
fetchStats();
}
};
}
return () => {
if (bc) bc.close();
if (autoRefreshRef.current) clearInterval(autoRefreshRef.current);
};
}, []);
// Handle auto-refresh toggle
React.useEffect(() => {
if (autoRefresh) {
autoRefreshRef.current = setInterval(fetchStats, 2000);
} else if (autoRefreshRef.current) {
clearInterval(autoRefreshRef.current);
autoRefreshRef.current = null;
}
return () => {
if (autoRefreshRef.current) clearInterval(autoRefreshRef.current);
};
}, [autoRefresh]);
// Dashboard summary calculations
const totalHits = stats.reduce((sum, s) => sum + s.cache_hits, 0);
const totalMisses = stats.reduce((sum, s) => sum + s.cache_misses, 0);
const avgInterpret = stats.length ? (stats.reduce((sum, s) => sum + s.last_interpret_time_ms, 0) / stats.length).toFixed(1) : 0;
const avgCompile = stats.length ? (stats.reduce((sum, s) => sum + s.last_compile_time_ms, 0) / stats.length).toFixed(1) : 0;
// Chart data
const chartData = {
labels: stats.map(s => s.slug),
datasets: [
{
label: 'Cache Hits',
data: stats.map(s => s.cache_hits),
backgroundColor: 'rgba(34,197,94,0.7)',
},
{
label: 'Cache Misses',
data: stats.map(s => s.cache_misses),
backgroundColor: 'rgba(239,68,68,0.7)',
},
],
};
const chartOptions: ChartOptions<'bar'> = {
responsive: true,
plugins: {
legend: { position: 'top' },
title: { display: true, text: 'Cache Hits & Misses per Post' },
},
scales: {
x: { stacked: true },
y: { stacked: true, beginAtZero: true },
},
};
return (
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-6">Rust Parser Status</h1>
{loading && <div>Loading...</div>}
{error && <div className="text-red-500">{error}</div>}
{!loading && !error && (
<div className="overflow-x-auto">
<table className="min-w-full border border-gray-300 bg-white shadow-md rounded">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-2 text-left">Slug</th>
<th className="px-4 py-2 text-right">Cache Hits</th>
<th className="px-4 py-2 text-right">Cache Misses</th>
<th className="px-4 py-2 text-right">Last Interpret Time (ms)</th>
<th className="px-4 py-2 text-right">Last Compile Time (ms)</th>
</tr>
</thead>
<tbody>
{stats.length === 0 ? (
<tr><td colSpan={5} className="text-center py-4">No stats available.</td></tr>
) : (
stats.map(stat => (
<tr key={stat.slug} className="border-t">
<td className="px-4 py-2 font-mono">{stat.slug}</td>
<td className="px-4 py-2 text-right">{stat.cache_hits}</td>
<td className="px-4 py-2 text-right">{stat.cache_misses}</td>
<td className="px-4 py-2 text-right">{stat.last_interpret_time_ms}</td>
<td className="px-4 py-2 text-right">{stat.last_compile_time_ms}</td>
</tr>
))
)}
</tbody>
</table>
<div className="p-8 max-w-6xl mx-auto">
<h1 className="text-3xl font-bold mb-8 text-center">Rust Parser Dashboard</h1>
<div className="flex justify-end gap-4 mb-4">
<button
onClick={fetchStats}
className="px-4 py-2 bg-blue-600 text-white rounded shadow hover:bg-blue-700"
>
Refresh
</button>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={autoRefresh}
onChange={e => setAutoRefresh(e.target.checked)}
className="form-checkbox"
/>
<span className="text-sm">Auto-refresh every 2s</span>
</label>
</div>
{loading && (
<div className="flex flex-col items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mb-4"></div>
<div className="text-lg">Loading stats...</div>
</div>
)}
{error && (
<div className="text-red-500 text-center text-lg">{error}</div>
)}
{!loading && !error && (
<>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-green-100 rounded-lg p-6 flex flex-col items-center shadow">
<span className="text-2xl font-bold text-green-700">{totalHits}</span>
<span className="text-gray-700 mt-2">Total Cache Hits</span>
</div>
<div className="bg-red-100 rounded-lg p-6 flex flex-col items-center shadow">
<span className="text-2xl font-bold text-red-700">{totalMisses}</span>
<span className="text-gray-700 mt-2">Total Cache Misses</span>
</div>
<div className="bg-blue-100 rounded-lg p-6 flex flex-col items-center shadow">
<span className="text-2xl font-bold text-blue-700">{avgInterpret} ms</span>
<span className="text-gray-700 mt-2">Avg Interpret Time</span>
</div>
<div className="bg-purple-100 rounded-lg p-6 flex flex-col items-center shadow">
<span className="text-2xl font-bold text-purple-700">{avgCompile} ms</span>
<span className="text-gray-700 mt-2">Avg Compile Time</span>
</div>
</div>
{/* Bar Chart */}
<div className="bg-white rounded-lg shadow p-6 mb-10">
<Bar data={chartData} options={chartOptions} height={120} />
</div>
{/* Raw Data Table */}
<div className="overflow-x-auto">
<table className="min-w-full border border-gray-300 bg-white shadow-md rounded">
<thead>
<tr className="bg-gray-100">
<th className="px-4 py-2 text-left">Slug</th>
<th className="px-4 py-2 text-right">Cache Hits</th>
<th className="px-4 py-2 text-right">Cache Misses</th>
<th className="px-4 py-2 text-right">Last Interpret Time (ms)</th>
<th className="px-4 py-2 text-right">Last Compile Time (ms)</th>
</tr>
</thead>
<tbody>
{stats.length === 0 ? (
<tr><td colSpan={5} className="text-center py-4">No stats available.</td></tr>
) : (
stats.map(stat => (
<tr key={stat.slug} className="border-t">
<td className="px-4 py-2 font-mono">{stat.slug}</td>
<td className="px-4 py-2 text-right">{stat.cache_hits}</td>
<td className="px-4 py-2 text-right">{stat.cache_misses}</td>
<td className="px-4 py-2 text-right">{stat.last_interpret_time_ms}</td>
<td className="px-4 py-2 text-right">{stat.last_compile_time_ms}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</>
)}
</div>
);
}