-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Inkscape shell mode. #8248
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
Inkscape shell mode. #8248
Conversation
1dee9b7
to
f44aaf1
Compare
That's a significant speed up! |
# set to `None`, which make `proc.communicate` or `proc.terminate` | ||
# fail. By relying on `atexit` we ensure the destructor runs before | ||
# `None`-setting occurs. | ||
atexit.register(self.__del__) |
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.
Would __del__
not be called a second time when this object is deleted as well?
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.
Yes, but __del__
can safely be executed twice (a file stream can be closed multiple times).
lib/matplotlib/testing/compare.py
Outdated
if window == terminator: | ||
break | ||
else: | ||
continue |
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.
Code doesn't do anything.
lib/matplotlib/testing/compare.py
Outdated
next(itertools.islice(it, i, i), None) # Advance `it` by `i`. | ||
while True: | ||
window = tuple(map(next, its)) | ||
if len(window) == n: |
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.
Could be flattened to:
while True:
window = tuple(map(next, its))
if len(window) != n or window == terminator:
break
or
while True:
window = tuple(map(next, its))
if len(window) != n:
# Ran out of data.
break
elif window == terminator:
# Found terminator.
break
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.
went for the first version
lib/matplotlib/testing/compare.py
Outdated
stream = iter(functools.partial(self._proc.stdout.read, 1), b"") | ||
terminator = (b"\n", b">") | ||
n = len(terminator) | ||
its = itertools.tee(stream, n) |
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.
If I understand this correctly, can you use the example from the itertools
documentation?
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = tee(iterable)
next(b, None)
return zip(a, 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.
I wrote a slightly more general version that can handle arbitrary long terminators. I would rather not have the implementation tied to the fact that we're looking exactly for a two-character terminator.
lib/matplotlib/testing/compare.py
Outdated
break | ||
|
||
def __call__(self, orig, dest): | ||
if not self._proc: |
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 think you also need to handle if inkscape crashed in the background.
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.
fixed
f44aaf1
to
d67380d
Compare
Related to that last comment, what happens when Inkscape fails to convert the image? Do we raise a something like FileNotFoundError, instead of something more specific? |
I don't know, but I don't think this PR changes anything about that... |
lib/matplotlib/testing/compare.py
Outdated
assert expectedImage.ndim == actualImage.ndim | ||
assert expectedImage.shape == actualImage.shape | ||
absDiffImage = abs(expectedImage - actualImage) | ||
assert expectedImage.shape[:2] == actualImage.shape[:2] |
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.
why the slices here?
lib/matplotlib/testing/compare.py
Outdated
assert expectedImage.shape == actualImage.shape | ||
absDiffImage = abs(expectedImage - actualImage) | ||
assert expectedImage.shape[:2] == actualImage.shape[:2] | ||
absDiffImage = np.abs(expectedImage[:, :, :3] - actualImage[:, :, :3]) |
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 why not check the alpha channel?
👍 but a bit concerned about not testing the alpha channel. |
d67380d
to
c3fab50
Compare
I was having some issues locally where in some cases the alpha channel would be dropped by the converter (leading to a shape mismatch on comparison) but they seem not to occur anymore. Restored the full comparison. |
Before, with Now, checking for the file occurs much later in the comparison, and any inkscape output is either gobbled up or always printed (but not necessarily at the right place, depends on pytest capture mode, probably.) |
Good point. Looks like this will raise In either case, perhaps it would be better to reraise as an ImageComparisonError? (but it's kind of an unrelated issue). |
Yes, it raises the same type of error, but my point is that you've no longer got the output from Inkscape coupled with the exception. |
Edit: inkscape uses |
c3fab50
to
2a25286
Compare
2a25286
to
9276eea
Compare
There seem to be some timeout problems with the Inkscape process. |
Yup, looking into it. |
9276eea
to
bd7865c
Compare
Temporarily closing to avoid spamming the build queue while I try to fix the timeout... |
d55b6ed
to
dc3cfd8
Compare
7dcc42a
to
3ab7b71
Compare
Looks like I got it to work. Main failure point was that old versions of inkscape (0.48.3, which is used on Travis) would block when stderr was redirected to a pipe and the pipe was not being read from (this is no longer the case on the most recent Inkscape). Instead, I just redirected stderr to a temporary file. Unless I am mistaken, the Appveyor build is 20min(!!) faster -- perhaps because repeatedly spawning inkscape was much slower on Windows? |
It's seems indeed much faster on windows now. Good job! I think this is going to be a real relief in our CI pipeline! |
PS: Please review carefully, the patch is not completely trivial (and was changed quite a bit since @tacaswell's original review, so I dismissed it). |
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.
Holy speedups, Batman!
I think this looks good.
lib/matplotlib/testing/compare.py
Outdated
raise OSError("Failed to start Inkscape") | ||
|
||
# Inkscape uses glib's `g_shell_parse_argv`, which has a consistent | ||
# behavior across platforms, so we can just use `shlex.quote`. |
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 this comment related to the next section (L210)?
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.
Yes. Moving it down. Will also clarify the comment regarding \n
(inkscape uses fgets so there's no choice).
# right... | ||
self._stderr.seek(0) | ||
raise ImageComparisonFailure( | ||
self._stderr.read().decode( |
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 fine as long as you've tried this out.
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 [1]: from matplotlib.testing.compare import _SVGConverter; _SVGConverter()("/tmp/quux.svg", "/tmp/foo.png")
---------------------------------------------------------------------------
ImageComparisonFailure Traceback (most recent call last)
<ipython-input-2-c896ab68b2ce> in <module>()
----> 1 from matplotlib.testing.compare import _SVGConverter; _SVGConverter()("/tmp/quux.svg", "/tmp/foo.png")
/home/antony/src/extern/matplotlib/lib/matplotlib/testing/compare.py in __call__(self, orig, dest)
225 raise ImageComparisonFailure(
226 self._stderr.read().decode(
--> 227 sys.getfilesystemencoding(), "replace"))
228
229 def __del__(self):
ImageComparisonFailure: ** Message: /dev/null is not a valid directory.
** Message: Inkscape will run with default settings, and new settings will not be saved.
** (inkscape:25211): WARNING **: Could not create extension error log file '/dev/null/extension-errors.log'
** (inkscape:25211): WARNING **: Can't open file: /tmp/quux.svg (doesn't exist)
** (inkscape:25211): WARNING **: Can't open file: /tmp/quux.svg (doesn't exist)
** (inkscape:25211): WARNING **: Specified document /tmp/quux.svg cannot be opened (does not exist or not a valid SVG file)
The first few warnings should be spurious (due to intentionally setting INKSCAPE_PROFILE_DIR
to a nonexistent folder) and I could technically crop them out, but I'd rather have the full info available just in case...
3ab7b71
to
8c66203
Compare
The appveyor failure is conda related, the travis failure is one of the flakey ones. Going to go ahead and merge this as is. |
xref #8242.
Using inkscape in shell mode (i.e., having to start it only once) makes the entire test suite run in 624s on my laptop, compared to 787s currently (and 594s using rsvg, which has its own issues mentioned in #8242).
attn @tacaswell @NelleV
Edit: This seems to shave 1-2min per build on Travis, which is a bit less but not inconsistent with my local testing.