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

Skip to content

[Bug]: coredump when combining xkcd, FigureCanvasTkAgg and FuncAnimation #24908

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

Open
bbbart opened this issue Jan 8, 2023 · 19 comments
Open

Comments

@bbbart
Copy link

bbbart commented Jan 8, 2023

Bug summary

When setting up a simple animation on a TkAgg canvas, everything works as expected, until the XKCD-style is applied.

Code for reproduction

import tkinter as tk                                                                
from datetime import date, datetime                                                 
from tkinter import ttk                                                             
                                                                                      
import matplotlib.animation as pltanim                                              
import matplotlib.pyplot as plt                                                     
from matplotlib.axes import Axes                                                    
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg                     
                                                                                      
                                                                                      
class DayPlotFrame(ttk.Frame):                                                      
    """TK Frame with matplotlib animation for live display of day progress."""   
                                                                                      
    def __init__(self, master=None):                                                
        """Initialize a new DayPlot."""                                             
        super().__init__(master=master)                                             
                                                                                    
        plt.xkcd()  # <- comment out this line and everything starts working again                                                                
        fig = plt.Figure()                                                          
        self.axes: Axes = fig.add_subplot()                                         
                                                                                    
        canvas = FigureCanvasTkAgg(fig, master=self)                                
        canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)                      
        self.animation = pltanim.FuncAnimation(                                     
            fig,                                                                    
            func=self.__run_animation,                                              
            init_func=self.__init_animation,                                        
        )               
        self.pack()                                                    
                                                                                       
    def __init_animation(self):                                                     
        """Configure the plot."""                                                   
        today = date.today()                                                        
                                                                                    
        self.axes.clear()                                                           
        self.axes.set_xlim(                                                         
            datetime(today.year, today.month, today.day, 8, 45, 0),                 
            datetime(today.year, today.month, today.day, 15, 11, 00),               
        )                                                                           
                                                                                      
    def __run_animation(self, frame):                                               
        """Update the plot according to the time of day."""                         
        width = datetime.now().replace(hour=10, minute=frame % 60)                  
        self.axes.barh(y=[1], width=width, color="blue")

ROOT = tk.Tk()
APP = DayPlotFrame(master=ROOT)
APP.mainloop()

Actual outcome

coredump

Expected outcome

the animation, but in XKCD style

Additional information

the init_func works just fine, the crash happens at the barh statement

Operating system

Arch

Matplotlib Version

3.6.2

Matplotlib Backend

TkAgg

Python version

Python 3.9.2

Jupyter version

No response

Installation

pip

@tacaswell
Copy link
Member

One minor change:

        import matplotlib.figure as mfigure
        fig = mfigure.Figure()  

that is unrelated but is a bit more reliable.


Additionally, in your animation your are creating a new bar each time (rather than updating the same bar) so the first frame we draw 1 bar, then 2, then 3 ... which is not helping.


I think the core of the problem here is related to the units....

attn @ksunden

@jklymak
Copy link
Member

jklymak commented Jan 10, 2023

Does running plt.xkcd somehow spin up a figure?

@bbbart
Copy link
Author

bbbart commented Jan 10, 2023

Additionally, in your animation your are creating a new bar each time (rather than updating the same bar) so the first frame we draw 1 bar, then 2, then 3 ... which is not helping.

You're right, sorry. I messed this up when reducing my code to a minimal (non-)working example.

I think the core of the problem here is related to the units....

Perhaps. Meanwhile, I figured out the following, narrowing down the issue some more:

from datetime import datetime
import matplotlib
import matplotlib.figure
matplotlib.use('svg')
import matplotlib.pyplot as plt

fig = matplotlib.figure.Figure()
axes = fig.add_subplot()
now = datetime.now()
day_start = now.replace(hour=8, minute=45, second=0)
day_end = now.replace(hour=15, minute=10, second=0)
axes.set_title(now.strftime("%A, %-d %B %Y"), fontsize=70)
axes.set_xlim(day_start, day_end)

axes.barh(y=[1], width=now)
plt.savefig('/tmp/plot.svg')  # this works just fine, output is about 15K

