From 89d36d2bea37a6122c69fde5b86ce4e892e9ef2c Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 20 Sep 2025 00:52:50 -0500 Subject: [PATCH 1/2] benches --- Cargo.lock | 399 ++++++++++++++++++ Cargo.toml | 2 + Justfile | 9 + codspeed.toml | 14 + crates/djls-semantic/Cargo.toml | 10 + crates/djls-semantic/benches/validator.rs | 117 +++++ crates/djls-templates/Cargo.toml | 10 + crates/djls-templates/benches/fixtures/mod.rs | 199 +++++++++ crates/djls-templates/benches/parser.rs | 127 ++++++ noxfile.py | 51 ++- .../templates/bench/completion_hotspots.html | 64 +++ .../djls_app/templates/bench/custom_tags.html | 40 ++ .../djls_app/templates/bench/dashboard.html | 66 +++ .../djls_app/templates/bench/error_storm.html | 45 ++ .../templates/bench/filter_chains.html | 43 ++ .../templates/bench/invalid_block.html | 16 + .../templates/bench/knowledge_base.html | 126 ++++++ .../templates/bench/micro_blocks.html | 52 +++ .../templates/bench/minified_stream.html | 1 + .../djls_app/templates/bench/simple.html | 14 + .../templates/bench/translation_heavy.html | 33 ++ 21 files changed, 1437 insertions(+), 1 deletion(-) create mode 100644 codspeed.toml create mode 100644 crates/djls-semantic/benches/validator.rs create mode 100644 crates/djls-templates/benches/fixtures/mod.rs create mode 100644 crates/djls-templates/benches/parser.rs create mode 100644 tests/project/djls_app/templates/bench/completion_hotspots.html create mode 100644 tests/project/djls_app/templates/bench/custom_tags.html create mode 100644 tests/project/djls_app/templates/bench/dashboard.html create mode 100644 tests/project/djls_app/templates/bench/error_storm.html create mode 100644 tests/project/djls_app/templates/bench/filter_chains.html create mode 100644 tests/project/djls_app/templates/bench/invalid_block.html create mode 100644 tests/project/djls_app/templates/bench/knowledge_base.html create mode 100644 tests/project/djls_app/templates/bench/micro_blocks.html create mode 100644 tests/project/djls_app/templates/bench/minified_stream.html create mode 100644 tests/project/djls_app/templates/bench/simple.html create mode 100644 tests/project/djls_app/templates/bench/translation_heavy.html diff --git a/Cargo.lock b/Cargo.lock index e44cf1e1..c69b2323 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.20" @@ -88,6 +94,15 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -132,6 +147,15 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -162,6 +186,12 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "bytes" version = "1.10.1" @@ -177,12 +207,51 @@ dependencies = [ "serde_core", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.5.47" @@ -223,12 +292,78 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "codspeed" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35584c5fcba8059780748866387fb97c5a203bcfc563fc3d0790af406727a117" +dependencies = [ + "anyhow", + "bincode", + "colored", + "glob", + "libc", + "nix", + "serde", + "serde_json", + "statrs", + "uuid", +] + +[[package]] +name = "codspeed-criterion-compat" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f6c1c6bed5fd84d319e8b0889da051daa361c79b7709c9394dfe1a882bba67" +dependencies = [ + "codspeed", + "codspeed-criterion-compat-walltime", + "colored", +] + +[[package]] +name = "codspeed-criterion-compat-walltime" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c989289ce6b1cbde72ed560496cb8fbf5aa14d5ef5666f168e7f87751038352e" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "codspeed", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "config" version = "0.15.15" @@ -299,6 +434,42 @@ dependencies = [ "libc", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -482,6 +653,8 @@ name = "djls-semantic" version = "0.0.0" dependencies = [ "camino", + "codspeed-criterion-compat", + "criterion", "djls-conf", "djls-source", "djls-templates", @@ -537,6 +710,8 @@ version = "0.0.0" dependencies = [ "anyhow", "camino", + "codspeed-criterion-compat", + "criterion", "djls-conf", "djls-source", "djls-workspace", @@ -784,6 +959,22 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -816,6 +1007,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "httparse" version = "1.10.1" @@ -991,18 +1188,48 @@ dependencies = [ "libc", ] +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "json5" version = "0.4.1" @@ -1142,6 +1369,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "notify" version = "8.2.0" @@ -1181,6 +1420,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -1202,6 +1450,12 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "option-ext" version = "0.2.0" @@ -1319,6 +1573,34 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1404,6 +1686,18 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.10" @@ -1469,6 +1763,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.20" @@ -1680,6 +1980,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1832,6 +2142,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.47.1" @@ -2096,6 +2416,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2133,6 +2464,74 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "8.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4464d65a..82bbd178 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ which = "8.0" # testing insta = { version = "1.43", features = ["yaml"] } tempfile = "3.22" +codspeed-criterion-compat = "3.0.2" +criterion = "0.5" [workspace.lints.clippy] pedantic = { level = "warn", priority = -1 } diff --git a/Justfile b/Justfile index 90347660..84f46937 100644 --- a/Justfile +++ b/Justfile @@ -42,3 +42,12 @@ test *ARGS: testall *ARGS: @just nox tests {{ ARGS }} + +bench-templates *ARGS: + cargo bench -p djls-templates --features bench {{ ARGS }} + +bench-semantic *ARGS: + cargo bench -p djls-semantic --features bench {{ ARGS }} + +bench-codspeed: + nox -s bench -- --features codspeed -- --codspeed diff --git a/codspeed.toml b/codspeed.toml new file mode 100644 index 00000000..f415624d --- /dev/null +++ b/codspeed.toml @@ -0,0 +1,14 @@ +version = 1 + +[[workflows]] +name = "benchmarks" +command = [ + "nox", + "-s", + "bench", + "--", + "--features", + "codspeed", + "--", + "--codspeed" +] diff --git a/crates/djls-semantic/Cargo.toml b/crates/djls-semantic/Cargo.toml index 17d13fd3..061e9876 100644 --- a/crates/djls-semantic/Cargo.toml +++ b/crates/djls-semantic/Cargo.toml @@ -3,6 +3,10 @@ name = "djls-semantic" version = "0.0.0" edition = "2021" +[features] +default = [] +codspeed = ["codspeed-criterion-compat"] + [dependencies] djls-conf = { workspace = true } djls-source = { workspace = true } @@ -14,10 +18,16 @@ rustc-hash = { workspace = true } salsa = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } +codspeed-criterion-compat = { workspace = true, optional = true } [dev-dependencies] insta = { workspace = true } tempfile = { workspace = true } +criterion = { workspace = true } + +[[bench]] +name = "validator" +harness = false [lints] workspace = true diff --git a/crates/djls-semantic/benches/validator.rs b/crates/djls-semantic/benches/validator.rs new file mode 100644 index 00000000..9c78fe11 --- /dev/null +++ b/crates/djls-semantic/benches/validator.rs @@ -0,0 +1,117 @@ +use std::collections::HashMap; +use std::io; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Duration; + +use camino::Utf8Path; +use camino::Utf8PathBuf; +#[cfg(feature = "codspeed")] +use codspeed_criterion_compat::criterion_group; +#[cfg(feature = "codspeed")] +use codspeed_criterion_compat::criterion_main; +#[cfg(feature = "codspeed")] +use codspeed_criterion_compat::Criterion; +use criterion::black_box; +#[cfg(not(feature = "codspeed"))] +use criterion::criterion_group; +#[cfg(not(feature = "codspeed"))] +use criterion::criterion_main; +use criterion::BatchSize; +#[cfg(not(feature = "codspeed"))] +use criterion::Criterion; +use djls_semantic::ValidationErrorAccumulator; +use djls_semantic::{ + self, +}; +use djls_source::File; + +#[path = "../../djls-templates/benches/fixtures/mod.rs"] +mod fixtures; + +#[salsa::db] +#[derive(Clone)] +struct BenchDb { + storage: salsa::Storage, + sources: Arc>>, + tag_specs: djls_semantic::TagSpecs, +} + +impl BenchDb { + fn new() -> Self { + Self { + storage: salsa::Storage::default(), + sources: Arc::new(Mutex::new(HashMap::new())), + tag_specs: djls_semantic::django_builtin_specs(), + } + } + + fn file_with_contents(&mut self, path: Utf8PathBuf, contents: &str) -> File { + self.sources + .lock() + .expect("sources lock poisoned") + .insert(path.clone(), contents.to_string()); + File::new(self, path, 0) + } +} + +#[salsa::db] +impl salsa::Database for BenchDb {} + +#[salsa::db] +impl djls_source::Db for BenchDb { + fn read_file_source(&self, path: &Utf8Path) -> io::Result { + let sources = self.sources.lock().expect("sources lock poisoned"); + Ok(sources.get(path).cloned().unwrap_or_default()) + } +} + +#[salsa::db] +impl djls_templates::Db for BenchDb {} + +#[salsa::db] +impl djls_semantic::Db for BenchDb { + fn tag_specs(&self) -> djls_semantic::TagSpecs { + self.tag_specs.clone() + } +} + +fn bench_validation(c: &mut Criterion) { + let mut group = c.benchmark_group("validate_nodelist"); + group.sample_size(60); + group.measurement_time(Duration::from_secs(8)); + + for fixture in fixtures::validation_fixtures() { + let name = fixture.name.clone(); + let contents = fixture.contents.clone(); + let path = fixture.file_path(); + + group.bench_function(name, move |b| { + let contents = contents.clone(); + let path = path.clone(); + + b.iter_batched( + move || { + let mut db = BenchDb::new(); + let file = db.file_with_contents(path.clone(), &contents); + (db, file) + }, + |(db, file)| { + if let Some(nodelist) = djls_templates::parse_template(&db, file) { + djls_semantic::validate_nodelist(&db, nodelist); + let errors = djls_semantic::validate_nodelist::accumulated::< + ValidationErrorAccumulator, + >(&db, nodelist); + black_box(errors); + } + }, + BatchSize::SmallInput, + ); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_validation); +criterion_main!(benches); diff --git a/crates/djls-templates/Cargo.toml b/crates/djls-templates/Cargo.toml index e3b867a2..dcea96c5 100644 --- a/crates/djls-templates/Cargo.toml +++ b/crates/djls-templates/Cargo.toml @@ -3,6 +3,10 @@ name = "djls-templates" version = "0.0.0" edition = "2021" +[features] +default = [] +codspeed = ["codspeed-criterion-compat"] + [dependencies] djls-conf = { workspace = true } djls-source = { workspace = true } @@ -13,11 +17,17 @@ salsa = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } +codspeed-criterion-compat = { workspace = true, optional = true } [dev-dependencies] camino = { workspace = true } insta = { workspace = true } tempfile = { workspace = true } +criterion = { workspace = true } + +[[bench]] +name = "parser" +harness = false [lints] workspace = true diff --git a/crates/djls-templates/benches/fixtures/mod.rs b/crates/djls-templates/benches/fixtures/mod.rs new file mode 100644 index 00000000..7e8a89d6 --- /dev/null +++ b/crates/djls-templates/benches/fixtures/mod.rs @@ -0,0 +1,199 @@ +#![allow(dead_code)] + +use std::env; +use std::fs; +use std::io; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::sync::OnceLock; + +use camino::Utf8PathBuf; + +#[derive(Clone)] +pub struct Fixture { + pub name: String, + pub slug: String, + pub contents: String, +} + +impl Fixture { + pub fn new( + name: impl Into, + slug: impl Into, + contents: impl Into, + ) -> Self { + Self { + name: name.into(), + slug: slug.into(), + contents: contents.into(), + } + } + + #[must_use] + pub fn file_path(&self) -> Utf8PathBuf { + Utf8PathBuf::from(format!("bench_{}.html", self.slug)) + } +} + +macro_rules! fixture { + ($name:literal, $slug:literal, $file:literal) => { + Fixture::new( + $name, + $slug, + include_str!(concat!( + env!("CARGO_WORKSPACE_DIR"), + "/tests/project/djls_app/templates/bench/", + $file + )), + ) + }; +} + +pub fn lex_parse_fixtures() -> Vec { + let mut fixtures = synthetic_lex_parse_fixtures(); + fixtures.extend(load_django_admin_fixtures()); + fixtures +} + +pub fn validation_fixtures() -> Vec { + let mut fixtures = synthetic_validation_fixtures(); + fixtures.extend(load_django_admin_fixtures()); + fixtures +} + +fn synthetic_lex_parse_fixtures() -> Vec { + vec![ + fixture!("Simple Dashboard", "simple", "simple.html"), + fixture!("Kanban Dashboard", "dashboard", "dashboard.html"), + fixture!("Knowledge Base", "knowledge_base", "knowledge_base.html"), + fixture!( + "Completion Hotspots", + "completion_hotspots", + "completion_hotspots.html" + ), + fixture!("Micro Blocks", "micro_blocks", "micro_blocks.html"), + fixture!("Custom Tags", "custom_tags", "custom_tags.html"), + fixture!( + "Translation Heavy", + "translation_heavy", + "translation_heavy.html" + ), + fixture!("Minified Stream", "minified_stream", "minified_stream.html"), + fixture!("Filter Chains", "filter_chains", "filter_chains.html"), + ] +} + +fn synthetic_validation_fixtures() -> Vec { + let mut fixtures = synthetic_lex_parse_fixtures(); + fixtures.push(fixture!("Error Storm", "error_storm", "error_storm.html")); + fixtures.push(fixture!( + "Invalid Block", + "invalid_block", + "invalid_block.html" + )); + fixtures +} + +static DJANGO_ADMIN_FIXTURES: OnceLock> = OnceLock::new(); + +fn load_django_admin_fixtures() -> Vec { + DJANGO_ADMIN_FIXTURES + .get_or_init(|| { + let Some(root) = discover_django_admin_templates() else { + return Vec::new(); + }; + + let limit = env::var("DJLS_BENCH_MAX_DJANGO_TEMPLATES") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(30); + + let mut collected = Vec::new(); + if let Err(err) = collect_templates(&root, limit, &mut collected) { + eprintln!( + "djls bench: failed to load Django templates from {}: {}", + root.display(), + err + ); + return Vec::new(); + } + + collected + }) + .clone() +} + +fn discover_django_admin_templates() -> Option { + if let Ok(path) = env::var("DJLS_BENCH_DJANGO_TEMPLATE_ROOT") { + return Some(PathBuf::from(path)); + } + + let python = env::var("DJLS_BENCH_PYTHON").unwrap_or_else(|_| "python".to_string()); + let script = "import django, os, sys; path = os.path.join(os.path.dirname(django.__file__), 'contrib', 'admin', 'templates'); sys.stdout.write(path)"; + + match Command::new(python).arg("-c").arg(script).output() { + Ok(output) if output.status.success() => { + let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if path.is_empty() { + None + } else { + Some(PathBuf::from(path)) + } + } + Ok(output) => { + eprintln!( + "djls bench: python reported an error discovering Django templates: {}", + String::from_utf8_lossy(&output.stderr) + ); + None + } + Err(err) => { + eprintln!("djls bench: unable to invoke python to locate Django templates: {err}"); + None + } + } +} + +fn collect_templates(root: &Path, limit: usize, fixtures: &mut Vec) -> io::Result<()> { + let mut stack = vec![root.to_path_buf()]; + + while let Some(dir) = stack.pop() { + for entry in fs::read_dir(&dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + stack.push(path); + continue; + } + + if path.extension().and_then(|ext| ext.to_str()) != Some("html") { + continue; + } + + let rel = path.strip_prefix(root).unwrap_or(&path); + let slug = format!("django_admin_{}", slug_from_rel(rel)); + let name = format!("django admin: {}", rel.display()); + let contents = fs::read_to_string(&path)?; + + fixtures.push(Fixture::new(name, slug, contents)); + + if fixtures.len() >= limit { + return Ok(()); + } + } + } + + Ok(()) +} + +fn slug_from_rel(path: &Path) -> String { + let raw = path.to_string_lossy(); + raw.chars() + .map(|ch| match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' => ch.to_ascii_lowercase(), + _ => '_', + }) + .collect() +} diff --git a/crates/djls-templates/benches/parser.rs b/crates/djls-templates/benches/parser.rs new file mode 100644 index 00000000..bc242a53 --- /dev/null +++ b/crates/djls-templates/benches/parser.rs @@ -0,0 +1,127 @@ +use std::collections::HashMap; +use std::io; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Duration; + +use camino::Utf8Path; +use camino::Utf8PathBuf; +#[cfg(feature = "codspeed")] +use codspeed_criterion_compat::criterion_group; +#[cfg(feature = "codspeed")] +use codspeed_criterion_compat::criterion_main; +#[cfg(feature = "codspeed")] +use codspeed_criterion_compat::Criterion; +use criterion::black_box; +#[cfg(not(feature = "codspeed"))] +use criterion::criterion_group; +#[cfg(not(feature = "codspeed"))] +use criterion::criterion_main; +use criterion::BatchSize; +#[cfg(not(feature = "codspeed"))] +use criterion::Criterion; +use djls_source::File; +use djls_templates::Lexer; +use djls_templates::{ + self, +}; + +mod fixtures; + +#[salsa::db] +#[derive(Clone)] +struct BenchDb { + storage: salsa::Storage, + sources: Arc>>, +} + +impl BenchDb { + fn new() -> Self { + Self { + storage: salsa::Storage::default(), + sources: Arc::new(Mutex::new(HashMap::new())), + } + } + + fn file_with_contents(&mut self, path: Utf8PathBuf, contents: &str) -> File { + self.sources + .lock() + .expect("sources lock poisoned") + .insert(path.clone(), contents.to_string()); + File::new(self, path, 0) + } +} + +#[salsa::db] +impl salsa::Database for BenchDb {} + +#[salsa::db] +impl djls_source::Db for BenchDb { + fn read_file_source(&self, path: &Utf8Path) -> io::Result { + let sources = self.sources.lock().expect("sources lock poisoned"); + Ok(sources.get(path).cloned().unwrap_or_default()) + } +} + +#[salsa::db] +impl djls_templates::Db for BenchDb {} + +fn bench_lexing(c: &mut Criterion) { + let mut group = c.benchmark_group("lexer_tokenize"); + group.sample_size(60); + group.measurement_time(Duration::from_secs(8)); + + for fixture in fixtures::lex_parse_fixtures() { + let name = fixture.name.clone(); + let contents = fixture.contents.clone(); + + group.bench_function(name, move |b| { + let contents = contents.clone(); + + b.iter_batched( + BenchDb::new, + |db| { + black_box(Lexer::new(&db, &contents).tokenize()); + }, + BatchSize::SmallInput, + ); + }); + } + + group.finish(); +} + +fn bench_parse_template(c: &mut Criterion) { + let mut group = c.benchmark_group("parse_template"); + group.sample_size(60); + group.measurement_time(Duration::from_secs(8)); + + for fixture in fixtures::lex_parse_fixtures() { + let name = fixture.name.clone(); + let contents = fixture.contents.clone(); + let path = fixture.file_path(); + + group.bench_function(name, move |b| { + let contents = contents.clone(); + let path = path.clone(); + + b.iter_batched( + move || { + let mut db = BenchDb::new(); + let file = db.file_with_contents(path.clone(), &contents); + (db, file) + }, + |(db, file)| { + let nodelist = djls_templates::parse_template(&db, file); + black_box(nodelist); + }, + BatchSize::SmallInput, + ); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_lexing, bench_parse_template); +criterion_main!(benches); diff --git a/noxfile.py b/noxfile.py index c61ce5b4..05cefbe7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,7 +2,6 @@ import json import os -import platform import re from pathlib import Path @@ -188,6 +187,56 @@ def cog(session): ) +@nox.session(python=PY_LATEST) +def bench(session): + django_version = DJ_LATEST + + session.run_install( + "uv", + "sync", + "--frozen", + "--inexact", + "--no-install-package", + "django", + "--python", + session.python, + env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location}, + ) + + if django_version == DJMAIN: + session.install( + "django @ https://github.com/django/django/archive/refs/heads/main.zip" + ) + else: + session.install(f"django=={django_version}") + + python_executable = Path(session.bin) / ( + "python.exe" if os.name == "nt" else "python" + ) + + posargs = list(session.posargs) + + def run_bench(package: str) -> None: + command = ["cargo", "bench", "-p", package, *posargs] + session.run( + *command, + external=True, + env={**os.environ, "DJLS_BENCH_PYTHON": str(python_executable)}, + ) + + if any(arg in {"-p", "--package", "--all", "--workspace"} for arg in posargs): + session.run( + "cargo", + "bench", + *posargs, + external=True, + env={**os.environ, "DJLS_BENCH_PYTHON": str(python_executable)}, + ) + else: + for package in ("djls-templates", "djls-semantic"): + run_bench(package) + + @nox.session def process_docs(session): session.run("uv", "run", "docs/processor.py") diff --git a/tests/project/djls_app/templates/bench/completion_hotspots.html b/tests/project/djls_app/templates/bench/completion_hotspots.html new file mode 100644 index 00000000..f487b775 --- /dev/null +++ b/tests/project/djls_app/templates/bench/completion_hotspots.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} +{% block content %} +
+ {% if show_header %} +
+

