-
Notifications
You must be signed in to change notification settings - Fork 1
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
Conversation
…ting 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
Codecov Report
@@ 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
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
@@ -13,21 +13,23 @@ jobs: | |||
matrix: | |||
python-version: ['2.7', '3.7', '3.8', '3.10'] | |||
runs-on: [ubuntu-latest] | |||
container: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for testing?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 = {} |
There was a problem hiding this comment.
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.
llsd/serde_xml.py
Outdated
"Construct a serializer." | ||
# Call the super class constructor so that we have the type map | ||
super(LLSDXMLFormatter, self).__init__() | ||
self._indent_atom = b'' |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for testing?
llsd/serde_binary.py
Outdated
@@ -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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good eye.
There was a problem hiding this comment.
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
llsd/serde_binary.py
Outdated
@@ -97,6 +100,7 @@ def _parse_map(self): | |||
count = 0 | |||
cc = self._getc() | |||
key = b'' | |||
self._depth = self._depth + 1 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
llsd/serde_xml.py
Outdated
# 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]) |
There was a problem hiding this comment.
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])
There was a problem hiding this comment.
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.
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! |
There was a problem hiding this 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) |
There was a problem hiding this comment.
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.
llsd/serde_xml.py
Outdated
# 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]) |
There was a problem hiding this comment.
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.
Performance optimizations are merged. |
A number of small perf optimizations:
Also, includes SL-19700 - limit of 200 on depth for formatting and parsing (all types)