Skip to content

SRV-439 - performance optimizations for string handling in xml formatting #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Sep 7, 2023

Conversation

roxanneskelly
Copy link
Contributor

@roxanneskelly roxanneskelly commented Jun 28, 2023

A number of small perf optimizations:

  • use 'translate' to translate all xml characters at once instead of doing multiple string translation passes.
  • construct strings inline using writelines instead of doing it through a function, in order to save function call cost.
    Also, includes SL-19700 - limit of 200 on depth for formatting and parsing (all types)

@codecov
Copy link

codecov bot commented Jun 28, 2023

Codecov Report

Merging #15 (6586308) into main (a63abbe) will decrease coverage by 0.27%.
The diff coverage is 95.78%.

@@            Coverage Diff             @@
##             main      #15      +/-   ##
==========================================
- Coverage   90.33%   90.07%   -0.27%     
==========================================
  Files           6        6              
  Lines         848      856       +8     
==========================================
+ Hits          766      771       +5     
- Misses         82       85       +3     
Files Changed Coverage Δ
llsd/serde_binary.py 93.95% <88.88%> (-0.33%) ⬇️
llsd/serde_notation.py 91.27% <93.75%> (+0.15%) ⬆️
llsd/base.py 86.89% <94.73%> (-0.57%) ⬇️
llsd/serde_xml.py 97.27% <100.00%> (+0.32%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@roxanneskelly roxanneskelly added the enhancement New feature or request label Jun 29, 2023
@roxanneskelly roxanneskelly changed the title SRV-439 - performance optimizations for string handling in xml format… SRV-439 - performance optimizations for string handling in xml formatting Jul 7, 2023
@@ -13,21 +13,23 @@ jobs:
matrix:
python-version: ['2.7', '3.7', '3.8', '3.10']
runs-on: [ubuntu-latest]
container:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because setup-python no longer supports 2.7 (as of June 19th,) even with previous renditions of setup-python (the underlying support was taken away,) we need to run the python build in a container.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

- name: Install python dependencies
run: |
pip install wheel build tox
pip install .[dev]
apt-get update
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The python container requires some jumping through hoops due to user account and git differences.

@@ -14,7 +13,21 @@
INVALID_XML_RE = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]')


XML_ESC_TRANS = {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use 'translate' which in python 3 allows replacement of all of the exception characters in one go, instead of calling replace multiple times. This is a significant speedup.

"Construct a serializer."
# Call the super class constructor so that we have the type map
super(LLSDXMLFormatter, self).__init__()
self._indent_atom = b''
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some of the 'pretty' formatting stuff is moved into the base formatter for code sharing. As _indent is a no-op, this is mostly non-impactful.

@@ -13,21 +13,23 @@ jobs:
matrix:
python-version: ['2.7', '3.7', '3.8', '3.10']
runs-on: [ubuntu-latest]
container:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for testing?

@@ -110,16 +114,19 @@ def _parse_map(self):
cc = self._getc()
if cc != b'}':
self._error("invalid map close token")
self._depth = self._depth - 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious: what's the performance hit from covering this (and below) _depth - 1 statement with try / finally ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, vaguely remember an issue, but it's really not a problem. Fixing.

llsd/base.py Outdated
"Convert node to a python object."
return NODE_HANDLERS[node.tag](node)
if depth > MAX_PARSE_DEPTH:
raise LLSDParseError("Cannot serialize depth of more than %d" % MAX_FORMAT_DEPTH)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be "parse" rather than "serialize," with MAX_PARSE_DEPTH rather than MAX_FORMAT_DEPTH?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good eye.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And as it's serializing, it'd be MAX_FORMAT_DEPTH

@@ -97,6 +100,7 @@ def _parse_map(self):
count = 0
cc = self._getc()
key = b''
self._depth = self._depth + 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can hope that the Python compiler optimizes this to a single _depth lookup, but then Python isn't known for its optimizations. Maybe self._depth += 1, and so forth for other _depth adjustments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I vaguely remember running into an issue with this, but maybe it was something else. Will fix

# as that results in another function call and is slightly less performant
if PY2: # pragma: no cover
return self.stream.writelines([b'<string>', _str_to_bytes(xml_esc(v)), b'</string>', self._eol])
self.stream.writelines([b'<string>', v.translate(XML_ESC_TRANS).encode('utf-8'), b'</string>', self._eol])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we could squeeze out even a touch more performance by moving the PY2 test to class-definition time instead of runtime:

if PY2:
    def _STRING(self, v):
        return self.stream.writelines([b'<string>', _str_to_bytes(xml_esc(v)), b'</string>', self._eol])
else:
    def _STRING(self, v):
        self.stream.writelines([b'<string>', v.translate(XML_ESC_TRANS).encode('utf-8'), b'</string>', self._eol])

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think so, at least in _STRING() and _URI() where we restate the whole implementation anyway.

I stipulate that the runtime PY2 test in _MAP() may be more efficient than having _MAP() call a conditionally-defined helper function, and I wouldn't want you to restate the common parts of the _MAP() implementation.

@LogLinden
Copy link
Contributor

LogLinden commented Aug 18, 2023

I just wanted to point out the coverage percent test is currently failing.

Sorry I was trying to cancel a comment and closed the whole PR!

@LogLinden LogLinden closed this Aug 18, 2023
@LogLinden LogLinden reopened this Aug 18, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Aug 18, 2023
Copy link
Contributor

@nat-goodspeed nat-goodspeed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I like the changes you made. I still think module-scope _to_python() needs tweaking, and there remain a couple unaddressed suggestions from my previous review.

llsd/base.py Outdated
@@ -317,7 +317,7 @@ def _array_to_python(node, depth=0):
def _to_python(node, depth=0):
"Convert node to a python object."
if depth > MAX_PARSE_DEPTH:
raise LLSDParseError("Cannot serialize depth of more than %d" % MAX_FORMAT_DEPTH)
raise LLSDSerializationError("Cannot serialize depth of more than %d" % MAX_FORMAT_DEPTH)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't _to_python() used by parsers? The docstring seems to suggest that we're deserializing rather than serializing. Even if I'm wrong about that, though, lines 319 and 320 are still inconsistent.

# as that results in another function call and is slightly less performant
if PY2: # pragma: no cover
return self.stream.writelines([b'<string>', _str_to_bytes(xml_esc(v)), b'</string>', self._eol])
self.stream.writelines([b'<string>', v.translate(XML_ESC_TRANS).encode('utf-8'), b'</string>', self._eol])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think so, at least in _STRING() and _URI() where we restate the whole implementation anyway.

I stipulate that the runtime PY2 test in _MAP() may be more efficient than having _MAP() call a conditionally-defined helper function, and I wouldn't want you to restate the common parts of the _MAP() implementation.

@roxanneskelly
Copy link
Contributor Author

Performance optimizations are merged.

@roxanneskelly roxanneskelly merged commit b703873 into main Sep 7, 2023
@roxanneskelly roxanneskelly deleted the SRV-439 branch September 7, 2023 20:13
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants