Skip to content

Performance Improvements #19

Open
Open

Description

@ianhi

Thoughts on how to improve performance

The biggest performance issue that currently every tracked figure gets a full redraw every frame: https://github.com/ianhi/mpl-playback/blob/06a7faf567e487b49d32468767fd90fb5f659ec9/mpl_playback/playback.py#L245-L248

This happens even if the event has already triggered a draw if for example a slider was moved.

Potential solutions:

  1. Only redraw figures if they have stale=True
  2. monkeypatch figure's draw events to prevent the earlier draws and only do our final draw
  3. In combination with 1 blit the fake cursors

Activity

  1. ianhi commented on Aug 18, 2023

    @ianhi
    CollaboratorAuthor

    lineprofiling playback:

    playback_events

    Timer unit: 1e-09 s
    
    Total time: 22.4593 s
    File: /home/ian/Documents/oss/mpl-extensions/mpl-playback/mpl_playback/playback.py
    Function: playback_events at line 145
    
    Line #      Hits         Time  Per Hit   % Time  Line Contents
    ==============================================================
       145                                           def playback_events(
       146                                               figures,
       147                                               events,
       148                                               meta,
       149                                               globals,
       150                                               outputs,
       151                                               fps=24,
       152                                               from_first_event=True,
       153                                               prog_bar=True,
       154                                               writer: Literal["pillow", "ffmpeg", "imagemagick", "avconv", None] = "pillow",
       155                                               **kwargs,
       156                                           ):
       157                                               """
       158                                               plays back events that have been
       159                                           
       160                                               Parameters
       161                                               ----------
       162                                               ....
       163                                               outputs : list of str or None
       164                                                   The paths to outputs. If None then the events will played back but
       165                                                   no outputs will saved.
       166                                               fps : int, default: 24
       167                                                   Frames per second of the output
       168                                               from_first_event : bool, default: True
       169                                                   Whether playback should start from the timing of the first recorded
       170                                                   event or from when recording was initiated.
       171                                               prog_bar : bool, default: True
       172                                                   Whether to display a progress bar. If tqdm is not
       173                                                   available then this kwarg has no effect.
       174                                               """
       175         1        669.0    669.0      0.0      accessors = {}
       176         1        295.0    295.0      0.0      fake_cursors = {}
       177         1        219.0    219.0      0.0      writers = []
       178         1        569.0    569.0      0.0      if writer == "ffmpeg" and FFMpegWriter.isAvailable():
       179                                                   writer = FFMpegWriter
       180         1        351.0    351.0      0.0      elif writer == "imagemagick" and ImageMagickWriter.isAvailable():
       181                                                   writer = ImageMagickWriter
       182         1        274.0    274.0      0.0      elif writer == "avconv" and AVConvWriter is not None and AVConvWriter.isAvailable():
       183                                                   writer = AVConvWriter
       184                                               else:
       185         1        463.0    463.0      0.0          writer = PillowWriter
       186                                           
       187         1        179.0    179.0      0.0      _figs = {}  # the actual figure objects
       188         1        200.0    200.0      0.0      transforms = {}
       189         2       2353.0   1176.5      0.0      for fig, out in zip(figures, outputs):
       190         1      46722.0  46722.0      0.0          _fig = extract_by_name(fig, globals)
       191         1        490.0    490.0      0.0          _figs[fig] = _fig
       192                                                   # pre invert for performance (i have no idea if this owuld get cached.)
       193                                                   # but if it's inverting a matrix then it's bad news to constantly invert
       194         1      66949.0  66949.0      0.0          transforms[fig] = _fig.transFigure.inverted().frozen()
       195         1        303.0    303.0      0.0          accessors[fig] = _fig
       196         3     915324.0 305108.0      0.0          fake_cursors[fig] = _fig.axes[-1].plot(
       197         1        371.0    371.0      0.0              [0, 0],
       198         1        218.0    218.0      0.0              [0, 0],
       199         1        196.0    196.0      0.0              "k",
       200         1        146.0    146.0      0.0              marker=6,
       201         1        453.0    453.0      0.0              markersize=15,
       202                                                       # confusing that this is necessary, see note in animate below
       203         1        290.0    290.0      0.0              transform=_fig.transFigure,
       204         1        134.0    134.0      0.0              clip_on=False,
       205         1        214.0    214.0      0.0              zorder=99999,
       206         1        291.0    291.0      0.0          )[0]
       207         1        273.0    273.0      0.0          if outputs is not None:
       208         1       9758.0   9758.0      0.0              writers.append(writer(fps))
       209         1      84690.0  84690.0      0.0              writers[-1].setup(_fig, out, len(events))
       210                                           
       211         1   44071219.0    4e+07      0.2      times, mock_events = gen_mock_events(events, globals, accessors)
       212         1        400.0    400.0      0.0      if from_first_event:
       213         1      14454.0  14454.0      0.0          times -= times[0]
       214         1       2102.0   2102.0      0.0      N_frames = int(times[-1] * fps)
       215                                               # map from frames to events
       216         1      21293.0  21293.0      0.0      event_frames = np.round(times * fps)
       217                                           
       218         1        548.0    548.0      0.0      if prog_bar and _prog_bar:
       219                                                   pbar = tqdm(total=N_frames)
       220                                           
       221                                               global animate
       222         1    2999841.0    3e+06      0.0      def animate(i):
       223                                                   idx = event_frames == i
       224                                                   if np.sum(idx) != 0:
       225                                                       for event in mock_events[idx]:
       226                                                           # event = mock_events[i]
       227                                                           accessors[event._figname].canvas.callbacks.process(event.name, event)
       228                                                           if event.name == "motion_notify_event":
       229                                                               # now set the cursor invisible so multiple don't show up
       230                                                               # if there are multiple figures
       231                                                               for fc in fake_cursors.values():
       232                                                                   fc.set_visible(False)
       233                                           
       234                                                               # It really seems as though this transform should be uncessary
       235                                                               # and with only a single figure it is uncesssary. But for reasons
       236                                                               # that are beyond me, when there are multiple figures this
       237                                                               # transform is crucial or else the cursor will show up in a weirdly
       238                                                               # scaled position. This is true even with setting `transform=None`
       239                                                               # on the origin `plot` call
       240                                                               f = _figs[event._figname]
       241                                                               xy = transforms[event._figname].transform([event.x, event.y])
       242                                                               fake_cursors[event._figname].set_data([xy[0]], [xy[1]])
       243                                                               fake_cursors[event._figname].set_visible(True)
       244                                           
       245                                                           # theres got to be a clever way to avoid doing these gazillion draws
       246                                                           # maybe monkeypatching the figure's draw event?
       247                                                           for f in _figs.values():
       248                                                               f.canvas.draw()
       249                                           
       250                                                   if prog_bar and _prog_bar:
       251                                                       pbar.update(1)
       252                                                   if outputs is not None:
       253                                                       for w in writers:
       254                                                           w.grab_frame()
       255                                           
       256       127     119495.0    940.9      0.0      for i in range(N_frames):
       257       126        2e+10    2e+08     89.8          animate(i)
       258                                           
       259         1        357.0    357.0      0.0      if outputs is not None:
       260         2       1492.0    746.0      0.0          for w in writers:
       261         1 2232775993.0    2e+09      9.9              w.finish()
    

    The animate function (which is 90% of execution time):

    Line #      Hits         Time  Per Hit   % Time  Line Contents
    ==============================================================
       222                                               def animate(i):
       223       126    1072116.0   8508.9      0.0          idx = event_frames == i
       224       126    3103144.0  24628.1      0.0          if np.sum(idx) != 0:
       225       250    1048384.0   4193.5      0.0              for event in mock_events[idx]:
       226                                                           # event = mock_events[i]
       227       157 6814067897.0    4e+07     30.5                  accessors[event._figname].canvas.callbacks.process(event.name, event)
       228       157     166248.0   1058.9      0.0                  if event.name == "motion_notify_event":
       229                                                               # now set the cursor invisible so multiple don't show up
       230                                                               # if there are multiple figures
       231       310     270923.0    873.9      0.0                      for fc in fake_cursors.values():
       232       155    2530040.0  16322.8      0.0                          fc.set_visible(False)
       233                                           
       234                                                               # It really seems as though this transform should be uncessary
       235                                                               # and with only a single figure it is uncesssary. But for reasons
       236                                                               # that are beyond me, when there are multiple figures this
       237                                                               # transform is crucial or else the cursor will show up in a weirdly
       238                                                               # scaled position. This is true even with setting `transform=None`
       239                                                               # on the origin `plot` call
       240       155     154760.0    998.5      0.0                      f = _figs[event._figname]
       241       155    1485817.0   9585.9      0.0                      xy = transforms[event._figname].transform([event.x, event.y])
       242       155    3826124.0  24684.7      0.0                      fake_cursors[event._figname].set_data([xy[0]], [xy[1]])
       243       155    1728288.0  11150.2      0.0                      fake_cursors[event._figname].set_visible(True)
       244                                           
       245                                                           # theres got to be a clever way to avoid doing these gazillion draws
       246                                                           # maybe monkeypatching the figure's draw event?
       247       314     263247.0    838.4      0.0                  for f in _figs.values():
       248       157 7250215170.0    5e+07     32.5                      f.canvas.draw()
       249                                           
       250       126     109055.0    865.5      0.0          if prog_bar and _prog_bar:
       251                                                       pbar.update(1)
       252       126      46160.0    366.3      0.0          if outputs is not None:
       253       252     217494.0    863.1      0.0              for w in writers:
       254       126 8236026249.0    7e+07     36.9                  w.grab_frame()
    
    
    
  2. ianhi commented on Aug 18, 2023

    @ianhi
    CollaboratorAuthor

    some big improvments in #20

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions