Skip to content

Commit 7d4ef11

Browse files
committed
perf: improve launcher and app startup responsiveness
* perf: improve ui startup responsiveness * perf: paint launcher and app windows earlier
1 parent d849d63 commit 7d4ef11

8 files changed

Lines changed: 120 additions & 36 deletions

File tree

+labkit/+ui/+app/create.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
debug = optionValue(opts, 'debug', []);
2323
ui = buildShellFromSpec(spec, debug);
24+
drawnow limitrate;
2425
installCloseGuard(ui.figure);
2526
ui = buildControlTabs(ui, spec.props.controlTabs, debug);
2627
ui = buildWorkspace(ui, spec.props.workspace, debug);

+labkit/+ui/+app/private/createTabbedWorkbenchShell.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
end
3939
ui.fig = uifigure(figArgs{:});
4040
applyGuiTestMode(ui.fig);
41+
paintVisibleFigure();
4142

4243
ui.main = uigridlayout(ui.fig, [1 3]);
4344
ui.main.ColumnWidth = {leftWidth, 6, '1x'};
@@ -119,6 +120,13 @@ function applyGuiTestMode(fig)
119120
end
120121
end
121122

123+
function paintVisibleFigure()
124+
if guiTestMode() ~= "visible"
125+
return;
126+
end
127+
drawnow limitrate;
128+
end
129+
122130
function [tab, panel] = createScrollableTab(parent, titleText)
123131
tab = uitab(parent, 'Title', titleText);
124132
host = uigridlayout(tab, [1 1]);

+labkit/+ui/+app/private/installPreviewScrollNavigation.m

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@ function installPreviewScrollNavigation(fig, axesHandles)
99
if isempty(axesHandles)
1010
return;
1111
end
12-
disableBuiltInWheelNavigation(axesHandles);
1312

1413
key = 'labkitPreviewScrollNavigation';
1514
if isappdata(fig, key)
1615
state = getappdata(fig, key);
1716
else
1817
state = struct();
1918
state.axes = gobjects(1, 0);
19+
state.preparedAxes = gobjects(1, 0);
2020
state.fallbackScrollFcn = fig.WindowScrollWheelFcn;
2121
state.callback = @onPreviewScroll;
2222
end
23+
if ~isfield(state, 'preparedAxes')
24+
state.preparedAxes = gobjects(1, 0);
25+
end
2326

2427
state.axes = uniqueAxes([normalizeAxes(state.axes), axesHandles]);
28+
state.preparedAxes = axesInSet(normalizeAxes(state.preparedAxes), state.axes);
2529
setappdata(fig, key, state);
2630

2731
current = fig.WindowScrollWheelFcn;
@@ -44,6 +48,7 @@ function onPreviewScroll(src, event)
4448
if scrollCount == 0
4549
return;
4650
end
51+
navState = prepareAxesForWheelNavigation(src, key, navState, ax);
4752
point = ax.CurrentPoint;
4853
labkit.ui.tool.zoomAxesAtPoint(ax, point(1, 1:2), scrollCount);
4954
end
@@ -169,22 +174,52 @@ function callFallback(fcn, src, event)
169174
axesHandles = axesHandles(keep);
170175
end
171176