plt.xkcd()
axes.barh(y=[1], width=now)
fig.savefig('/tmp/plot.svg')  # this doesn't crash now, but outputs a 1.5G SVG file!

Without the set_xlim it goes back to working as expected however.

@dstansby
Copy link
Member

How odd! You are setting the width of the bar to a datetime, which is meaningless - plotting something with a width of 2022-01-01 00:00:00 doesn't mean anything, and the width should be a timedelta. Matplotlib shouldn't let you do this!

Regardless, plotting a .png file works fine, but does take a lot longer than I'd expect when the limits are set, which I suspect is linked to the svg issue here:
plot

from datetime import datetime
import matplotlib.pyplot as plt

now = datetime.now()
day_start = now.replace(hour=8, minute=45, second=0)
day_end = now.replace(hour=15, minute=10, second=0)

fig = plt.figure()
axes = fig.add_subplot()
plt.xkcd()
axes.barh(y=[1], width=now)
axes.set_xlim(day_start, day_end)
fig.savefig('/tmp/plot.png') # Plots fine but takes a while
# fig.savefig('/tmp/plot.svg') # Takes a while and generates a 3.09 GB file!

@dstansby
Copy link
Member

Aha, looking at the above plot what might be happening is the .svg is saving every point of the wiggly line (enabled by plt.xkcd(), regardless of whether it's in bounds of the axes or not. I don't have time to look now, but there could be an existing issue about the svg backend and clipping.

@jklymak
Copy link
Member

jklymak commented Jan 10, 2023

So this doesn't happen if the width is a timedelta? Agree that it is probably an hugely wide rectangle that xkcd is trying to add 1e9 wiggles to.

@bbbart
Copy link
Author

bbbart commented Jan 16, 2023

How odd! You are setting the width of the bar to a datetime, which is meaningless - plotting something with a width of 2022-01-01 00:00:00 doesn't mean anything, and the width should be a timedelta. Matplotlib shouldn't let you do this!

I'm not sure this is true? I think width is a bit of a misnomer here. When I plot a "width of 5", I actually want the bar to go from xmin to an absolute 5, not from xmin to xmin + 5, right?

Moreover, trying to specify a timedelta for the width, results in a complaint from Matplotlib: matplotlib.units.ConversionError: Failed to convert value(s) to axis units.

Anyway, this is besides the point here, I guess.

@bbbart
Copy link
Author

bbbart commented Jan 16, 2023

So this doesn't happen if the width is a timedelta? Agree that it is probably an hugely wide rectangle that xkcd is trying to add 1e9 wiggles to.

ah that could be a pointer in the right direction. A workaround I'm using now is the following, which works well:

from datetime import datetime, timedelta
import matplotlib
import matplotlib.figure
matplotlib.use('svg')
import matplotlib.pyplot as plt

fig = matplotlib.figure.Figure()
axes = fig.add_subplot()
now = datetime.now()
day_start = now.replace(hour=8, minute=45, second=0)
day_end = now.replace(hour=15, minute=10, second=0)
axes.set_title(now.strftime("%A, %-d %B %Y"), fontsize=70)
axes.set_xlim(day_start, day_end)
axes.set_ylim(0.5, 1.5)

axes.barh(y=[1], width=now)
plt.savefig('/tmp/plot.svg')  # this works just fine, output is about 15K

plt.xkcd()
# axes.barh(y=[1], width=now)
# fig.savefig('/tmp/plot-xkcd.svg')  # this outputs a 2.9G SVG file!

axes.broken_barh([(day_start, now - day_start)], (0.6, 0.8))
fig.savefig('/tmp/plot-xkcd-brokenbarh.svg')  # back to a sensible 65K SVG

Somehow XKCD style applied to a broken_barh works just fine.

(note that here we indeed speak of an actual starting point (datetime) and a width (timedelta))

Not sure this helps?

@dstansby
Copy link
Member

Ah my bad, I got confused about what bar/barh does! I agree with everything above, and setting width to a timedelta should error. Using broken_barh seems like the right way to go here 👍

So I think this issue boils down to the svg backend not clipping paths outside the bounds of the axes, which is tracked in #9488. As such I'll close this issue - feel free to comment if I've got anything wrong there, and we can re-open (and thanks again for reporting!)

@bbbart bbbart changed the title [Bug]: coredump when combingin xkcd, FigureCanvasTkAgg and FuncAnimation [Bug]: coredump when combining xkcd, FigureCanvasTkAgg and FuncAnimation Jan 17, 2023
@bbbart
Copy link
Author

bbbart commented Jan 17, 2023

I actually think it's a bit more complex. Look at the impact of set_xlim in the following example:

from datetime import datetime
from time import perf_counter

import matplotlib
import matplotlib.figure

matplotlib.use("agg")

import matplotlib.pyplot as plt

tic = perf_counter()
fig = matplotlib.figure.Figure()
axes = fig.add_subplot()
now = datetime.now()
day_start = now.replace(hour=8, minute=45, second=0)
day_end = now.replace(hour=15, minute=10, second=0)
axes.set_title(now.strftime("%A, %-d %B %Y"), fontsize=70)
axes.set_ylim(0.5, 1.5)

plt.xkcd()
axes.barh(y=[1], width=now)
fig.savefig("/tmp/plot-noxlim.png")  # this completes after about 0.05 seconds
print(perf_counter() - tic)

axes.set_xlim(day_start, day_end)
axes.barh(y=[1], width=now)
fig.savefig("/tmp/plot-xlim.png")  # this takes over 59 seconds
print(perf_counter() - tic)

Running it without the plot.xkcd() line results in near instant completion of the entire script.

So, I'm now thinking that

  • it has nothing to do with animations (as I initially reported)
  • the problem is independent of the backend (as hinted at above)
  • the bug only exists in XKCD-mode (that much we know is true :-))
  • it doesn't lead to a coredump straight away, just to a lot of CPU and memory usage

@dstansby dstansby reopened this Jan 17, 2023
@jklymak
Copy link
Member

jklymak commented Jan 17, 2023

Still confused as to whether this has anything to do with datetime or not. If you concoct an example without datetime, does the same thing happen?

@tacaswell
Copy link
Member

I still think that @dstansby 's analysis is correct. In the case of non-xkcd we are drawing a rectangle with no fancy effects so the path is very simple (only 5 vertexes a moveto, 3 lineto, and a close) which is fine in all cases because the cost is independent of the view limits. In the case of SVG 4 points (even if way out of range) is still just 4+1 points and in the Agg cases we only have to do 4 path/clip box intersection computations.

In the case of XKCD style which puts a screen-space oscillation on the path takes our very simple path and makes it more complex proportional to the size of the length of the path in screen space. If the whole path fits on the screen this is fine, but if you make the view limit a tiny fraction of the total (like 1/18_000 kth) then we have a lot of ripples. This either results in a huge number of points which eventually exhausts the svg output or a very large number of (non-intersecting) intersections to compute in the AGG case.

@dstansby
Copy link
Member

dstansby commented Jan 18, 2023

Here's a minimal example:

import matplotlib.pyplot as plt

plt.xkcd()
fig, ax = plt.subplots()

ax.bar(0, height=1e4)
ax.set_ylim(0, 1)
plt.show()

This takes a long time (~seconds) to render the image on my laptop. Profiling to see where time is spent gives:

7.744 <module>  test.py:1
├─ 7.278 savefig  matplotlib/pyplot.py:993
│  └─ 7.278 Figure.savefig  matplotlib/figure.py:3202
│     └─ 7.278 FigureCanvasMac.print_figure  matplotlib/backend_bases.py:2237
│        └─ 7.278 <lambda>  matplotlib/backend_bases.py:2228
│           └─ 7.278 FigureCanvasMac.print_png  matplotlib/backends/backend_agg.py:462
│              └─ 7.278 FigureCanvasMac._print_pil  matplotlib/backends/backend_agg.py:452
│                 ├─ 6.921 FigureCanvasMac.draw  matplotlib/backends/backend_agg.py:392
│                 │  └─ 6.920 draw_wrapper  matplotlib/artist.py:93
│                 │     └─ 6.920 draw_wrapper  matplotlib/artist.py:54
│                 │        └─ 6.920 Figure.draw  matplotlib/figure.py:3102
│                 │           └─ 6.920 _draw_list_compositing_images  matplotlib/image.py:113
│                 │              └─ 6.920 draw_wrapper  matplotlib/artist.py:54
│                 │                 └─ 6.920 Axes.draw  matplotlib/axes/_base.py:3001
│                 │                    └─ 6.919 _draw_list_compositing_images  matplotlib/image.py:113
│                 │                       └─ 6.919 draw_wrapper  matplotlib/artist.py:54
│                 │                          └─ 6.881 Rectangle.draw  matplotlib/patches.py:582
│                 │                             └─ 6.881 Rectangle._draw_paths_with_artist_properties  matplotlib/patches.py:533
│                 │                                └─ 6.881 PathEffectRenderer.draw_path  matplotlib/patheffects.py:99
│                 │                                   └─ 6.881 withStroke.draw_path  matplotlib/patheffects.py:171
│                 │                                      ├─ 5.756 withStroke.draw_path  matplotlib/patheffects.py:207
│                 │                                      │  └─ 5.756 RendererAgg.draw_path  matplotlib/backends/backend_agg.py:109
│                 │                                      │     └─ 5.756 RendererAgg.draw_path  None

... more lines below that I haven't included

which confirms that time is spent in drawing the path.

I'm not sure the best way to go here. Someone who knows more about path clipping might have a better idea if this is 'just life', or if there's any space for optimisations here?

@oscargus
Copy link
Member

About half of the time seems to be spent on these two lines:

m_p += pow(m_randomness, d_rand * 2.0 - 1.0);
double r = sin(m_p / (m_length / (d_M_PI * 2.0))) * m_scale;

So, yes, it looks like reducing the amount of drawing is really the best way around it.

@anntzer
Copy link
Contributor

anntzer commented Jan 18, 2023

I don't immediately see why clipping would not work with the wiggly path (perhaps clip to a slightly larger box so that if a line is "just beyond" the clip its wiggles can still come within the original clipbox; I think the size of the wiggle is bounded anyways).

@tacaswell
Copy link
Member

I suspect the issue is that we wiggle then clip, not clip then wiggle. So while clipping "works" we spend a lot of time agreeing that a wiggle very far away is indeed very far away.

@anntzer
Copy link
Contributor

anntzer commented Jan 18, 2023

If I read

template <class PathIterator>
inline void
RendererAgg::draw_path(GCAgg &gc, PathIterator &path, agg::trans_affine &trans, agg::rgba &color)
{
    typedef agg::conv_transform<py::PathIterator> transformed_path_t;
    typedef PathNanRemover<transformed_path_t> nan_removed_t;
    typedef PathClipper<nan_removed_t> clipped_t;
    typedef PathSnapper<clipped_t> snapped_t;
    typedef PathSimplifier<snapped_t> simplify_t;
    typedef agg::conv_curve<simplify_t> curve_t;
    typedef Sketch<curve_t> sketch_t;
...
    transformed_path_t tpath(path, trans);
    nan_removed_t nan_removed(tpath, true, path.has_codes());
    clipped_t clipped(nan_removed, clip, width, height);
    snapped_t snapped(clipped, gc.snap_mode, path.total_vertices(), snapping_linewidth);
    simplify_t simplified(snapped, simplify, path.simplify_threshold());
    curve_t curve(simplified);
    sketch_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness);

    _draw_path(sketch, has_clippath, face, gc);
}

correctly, we clip first and wiggle (sketch) last.

@tacaswell
Copy link
Member

hmm, I agree with that reading. Does clipping not do what we think it does? There are some cut-outs is clipping, I think for closed paths(?), that disable it when we can not be sure we can clip it correctly.....

@oscargus
Copy link
Member

oscargus commented Jan 19, 2023

I modified the pow-computation a bit in #24964, about halving the time spent on that line (from 33% to 16% of the total time). Still, this is a linear decrease so although beneficial will not fix the actual issue.

Edit: will be faster on x86, should not be slower on other architectures. Read the comments to see why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants