Skip to content

Commit

Permalink
Try not to use fmt::Formatters
Browse files Browse the repository at this point in the history
When you write an `fmt::Display`able type into a `fmt::Write` sink, a
`fmt::Formatter` is used to facilitate the writing. It stores a
`&dyn fmt::Write` reference to the sink, so less duplicated byte code
is generated, but any possible optimizations before void.

This PR lets rinja try to avoid using the normal `write!()` machinery.
Indeed, not `write!()` call is made directly anymore in the generated
code, but only though a `WriteWritable` helper trait. This aids the
runtime performance quite a bit:

```text
$ cd testing/benches
$ cargo bench --bench all

Big table               time:   [405.60 µs 406.46 µs 407.33 µs]
                        change: [-12.463% -12.239% -12.022%] (p = 0.00 < 0.05)
                        Performance has improved.

Teams                   time:   [200.00 ns 200.30 ns 200.61 ns]
                        change: [-51.953% -51.462% -51.036%] (p = 0.00 < 0.05)
                        Performance has improved.
```
  • Loading branch information
Kijewski committed Jul 22, 2024
1 parent 9826399 commit 68d4e50
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 147 deletions.
4 changes: 4 additions & 0 deletions rinja/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ with-warp = ["rinja_derive/with-warp"]

[dependencies]
rinja_derive = { version = "0.2.0", path = "../rinja_derive" }

itoa = "1.0.11"
ryu = "1.0.18"

humansize = { version = "2", optional = true }
num-traits = { version = "0.2.6", optional = true }
percent-encoding = { version = "2.1.0", optional = true }
Expand Down
205 changes: 200 additions & 5 deletions rinja/src/filters/escape.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::convert::Infallible;
use std::fmt::{self, Display, Formatter, Write};
use std::num::NonZeroU8;
use std::str;
use std::{borrow, str};

/// Marks a string (or other `Display` type) as safe
///
Expand Down Expand Up @@ -59,6 +59,13 @@ impl<T: fmt::Display, E: Escaper> fmt::Display for EscapeDisplay<T, E> {
}
}

impl<T: AsRef<str> + ?Sized, E: Escaper> FastWritable for EscapeDisplay<&T, E> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
self.1.write_escaped_str(dest, self.0.as_ref())
}
}

/// Alias for [`escape()`]
#[inline]
pub fn e(text: impl fmt::Display, escaper: impl Escaper) -> Result<Safe<impl Display>, Infallible> {
Expand Down Expand Up @@ -158,6 +165,10 @@ impl Escaper for Text {
}
}

/// Escapers are used to make generated text safe for printing in some context.
///
/// E.g. in an [`Html`] context, any and all generated text can be used in HTML/XML text nodes and
/// attributes, without for for maliciously injected data.
pub trait Escaper: Copy {
fn write_escaped_str<W: Write>(&self, fmt: W, string: &str) -> fmt::Result;

Expand Down Expand Up @@ -210,8 +221,6 @@ impl<'a, T: fmt::Display + ?Sized, E: Escaper> AutoEscape for &&AutoEscaper<'a,
/// Better safe than sorry!
pub trait HtmlSafe: fmt::Display {}

impl<T: HtmlSafe + ?Sized> HtmlSafe for &T {}

/// Don't escape HTML safe types
impl<'a, T: HtmlSafe + ?Sized> AutoEscape for &AutoEscaper<'a, T, Html> {
type Escaped = &'a T;
Expand Down Expand Up @@ -309,6 +318,20 @@ const _: () = {
NeedsEscaping(&'a T, E),
}

impl<T, E> FastWritable for Wrapped<'_, T, E>
where
T: AsRef<str> + fmt::Display + ?Sized,
E: Escaper,
{
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
match self {
Wrapped::Safe(t) => dest.write_str(t.as_ref()),
Wrapped::NeedsEscaping(t, e) => e.write_escaped_str(dest, t.as_ref()),
}
}
}

impl<T: fmt::Display + ?Sized, E: Escaper> fmt::Display for Wrapped<'_, T, E> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match *self {
Expand Down Expand Up @@ -424,6 +447,7 @@ mark_html_safe! {
std::num::NonZeroU64, std::num::NonZeroU128, std::num::NonZeroUsize,
}

impl<T: HtmlSafe + ?Sized> HtmlSafe for &T {}
impl<T: HtmlSafe + ?Sized> HtmlSafe for Box<T> {}
impl<T: HtmlSafe + ?Sized> HtmlSafe for std::cell::Ref<'_, T> {}
impl<T: HtmlSafe + ?Sized> HtmlSafe for std::cell::RefMut<'_, T> {}
Expand All @@ -435,13 +459,169 @@ impl<T: HtmlSafe + ?Sized> HtmlSafe for std::sync::RwLockWriteGuard<'_, T> {}
impl<T: HtmlSafe> HtmlSafe for std::num::Wrapping<T> {}
impl<T: fmt::Display> HtmlSafe for HtmlSafeOutput<T> {}

impl<T> HtmlSafe for std::borrow::Cow<'_, T>
impl<T> HtmlSafe for borrow::Cow<'_, T>
where
T: HtmlSafe + std::borrow::ToOwned + ?Sized,
T: HtmlSafe + borrow::ToOwned + ?Sized,
T::Owned: HtmlSafe,
{
}

impl<T: HtmlSafe> HtmlSafe for std::pin::Pin<&T> {}

/// Used internally by rinja to select the appropriate [`write!()`] mechanism
pub struct Writable<'a, S: ?Sized>(pub &'a S);

/// Used internally by rinja to select the appropriate [`write!()`] mechanism
pub trait WriteWritable {
fn rinja_write<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result;
}

/// Used internally by rinja to speed up writing some types.
///
/// Types implementing this trait can be written without needing to employ an [`fmt::Formatter`].
pub trait FastWritable {
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result;
}

const _: () = {
macro_rules! specialize_ref {
($T:ident => $($ty:ty)*) => { $(
impl<$T: FastWritable + ?Sized> FastWritable for $ty {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
<$T>::write_into(self, dest)
}
}
)* };
}

specialize_ref! {
T =>
&T
Box<T>
std::cell::Ref<'_, T>
std::cell::RefMut<'_, T>
std::rc::Rc<T>
std::sync::Arc<T>
std::sync::MutexGuard<'_, T>
std::sync::RwLockReadGuard<'_, T>
std::sync::RwLockWriteGuard<'_, T>
}

impl<T: FastWritable + ToOwned> FastWritable for borrow::Cow<'_, T> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
T::write_into(self.as_ref(), dest)
}
}

