From add06dafdf874b1b419e5eef918c6b1131ab09fd Mon Sep 17 00:00:00 2001 From: HampusM Date: Sat, 25 Mar 2023 17:42:28 +0100 Subject: perf: improve XML deserialization speed --- src/command.rs | 559 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 497 insertions(+), 62 deletions(-) (limited to 'src/command.rs') diff --git a/src/command.rs b/src/command.rs index c7ada95..2ba92ea 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,5 +1,14 @@ //! OpenGL command. -use crate::xml::element::{Elements, FromElements}; +use quick_xml::events::BytesStart; + +use crate::deserialization::{ + Deserialize, + DeserializeWithFn, + Deserializer, + DeserializerError, + IgnoreEnd, + ResultExt, +}; /// A command. #[derive(Debug, Clone, PartialEq, Eq)] @@ -38,25 +47,21 @@ impl Command } } -impl FromElements for Command +impl Deserialize for Command { type Error = Error; - fn from_elements( - elements: &crate::xml::element::Elements, + fn deserialize( + start: &BytesStart, + deserializer: &mut TDeserializer, ) -> Result { - let proto_element = elements - .get_first_tagged_element("proto") - .ok_or(Self::Error::MissingPrototype)?; + let prototype = deserializer.de_tag::("proto", IgnoreEnd::No)?; - let prototype = Prototype::from_elements(proto_element.child_elements())?; + let parameters = deserializer.de_tag_list::("param")?; - let parameters = elements - .get_all_tagged_elements_with_name("param") - .into_iter() - .map(|param_element| Parameter::from_elements(param_element.child_elements())) - .collect::, _>>()?; + deserializer + .skip_to_tag_end(std::str::from_utf8(start.name().as_ref()).unwrap())?; Ok(Self { prototype, @@ -80,6 +85,10 @@ pub enum Error /// Invalid parameter. #[error("Invalid parameter")] InvalidParameter(#[from] ParameterError), + + /// Deserialization failed. + #[error("Deserialization failed")] + DeserializationFailed(#[from] DeserializerError), } /// A command prototype. @@ -116,23 +125,22 @@ impl Prototype } } -impl FromElements for Prototype +impl Deserialize for Prototype { type Error = PrototypeError; - fn from_elements( - elements: &crate::xml::element::Elements, + fn deserialize( + _start: &BytesStart, + deserializer: &mut TDeserializer, ) -> Result { - let name = elements - .get_first_tagged_element("name") - .ok_or(Self::Error::MissingName)? - .child_elements() - .get_first_text_element() - .cloned() - .unwrap_or_default(); + let return_type = deserialize_type::(deserializer)?; - let return_type = find_type(elements); + let name = deserializer.de_tag_with::<_, _, DeserializeWithFn<_, _, _>>( + "name", + IgnoreEnd::No, + |_, deserializer| deserializer.de_text(), + )?; Ok(Self { name, return_type }) } @@ -145,6 +153,14 @@ pub enum PrototypeError /// No 'name' element was found. #[error("No 'name' element was found")] MissingName, + + /// No return type was found. + #[error("No return type was found")] + MissingReturnType, + + /// Deserialization failed. + #[error("Deserialization failed")] + DeserializationFailed(#[from] DeserializerError), } /// A command parameter. @@ -181,21 +197,22 @@ impl Parameter } } -impl FromElements for Parameter +impl Deserialize for Parameter { type Error = ParameterError; - fn from_elements(elements: &Elements) -> Result + fn deserialize( + _start: &BytesStart, + deserializer: &mut TDeserializer, + ) -> Result { - let name = elements - .get_first_tagged_element("name") - .ok_or(Self::Error::MissingName)? - .child_elements() - .get_first_text_element() - .cloned() - .unwrap_or_default(); + let ty = deserialize_type::(deserializer)?; - let ty = find_type(elements); + let name = deserializer.de_tag_with::<_, _, DeserializeWithFn<_, _, _>>( + "name", + IgnoreEnd::No, + |_, deserializer| deserializer.de_text(), + )?; Ok(Self { name, ty }) } @@ -208,46 +225,47 @@ pub enum ParameterError /// No 'name' element was found. #[error("No 'name' element was found")] MissingName, + + /// Deserialization failed. + #[error("Deserialization failed")] + DeserializationFailed(#[from] DeserializerError), } -fn find_type(elements: &Elements) -> String +fn deserialize_type(deserializer: &mut impl Deserializer) -> Result +where + Err: From, { - let text_type_parts = elements - .get_all_text_elements() - .into_iter() - .map(|text_type_part| text_type_part.trim()) - .filter(|text_type_part| !text_type_part.is_empty()) - .collect::>(); + let type_before = deserializer.de_text().try_event()?; - let opt_ptype_text = get_ptype_text(elements); + let type_ptype = deserializer + .de_tag_with::<_, _, DeserializeWithFn<_, _, _>>( + "ptype", + IgnoreEnd::No, + |_, deserializer| deserializer.de_text(), + ) + .try_event()?; - opt_ptype_text.map_or_else( - || join_space_strs(text_type_parts.iter()), - |ptype_text| { - let Some(first_part) = text_type_parts.first() else { - return ptype_text.clone(); - }; + let type_after = deserializer.de_text().try_event()?; - let before = if *first_part == "const" { "const " } else { "" }; + let type_before_after = [type_before.clone(), type_after.clone()] + .into_iter() + .flatten(); - let after_start_index = usize::from(*first_part == "const"); + Ok(type_ptype.map_or_else( + || join_space_strs(type_before_after), + |ptype_text| { + let before = type_before + .map(|before| format!("{before} ")) + .unwrap_or_default(); format!( - "{before}{ptype_text} {}", - text_type_parts - .get(after_start_index..) - .map(|parts| join_space_strs(parts.iter())) + "{before}{ptype_text}{}", + type_after + .map(|after| format!(" {after}")) .unwrap_or_default() ) }, - ) -} - -fn get_ptype_text(elements: &Elements) -> Option<&String> -{ - let ptype_element = elements.get_first_tagged_element("ptype")?; - - ptype_element.child_elements().get_first_text_element() + )) } fn join_space_strs(strings: Strings) -> String @@ -261,3 +279,420 @@ where .collect::>() .join(" ") } + +#[cfg(test)] +mod tests +{ + use pretty_assertions::assert_str_eq; + use quick_xml::events::Event; + use ridicule::mock; + use ridicule::predicate::{always, eq, function}; + + use super::*; + + mock! { + MockDeserializer {} + + impl Deserializer for MockDeserializer { + fn de_tag( + &mut self, + tag_name: &str, + ignore_end: IgnoreEnd, + ) -> Result; + + fn de_tag_with( + &mut self, + tag_name: &str, + ignore_end: IgnoreEnd, + deserialize: DeserializeFn, + ) -> Result + where + Err: std::error::Error + Send + Sync + 'static, + DeserializeFn: FnOnce(&BytesStart, &mut MockDeserializer) -> Result; + + fn de_tag_list( + &mut self, + tag_name: &str + ) -> Result, DeserializerError>; + + fn de_text(&mut self) -> Result; + + fn skip_to_tag_start(&mut self, tag_name: &str) -> Result<(), DeserializerError>; + + fn skip_to_tag_end(&mut self, tag_name: &str) -> Result<(), DeserializerError>; + } + } + + #[test] + fn deserialize_prototype_works_with_ptype() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer + .expect_de_text() + .returning(|_| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "text".to_string(), + found_event: Event::Start(BytesStart::new("ptype")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("GLuint".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("ptype"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "text".to_string(), + found_event: Event::Start(BytesStart::new("name")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("glDoComplicatedThing".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("name"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + let prototype = + Prototype::deserialize(&BytesStart::new("proto"), &mut mock_deserializer) + .expect("Expected Ok"); + + assert_str_eq!(prototype.name, "glDoComplicatedThing"); + assert_str_eq!(prototype.return_type, "GLuint"); + } + + #[test] + fn deserialize_prototype_works_with_text() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("void".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("ptype"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|_, _, _, _| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "start".to_string(), + found_event: Event::Start(BytesStart::new("name")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "text".to_string(), + found_event: Event::Start(BytesStart::new("name")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("glDoSomeThing".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("name"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + let prototype = + Prototype::deserialize(&BytesStart::new("proto"), &mut mock_deserializer) + .expect("Expected Ok"); + + assert_str_eq!(prototype.name, "glDoSomeThing"); + assert_str_eq!(prototype.return_type, "void"); + } + + #[test] + fn deserialize_parameter_works_with_ptype_only() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer + .expect_de_text() + .returning(|_| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "text".to_string(), + found_event: Event::Start(BytesStart::new("ptype")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("GLenum".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("ptype"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "text".to_string(), + found_event: Event::Start(BytesStart::new("name")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("value".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("name"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + let parameter = + Parameter::deserialize(&BytesStart::new("param"), &mut mock_deserializer) + .expect("Expected Ok"); + + assert_str_eq!(parameter.name, "value"); + assert_str_eq!(parameter.ty, "GLenum"); + } + + #[test] + fn deserialize_parameter_works_with_ptype_and_text_after() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer + .expect_de_text() + .returning(|_| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "text".to_string(), + found_event: Event::Start(BytesStart::new("ptype")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("GLchar".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("ptype"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("*".to_string())) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("source".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("name"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + let parameter = + Parameter::deserialize(&BytesStart::new("param"), &mut mock_deserializer) + .expect("Expected Ok"); + + assert_str_eq!(parameter.name, "source"); + assert_str_eq!(parameter.ty, "GLchar *"); + } + + #[test] + fn deserialize_parameter_works_with_ptype_and_text_before_and_after() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("const".to_string())) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("GLchar".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("ptype"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("*".to_string())) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("name".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("name"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + let parameter = + Parameter::deserialize(&BytesStart::new("param"), &mut mock_deserializer) + .expect("Expected Ok"); + + assert_str_eq!(parameter.name, "name"); + assert_str_eq!(parameter.ty, "const GLchar *"); + } + + #[test] + fn deserialize_parameter_works_with_text() + { + let mut mock_deserializer = MockDeserializer::new(); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("void *".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("ptype"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|_, _, _, _| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "start".to_string(), + found_event: Event::Start(BytesStart::new("name")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| { + Err(DeserializerError::UnexpectedEvent { + expected_event_name: "text".to_string(), + found_event: Event::Start(BytesStart::new("name")), + }) + }) + .times(1); + + mock_deserializer + .expect_de_text() + .returning(|_| Ok("pixels".to_string())) + .times(1); + + mock_deserializer + .expect_de_tag_with::>() + .with( + eq("name"), + function(|ignore_end| matches!(ignore_end, IgnoreEnd::No)), + always(), + ) + .returning(|deserializer, tag_name, _, func| { + func(&BytesStart::new(tag_name), deserializer) + }) + .times(1); + + let parameter = + Parameter::deserialize(&BytesStart::new("param"), &mut mock_deserializer) + .expect("Expected Ok"); + + assert_str_eq!(parameter.name, "pixels"); + assert_str_eq!(parameter.ty, "void *"); + } +} -- cgit v1.2.3-18-g5258