172-
function disableBuiltInWheelNavigation(axesHandles)
173-
for k = 1:numel(axesHandles)
174-
ax = axesHandles(k);
175-
try
176-
disableDefaultInteractivity(ax);
177-
catch
178-
end
179-
try
180-
ax.Interactions = [];
181-
catch
177+
function state = prepareAxesForWheelNavigation(fig, key, state, ax)
178+
if axesContains(state.preparedAxes, ax)
179+
return;
180+
end
181+
disableBuiltInWheelNavigation(ax);
182+
state.preparedAxes = uniqueAxes([normalizeAxes(state.preparedAxes), ax]);
183+
if isValidHandle(fig)
184+
setappdata(fig, key, state);
185+
end
186+
end
187+
188+
function disableBuiltInWheelNavigation(ax)
189+
try
190+
disableDefaultInteractivity(ax);
191+
catch
192+
end
193+
try
194+
ax.Interactions = [];
195+
catch
196+
end
197+
try
198+
if ~strcmp(ax.Toolbar.Visible, 'on')
199+
ax.Toolbar.Visible = 'on';
182200
end
183-
try
184-
if ~strcmp(ax.Toolbar.Visible, 'on')
185-
ax.Toolbar.Visible = 'on';
186-
end
187-
catch
201+
catch
202+
end
203+
end
204+
205+
function axesHandles = axesInSet(axesHandles, allowedAxes)
206+
keep = false(size(axesHandles));
207+
for k = 1:numel(axesHandles)
208+
keep(k) = axesContains(allowedAxes, axesHandles(k));
209+
end
210+
axesHandles = axesHandles(keep);
211+
end
212+
213+
function tf = axesContains(axesHandles, ax)
214+
tf = false;
215+
axesHandles = normalizeAxes(axesHandles);
216+
if ~isValidHandle(ax)
217+
return;
218+
end
219+
for k = 1:numel(axesHandles)
220+
if isequal(axesHandles(k), ax)
221+
tf = true;
222+
return;
188223
end
189224
end
190225
end

+labkit/+ui/version.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
% compatible contract ranges implemented by this code, contract status,
1313
% and a short maintainer note.
1414

15-
info = labkit.contract.versionInfo("ui", "3.4.2", ">=3.0 <4", ...
16-
"stable", "UI 3.x app/spec/view/tool/diag contract with debounced parameter callbacks, spinner-backed panners, reusable image preview redraws, compact file panels, toolPanel file entry helpers, filePanel title context, preview-area row/column sizing, debug trace, debug artifact sample/output folders, hidden-test-safe alerts, close guard, crash reports, output folder prompts, and default text fitting.");
15+
info = labkit.contract.versionInfo("ui", "3.4.4", ">=3.0 <4", ...
16+
"stable", "UI 3.x app/spec/view/tool/diag contract with debounced parameter callbacks, spinner-backed panners, reusable image preview redraws, compact file panels, toolPanel file entry helpers, filePanel title context, preview-area row/column sizing, visible-window early paint, lazy preview scroll-interaction setup, debug trace, debug artifact sample/output folders, hidden-test-safe alerts, close guard, crash reports, output folder prompts, and default text fitting.");
1717
end

labkit_launcher.m

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,9 @@
3535
return;
3636
end
3737

38-
root = fileparts(mfilename('fullpath'));
39-
apps = discoverApps(root);
40-
4138
if mode == "list"
39+
root = fileparts(mfilename('fullpath'));
40+
apps = discoverApps(root);
4241
varargout = {appCatalogTable(apps)};
4342
return;
4443
end
@@ -47,8 +46,9 @@
4746
'labkit_launcher returns at most the launcher figure handle.');
4847
end
4948

49+
root = fileparts(mfilename('fullpath'));
5050
initializeLauncherPath(root);
51-
fig = runLauncher(root, apps);
51+
fig = runLauncher(root);
5252
if nargout == 1
5353
varargout = {fig};
5454
end
@@ -86,7 +86,7 @@
8686
info = struct( ...
8787
"name", "labkit_launcher", ...
8888
"displayName", "LabKit App Launcher", ...
89-
"version", "1.2.2", ...
89+
"version", "1.2.3", ...
9090
"updated", "2026-07-02");
9191
end
9292

@@ -109,7 +109,7 @@ function addPathIfMissing(folder, varargin)
109109

110110
%% Section: Main launcher window
111111

112-
function fig = runLauncher(root, apps)
112+
function fig = runLauncher(root)
113113
panelFontSize = 15;
114114
tableFontSize = 15;
115115