{{ project.name }}

+

{{ project.summary }}

+ +
+ {% endif %} + + + +
+ {% for lane in project.swimlanes %} +
+
+

{{ lane.title }}

+ {{ lane.cards|length }} + {% if lane.can_add %} + + {% endif %} +
+ +
    + {% for card in lane.cards %} +
  • +
    {{ card.title }}
    +
    + {{ card.owner|default:"Unassigned" }} + {{ card.due_at|date:"M j" }} +
    + {% if card.description %} +

    {{ card.description|truncatechars:160 }}

    + {% endif %} + {% include "components/card-footer.html" with card=card %} +
  • + {% empty %} +
  • {% trans "No work items" %}
  • + {% endfor %} +
+
+ {% empty %} +

{% trans "No swimlanes configured" %}

+ {% endfor %} +
+ +
+ {% if project.has_more %} + + {% else %} + {% trans "End of results" %} + {% endif %} +
+
+{% endblock %} diff --git a/tests/project/djls_app/templates/bench/custom_tags.html b/tests/project/djls_app/templates/bench/custom_tags.html new file mode 100644 index 00000000..f9508534 --- /dev/null +++ b/tests/project/djls_app/templates/bench/custom_tags.html @@ -0,0 +1,40 @@ +{% load cache highlight markdown extra_tags %} +{% cache 300 project_cache_key project.id %} +
+
+

