From d7ae02552b34d133f6d29e6f24e25a24446b17fb Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Mon, 17 Oct 2022 17:44:14 +0200 Subject: [PATCH] Switch to `base64-simd` This should improve the performance of parsing and formatting Base64. My machine unfortunately doesn't have AVX2, so while it made a difference, it wasn't as drastic as it could've been. Base64 however is the slowest part when parsing splits at the moment, unless images need to be resized, so it makes sense to improve the performance by using SIMD. --- Cargo.toml | 6 +++-- src/layout/parser/mod.rs | 43 +++++++++++++++++++------------ src/run/parser/livesplit.rs | 21 ++++++++++----- src/run/parser/llanfair_gered.rs | 24 ++++++++++++----- src/run/saver/livesplit.rs | 44 +++++++++++++++++++------------- src/settings/image/mod.rs | 33 +++++++++++++++++++----- 6 files changed, 115 insertions(+), 56 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 97ab1b25f..79b2f9cbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ members = ["capi", "capi/bind_gen", "crates/*"] [dependencies] # core -base64 = { version = "0.13.0", default-features = false, features = ["alloc"] } +base64-simd = { version = "0.7.0", default-features = false } bytemuck = { version = "1.9.1", default-features = false, features = ["derive"] } cfg-if = "1.0.0" itoa = { version = "1.0.3", default-features = false } @@ -115,13 +115,15 @@ criterion = "0.4.0" [features] default = ["image-shrinking", "std"] std = [ + "base64-simd/detect", + "base64-simd/std", "image", "libc", "livesplit-hotkey/std", "memchr/std", "rustybuzz?/std", - "serde/std", "serde_json/std", + "serde/std", "simdutf8/std", "snafu/std", "time/formatting", diff --git a/src/layout/parser/mod.rs b/src/layout/parser/mod.rs index 1c3452254..e845ac9c0 100644 --- a/src/layout/parser/mod.rs +++ b/src/layout/parser/mod.rs @@ -1,5 +1,7 @@ //! Provides the parser for layout files of the original LiveSplit. +use base64_simd::Base64; + use super::{Component, Layout, LayoutDirection}; use crate::{ component::{separator, timer::DeltaGradient}, @@ -19,7 +21,7 @@ use crate::{ Reader, }, }; -use core::str; +use core::{mem::MaybeUninit, num::ParseIntError, str}; mod blank_space; mod current_comparison; @@ -61,7 +63,7 @@ pub enum Error { /// Failed to parse an integer. ParseInt { /// The underlying error. - source: core::num::ParseIntError, + source: ParseIntError, }, /// Failed to parse a boolean. ParseBool, @@ -91,8 +93,8 @@ impl From for Error { } } -impl From for Error { - fn from(source: core::num::ParseIntError) -> Self { +impl From for Error { + fn from(source: ParseIntError) -> Self { Self::ParseInt { source } } } @@ -151,7 +153,12 @@ impl GradientType for DeltaGradientKind { fn build(self, first: Color, second: Color) -> Self::Built { match self { DeltaGradientKind::Transparent => Gradient::Transparent.into(), - DeltaGradientKind::Plain => Gradient::Plain(first).into(), + DeltaGradientKind::Plain => if first.alpha == 0.0 { + Gradient::Transparent + } else { + Gradient::Plain(first) + } + .into(), DeltaGradientKind::Vertical => Gradient::Vertical(first, second).into(), DeltaGradientKind::Horizontal => Gradient::Horizontal(first, second).into(), DeltaGradientKind::PlainWithDeltaColor => DeltaGradient::DeltaPlain, @@ -167,13 +174,10 @@ impl GradientType for GradientKind { GradientKind::Transparent } fn parse(kind: &str) -> Result { - // FIXME: Implement delta color support properly: - // https://github.com/LiveSplit/livesplit-core/issues/380 - Ok(match kind { - "Plain" | "PlainWithDeltaColor" => GradientKind::Plain, - "Vertical" | "VerticalWithDeltaColor" => GradientKind::Vertical, - "Horizontal" | "HorizontalWithDeltaColor" => GradientKind::Horizontal, + "Plain" => GradientKind::Plain, + "Vertical" => GradientKind::Vertical, + "Horizontal" => GradientKind::Horizontal, _ => return Err(Error::ParseGradientType), }) } @@ -297,7 +301,7 @@ where }) } -fn font(reader: &mut Reader<'_>, font_buf: &mut Vec, f: F) -> Result<()> +fn font(reader: &mut Reader<'_>, font_buf: &mut Vec>, f: F) -> Result<()> where F: FnOnce(Font), { @@ -317,11 +321,18 @@ where // The full definition can be found here: // https://referencesource.microsoft.com/#System.Drawing/commonui/System/Drawing/Advanced/Font.cs,130 - let rem = text.get(304..).ok_or(Error::ParseFont)?; - font_buf.clear(); - base64::decode_config_buf(rem, base64::STANDARD, font_buf).map_err(|_| Error::ParseFont)?; + let rem = text.as_bytes().get(304..).ok_or(Error::ParseFont)?; + + font_buf.resize( + Base64::STANDARD.estimated_decoded_length(rem.len()), + MaybeUninit::uninit(), + ); + + let decoded = Base64::STANDARD + .decode(rem, base64_simd::OutBuf::uninit(font_buf)) + .map_err(|_| Error::ParseFont)?; - let mut cursor = font_buf.get(1..).ok_or(Error::ParseFont)?.iter(); + let mut cursor = decoded.get(1..).ok_or(Error::ParseFont)?.iter(); // Strings are encoded as varint for the length + the UTF-8 string data. let mut len = 0; diff --git a/src/run/parser/livesplit.rs b/src/run/parser/livesplit.rs index 8543cc015..44d7cf3e8 100644 --- a/src/run/parser/livesplit.rs +++ b/src/run/parser/livesplit.rs @@ -14,7 +14,8 @@ use crate::{ AtomicDateTime, DateTime, Run, RunMetadata, Segment, Time, TimeSpan, }; use alloc::borrow::Cow; -use core::str; +use base64_simd::Base64; +use core::{mem::MaybeUninit, str}; use time::{Date, PrimitiveDateTime}; /// The Error type for splits files that couldn't be parsed by the LiveSplit @@ -133,17 +134,23 @@ fn parse_date_time(text: &str) -> Result { .ok_or(Error::ParseDate) } -fn image(reader: &mut Reader<'_>, image_buf: &mut Vec, f: F) -> Result<()> +fn image(reader: &mut Reader<'_>, image_buf: &mut Vec>, f: F) -> Result<()> where F: FnOnce(&[u8]), { text_as_escaped_string_err(reader, |text| { if text.len() >= 216 { - image_buf.clear(); - if base64::decode_config_buf(&text.as_bytes()[212..], base64::STANDARD, image_buf) - .is_ok() + let src = &text.as_bytes()[212..]; + + image_buf.resize( + Base64::STANDARD.estimated_decoded_length(src.len()), + MaybeUninit::uninit(), + ); + + if let Ok(decoded) = + Base64::STANDARD.decode(src, base64_simd::OutBuf::uninit(image_buf)) { - f(&image_buf[2..image_buf.len() - 1]); + f(&decoded[2..decoded.len() - 1]); return Ok(()); } } @@ -277,7 +284,7 @@ fn parse_metadata( fn parse_segment( version: Version, reader: &mut Reader<'_>, - image_buf: &mut Vec, + image_buf: &mut Vec>, run: &mut Run, ) -> Result { let mut segment = Segment::new(""); diff --git a/src/run/parser/llanfair_gered.rs b/src/run/parser/llanfair_gered.rs index 0b7202d95..9c77cd64d 100644 --- a/src/run/parser/llanfair_gered.rs +++ b/src/run/parser/llanfair_gered.rs @@ -1,5 +1,7 @@ //! Provides the parser for splits files used by Gered's Llanfair fork. +use core::mem::MaybeUninit; + #[cfg(feature = "std")] use crate::util::byte_parsing::big_endian::strip_u32; #[cfg(feature = "std")] @@ -83,20 +85,30 @@ where #[cfg(feature = "std")] fn image( reader: &mut Reader<'_>, - raw_buf: &mut Vec, + raw_buf: &mut Vec>, png_buf: &mut Vec, mut f: F, ) -> Result<()> where F: FnMut(&[u8]), { + use base64_simd::Base64; + single_child(reader, "ImageIcon", |reader, _| { let (width, height, image) = text_as_str_err::<_, _, Error>(reader, |t| { - raw_buf.clear(); - base64::decode_config_buf(&*t, base64::STANDARD, raw_buf).map_err(|_| Error::Image)?; + let src = t.as_bytes(); + + raw_buf.resize( + Base64::STANDARD.estimated_decoded_length(src.len()), + MaybeUninit::uninit(), + ); + + let decoded = Base64::STANDARD + .decode(src, base64_simd::OutBuf::uninit(raw_buf)) + .map_err(|_| Error::Image)?; let (width, height); - let mut cursor = raw_buf.get(0xD1..).ok_or(Error::Image)?; + let mut cursor = decoded.get(0xD1..).ok_or(Error::Image)?; height = strip_u32(&mut cursor).ok_or(Error::Image)?; width = strip_u32(&mut cursor).ok_or(Error::Image)?; @@ -108,7 +120,7 @@ where Ok(( width, height, - raw_buf.get(0xFE..0xFE + len).ok_or(Error::Image)?, + decoded.get(0xFE..0xFE + len).ok_or(Error::Image)?, )) })?; @@ -126,7 +138,7 @@ where fn parse_segment( total_time: &mut TimeSpan, reader: &mut Reader<'_>, - _raw_buf: &mut Vec, + _raw_buf: &mut Vec>, _png_buf: &mut Vec, ) -> Result { single_child(reader, "Segment", |reader, _| { diff --git a/src/run/saver/livesplit.rs b/src/run/saver/livesplit.rs index b421c0c76..391454fd8 100644 --- a/src/run/saver/livesplit.rs +++ b/src/run/saver/livesplit.rs @@ -32,7 +32,8 @@ use crate::{ DateTime, Run, Time, Timer, TimerPhase, }; use alloc::borrow::Cow; -use core::fmt; +use base64_simd::Base64; +use core::{fmt, mem::MaybeUninit}; use time::UtcOffset; const LSS_IMAGE_HEADER: &[u8; 156] = include_bytes!("lss_image_header.bin"); @@ -65,27 +66,34 @@ fn image( writer: &mut Writer, tag: &str, image: &Image, - base64_buf: &mut String, + base64_buf: &mut Vec>, image_buf: &mut Cow<'_, [u8]>, ) -> fmt::Result { writer.tag(tag, |tag| { let image_data = image.data(); - if !image_data.is_empty() { - let len = image_data.len(); - let image_buf = image_buf.to_mut(); - image_buf.truncate(LSS_IMAGE_HEADER.len()); - image_buf.reserve(len + 6); - image_buf.extend(&(len as u32).to_le_bytes()); - image_buf.push(0x2); - image_buf.extend(image_data); - image_buf.push(0xB); - base64_buf.clear(); - base64::encode_config_buf(image_buf, base64::STANDARD, base64_buf); - if !base64_buf.is_empty() { - return tag.content(|writer| writer.cdata(Text::new_escaped(base64_buf))); - } + if image_data.is_empty() { + return Ok(()); } - Ok(()) + + let len = image_data.len(); + let image_buf = image_buf.to_mut(); + image_buf.truncate(LSS_IMAGE_HEADER.len()); + image_buf.reserve(len + 6); + image_buf.extend(&(len as u32).to_le_bytes()); + image_buf.push(0x2); + image_buf.extend(image_data); + image_buf.push(0xB); + + base64_buf.resize( + Base64::STANDARD.encoded_length(image_buf.len()), + MaybeUninit::uninit(), + ); + + let encoded = Base64::STANDARD + .encode_as_str(image_buf, base64_simd::OutBuf::uninit(base64_buf)) + .unwrap(); + + tag.content(|writer| writer.cdata(Text::new_escaped(encoded))) }) } @@ -164,7 +172,7 @@ pub fn save_timer(timer: &Timer, writer: W) -> fmt::Result { pub fn save_run(run: &Run, writer: W) -> fmt::Result { let writer = &mut Writer::new_with_default_header(writer)?; - let base64_buf = &mut String::new(); + let base64_buf = &mut Vec::new(); let image_buf = &mut Cow::Borrowed(&LSS_IMAGE_HEADER[..]); writer.tag_with_content("Run", [("version", Text::new_escaped("1.8.0"))], |writer| { diff --git a/src/settings/image/mod.rs b/src/settings/image/mod.rs index 7d751d803..105e063f7 100644 --- a/src/settings/image/mod.rs +++ b/src/settings/image/mod.rs @@ -1,5 +1,5 @@ use crate::platform::prelude::*; -use base64::{display::Base64Display, STANDARD}; +use base64_simd::Base64; use core::{ ops::Deref, sync::atomic::{AtomicUsize, Ordering}, @@ -54,10 +54,26 @@ impl Serialize for ImageData { { if serializer.is_human_readable() { if !self.0.is_empty() { - serializer.collect_str(&format_args!( - "data:;base64,{}", - Base64Display::with_config(&self.0, STANDARD) - )) + let mut buf = String::from("data:;base64,"); + + // SAFETY: We encode Base64 to the end of the string, which is + // always valid UTF-8. Once we've written it, we simply increase + // the length of the buffer by the amount of bytes written. + unsafe { + let buf = buf.as_mut_vec(); + let encoded_len = Base64::STANDARD.encoded_length(self.0.len()); + buf.reserve_exact(encoded_len); + let additional_len = Base64::STANDARD + .encode( + &self.0, + base64_simd::OutBuf::uninit(buf.spare_capacity_mut()), + ) + .unwrap() + .len(); + buf.set_len(buf.len() + additional_len); + } + + serializer.serialize_str(&buf) } else { serializer.serialize_str("") } @@ -77,8 +93,11 @@ impl<'de> Deserialize<'de> for ImageData { if data.is_empty() { Ok(ImageData(Box::new([]))) } else if let Some(encoded_image_data) = data.strip_prefix("data:;base64,") { - let image_data = base64::decode(encoded_image_data).map_err(de::Error::custom)?; - Ok(ImageData(image_data.into_boxed_slice())) + let image_data = Base64::STANDARD + .decode_to_boxed_bytes(encoded_image_data.as_bytes()) + .map_err(de::Error::custom)?; + + Ok(ImageData(image_data)) } else { Err(de::Error::custom("Invalid Data URL for image")) }