@@ -123,6 +123,7 @@ function addPathIfMissing(folder, varargin)
123123
fig = uifigure(figArgs{:});
124124
fig.Name = char(launcherVersionTitle());
125125
applyLauncherGuiTestMode(fig);
126+
paintVisibleLauncherFigure();
126127
main = uigridlayout(fig, [1 3]);
127128
main.ColumnWidth = {360, 5, '1x'};
128129
main.RowHeight = {'1x'};
@@ -199,7 +200,7 @@ function addPathIfMissing(folder, varargin)
199200
btnProfile.Layout.Column = 2;
200201
if isprop(btnProfile, 'Tooltip')
201202
btnProfile.Tooltip = ['Profile the next app launched from this launcher ' ...
202-
'until that app window closes.'];
203+
'until that app window closes. Saves the report without opening a browser.'];
203204
end
204205
txtInfo = uitextarea(controlsGrid, 'Editable', 'off', 'Value', {'Ready.'});
205206

@@ -220,8 +221,17 @@ function addPathIfMissing(folder, varargin)
220221
ui.controls.appTable = struct('table', appTable);
221222
setappdata(fig, 'labkitUiRegistry', ui);
222223

223-
state = struct('apps', apps, 'visibleApps', apps, 'selectedRow', 1, ...
224-
'status', integrityStatus(root, apps), 'profileNextLaunch', false);
224+
state = struct('apps', emptyAppStruct(), 'visibleApps', emptyAppStruct(), ...
225+
'selectedRow', 1, 'status', "Loading app list...", ...
226+
'profileNextLaunch', false);
227+
appTable.Data = cell(0, 5);
228+
setLaunchEnabled(false);
229+
updateInfo("Loading app list...");
230+
drawnow limitrate;
231+
232+
state.apps = discoverApps(root);
233+
state.visibleApps = state.apps;
234+
state.status = integrityStatus(root, state.apps);
225235
refreshTable();
226236