{{ project.title|titlecase }}

+

{{ project.subtitle|default:"" }}

+ {% breadcrumb project as crumb_list %} + +
+ + {% tabset project.tabs as tabs %} + {% for tab in tabs %} + {% tab tab.label %} + {% render_widget tab.component with project=project user=request.user %} + {% endtab %} + {% endfor %} + {% endtabset %} + +
+ {% markdown %} + {{ project.summary_markdown }} + {% endmarkdown %} +
+ +
+ {% for entry in project.timeline|slice:":20" %} +
+ {% highlight entry.diff language "diff" %} +
+ {% empty %} +

{% trans "No activity yet." %}

+ {% endfor %} +
+
+{% endcache %} diff --git a/tests/project/djls_app/templates/bench/dashboard.html b/tests/project/djls_app/templates/bench/dashboard.html new file mode 100644 index 00000000..3754fcbc --- /dev/null +++ b/tests/project/djls_app/templates/bench/dashboard.html @@ -0,0 +1,66 @@ +{% extends "layout.html" %} +{% load humanize %} + +{% block content %} + + +
+
+ {{ project.open_tasks }} + Open Tasks +
+
+ {{ project.completed_tasks }} + Completed +
+
+ {{ project.members|length }} + Team Members +
+
+ + {% include "components/progress.html" with progress=project.progress %} + +
+ {% for column in project.columns %} +
+
+

