Skip to content

Commit da4b961

Browse files
authored
Merge pull request #3854 from Flow-Launcher/plugin_initialization
Asynchronous Loading & Initialization Plugin Model to Improve Window Startup Speed
2 parents 3690042 + fb84cb0 commit da4b961

File tree

14 files changed

+562
-253
lines changed

14 files changed

+562
-253
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Flow.Launcher.Plugin;
2+
3+
namespace Flow.Launcher.Core.Plugin;
4+
5+
public interface IResultUpdateRegister
6+
{
7+
/// <summary>
8+
/// Register a plugin to receive results updated event.
9+
/// </summary>
10+
/// <param name="pair"></param>
11+
void RegisterResultsUpdatedEvent(PluginPair pair);
12+
}

Flow.Launcher.Core/Plugin/PluginManager.cs

Lines changed: 355 additions & 132 deletions
Large diffs are not rendered by default.

Flow.Launcher.Core/Plugin/PluginsLoader.cs

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static List<PluginPair> Plugins(List<PluginMetadata> metadatas, PluginsSe
4848
return plugins;
4949
}
5050

51-
private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source)
51+
private static List<PluginPair> DotNetPlugins(List<PluginMetadata> source)
5252
{
5353
var erroredPlugins = new List<string>();
5454

@@ -58,55 +58,57 @@ private static IEnumerable<PluginPair> DotNetPlugins(List<PluginMetadata> source
5858
foreach (var metadata in metadatas)
5959
{
6060
var milliseconds = PublicApi.Instance.StopwatchLogDebug(ClassName, $"Constructor init cost for {metadata.Name}", () =>
61-
{
62-
Assembly assembly = null;
63-
IAsyncPlugin plugin = null;
61+
{
62+
Assembly assembly = null;
63+
IAsyncPlugin plugin = null;
6464

65-
try
66-
{
67-
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
68-
assembly = assemblyLoader.LoadAssemblyAndDependencies();
65+
try
66+
{
67+
var assemblyLoader = new PluginAssemblyLoader(metadata.ExecuteFilePath);
68+
assembly = assemblyLoader.LoadAssemblyAndDependencies();
6969

70-
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
71-
typeof(IAsyncPlugin));
70+
var type = assemblyLoader.FromAssemblyGetTypeOfInterface(assembly,
71+
typeof(IAsyncPlugin));
7272

73-
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
73+
plugin = Activator.CreateInstance(type) as IAsyncPlugin;
7474

75-
metadata.AssemblyName = assembly.GetName().Name;
76-
}
75+
metadata.AssemblyName = assembly.GetName().Name;
76+
}
7777
#if DEBUG
78-
catch (Exception)
79-
{
80-
throw;
81-
}
78+
catch (Exception)
79+
{
80+
throw;
81+
}
8282
#else
83-
catch (Exception e) when (assembly == null)
84-
{
85-
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
86-
}
87-
catch (InvalidOperationException e)
88-
{
89-
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
90-
}
91-
catch (ReflectionTypeLoadException e)
92-
{
93-
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
94-
}
95-
catch (Exception e)
96-
{
97-
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
98-
}
83+
catch (Exception e) when (assembly == null)
84+
{
85+
PublicApi.Instance.LogException(ClassName, $"Couldn't load assembly for the plugin: {metadata.Name}", e);
86+
}
87+
catch (InvalidOperationException e)
88+
{
89+
PublicApi.Instance.LogException(ClassName, $"Can't find the required IPlugin interface for the plugin: <{metadata.Name}>", e);
90+
}
91+
catch (ReflectionTypeLoadException e)
92+
{
93+
PublicApi.Instance.LogException(ClassName, $"The GetTypes method was unable to load assembly types for the plugin: <{metadata.Name}>", e);
94+
}
95+
catch (Exception e)
96+
{
97+
PublicApi.Instance.LogException(ClassName, $"The following plugin has errored and can not be loaded: <{metadata.Name}>", e);
98+
}
9999
#endif
100100

101-
if (plugin == null)
102-
{
103-
erroredPlugins.Add(metadata.Name);
104-
return;
105-
}
101+
if (plugin == null)
102+
{
103+
erroredPlugins.Add(metadata.Name);
104+
return;
105+
}
106+
107+
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
108+
});
106109

107-
plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata });
108-
});
109110
metadata.InitTime += milliseconds;
111+
PublicApi.Instance.LogDebug(ClassName, $"Constructor cost for <{metadata.Name}> is <{metadata.InitTime}ms>");
110112
}
111113

112114
if (erroredPlugins.Count > 0)

Flow.Launcher.Core/Resource/Internationalization.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,22 @@ public static void UpdatePluginMetadataTranslations()
377377
}
378378
}
379379

380+
public static void UpdatePluginMetadataTranslation(PluginPair p)
381+
{
382+
// Update plugin metadata name & description
383+
if (p.Plugin is not IPluginI18n pluginI18N) return;
384+
try
385+
{
386+
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
387+
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
388+
pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture);
389+
}
390+
catch (Exception e)
391+
{
392+
PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e);
393+
}
394+
}
395+
380396
#endregion
381397

382398
#region IDisposable

Flow.Launcher.Infrastructure/DialogJump/DialogJump.cs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Windows.Win32;
1414
using Windows.Win32.Foundation;
1515
using Windows.Win32.UI.Accessibility;
16+
using System.Collections.Concurrent;
1617

1718
namespace Flow.Launcher.Infrastructure.DialogJump
1819
{
@@ -60,12 +61,12 @@ public static class DialogJump
6061

6162
private static HWND _mainWindowHandle = HWND.Null;
6263

63-
private static readonly Dictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
64+
private static readonly ConcurrentDictionary<DialogJumpExplorerPair, IDialogJumpExplorerWindow> _dialogJumpExplorers = new();
6465

6566
private static DialogJumpExplorerPair _lastExplorer = null;
6667
private static readonly Lock _lastExplorerLock = new();
6768

68-
private static readonly Dictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
69+
private static readonly ConcurrentDictionary<DialogJumpDialogPair, IDialogJumpDialogWindow> _dialogJumpDialogs = new();
6970

7071
private static IDialogJumpDialogWindow _dialogWindow = null;
7172
private static readonly Lock _dialogWindowLock = new();
@@ -101,22 +102,13 @@ public static class DialogJump
101102

102103
#region Initialize & Setup
103104

104-
public static void InitializeDialogJump(IList<DialogJumpExplorerPair> dialogJumpExplorers,
105-
IList<DialogJumpDialogPair> dialogJumpDialogs)
105+
public static void InitializeDialogJump()
106106
{
107107
if (_initialized) return;
108108

109-
// Initialize Dialog Jump explorers & dialogs
110-
_dialogJumpExplorers.Add(WindowsDialogJumpExplorer, null);
111-
foreach (var explorer in dialogJumpExplorers)
112-
{
113-
_dialogJumpExplorers.Add(explorer, null);
114-
}
115-
_dialogJumpDialogs.Add(WindowsDialogJumpDialog, null);
116-
foreach (var dialog in dialogJumpDialogs)
117-
{
118-
_dialogJumpDialogs.Add(dialog, null);
119-
}
109+
// Initialize preinstalled Dialog Jump explorers & dialogs
110+
_dialogJumpExplorers.TryAdd(WindowsDialogJumpExplorer, null);
111+
_dialogJumpDialogs.TryAdd(WindowsDialogJumpDialog, null);
120112

121113
// Initialize main window handle
122114
_mainWindowHandle = Win32Helper.GetMainWindowHandle();
@@ -131,6 +123,29 @@ public static void InitializeDialogJump(IList<DialogJumpExplorerPair> dialogJump
131123
_initialized = true;
132124
}
133125

126+
public static void InitializeDialogJumpPlugin(PluginPair pair)
127+
{
128+
// Add Dialog Jump explorers & dialogs
129+
if (pair.Plugin is IDialogJumpExplorer explorer)
130+
{
131+
var dialogJumpExplorer = new DialogJumpExplorerPair
132+
{
133+
Plugin = explorer,
134+
Metadata = pair.Metadata
135+
};
136+
_dialogJumpExplorers.TryAdd(dialogJumpExplorer, null);
137+
}
138+
if (pair.Plugin is IDialogJumpDialog dialog)
139+
{
140+
var dialogJumpDialog = new DialogJumpDialogPair
141+
{
142+
Plugin = dialog,
143+
Metadata = pair.Metadata
144+
};
145+
_dialogJumpDialogs.TryAdd(dialogJumpDialog, null);
146+
}
147+
}
148+
134149
public static void SetupDialogJump(bool enabled)
135150
{
136151
if (enabled == _enabled) return;

Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.ComponentModel;
44
using System.IO;
@@ -173,9 +173,21 @@ public interface IPublicAPI
173173
/// <summary>
174174
/// Get all loaded plugins
175175
/// </summary>
176+
/// <remarks>
177+
/// Will also return any plugins not fully initialized yet
178+
/// </remarks>
176179
/// <returns></returns>
177180
List<PluginPair> GetAllPlugins();
178181

182+
/// <summary>
183+
/// Get all initialized plugins
184+
/// </summary>
185+
/// <param name="includeFailed">
186+
/// Whether to include plugins that failed to initialize
187+
/// </param>
188+
/// <returns></returns>
189+
List<PluginPair> GetAllInitializedPlugins(bool includeFailed);
190+
179191
/// <summary>
180192
/// Registers a callback function for global keyboard events.
181193
/// </summary>

Flow.Launcher/App.xaml.cs

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,14 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
187187
// So set to OnExplicitShutdown to prevent the application from shutting down before main window is created
188188
Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;
189189

190+
// Setup log level before any logging is done
190191
Log.SetLogLevel(_settings.LogLevel);
191192

192193
// Update dynamic resources base on settings
193194
Current.Resources["SettingWindowFont"] = new FontFamily(_settings.SettingWindowFont);
194195
Current.Resources["ContentControlThemeFontFamily"] = new FontFamily(_settings.SettingWindowFont);
195196

197+
// Initialize notification system before any notification api is called
196198
Notification.Install();
197199

198200
// Enable Win32 dark mode if the system is in dark mode before creating all windows
@@ -201,6 +203,7 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
201203
// Initialize language before portable clean up since it needs translations
202204
await _internationalization.InitializeLanguageAsync();
203205

206+
// Clean up after portability update
204207
Ioc.Default.GetRequiredService<Portable>().PreStartCleanUpAfterPortabilityUpdate();
205208

206209
API.LogInfo(ClassName, "Begin Flow Launcher startup ----------------------------------------------------");
@@ -210,52 +213,66 @@ await API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
210213
RegisterDispatcherUnhandledException();
211214
RegisterTaskSchedulerUnhandledException();
212215

213-
var imageLoadertask = ImageLoader.InitializeAsync();
214-
215-
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
216-
217-
PluginManager.LoadPlugins(_settings.PluginSettings);
218-
219-
// Register ResultsUpdated event after all plugins are loaded
220-
Ioc.Default.GetRequiredService<MainViewModel>().RegisterResultsUpdatedEvent();
216+
var imageLoaderTask = ImageLoader.InitializeAsync();
221217

222218
Http.Proxy = _settings.Proxy;
223219

224220
// Initialize plugin manifest before initializing plugins so that they can use the manifest instantly
225221
await API.UpdatePluginManifestAsync();
226222

227-
await PluginManager.InitializePluginsAsync();
228-
229-
// Update plugin titles after plugins are initialized with their api instances
230-
Internationalization.UpdatePluginMetadataTranslations();
231-
232-
await imageLoadertask;
223+
await imageLoaderTask;
233224

234225
_mainWindow = new MainWindow();
235226

236227
Current.MainWindow = _mainWindow;
237228
Current.MainWindow.Title = Constant.FlowLauncher;
238229

230+
// Initialize Dialog Jump before hotkey mapper since hotkey mapper will register its hotkey
231+
// Initialize Dialog Jump after main window is created so that it can access main window handle
232+
DialogJump.InitializeDialogJump();
233+
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
234+
239235
// Initialize hotkey mapper instantly after main window is created because
240236
// it will steal focus from main window which causes window hide
241237
HotKeyMapper.Initialize();
242238

243239
// Initialize theme for main window
244240
Ioc.Default.GetRequiredService<Theme>().ChangeTheme();
245241

246-
DialogJump.InitializeDialogJump(PluginManager.GetDialogJumpExplorers(), PluginManager.GetDialogJumpDialogs());
247-
DialogJump.SetupDialogJump(_settings.EnableDialogJump);
248-
249242
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
250243

251244
RegisterExitEvents();
252245

253246
AutoStartup();
254247
AutoUpdates();
255-
AutoPluginUpdates();
256248

257249
API.SaveAppAllSettings();
258-
API.LogInfo(ClassName, "End Flow Launcher startup ----------------------------------------------------");
250+
API.LogInfo(ClassName, "End Flow Launcher startup ------------------------------------------------------");
251+
252+
_ = API.StopwatchLogInfoAsync(ClassName, "Startup cost", async () =>
253+
{
254+
API.LogInfo(ClassName, "Begin plugin initialization ----------------------------------------------------");
255+
256+
AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate(_settings);
257+
258+
PluginManager.LoadPlugins(_settings.PluginSettings);
259+
260+
await PluginManager.InitializePluginsAsync(_mainVM);
261+
262+
// Refresh home page after plugins are initialized because users may open main window during plugin initialization
263+
// And home page is created without full plugin list
264+
if (_settings.ShowHomePage && _mainVM.QueryResultsSelected() && string.IsNullOrEmpty(_mainVM.QueryText))
265+
{
266+
_mainVM.QueryResults();
267+
}
268+
269+
AutoPluginUpdates();
270+
271+
// Save all settings since we possibly update the plugin environment paths
272+
API.SaveAppAllSettings();
273+
274+
API.LogInfo(ClassName, "End plugin initialization ------------------------------------------------------");
275+
});
259276
});
260277
}
261278

