I am writing mass spectrometry data reduction software and am having a problem where one of the PySide6-based GUIs is slow to respond on Windows. In this GUI, the user is presented with six different raw data plots corresponding to data from a single analysis; they may interact with the data, or go to the next/previous analysis:
On Mac, this process is extremely snappy: the plots are updated almost instantly after clicking Next Analysis. On Windows, however, it has a notable delay. For example, if I mash the “Next Analysis” button ~30 times in a row, it takes several seconds after I’m done clicking for the program to catch up–it’s still six or seven analyses behind when I stop clicking.
The major bottleneck appears to be the matplotlib figure drawing process, however, this doesn’t account for the entire delay. For example, when I use a timer to track the time from button click to GUI update, it declares that the GUI was updated in ~0.25 seconds, about 0.2 seconds of which is related to matplotlib, but the GUI still isn’t updated by the point that that print statement arrives–there’s yet another delay, approximately the same length of time, such that the GUI takes about 0.5 seconds to respond and update. I have no idea how to track or identify the source of this delay because that print statement is the last line of code executed.
Here is the call to plot the figure in the GUI’s logic class:
# clear the plot, generate the figure, and draw the plot to the canvas
def update_plots(self, index):
start_time = time()
profiler = cProfile.Profile()
profiler.enable()
self.fig.clf()
generate_fitting_figure(
self.sequence_data[index], # DataEntry object of analysis to plot
self.fit_type, # fitting model, e.g. 'Linear'
self.fig # figure object to plot on
)
self.canvas.draw_idle()
profiler.disable()
stats = pstats.Stats(profiler).sort_stats('cumtime')
stats.print_stats(10)
print(f"Time to generate plot: {time() - start_time}")
and the results from the profiler:
ncalls tottime percall cumtime percall filename:lineno(function)
12 0.001 0.000 0.163 0.014 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibaxes_base.py:1183(cla)
1 0.000 0.000 0.117 0.117 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibfigure.py:974(clf)
1 0.000 0.000 0.117 0.117 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibfigure.py:2812(clear)
1 0.000 0.000 0.117 0.117 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibfigure.py:938(clear)
84/60 0.000 0.000 0.113 0.002 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibaxis.py:934(set_clip_path)
108 0.000 0.000 0.112 0.001 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibaxis.py:790(clear)
132/72 0.000 0.000 0.104 0.001 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibaxis.py:581(__get__)
156/132 0.002 0.000 0.096 0.001 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibaxis.py:830(reset_ticks)
1 0.000 0.000 0.088 0.088 c:UsersatomgOneDrive - Thermochron Systems LLCheman_codeProraw_datafittingplot_raw_data_fits.py:12(generate_fitting_figure)
48 0.000 0.000 0.080 0.002 C:UsersatomgOneDrive - Thermochron Systems LLCheman_codePro.venvlibsite-packagesmatplotlibspines.py:219(clear)
and the plotting function itself:
import numpy as np
from analysis_class import Analysis
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from basic_calcs import cubic_curve, natural_log_curve
"""
generate_fitting_figure
Generate a raw data figure with 6 subplots, one for each mass, plus one for the 4/3 Ratio
The data are plotted along with an average, linear, cubic, or natural logarithmic fit
"""
def generate_fitting_figure(analysis: Analysis, fit_type: str, fig: Figure):
# plot the average fit
def draw_average(t: np, y: np, ax: Axes):
average_y = np.mean(y)
trend_t = [t.min(), t.max()]
trend_y = [average_y, average_y]
ax.plot(trend_t, trend_y, color = "black", zorder = 2)
# plot the linear trendline
def draw_linear(t: np, y: np, ax: Axes):
trend = np.polyfit(t, y, 1)
trend_t = [t.min(), t.max()]
trend_y = np.polyval(trend, trend_t)
ax.plot(trend_t, trend_y, color = "black", zorder = 2)
# plot the cubic function
def draw_cubic(t: np, y: np, ax: Axes):
fitted_t, fitted_y = cubic_curve(t, y)
ax.plot(fitted_t, fitted_y, color = "black", zorder = 2)
# plot the double logarithm function
def draw_natural_log(t: np, y: np, ax: Axes):
fitted_t, fitted_y = natural_log_curve(t, y)
ax.plot(fitted_t, fitted_y, color = "black", zorder = 2)
"""
main function begins here
"""
# set the title
fig.suptitle(f"He {analysis.helium_number}: {analysis.label}")
# define the 2x3 grid of subplots
axes = fig.subplots(2,3)
# get the first and last mass for the plots based on the raw data
first_amu = analysis.first_mass
final_amu = analysis.last_mass
# plot formatting
plot_kwargs = {
"3 amu": {"color": "blue"},
"4 amu": {"color": "red"},
"4/3 Ratio": {"color": "purple"},
first_amu: {"edgecolor": "black", "facecolor": "white", "linewidth": 1},
final_amu: {"color": "magenta"},
"baseline": {"color": "gray"}
}
# iterate over the axes and plot the data
for ax, mass in zip(axes.flatten(), ["3 amu", "4 amu", "4/3 Ratio", first_amu, final_amu, "baseline"]):
ax: Axes
mass: str
# plot the data points
active_indices = analysis.data_status[mass]
t = analysis.time_sec[active_indices==1]
y = analysis.raw_data[mass][active_indices==1]
ax.scatter(t, y, **plot_kwargs[mass], zorder=3)
ax.scatter(analysis.time_sec[active_indices==0], analysis.raw_data[mass][active_indices==0], marker="x", color="gray", zorder=3)
# plot the fit based on fit_type
if mass == "baseline": # only plot average for baseline
draw_average(t, y, ax)
else: # plot the other fits based on fit_type
if fit_type == "Average": # for average, plot the average line
draw_average(t, y, ax)
elif fit_type == "Linear": # for linear, plot the linear trendline
draw_linear(t, y, ax)
elif fit_type == "Cubic": # plot cubic function
draw_cubic(t, y, ax)
elif fit_type == "Natural logarithm": # plot double exponential function
draw_natural_log(t, y, ax)
else:
raise ValueError(f"Invalid fit type: {fit_type}")
# set the grid and labels
ax.grid(True, zorder=1)
ax.set_xlabel("Time (sec)")
if mass == "4 amu / 3 amu Ratio":
ax.set_ylabel("4 amu / 3 amu × 1000")
else:
ax.set_ylabel("Intensity (A)")
if mass == "baseline":
ax.set_title("Baseline")
else:
ax.set_title(mass)
ax.set_xlim(left=0) # do not move up as this will fuck the axes
I created a minimal reproducible example, however, even with far more data points than the ‘real thing’ it executes in about 0.05 seconds, so that isn’t useful.
How can I track the sources of the delays in the GUI updates and reduce the processing time?
Here are:
- The full GUI class definition: https://pastebin.com/Ci4GpG7y
- The GUI logic script: https://pastebin.com/ipqapiPA