fancy caching works now
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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
30
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user