impl<T: FastWritable> FastWritable for std::pin::Pin<&T> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
T::write_into(self, dest)
}
}

macro_rules! specialize_int {
($($ty:ty)*) => { $(
impl FastWritable for $ty {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
dest.write_str(itoa::Buffer::new().format(*self))
}
}
)* };
}

specialize_int!(
u8 u16 u32 u64 u128 usize
i8 i16 i32 i64 i128 isize
);

macro_rules! specialize_nz_int {
($($id:ident)*) => { $(
impl FastWritable for core::num::$id {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
dest.write_str(itoa::Buffer::new().format(self.get()))
}
}
)* };
}

specialize_nz_int!(
NonZeroU8 NonZeroU16 NonZeroU32 NonZeroU64 NonZeroU128 NonZeroUsize
NonZeroI8 NonZeroI16 NonZeroI32 NonZeroI64 NonZeroI128 NonZeroIsize
);

macro_rules! specialize_float {
($($ty:ty)*) => { $(
impl FastWritable for $ty {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
dest.write_str(ryu::Buffer::new().format(*self))
}
}
)* };
}

specialize_float!(f32 f64);

impl FastWritable for str {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
dest.write_str(self)
}
}

impl FastWritable for String {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
dest.write_str(self)
}
}

impl FastWritable for bool {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
dest.write_str(match self {
true => "true",
false => "false",
})
}
}

impl FastWritable for char {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
dest.write_char(*self)
}
}

impl FastWritable for fmt::Arguments<'_> {
fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
match self.as_str() {
Some(s) => dest.write_str(s),
None => dest.write_fmt(*self),
}
}
}

impl<'a, S: FastWritable + ?Sized> WriteWritable for &Writable<'a, S> {
#[inline]
fn rinja_write<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
self.0.write_into(dest)
}
}

impl<'a, S: fmt::Display + ?Sized> WriteWritable for &&Writable<'a, S> {
#[inline]
fn rinja_write<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result {
write!(dest, "{}", self.0)
}
}
};

#[test]
fn test_escape() {
assert_eq!(escape("", Html).unwrap().to_string(), "");
Expand Down Expand Up @@ -564,4 +744,19 @@ fn test_html_safe_marker() {
.to_string(),
"&#60;script&#62;",
);

assert_eq!(
(&&AutoEscaper::new(&Safe(std::pin::Pin::new(&Script1)), Html))
.rinja_auto_escape()
.unwrap()
.to_string(),
"<script>",
);
assert_eq!(
(&&AutoEscaper::new(&Safe(std::pin::Pin::new(&Script2)), Html))
.rinja_auto_escape()
.unwrap()
.to_string(),
"<script>",
);
}
11 changes: 9 additions & 2 deletions rinja/src/filters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
//!
//! Contains all the built-in filter functions for use in templates.
//! You can define your own filters, as well.
//!
//! ## Note
//!
//! All **result types of any filter function** in this module is **subject to change** at any
//! point, and is **not indicated by as semver breaking** version bump.
//! The traits [`AutoEscape`] and [`WriteWritable`] are used by [`rinja_derive`]'s generated code
//! to work with all compatible types.
mod escape;
#[cfg(feature = "serde_json")]
Expand All @@ -12,8 +19,8 @@ use std::convert::Infallible;
use std::fmt::{self, Write};

pub use escape::{
e, escape, safe, AutoEscape, AutoEscaper, Escaper, Html, HtmlSafe, HtmlSafeOutput, MaybeSafe,
Safe, Text, Unsafe,
e, escape, safe, AutoEscape, AutoEscaper, Escaper, FastWritable, Html, HtmlSafe,
HtmlSafeOutput, MaybeSafe, Safe, Text, Unsafe, Writable, WriteWritable,
};
#[cfg(feature = "humansize")]
use humansize::{ISizeFormatter, ToF64, DECIMAL};
Expand Down
Loading

0 comments on commit 68d4e50

Please sign in to comment.