Skip to content
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

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

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

No branches or pull requests

1 participant