Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Performance Improvements #19

Open
ianhi opened this issue Nov 18, 2022 · 2 comments
Open

Performance Improvements #19

ianhi opened this issue Nov 18, 2022 · 2 comments

Comments

@ianhi
Copy link
Collaborator

ianhi commented Nov 18, 2022

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
@ianhi
Copy link
Collaborator Author

ianhi commented Aug 18, 2023

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


@ianhi
Copy link
Collaborator Author

ianhi commented Aug 18, 2023

some big improvments in #20

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant