Sfoglia il codice sorgente

add timestamps to incremental rendering

Evan Almloff 2 anni fa
parent
commit
28f875857e

+ 8 - 1
packages/router/Cargo.toml

@@ -25,10 +25,12 @@ js-sys = { version = "0.3.63", optional = true }
 gloo-utils = { version = "0.1.6", optional = true }
 dioxus-ssr = { path = "../ssr", optional = true }
 lru = { version = "0.10.0", optional = true }
+radix_trie = { version = "0.2.1", optional = true }
+rustc-hash = "1.1.0"
 
 [features]
 default = ["web", "ssr"]
-ssr = ["dioxus-ssr", "lru"]
+ssr = ["dioxus-ssr", "lru", "radix_trie"]
 wasm_test = []
 serde = ["dep:serde", "gloo-utils/serde"]
 web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
@@ -36,6 +38,11 @@ web = ["gloo", "web-sys", "wasm-bindgen", "gloo-utils", "js-sys"]
 [dev-dependencies]
 dioxus = { path = "../dioxus" }
 dioxus-ssr = { path = "../ssr" }
+criterion = "0.3"
+
+[[bench]]
+name = "incremental"
+harness = false
 
 [target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
 dioxus-desktop = { path = "../desktop" }

+ 187 - 0
packages/router/benches/incremental.rs

@@ -0,0 +1,187 @@
+#![allow(unused, non_snake_case)]
+
+use std::time::Duration;
+
+use dioxus::prelude::*;
+use dioxus_router::prelude::*;
+
+use criterion::{black_box, criterion_group, criterion_main, Criterion};
+use dioxus_router::ssr::{DefaultRenderer, IncrementalRenderer};
+use dioxus_ssr::Renderer;
+
+pub fn criterion_benchmark(c: &mut Criterion) {
+    c.bench_function("build 1000 routes", |b| {
+        let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
+            before_body: r#"<!DOCTYPE html>
+            <html lang="en">
+            <head>
+                <meta charset="UTF-8">
+                <meta name="viewport" content="width=device-width,
+                initial-scale=1.0">
+                <title>Dioxus Application</title>
+            </head>
+            <body>"#
+                .to_string(),
+            after_body: r#"</body>
+            </html>"#
+                .to_string(),
+        })
+        .static_dir("./static")
+        .invalidate_after(Duration::from_secs(10))
+        .build();
+
+        b.iter(|| {
+            for id in 0..100 {
+                for id in 0..10 {
+                    renderer
+                        .render(Route::Post { id }, &mut std::io::sink())
+                        .unwrap();
+                }
+            }
+        })
+    });
+    c.bench_function("build 1000 routes no memory cache", |b| {
+        let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
+            before_body: r#"<!DOCTYPE html>
+            <html lang="en">
+            <head>
+                <meta charset="UTF-8">
+                <meta name="viewport" content="width=device-width,
+                initial-scale=1.0">
+                <title>Dioxus Application</title>
+            </head>
+            <body>"#
+                .to_string(),
+            after_body: r#"</body>
+            </html>"#
+                .to_string(),
+        })
+        .static_dir("./static")
+        .memory_cache_limit(0)
+        .invalidate_after(Duration::from_secs(10))
+        .build();
+
+        b.iter(|| {
+            for id in 0..1000 {
+                renderer
+                    .render(Route::Post { id }, &mut std::io::sink())
+                    .unwrap();
+            }
+        })
+    });
+    c.bench_function("build 1000 routes no cache", |b| {
+        let mut renderer = Renderer::default();
+
+        b.iter(|| {
+            for id in 0..1000 {
+                let mut vdom = VirtualDom::new_with_props(
+                    RenderPath,
+                    RenderPathProps::builder().path(Route::Post { id }).build(),
+                );
+
+                vdom.rebuild();
+
+                struct Ignore;
+
+                impl std::fmt::Write for Ignore {
+                    fn write_str(&mut self, s: &str) -> std::fmt::Result {
+                        Ok(())
+                    }
+                }
+
+                renderer.render_to(&mut Ignore, &vdom).unwrap();
+            }
+        })
+    });
+    c.bench_function("cache static", |b| {
+        b.iter(|| {
+            let mut renderer = IncrementalRenderer::builder(DefaultRenderer {
+                before_body: r#"<!DOCTYPE html>
+                <html lang="en">
+                <head>
+                    <meta charset="UTF-8">
+                    <meta name="viewport" content="width=device-width,
+                    initial-scale=1.0">
+                    <title>Dioxus Application</title>
+                </head>
+                <body>"#
+                    .to_string(),
+                after_body: r#"</body>
+                </html>"#
+                    .to_string(),
+            })
+            .static_dir("./static")
+            .build();
+
+            renderer.pre_cache_static_routes::<Route>().unwrap();
+        })
+    });
+}
+
+criterion_group!(benches, criterion_benchmark);
+criterion_main!(benches);
+
+#[inline_props]
+fn Blog(cx: Scope) -> Element {
+    render! {
+        div {
+            "Blog"
+        }
+    }
+}
+
+#[inline_props]
+fn Post(cx: Scope, id: usize) -> Element {
+    render! {
+        for _ in 0..*id {
+            div {
+                "PostId: {id}"
+            }
+        }
+    }
+}
+
+#[inline_props]
+fn PostHome(cx: Scope) -> Element {
+    render! {
+        div {
+            "Post"
+        }
+    }
+}
+
+#[inline_props]
+fn Home(cx: Scope) -> Element {
+    render! {
+        div {
+            "Home"
+        }
+    }
+}
+
+#[rustfmt::skip]
+#[derive(Clone, Debug, PartialEq, Routable)]
+enum Route {
+    #[nest("/blog")]
+        #[route("/")]
+        Blog {},
+        #[route("/post/index")]
+        PostHome {},
+        #[route("/post/:id")]
+        Post {
+            id: usize,
+        },
+    #[end_nest]
+    #[route("/")]
+    Home {},
+}
+
+#[inline_props]
+fn RenderPath(cx: Scope, path: Route) -> Element {
+    let path = path.clone();
+    render! {
+        Router {
+            config: || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
+        }
+    }
+}

+ 8 - 3
packages/router/examples/static_generation.rs

@@ -1,5 +1,7 @@
 #![allow(non_snake_case)]
 
+use std::time::Duration;
+
 use dioxus::prelude::*;
 use dioxus_router::prelude::*;
 
@@ -22,13 +24,16 @@ fn main() {
             .to_string(),
     })
     .static_dir("./static")
+    .invalidate_after(Duration::from_secs(10))
     .build();
 
-    renderer.pre_cache_static::<Route>();
+    renderer.pre_cache_static_routes::<Route>().unwrap();
 
-    for _ in 0..2 {
+    for _ in 0..1_000_000 {
         for id in 0..10 {
-            renderer.render(Route::Post { id });
+            renderer
+                .render(Route::Post { id }, &mut std::io::sink())
+                .unwrap();
         }
     }
 }

+ 218 - 45
packages/router/src/ssr.rs

@@ -4,17 +4,22 @@
 
 use crate::prelude::*;
 use dioxus::prelude::*;
+use rustc_hash::FxHasher;
 use std::{
-    io::{Read, Write},
+    hash::BuildHasherDefault,
+    io::Write,
     num::NonZeroUsize,
     path::{Path, PathBuf},
     str::FromStr,
+    time::{Duration, SystemTime},
 };
 
 /// Something that can render a HTML page from a body.
 pub trait RenderHTML {
-    /// Render a HTML page from a body.
-    fn render_html(&self, body: &str) -> String;
+    /// Render the HTML before the body
+    fn render_before_body<R: Write>(&self, to: &mut R) -> Result<(), IncrementalRendererError>;
+    /// Render the HTML after the body
+    fn render_after_body<R: Write>(&self, to: &mut R) -> Result<(), IncrementalRendererError>;
 }
 
 /// The default page renderer
@@ -45,8 +50,14 @@ impl Default for DefaultRenderer {
 }
 
 impl RenderHTML for DefaultRenderer {
-    fn render_html(&self, body: &str) -> String {
-        format!("{}{}{}", self.before_body, body, self.after_body)
+    fn render_before_body<R: Write>(&self, to: &mut R) -> Result<(), IncrementalRendererError> {
+        to.write_all(self.before_body.as_bytes())?;
+        Ok(())
+    }
+
+    fn render_after_body<R: Write>(&self, to: &mut R) -> Result<(), IncrementalRendererError> {
+        to.write_all(self.after_body.as_bytes())?;
+        Ok(())
     }
 }
 
@@ -54,6 +65,7 @@ impl RenderHTML for DefaultRenderer {
 pub struct IncrementalRendererConfig<R: RenderHTML> {
     static_dir: PathBuf,
     memory_cache_limit: usize,
+    invalidate_after: Option<Duration>,
     render: R,
 }
 
@@ -68,7 +80,8 @@ impl<R: RenderHTML> IncrementalRendererConfig<R> {
     pub fn new(render: R) -> Self {
         Self {
             static_dir: PathBuf::from("./static"),
-            memory_cache_limit: 100,
+            memory_cache_limit: 10000,
+            invalidate_after: None,
             render,
         }
     }
@@ -85,13 +98,21 @@ impl<R: RenderHTML> IncrementalRendererConfig<R> {
         self
     }
 
+    /// Set the invalidation time.
+    pub fn invalidate_after(mut self, invalidate_after: Duration) -> Self {
+        self.invalidate_after = Some(invalidate_after);
+        self
+    }
+
     /// Build the incremental renderer.
     pub fn build(self) -> IncrementalRenderer<R> {
         IncrementalRenderer {
             static_dir: self.static_dir,
             memory_cache: NonZeroUsize::new(self.memory_cache_limit)
-                .map(|limit| lru::LruCache::new(limit)),
+                .map(|limit| lru::LruCache::with_hasher(limit, Default::default())),
+            invalidate_after: self.invalidate_after,
             render: self.render,
+            ssr_renderer: dioxus_ssr::Renderer::new(),
         }
     }
 }
@@ -99,7 +120,10 @@ impl<R: RenderHTML> IncrementalRendererConfig<R> {
 /// An incremental renderer.
 pub struct IncrementalRenderer<R: RenderHTML> {
     static_dir: PathBuf,
-    memory_cache: Option<lru::LruCache<String, String>>,
+    memory_cache:
+        Option<lru::LruCache<String, (SystemTime, Vec<u8>), BuildHasherDefault<FxHasher>>>,
+    invalidate_after: Option<Duration>,
+    ssr_renderer: dioxus_ssr::Renderer,
     render: R,
 }
 
@@ -109,80 +133,157 @@ impl<R: RenderHTML> IncrementalRenderer<R> {
         IncrementalRendererConfig::new(renderer)
     }
 
-    fn render_uncached<Rt>(&self, route: Rt) -> String
+    fn track_timestamps(&self) -> bool {
+        self.invalidate_after.is_some()
+    }
+
+    fn render_and_cache<Rt>(
+        &mut self,
+        route: Rt,
+        output: &mut impl Write,
+    ) -> Result<(), IncrementalRendererError>
     where
         Rt: Routable,
         <Rt as FromStr>::Err: std::fmt::Display,
     {
+        let route_str = route.to_string();
         let mut vdom = VirtualDom::new_with_props(RenderPath, RenderPathProps { path: route });
         let _ = vdom.rebuild();
 
-        let body = dioxus_ssr::render(&vdom);
+        let mut html_buffer = WriteBuffer { buffer: Vec::new() };
+        self.render.render_before_body(&mut html_buffer)?;
+        self.ssr_renderer.render_to(&mut html_buffer, &mut vdom)?;
+        self.render.render_after_body(&mut html_buffer)?;
+        let html_buffer = html_buffer.buffer;
+
+        output.write_all(&html_buffer)?;
 
-        self.render.render_html(&body)
+        self.add_to_cache(route_str, html_buffer)
     }
 
-    fn add_to_cache(&mut self, route: String, html: String) {
+    fn add_to_cache(
+        &mut self,
+        route: String,
+        html: Vec<u8>,
+    ) -> Result<(), IncrementalRendererError> {
         let file_path = self.route_as_path(&route);
         if let Some(parent) = file_path.parent() {
             if !parent.exists() {
-                std::fs::create_dir_all(parent).unwrap();
+                std::fs::create_dir_all(parent)?;
             }
         }
-        let file = std::fs::File::create(dbg!(file_path)).unwrap();
+        let file = std::fs::File::create(file_path)?;
         let mut file = std::io::BufWriter::new(file);
-        file.write_all(html.as_bytes()).unwrap();
+        file.write_all(&html)?;
         self.add_to_memory_cache(route, html);
+        Ok(())
     }
 
-    fn add_to_memory_cache<K: AsRef<str> + ToString, V: ToString>(&mut self, route: K, html: V) {
+    fn add_to_memory_cache(&mut self, route: String, html: Vec<u8>) {
         if let Some(cache) = self.memory_cache.as_mut() {
-            if cache.contains(route.as_ref()) {
-                cache.promote(route.as_ref())
-            } else {
-                cache.put(route.to_string(), html.to_string());
-            }
+            cache.put(route.to_string(), (SystemTime::now(), html));
         }
     }
 
-    fn search_cache(&mut self, route: String) -> Option<String> {
-        if let Some(cache_hit) = self
+    fn promote_memory_cache<K: AsRef<str>>(&mut self, route: K) {
+        if let Some(cache) = self.memory_cache.as_mut() {
+            cache.promote(route.as_ref())
+        }
+    }
+
+    fn search_cache(
+        &mut self,
+        route: String,
+        output: &mut impl Write,
+    ) -> Result<bool, IncrementalRendererError> {
+        if let Some((timestamp, cache_hit)) = self
             .memory_cache
             .as_mut()
-            .and_then(|cache| cache.get(&route).cloned())
+            .and_then(|cache| cache.get(&route))
         {
-            Some(cache_hit)
-        } else {
-            let file_path = self.route_as_path(&route);
-            if let Ok(file) = std::fs::File::open(file_path) {
-                let mut file = std::io::BufReader::new(file);
-                let mut html = String::new();
-                file.read_to_string(&mut html).ok()?;
-                self.add_to_memory_cache(route, html.clone());
-                Some(html)
+            if let (Ok(elapsed), Some(invalidate_after)) =
+                (timestamp.elapsed(), self.invalidate_after)
+            {
+                if elapsed < invalidate_after {
+                    log::trace!("memory cache hit {:?}", route);
+                    output.write_all(cache_hit)?;
+                    return Ok(true);
+                }
             } else {
-                None
+                log::trace!("memory cache hit {:?}", route);
+                output.write_all(cache_hit)?;
+                return Ok(true);
+            }
+        }
+        if let Some(file_path) = self.find_file(&route) {
+            if let Ok(file) = std::fs::File::open(file_path.full_path) {
+                let mut file = std::io::BufReader::new(file);
+                std::io::copy(&mut file, output)?;
+                log::trace!("file cache hit {:?}", route);
+                self.promote_memory_cache(&route);
+                return Ok(true);
             }
         }
+        Ok(false)
     }
 
     /// Render a route or get it from cache.
-    pub fn render<Rt>(&mut self, route: Rt) -> String
+    pub fn render<Rt>(
+        &mut self,
+        route: Rt,
+        output: &mut impl Write,
+    ) -> Result<(), IncrementalRendererError>
     where
         Rt: Routable,
         <Rt as FromStr>::Err: std::fmt::Display,
     {
         // check if this route is cached
-        if let Some(html) = self.search_cache(route.to_string()) {
-            return html;
+        if !self.search_cache(route.to_string(), output)? {
+            // if not, create it
+            self.render_and_cache(route, output)?;
+            log::trace!("cache miss");
         }
 
-        // if not, create it
-        println!("cache miss");
-        let html = self.render_uncached(route.clone());
-        self.add_to_cache(route.to_string(), html.clone());
+        Ok(())
+    }
 
-        html
+    fn find_file(&self, route: &str) -> Option<ValidCachedPath> {
+        let mut file_path = self.static_dir.clone();
+        for segment in route.split('/') {
+            file_path.push(segment);
+        }
+        if let Some(deadline) = self.invalidate_after {
+            // find the first file that matches the route and is a html file
+            file_path.push("index");
+            if let Ok(dir) = std::fs::read_dir(file_path) {
+                let mut file = None;
+                for entry in dir.flatten() {
+                    if let Some(cached_path) = ValidCachedPath::try_from_path(entry.path()) {
+                        if let Ok(elapsed) = cached_path.timestamp.elapsed() {
+                            if elapsed < deadline {
+                                file = Some(cached_path);
+                                continue;
+                            }
+                        }
+                        // if the timestamp is invalid or passed, delete the file
+                        if let Err(err) = std::fs::remove_file(entry.path()) {
+                            log::error!("Failed to remove file: {}", err);
+                        }
+                    }
+                }
+                file
+            } else {
+                None
+            }
+        } else {
+            file_path.push("index.html");
+            file_path.exists().then_some({
+                ValidCachedPath {
+                    full_path: file_path,
+                    timestamp: SystemTime::now(),
+                }
+            })
+        }
     }
 
     fn route_as_path(&self, route: &str) -> PathBuf {
@@ -190,13 +291,18 @@ impl<R: RenderHTML> IncrementalRenderer<R> {
         for segment in route.split('/') {
             file_path.push(segment);
         }
-        file_path.push("index");
+        if self.track_timestamps() {
+            file_path.push("index");
+            file_path.push(timestamp());
+        } else {
+            file_path.push("index");
+        }
         file_path.set_extension("html");
         file_path
     }
 
     /// Pre-cache all static routes.
-    pub fn pre_cache_static<Rt>(&mut self)
+    pub fn pre_cache_static_routes<Rt>(&mut self) -> Result<(), IncrementalRendererError>
     where
         Rt: Routable,
         <Rt as FromStr>::Err: std::fmt::Display,
@@ -225,7 +331,7 @@ impl<R: RenderHTML> IncrementalRenderer<R> {
             if is_static {
                 match Rt::from_str(&full_path) {
                     Ok(route) => {
-                        let _ = self.render(route);
+                        let _ = self.render(route, &mut std::io::sink())?;
                     }
                     Err(e) => {
                         log::error!("Error pre-caching static route: {}", e);
@@ -233,9 +339,65 @@ impl<R: RenderHTML> IncrementalRenderer<R> {
                 }
             }
         }
+
+        Ok(())
     }
 }
 
+struct WriteBuffer {
+    buffer: Vec<u8>,
+}
+
+impl std::fmt::Write for WriteBuffer {
+    fn write_str(&mut self, s: &str) -> std::fmt::Result {
+        self.buffer.extend_from_slice(s.as_bytes());
+        Ok(())
+    }
+}
+
+impl Write for WriteBuffer {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        self.buffer.write(buf)
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        self.buffer.flush()
+    }
+}
+
+struct ValidCachedPath {
+    full_path: PathBuf,
+    timestamp: std::time::SystemTime,
+}
+
+impl ValidCachedPath {
+    fn try_from_path(value: PathBuf) -> Option<Self> {
+        if value.extension() != Some(std::ffi::OsStr::new("html")) {
+            return None;
+        }
+        let timestamp = decode_timestamp(&value.file_stem()?.to_str()?)?;
+        let full_path = value;
+        Some(Self {
+            full_path,
+            timestamp,
+        })
+    }
+}
+
+fn decode_timestamp(timestamp: &str) -> Option<std::time::SystemTime> {
+    let timestamp = u64::from_str_radix(timestamp, 16).ok()?;
+    Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(timestamp))
+}
+
+fn timestamp() -> String {
+    let datetime = std::time::SystemTime::now();
+    let timestamp = datetime
+        .duration_since(std::time::UNIX_EPOCH)
+        .unwrap()
+        .as_secs();
+    format!("{:x}", timestamp)
+}
+
 #[inline_props]
 fn RenderPath<R>(cx: Scope, path: R) -> Element
 where
@@ -249,3 +411,14 @@ where
         }
     }
 }
+
+/// An error that can occur while rendering a route or retrieving a cached route.
+#[derive(Debug, thiserror::Error)]
+pub enum IncrementalRendererError {
+    /// An formatting error occurred while rendering a route.
+    #[error("RenderError: {0}")]
+    RenderError(#[from] std::fmt::Error),
+    /// An IO error occurred while rendering a route.
+    #[error("IoError: {0}")]
+    IoError(#[from] std::io::Error),
+}