{{ column.name }}

+ {{ column.cards|length }} +
+
    + {% for card in column.cards %} +
  • +

    {{ card.title }}

    +

    {{ card.summary }}

    +
    + {{ card.assignee|default:"Unassigned" }} + {{ card.updated_at|naturaltime }} +
    +
  • + {% empty %} +
  • No work items in this column.
  • + {% endfor %} +
+
+ {% endfor %} +
+ +
+

Recent Activity

+
    + {% for event in project.activity|slice:":25" %} +
  • + {{ event.actor.get_full_name }} + {{ event.description }} + +
  • + {% empty %} +
  • No activity to show.
  • + {% endfor %} +
+
+{% endblock %} diff --git a/tests/project/djls_app/templates/bench/error_storm.html b/tests/project/djls_app/templates/bench/error_storm.html new file mode 100644 index 00000000..e7dc56a7 --- /dev/null +++ b/tests/project/djls_app/templates/bench/error_storm.html @@ -0,0 +1,45 @@ +{% block content %} + {% if project.is_active %} +
+

{{ project.title }}

+ {% for task in project.tasks %} +
+

{{ task.title }}

+ {% if task.assignee %} +

{{ task.assignee.full_name }}

+ {% else %} +

{{ UNKNOWN_USER }}

+ {% endif %} + {% if task.subtasks %} +
    + {% for subtask in task.subtasks %} +
  • {% if subtask.done %}✓{% endif %} {{ subtask.title }}
  • + {% empty %} +
  • {% trans "No subtasks" %}
  • + {% endfor %} + {% endif %} + {% if task.notes %} +
    + {{ task.notes|safe } +
    + {% endif %} +
+ {% endfor %} +
+ {% else %} +

{% trans "Project disabled" %}

+ {% endif %} + + {% if project.unbalanced %} + {% if project.needs_review %} + {% else %} + {% endif %} + {% endif %} + + {% for orphan in project.orphans %} + {% if orphan %} + {% else %} + {% endfor %} + + {% include "components/missing-end.html" with data=project %} +{% endblock %} diff --git a/tests/project/djls_app/templates/bench/filter_chains.html b/tests/project/djls_app/templates/bench/filter_chains.html new file mode 100644 index 00000000..79c74a0e --- /dev/null +++ b/tests/project/djls_app/templates/bench/filter_chains.html @@ -0,0 +1,43 @@ +{% block content %} + + + + + + + + + + {% for metric in metrics %} + + + + + + {% endfor %} + +
{% trans "Metric" %}{% trans "Value" %}{% trans "Trend" %}
{{ metric.name|capfirst }} + {{ metric.value|default_if_none:"0"|floatformat:2|intcomma }} + + {{ metric.trend|default:"flat"|lower|slice:":12"|slugify }} +
+ +
+ {% for note in notes %} +
+

{{ note.title|default:""|title }}

+
+ {{ note.body|default:""|linebreaksbr|truncatewords_html:60|safe }} +
+
+ {{ note.author|default:""|striptags|truncatechars:24 }} + +
+
+ {% empty %} +

{% trans "No notes available." %}

+ {% endfor %} +
+{% endblock %} diff --git a/tests/project/djls_app/templates/bench/invalid_block.html b/tests/project/djls_app/templates/bench/invalid_block.html new file mode 100644 index 00000000..a854b3f2 --- /dev/null +++ b/tests/project/djls_app/templates/bench/invalid_block.html @@ -0,0 +1,16 @@ +{% block main %} +

{{ title }}

+ {% if items %} +
    + {% for item in items %} +
  • {{ item.name }}
  • + {% if item.children %} +
      + {% for child in item.children %} +
    • {{ child }}
    • + {% endfor %} +
    + {% endif %} +
+ {% endif %} + {% comment %} Missing {% endblock main %} on purpose {% endcomment %} diff --git a/tests/project/djls_app/templates/bench/knowledge_base.html b/tests/project/djls_app/templates/bench/knowledge_base.html new file mode 100644 index 00000000..8e817ed4 --- /dev/null +++ b/tests/project/djls_app/templates/bench/knowledge_base.html @@ -0,0 +1,126 @@ +{% extends "knowledge/layout.html" %} +{% block sidebar %} + +{% endblock %} + +{% block content %} +
+

{{ knowledge_base.title }}

+

{{ knowledge_base.summary }}

+
+ +
+
+ + {% for section in knowledge_base.sections %} +
+
+

{{ section.title }}

+

{{ section.description }}

+
+ {% for article in section.articles %} +
+
+

{{ article.title }}

+

+ {% if article.author %} + {{ article.author.get_full_name }} • + {% endif %} + {{ article.updated_at|date:"F j, Y" }} • + {{ article.read_time }} min read +

+ +
+
+ {% for block in article.body %} + {% if block.type == 'paragraph' %} +

{{ block.value }}

+ {% elif block.type == 'code' %} +
{{ block.value }}
+ {% elif block.type == 'note' %} + + {% elif block.type == 'image' %} +
+ {{ block.alt }} + {% if block.caption %} +
{{ block.caption }}
+ {% endif %} +
+ {% endif %} + {% endfor %} +
+
+
+ {% for reaction in article.reactions %} + + {% endfor %} +
+ +
+
+ {% empty %} +

No articles in this section yet.

+ {% endfor %} +
+ {% empty %} +

No sections found.

+ {% endfor %} + +
+

Frequently Asked Questions

+
+ {% for faq in knowledge_base.faqs %} +
{{ faq.question }}
+
{{ faq.answer }}
+ {% empty %} +
No FAQs yet.
+ {% endfor %} +
+
+ + +{% endblock %} diff --git a/tests/project/djls_app/templates/bench/micro_blocks.html b/tests/project/djls_app/templates/bench/micro_blocks.html new file mode 100644 index 00000000..9d153a82 --- /dev/null +++ b/tests/project/djls_app/templates/bench/micro_blocks.html @@ -0,0 +1,52 @@ +{% block content %} + {% if feature_flags.navigation %} + + {% endif %} + + {% for widget in widgets %} +
+ {% if widget.title %} +

{{ widget.title }}

+ {% endif %} + {% if widget.show_summary %} +

{{ widget.summary }}

+ {% endif %} + + {% if widget.items %} +
    + {% for item in widget.items %} +
  • + {{ item.label }} + {% if item.status == "ok" %} + OK + {% elif item.status == "warn" %} + WARN + {% else %} + ERR + {% endif %} +
  • + {% endfor %} +
+ {% else %} +

{% trans "Nothing to display" %}

+ {% endif %} +
+ {% empty %} +
+ {% if user.is_authenticated %} +

{% trans "No widgets configured yet." %}

+ {% else %} +

{% trans "Sign in to configure widgets." %}

+ {% endif %} +
+ {% endfor %} + + {% include "components/support-links.html" %} +{% endblock %} diff --git a/tests/project/djls_app/templates/bench/minified_stream.html b/tests/project/djls_app/templates/bench/minified_stream.html new file mode 100644 index 00000000..f7404e8f --- /dev/null +++ b/tests/project/djls_app/templates/bench/minified_stream.html @@ -0,0 +1 @@ +{%if page%}

{{page.title|default:"Untitled"}}

{{page.subtitle}}

{%for block in page.blocks%}{%include block.template with block=block only%}{%empty%}

{{"No blocks"|upper}}

{%endfor%}
{%if page.updated_at%}{%endif%}{%if page.author%}{{page.author.get_full_name}}{%endif%}
{%else%}

{{"Page not found"|capfirst}}

{%endif%} diff --git a/tests/project/djls_app/templates/bench/simple.html b/tests/project/djls_app/templates/bench/simple.html new file mode 100644 index 00000000..2b1e6b12 --- /dev/null +++ b/tests/project/djls_app/templates/bench/simple.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block content %} +

{{ page_title|default:"Dashboard" }}

+

Welcome, {{ user.first_name }}!

+ {% if alerts %} +
    + {% for alert in alerts %} +
  • {{ alert.message }}
  • + {% endfor %} +
+ {% else %} +

No alerts right now.

+ {% endif %} +{% endblock %} diff --git a/tests/project/djls_app/templates/bench/translation_heavy.html b/tests/project/djls_app/templates/bench/translation_heavy.html new file mode 100644 index 00000000..58871ced --- /dev/null +++ b/tests/project/djls_app/templates/bench/translation_heavy.html @@ -0,0 +1,33 @@ +{% load i18n %} +
+ {% blocktrans with name=user.get_short_name %} + Welcome back, {{ name }}. + {% endblocktrans %} + + {% blocktrans trimmed with count=notifications|length %} + You have {{ count }} notification. + {% plural %} + You have {{ count }} notifications. + {% endblocktrans %} + +
+ {% csrf_token %} + + + +
+
+ +
+ {% for faq in faqs %} + {% blocktrans with question=faq.question answer=faq.answer %} + Question: {{ question }}. Answer: {{ answer }}. + {% endblocktrans %} + {% empty %} +

{% trans "No FAQs available." %}

+ {% endfor %} +
From ed2a58fc825f63bd55e13daeba274e9bf423ef0d Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 20 Sep 2025 08:10:47 -0500 Subject: [PATCH 2/2] boop --- crates/djls-semantic/benches/validator.rs | 86 ++++++++++++++++++++++- crates/djls-templates/benches/parser.rs | 74 ++++++++++++++++++- 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/crates/djls-semantic/benches/validator.rs b/crates/djls-semantic/benches/validator.rs index 9c78fe11..4f323552 100644 --- a/crates/djls-semantic/benches/validator.rs +++ b/crates/djls-semantic/benches/validator.rs @@ -1,5 +1,7 @@ +use std::cell::RefCell; use std::collections::HashMap; use std::io; +use std::rc::Rc; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; @@ -53,6 +55,15 @@ impl BenchDb { .insert(path.clone(), contents.to_string()); File::new(self, path, 0) } + + fn set_file_contents(&mut self, file: File, contents: &str, revision: u64) { + let path = file.path(self); + self.sources + .lock() + .expect("sources lock poisoned") + .insert(path.clone(), contents.to_string()); + file.set_revision(self, revision); + } } #[salsa::db] @@ -93,7 +104,7 @@ fn bench_validation(c: &mut Criterion) { b.iter_batched( move || { let mut db = BenchDb::new(); - let file = db.file_with_contents(path.clone(), &contents); + let file = db.file_with_contents(path.clone(), contents.as_str()); (db, file) }, |(db, file)| { @@ -113,5 +124,76 @@ fn bench_validation(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, bench_validation); +fn bench_validation_incremental(c: &mut Criterion) { + let mut group = c.benchmark_group("validate_nodelist_incremental"); + group.sample_size(40); + group.measurement_time(Duration::from_secs(6)); + + for fixture in fixtures::lex_parse_fixtures().into_iter().take(3) { + let path = fixture.file_path(); + let contents = fixture.contents.clone(); + let alternate = format!( + "{}\n{{% comment %}}bench-toggle{{% endcomment %}}", + contents + ); + + // Cached validation benchmark + let mut warm_db = BenchDb::new(); + let warm_file = warm_db.file_with_contents(path.clone(), contents.as_str()); + if let Some(nodelist) = djls_templates::parse_template(&warm_db, warm_file) { + djls_semantic::validate_nodelist(&warm_db, nodelist); + let cached_name = format!("{} (cached)", fixture.name); + group.bench_function(cached_name, { + let warm_db = warm_db; + move |b| { + b.iter(|| { + if let Some(nodelist) = djls_templates::parse_template(&warm_db, warm_file) + { + djls_semantic::validate_nodelist(&warm_db, nodelist); + let errors = djls_semantic::validate_nodelist::accumulated::< + ValidationErrorAccumulator, + >(&warm_db, nodelist); + black_box(errors); + } + }); + } + }); + } + + // Incremental edit benchmark + let db = Rc::new(RefCell::new(BenchDb::new())); + let file = { + let mut db_mut = db.borrow_mut(); + db_mut.file_with_contents(path.clone(), contents.as_str()) + }; + let incremental_name = format!("{} (edit)", fixture.name); + group.bench_function(incremental_name, move |b| { + let db = Rc::clone(&db); + let base = contents.clone(); + let alt = alternate.clone(); + let mut revision = 1u64; + let mut toggle = false; + + b.iter(|| { + let mut db_mut = db.borrow_mut(); + let text = if toggle { base.as_str() } else { alt.as_str() }; + toggle = !toggle; + db_mut.set_file_contents(file, text, revision); + revision = revision.wrapping_add(1); + + if let Some(nodelist) = djls_templates::parse_template(&*db_mut, file) { + djls_semantic::validate_nodelist(&*db_mut, nodelist); + let errors = djls_semantic::validate_nodelist::accumulated::< + ValidationErrorAccumulator, + >(&*db_mut, nodelist); + black_box(errors); + } + }); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_validation, bench_validation_incremental); criterion_main!(benches); diff --git a/crates/djls-templates/benches/parser.rs b/crates/djls-templates/benches/parser.rs index bc242a53..876362a4 100644 --- a/crates/djls-templates/benches/parser.rs +++ b/crates/djls-templates/benches/parser.rs @@ -1,5 +1,7 @@ +use std::cell::RefCell; use std::collections::HashMap; use std::io; +use std::rc::Rc; use std::sync::Arc; use std::sync::Mutex; use std::time::Duration; @@ -50,6 +52,15 @@ impl BenchDb { .insert(path.clone(), contents.to_string()); File::new(self, path, 0) } + + fn set_file_contents(&mut self, file: File, contents: &str, revision: u64) { + let path = file.path(self); + self.sources + .lock() + .expect("sources lock poisoned") + .insert(path.clone(), contents.to_string()); + file.set_revision(self, revision); + } } #[salsa::db] @@ -123,5 +134,66 @@ fn bench_parse_template(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, bench_lexing, bench_parse_template); +fn bench_incremental_parse(c: &mut Criterion) { + let mut group = c.benchmark_group("parse_template_incremental"); + group.sample_size(40); + group.measurement_time(Duration::from_secs(6)); + + for fixture in fixtures::lex_parse_fixtures().into_iter().take(3) { + let path = fixture.file_path(); + let contents = fixture.contents.clone(); + let alternate = format!( + "{}\n{{% comment %}}bench-toggle{{% endcomment %}}", + contents + ); + + // Cached retrieval benchmark + let mut warm_db = BenchDb::new(); + let warm_file = warm_db.file_with_contents(path.clone(), contents.as_str()); + let _ = djls_templates::parse_template(&warm_db, warm_file); + let cached_name = format!("{} (cached)", fixture.name); + group.bench_function(cached_name, { + let warm_db = warm_db; + move |b| { + b.iter(|| { + black_box(djls_templates::parse_template(&warm_db, warm_file)); + }); + } + }); + + // Incremental edit benchmark + let db = Rc::new(RefCell::new(BenchDb::new())); + let file = { + let mut db_mut = db.borrow_mut(); + db_mut.file_with_contents(path.clone(), contents.as_str()) + }; + let incremental_name = format!("{} (edit)", fixture.name); + group.bench_function(incremental_name, move |b| { + let db = Rc::clone(&db); + let base = contents.clone(); + let alt = alternate.clone(); + let mut revision = 1u64; + let mut toggle = false; + + b.iter(|| { + let mut db_mut = db.borrow_mut(); + let text = if toggle { base.as_str() } else { alt.as_str() }; + toggle = !toggle; + db_mut.set_file_contents(file, text, revision); + revision = revision.wrapping_add(1); + let result = djls_templates::parse_template(&*db_mut, file); + black_box(result); + }); + }); + } + + group.finish(); +} + +criterion_group!( + benches, + bench_lexing, + bench_parse_template, + bench_incremental_parse +); criterion_main!(benches);