Repository: tefra/xsdata

Issue:
XML parsing approximately 200 times slower than parsing to in-built xml.etree.ElementTree.Element
Using:

- xsdata 24.6
- Python 3.12.3

I'd like to ask about the performance of xsdata XML parsing. In my benchmarking I found it to be approximately 200 times slower than parsing to the in-built xml.etree.ElementTree.Element. I was expecting xsdata to be a little slower but this difference seems to be extreme. I tried both XmlEventHandler and LxmlEventHandler and got similar results. 

Is this difference expected? If it's expected then I apologise for raising this as an issue.

Test script:

```python
from pathlib import Path
import time
import xml.etree.ElementTree
import my_dataclass
from xsdata.formats.dataclass.parsers import XmlParser
from xsdata.formats.dataclass.parsers.handlers import XmlEventHandler

TEST_ITERATIONS = 2000
my_path = Path(__file__).parent
xml_file = my_path / "input.xml"

def main():
    with xml_file.open() as f:
        file_contents = f.read()

    start_time = time.time()
    using_in_built_element(file_contents, TEST_ITERATIONS)
    end_time = time.time()
    time_using_in_built_element = end_time - start_time
    print("Time using Python xml.etree.ElementTree.Element:", time_using_in_built_element)

    start_time = time.time()
    using_xsdata(file_contents, TEST_ITERATIONS)
    end_time = time.time()
    time_using_xsdata = end_time - start_time
    print("Time using xsdata:", time_using_xsdata)
    print ("Ratio:", time_using_xsdata / time_using_in_built_element)


def using_in_built_element(xml_string, iterations):
    for _ in range(iterations):
        xml_root = xml.etree.ElementTree.fromstring(xml_string)

def using_xsdata(xml_string, iterations):
    parser = XmlParser(handler=XmlEventHandler)
    for _ in range(iterations):
        record_as_obj = parser.from_string(xml_string, my_dataclass.LogRecord)

if __name__ == "__main__":
    main()
```


My results:
```
Time using Python xml.etree.ElementTree.Element: 0.2820000648498535
Time using xsdata: 55.9834668636322
Ratio: 198.52288648741876
```

I've attached this script, "input.xml" and "my_dataclass.py": [xsdata_xml_parse_performance.zip](https://github.com/user-attachments/files/15958630/xsdata_xml_parse_performance.zip)


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tefra/xsdata/pull/1066?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.74%. Comparing base [(`cce0a16`)](https://app.codecov.io/gh/tefra/xsdata/commit/cce0a16680b59ce50d1349f9cc0a891054838ad0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) to head [(`1f05835`)](https://app.codecov.io/gh/tefra/xsdata/commit/1f0583529c8f77c08b84888b047d7a1ffadccfa2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              main    #1066      +/-   ##
===========================================
- Coverage   100.00%   99.74%   -0.26%     
===========================================
  Files          115      115              
  Lines         9238     9259      +21     
  Branches      2179     2103      -76     
===========================================
- Hits          9238     9235       -3     
- Misses           0       20      +20     
- Partials         0        4       +4     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tefra/xsdata/pull/1066?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


Patch:
diff --git a/xsdata/formats/dataclass/parsers/nodes/union.py b/xsdata/formats/dataclass/parsers/nodes/union.py
index 68377336..6839ad58 100644
--- a/xsdata/formats/dataclass/parsers/nodes/union.py
+++ b/xsdata/formats/dataclass/parsers/nodes/union.py
@@ -1,7 +1,8 @@
 import copy
+import functools
 from contextlib import suppress
 from dataclasses import replace
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Dict, List, Optional, Tuple, Type
 
 from xsdata.exceptions import ParserError
 from xsdata.formats.dataclass.context import XmlContext
@@ -40,6 +41,7 @@ class UnionNode(XmlNode):
         "context",
         "level",
         "events",
+        "candidates",
     )
 
     def __init__(
@@ -60,8 +62,41 @@ class UnionNode(XmlNode):
         self.config = config
         self.context = context
         self.level = 0
+        self.candidates = self.filter_candidates()
         self.events: List[Tuple[str, str, Any, Any]] = []
 
+    def filter_candidates(self) -> List[Type]:
+        """Filter union candidates by fixed attributes."""
+        candidates = list(self.var.types)
+        fixed_attribute = functools.partial(
+            self.filter_fixed_attrs, parent_ns=target_uri(self.var.qname)
+        )
+
+        return list(filter(fixed_attribute, candidates))
+
+    def filter_fixed_attrs(self, candidate: Type, parent_ns: str) -> bool:
+        """Return whether the node attrs are incompatible with fixed attrs.
+
+        Args:
+            candidate: The candidate type
+            parent_ns: The parent namespace
+        """
+        if not self.context.class_type.is_model(candidate):
+            return not self.attrs
+
+        meta = self.context.build(candidate, parent_ns=parent_ns)
+        for qname, value in self.attrs.items():
+            var = meta.find_attribute(qname)
+            if not var or var.init:
+                continue
+
+            try:
+                ParserUtils.validate_fixed_value(meta, var, value)
+            except ParserError:
+                return False
+
+        return True
+
     def child(self, qname: str, attrs: Dict, ns_map: Dict, position: int) -> XmlNode:
         """Record the event for the child element.
 
@@ -120,29 +155,31 @@ class UnionNode(XmlNode):
         parent_namespace = target_uri(qname)
         config = replace(self.config, fail_on_converter_warnings=True)
 
-        for clazz in self.var.types:
-            candidate = None
+        for candidate in self.candidates:
+            result = None
             with suppress(Exception):
-                if self.context.class_type.is_model(clazz):
-                    self.context.build(clazz, parent_ns=parent_namespace)
+                if self.context.class_type.is_model(candidate):
+                    self.context.build(candidate, parent_ns=parent_namespace)
                     parser = NodeParser(
-                        config=config, context=self.context, handler=EventsHandler
+                        config=config,
+                        context=self.context,
+                        handler=EventsHandler,
                     )
-                    candidate = parser.parse(self.events, clazz)
+                    result = parser.parse(self.events, candidate)
                 else:
-                    candidate = ParserUtils.parse_var(
+                    result = ParserUtils.parse_var(
                         meta=self.meta,
                         var=self.var,
                         config=config,
                         value=text,
-                        types=[clazz],
+                        types=[candidate],
                         ns_map=self.ns_map,
                     )
 
-            score = self.context.class_type.score_object(candidate)
+            score = self.context.class_type.score_object(result)
             if score > max_score:
                 max_score = score
-                obj = candidate
+                obj = result
 
         if obj:
             objects.append((self.var.qname, obj))
diff --git a/xsdata/formats/dataclass/parsers/utils.py b/xsdata/formats/dataclass/parsers/utils.py
index 1a774210..1c3b1ba4 100644
--- a/xsdata/formats/dataclass/parsers/utils.py
+++ b/xsdata/formats/dataclass/parsers/utils.py
@@ -228,7 +228,7 @@ class ParserUtils:
         Special cases
             - float nans are never equal in python
             - strings with whitespaces, need trimming
-
+            - comparing raw str values
         """
 
         default_value = var.default() if callable(var.default) else var.default
@@ -244,6 +244,9 @@ class ParserUtils:
         ):
             return
 
