Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 1 commit into from
Mar 15, 2017
Merged

Conversation

anntzer
Copy link
Contributor

@anntzer anntzer commented Mar 9, 2017

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.

@anntzer anntzer force-pushed the inkscape-shell-mode branch 2 times, most recently from 1dee9b7 to f44aaf1 Compare March 9, 2017 05:38
@NelleV
Copy link
Member

NelleV commented Mar 9, 2017

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__)
Copy link
Member

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?

Copy link
Contributor Author

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).

if window == terminator:
break
else:
continue
Copy link
Member

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.

next(itertools.islice(it, i, i), None) # Advance `it` by `i`.
while True:
window = tuple(map(next, its))
if len(window) == n:
Copy link
Member

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

Copy link
Contributor Author

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

stream = iter(functools.partial(self._proc.stdout.read, 1), b"")
terminator = (b"\n", b">")
n = len(terminator)
its = itertools.tee(stream, n)
Copy link
Member

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)

Copy link
Contributor Author

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.

break

def __call__(self, orig, dest):
if not self._proc:
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

@anntzer anntzer force-pushed the inkscape-shell-mode branch from f44aaf1 to d67380d Compare March 10, 2017 01:30
@QuLogic
Copy link
Member

QuLogic commented Mar 10, 2017

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?

@anntzer
Copy link
Contributor Author

anntzer commented Mar 10, 2017

I don't know, but I don't think this PR changes anything about that...

assert expectedImage.ndim == actualImage.ndim
assert expectedImage.shape == actualImage.shape
absDiffImage = abs(expectedImage - actualImage)
assert expectedImage.shape[:2] == actualImage.shape[:2]
Copy link
Member

Choose a reason for hiding this comment

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

why the slices here?

assert expectedImage.shape == actualImage.shape
absDiffImage = abs(expectedImage - actualImage)
assert expectedImage.shape[:2] == actualImage.shape[:2]
absDiffImage = np.abs(expectedImage[:, :, :3] - actualImage[:, :, :3])
Copy link
Member

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?

@tacaswell
Copy link
Member

👍 but a bit concerned about not testing the alpha channel.

@tacaswell tacaswell added this to the 2.1 (next point release) milestone Mar 10, 2017
@anntzer anntzer force-pushed the inkscape-shell-mode branch from d67380d to c3fab50 Compare March 10, 2017 20:55
@anntzer
Copy link
Contributor Author

anntzer commented Mar 10, 2017

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.

tacaswell
tacaswell previously approved these changes Mar 10, 2017
@QuLogic
Copy link
Member

QuLogic commented Mar 10, 2017

I don't know, but I don't think this PR changes anything about that...

Before, with make_external_conversion_command, both stdout and stderr were captured and if the return code was nonzero or the output file didn't exist, it would raise an exception with both of these outputs.

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.)

@anntzer
Copy link
Contributor Author

anntzer commented Mar 10, 2017

Good point. Looks like this will raise FileNotFoundError (or whatever OSError is appropriate) when compare_images calls read_png_int (that function raises the correct exception, I've checked). This is consistent with the behavior of make_external_conversion_command.

In either case, perhaps it would be better to reraise as an ImageComparisonError? (but it's kind of an unrelated issue).

@QuLogic
Copy link
Member

QuLogic commented Mar 12, 2017

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.

@anntzer
Copy link
Contributor Author

anntzer commented Mar 12, 2017

  • The Appveyor failure seems real.
  • The way arguments are passed will probably fail if the some path names contain a space (which is always possible via the directory name...). I need to check how inkscape parses this -- including on Windows :/

Edit: inkscape uses g_shell_parse_argv (http://bazaar.launchpad.net/~inkscape.dev/inkscape/trunk/view/head:/src/main.cpp#L1234) which has a consistent cross-platform behavior (https://developer.gnome.org/glib/stable/glib-Shell-related-Utilities.html#g-shell-parse-argv); it looks like just using shlex.quote is enough.

@anntzer anntzer force-pushed the inkscape-shell-mode branch from c3fab50 to 2a25286 Compare March 12, 2017 07:44
@NelleV NelleV changed the title Inkscape shell mode. [MRG+1] Inkscape shell mode. Mar 12, 2017
@anntzer anntzer force-pushed the inkscape-shell-mode branch from 2a25286 to 9276eea Compare March 13, 2017 00:38
@QuLogic
Copy link
Member

QuLogic commented Mar 13, 2017

There seem to be some timeout problems with the Inkscape process.

@anntzer
Copy link
Contributor Author

anntzer commented Mar 13, 2017

Yup, looking into it.

@anntzer anntzer force-pushed the inkscape-shell-mode branch from 9276eea to bd7865c Compare March 13, 2017 01:48
@anntzer
Copy link
Contributor Author

anntzer commented Mar 13, 2017

Temporarily closing to avoid spamming the build queue while I try to fix the timeout...

@anntzer anntzer closed this Mar 13, 2017
@anntzer anntzer reopened this Mar 13, 2017
@anntzer anntzer force-pushed the inkscape-shell-mode branch 2 times, most recently from d55b6ed to dc3cfd8 Compare March 13, 2017 15:56
@anntzer anntzer force-pushed the inkscape-shell-mode branch 2 times, most recently from 7dcc42a to 3ab7b71 Compare March 13, 2017 21:35
@anntzer
Copy link
Contributor Author

anntzer commented Mar 14, 2017

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?

@NelleV
Copy link
Member

NelleV commented Mar 14, 2017

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!

@anntzer anntzer dismissed tacaswell’s stale review March 14, 2017 01:30

Heavily rewritten PR

@anntzer anntzer changed the title [MRG+1] Inkscape shell mode. [MRG] Inkscape shell mode. Mar 14, 2017
@anntzer
Copy link
Contributor Author

anntzer commented Mar 14, 2017

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).

Copy link
Member

@QuLogic QuLogic left a 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.

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`.
Copy link
Member

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)?

Copy link
Contributor Author

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(
Copy link
Member

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.

Copy link
Contributor Author

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...

@anntzer anntzer force-pushed the inkscape-shell-mode branch from 3ab7b71 to 8c66203 Compare March 14, 2017 22:39
@tacaswell
Copy link
Member

The appveyor failure is conda related, the travis failure is one of the flakey ones. Going to go ahead and merge this as is.

@tacaswell tacaswell merged commit f5b6246 into matplotlib:master Mar 15, 2017
@anntzer anntzer deleted the inkscape-shell-mode branch March 15, 2017 03:53
@QuLogic QuLogic changed the title [MRG] Inkscape shell mode. Inkscape shell mode. Apr 30, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants