Skip to content

Commit

Permalink
Switch to base64-simd
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
CryZe committed Oct 17, 2022
1 parent 209c0a9 commit d7ae025
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 56 deletions.
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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",
Expand Down
43 changes: 27 additions & 16 deletions src/layout/parser/mod.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -19,7 +21,7 @@ use crate::{
Reader,
},
};
use core::str;
use core::{mem::MaybeUninit, num::ParseIntError, str};

mod blank_space;
mod current_comparison;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -91,8 +93,8 @@ impl From<XmlError> for Error {
}
}

impl From<core::num::ParseIntError> for Error {
fn from(source: core::num::ParseIntError) -> Self {
impl From<ParseIntError> for Error {
fn from(source: ParseIntError) -> Self {
Self::ParseInt { source }
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -167,13 +174,10 @@ impl GradientType for GradientKind {
GradientKind::Transparent
}
fn parse(kind: &str) -> Result<Self> {
// 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),
})
}
Expand Down Expand Up @@ -297,7 +301,7 @@ where
})
}

fn font<F>(reader: &mut Reader<'_>, font_buf: &mut Vec<u8>, f: F) -> Result<()>
fn font<F>(reader: &mut Reader<'_>, font_buf: &mut Vec<MaybeUninit<u8>>, f: F) -> Result<()>
where
F: FnOnce(Font),
{
Expand All @@ -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;
Expand Down
21 changes: 14 additions & 7 deletions src/run/parser/livesplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -133,17 +134,23 @@ fn parse_date_time(text: &str) -> Result<DateTime> {
.ok_or(Error::ParseDate)
}

fn image<F>(reader: &mut Reader<'_>, image_buf: &mut Vec<u8>, f: F) -> Result<()>
fn image<F>(reader: &mut Reader<'_>, image_buf: &mut Vec<MaybeUninit<u8>>, 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(());
}
}
Expand Down Expand Up @@ -277,7 +284,7 @@ fn parse_metadata(
fn parse_segment(
version: Version,
reader: &mut Reader<'_>,
image_buf: &mut Vec<u8>,
image_buf: &mut Vec<MaybeUninit<u8>>,
run: &mut Run,
) -> Result<Segment> {
let mut segment = Segment::new("");
Expand Down
24 changes: 18 additions & 6 deletions src/run/parser/llanfair_gered.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down Expand Up @@ -83,20 +85,30 @@ where
#[cfg(feature = "std")]
fn image<F>(
reader: &mut Reader<'_>,
raw_buf: &mut Vec<u8>,
raw_buf: &mut Vec<MaybeUninit<u8>>,
png_buf: &mut Vec<u8>,
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)?;

Expand All @@ -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)?,
))
})?;

Expand All @@ -126,7 +138,7 @@ where
fn parse_segment(
total_time: &mut TimeSpan,
reader: &mut Reader<'_>,
_raw_buf: &mut Vec<u8>,
_raw_buf: &mut Vec<MaybeUninit<u8>>,
_png_buf: &mut Vec<u8>,
) -> Result<Segment> {
single_child(reader, "Segment", |reader, _| {
Expand Down
44 changes: 26 additions & 18 deletions src/run/saver/livesplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -65,27 +66,34 @@ fn image<W: fmt::Write>(
writer: &mut Writer<W>,
tag: &str,
image: &Image,
base64_buf: &mut String,
base64_buf: &mut Vec<MaybeUninit<u8>>,
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)))
})
}

Expand Down Expand Up @@ -164,7 +172,7 @@ pub fn save_timer<W: fmt::Write>(timer: &Timer, writer: W) -> fmt::Result {
pub fn save_run<W: fmt::Write>(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| {
Expand Down
33 changes: 26 additions & 7 deletions src/settings/image/mod.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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("")
}
Expand All @@ -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"))
}
Expand Down

0 comments on commit d7ae025

Please sign in to comment.