+        if isinstance(value, str) and not isinstance(default_value, str):
+            default_value = converter.serialize(default_value, format=var.format)
+
         if default_value != value:
             raise ParserError(
                 f"Fixed value mismatch {meta.qname}:{var.qname}, `{default_value} != {value}`"


Test patch:
diff --git a/tests/formats/dataclass/parsers/nodes/test_union.py b/tests/formats/dataclass/parsers/nodes/test_union.py
index 15a44b51..67f533b9 100644
--- a/tests/formats/dataclass/parsers/nodes/test_union.py
+++ b/tests/formats/dataclass/parsers/nodes/test_union.py
@@ -1,4 +1,4 @@
-from dataclasses import make_dataclass
+from dataclasses import field, make_dataclass
 from typing import Union
 from unittest import TestCase
 
@@ -60,6 +60,41 @@ class UnionNodeTests(TestCase):
         self.assertEqual(0, node.level)
         self.assertEqual([("end", "bar", "text", "tail")], node.events)
 
+    def test_filter_fixed_attrs(self):
+        a = make_dataclass(
+            "A",
+            [("x", int, field(init=False, default=1, metadata={"type": "Attribute"}))],
+        )
+        b = make_dataclass(
+            "A",
+            [("x", int, field(init=False, default=2, metadata={"type": "Attribute"}))],
+        )
+
+        root = make_dataclass("Root", [("value", Union[a, b, int])])
+        meta = self.context.build(root)
+        var = next(meta.find_children("value"))
+        node = UnionNode(
+            meta=meta,
+            var=var,
+            position=0,
+            config=self.config,
+            context=self.context,
+            attrs={"x": 2},
+            ns_map={},
+        )
+        self.assertEqual([b], node.candidates)
+
+        node = UnionNode(
+            meta=meta,
+            var=var,
+            position=0,
+            config=self.config,
+            context=self.context,
+            attrs={},
+            ns_map={},
+        )
+        self.assertEqual([a, b, int], node.candidates)
+
     def test_bind_returns_best_matching_object(self):
         item = make_dataclass(
             "Item", [("value", str), ("a", int, attribute()), ("b", int, attribute())]
@@ -95,8 +130,15 @@ class UnionNodeTests(TestCase):
         self.assertIsNot(node.attrs, node.events[0][2])
         self.assertIs(node.ns_map, node.events[0][3])
 
-        node.events.clear()
-        node.attrs.clear()
+        node = UnionNode(
+            meta=meta,
+            var=var,
+            position=0,
+            config=self.config,
+            context=self.context,
+            attrs={},
+            ns_map=ns_map,
+        )
         self.assertTrue(node.bind("item", "1", None, objects))
         self.assertEqual(1, objects[-1][1])
 
diff --git a/tests/formats/dataclass/parsers/test_utils.py b/tests/formats/dataclass/parsers/test_utils.py
index 61844489..4dfaca43 100644
--- a/tests/formats/dataclass/parsers/test_utils.py
+++ b/tests/formats/dataclass/parsers/test_utils.py
@@ -7,7 +7,7 @@ from xsdata.formats.converter import ConverterFactory
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
-from xsdata.models.enums import Namespace, QNames
+from xsdata.models.enums import Namespace, ProcessType, QNames
 from xsdata.utils.testing import FactoryTestCase, XmlMetaFactory, XmlVarFactory
 
 
@@ -117,6 +117,9 @@ class ParserUtilsTests(FactoryTestCase):
         var = XmlVarFactory.create("fixed", default=lambda: float("nan"))
         ParserUtils.validate_fixed_value(meta, var, float("nan"))
 
+        var = XmlVarFactory.create("fixed", default=lambda: ProcessType.LAX)
+        ParserUtils.validate_fixed_value(meta, var, "lax")
+
     def test_parse_var_with_error(self):
         meta = XmlMetaFactory.create(clazz=TypeA, qname="foo")
         var = XmlVarFactory.create("fixed", default="a")