227237
function onRefreshApps(varargin)
@@ -1110,7 +1120,7 @@ function validateCleanArtifactsTarget(root, target, relativeTarget)
11101120
targetFile = fullfile(root, char(app.relativePath));
11111121
target = @() launchProfileTarget(app, debugMode);
11121122
[htmlFile, artifacts] = profileLabKitTarget(target, htmlFile, ...
1113-
'OpenReport', launcherGuiTestMode() ~= "hidden", ...
1123+
'OpenReport', false, ...
11141124
'WaitForGuiClose', true, ...
11151125
'CloseFiguresAfterRun', false, ...
11161126
'ProjectRoot', root, ...
@@ -1482,6 +1492,13 @@ function applyLauncherGuiTestMode(fig)
14821492
end
14831493
end
14841494

1495+
function paintVisibleLauncherFigure()
1496+
if launcherGuiTestMode() ~= "visible"
1497+
return;
1498+
end
1499+
drawnow limitrate;
1500+
end
1501+
14851502
function tf = confirmUpdate(root, sourceLabel)
14861503
message = sprintf(['Download %s zip and replace the LabKit runtime in:\n\n%s\n\n' ...
14871504
'The LabKit folder should contain only LabKit runtime files. Before ' ...

tests/cases/gui/labkit_framework/ui/GuiLayoutUiAxesWorkbenchTest.m

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ function verify_gui_layout_ui_axes_workbench()
4141
plot(plotAx, 1:3, [1 4 2], 'DisplayName', 'probe');
4242
assert(~isempty(ui.figure.WindowScrollWheelFcn), ...
4343
'previewArea should install LabKit-managed scroll-wheel navigation.');
44-
assert(isempty(plotAx.Interactions), ...
45-
'previewArea should not rely on focus-gated built-in axes interactions.');
44+
assertPreviewScrollNotPrepared(ui.figure, plotAx, ...
45+
'previewArea should defer built-in axes interaction cleanup until scroll is used.');
4646
plotAx.XLim = [0 10];
4747
plotAx.YLim = [0 20];
4848
didZoomPlot = labkit.ui.tool.zoomAxesAtPoint(plotAx, [5 10], -1);
@@ -98,8 +98,8 @@ function verify_gui_layout_ui_axes_workbench()
9898
hFirstImage = labkit.ui.view.drawImage(ui, 'plotPreview', ...
9999
zeros(12, 16, 3, 'uint8'), 'axis', 'image', 'title', 'Image Probe');
100100
imgAx = ui.controls.plotPreview.axesById.image;
101-
assert(isempty(imgAx.Interactions), ...
102-
'drawImage should not re-enable focus-gated built-in zoom interactions.');
101+
assertPreviewScrollNotPrepared(ui.figure, imgAx, ...
102+
'drawImage should not force eager built-in axes interaction cleanup.');
103103
didZoomImage = labkit.ui.tool.zoomAxesAtPoint(imgAx, [8 6], -2);
104104
assert(didZoomImage && imgAx.XLim(1) >= 0.5 && imgAx.XLim(2) <= 16.5 && ...
105105
imgAx.YLim(1) >= 0.5 && imgAx.YLim(2) <= 12.5, ...
@@ -146,3 +146,24 @@ function noop(varargin)
146146
function value = relativeLogPosition(anchor, limits)
147147
value = (log10(anchor) - log10(limits(1))) ./ diff(log10(limits));
148148
end
149+
150+
function assertPreviewScrollNotPrepared(fig, ax, message)
151+
state = getappdata(fig, 'labkitPreviewScrollNavigation');
152+
assert(isstruct(state) && isfield(state, 'preparedAxes'), ...
153+
'previewArea should track lazy scroll preparation state.');
154+
assert(~axesContains(state.preparedAxes, ax), message);
155+
end
156+
157+
function tf = axesContains(axesHandles, ax)
158+
tf = false;
159+
if isempty(axesHandles)
160+
return;
161+
end
162+
axesHandles = axesHandles(:).';
163+
for k = 1:numel(axesHandles)
164+
if isvalid(axesHandles(k)) && isequal(axesHandles(k), ax)
165+
tf = true;
166+
return;
167+
end
168+
end
169+
end

tests/cases/gui/project/launcher/LauncherProfilerTest.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ function performance_profile_action_uses_profile_tool(testCase)
1313
'Launcher performance profile action should use the shared profileLabKitTarget tool.');
1414
testCase.verifyFalse(isempty(strfind(body, '''WaitForGuiClose'', true')), ...
1515
'Launcher performance profile should continue until the launched app window closes.');
16+
testCase.verifyFalse(isempty(strfind(body, '''OpenReport'', false')), ...
17+
'Launcher performance profile should save artifacts without opening a browser.');
1618
testCase.verifyFalse(isempty(strfind(body, ...
1719
'''artifacts'', ''profile'', ''launcher-app-session''')), ...
1820
'Launcher performance profile reports should use the launcher app-session artifact folder.');

tools/profiling/profileLabKitTarget.m

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
% htmlFile generated HTML report path
1212
% artifacts struct with htmlFile, jsonFile, and functionCount
1313
% Options:
14-
% OpenReport true opens the generated HTML report.
14+
% OpenReport false opens no browser; true opens the generated HTML report.
1515
% WaitForGuiClose true waits for newly opened figures before export.
1616
% CloseFiguresAfterRun false closes newly opened figures after profiling.
1717
% ChangeFolder true cd's to a resolved .m file folder while profiling.
@@ -28,9 +28,9 @@
2828
%
2929
% Examples:
3030
% addpath(fullfile('tools', 'profiling'))
31+
% profileLabKitTarget("labkit_launcher", [], "WaitForGuiClose", false, ...
32+
% "PrintSummary", true)
3133
% profileLabKitTarget("labkit_launcher", [], "OpenReport", true)
32-
% profileLabKitTarget("labkit_launcher", [], "OpenReport", false, ...
33-
% "WaitForGuiClose", false, "PrintSummary", true)
3434

3535
if nargin < 1
3636
target = [];
@@ -129,7 +129,7 @@
129129
function opt = parseProfileOptions(varargin)
130130
p = inputParser;
131131
p.FunctionName = 'profileLabKitTarget';
132-
addParameter(p, 'OpenReport', true, @isLogicalScalar);
132+
addParameter(p, 'OpenReport', false, @isLogicalScalar);
133133
addParameter(p, 'WaitForGuiClose', true, @isLogicalScalar);
134134
addParameter(p, 'CloseFiguresAfterRun', false, @isLogicalScalar);
135135
addParameter(p, 'ChangeFolder', true, @isLogicalScalar);

0 commit comments

Comments
 (0)