Flow.Launcher/Helper/ResultHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static class ResultHelper
2020
{
2121
var plugin = PluginManager.GetPluginForId(pluginId);
2222
if (plugin == null) return null;
23-
var query = QueryBuilder.Build(rawQuery, PluginManager.NonGlobalPlugins);
23+
var query = QueryBuilder.Build(rawQuery, PluginManager.GetNonGlobalPlugins());
2424
if (query == null) return null;
2525
try
2626
{

Flow.Launcher/Languages/en.xaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
<system:String x:Key="PositionReset">Position Reset</system:String>
6767
<system:String x:Key="PositionResetToolTip">Reset search window position</system:String>
6868
<system:String x:Key="queryTextBoxPlaceholder">Type here to search</system:String>
69+
<system:String x:Key="pluginStillInitializing">{0}: This plugin is still initializing...</system:String>
70+
<system:String x:Key="pluginStillInitializingSubtitle">Select this result to requery</system:String>
71+
<system:String x:Key="pluginFailedToRespond">{0}: Failed to respond!</system:String>
72+
<system:String x:Key="pluginFailedToRespondSubtitle">Select this result for more info</system:String>
6973

7074
<!-- Setting General -->
7175
<system:String x:Key="flowlauncher_settings">Settings</system:String>

Flow.Launcher/MainWindow.xaml.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ private void OnKeyDown(object sender, KeyEventArgs e)
477477
&& QueryTextBox.CaretIndex == QueryTextBox.Text.Length)
478478
{
479479
var queryWithoutActionKeyword =
480-
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.NonGlobalPlugins)?.Search;
480+
QueryBuilder.Build(QueryTextBox.Text.Trim(), PluginManager.GetNonGlobalPlugins())?.Search;
481481

482482
if (FilesFolders.IsLocationPathString(queryWithoutActionKeyword))
483483
{

0 commit comments

Comments
 (0)