Lazy-init details for app items (#552)

Use less memory on apps by lazy-init'ing details, icons
This commit is contained in:
Mike Griese
2025-03-16 06:07:56 -05:00
committed by GitHub
parent 16650db1c9
commit f762f0dc37
17 changed files with 100 additions and 73 deletions

View File

@@ -3,9 +3,11 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions;
@@ -52,34 +54,22 @@ public sealed partial class AllAppsPage : ListPage
{
this.IsLoading = true;
Stopwatch stopwatch = new();
stopwatch.Start();
var apps = GetPrograms();
var useThumbnails = AllAppsSettings.Instance.UseThumbnails;
this.allAppsSection = apps
.Select((app) => new AppListItem(app))
.Select((app) => new AppListItem(app, useThumbnails))
.ToArray();
this.IsLoading = false;
AppCache.Instance.Value.ResetReloadFlag();
var useThumbnails = AllAppsSettings.Instance.UseThumbnails;
// if (useThumbnails)
// {
Task.Run(async () =>
{
foreach (var appListItem in this.allAppsSection)
{
await appListItem.FetchIcon(useThumbnails);
}
});
// }
// else
// {
// foreach (var appListItem in this.allAppsSection)
// {
// appListItem.FetchIcon(useThumbnails).ConfigureAwait(false);
// }
// }
stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
}
internal List<AppItem> GetPrograms()

View File

@@ -2,8 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
@@ -14,18 +16,32 @@ internal sealed partial class AppListItem : ListItem
private readonly AppItem _app;
private static readonly Tag _appTag = new("App");
public AppListItem(AppItem app)
private readonly Lazy<Details> _details;
private readonly Lazy<IconInfo> _icon;
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
public AppListItem(AppItem app, bool useThumbnails)
: base(new AppCommand(app))
{
_app = app;
Title = app.Name;
Subtitle = app.Subtitle;
Tags = [_appTag];
MoreCommands = _app.Commands!.ToArray();
_details = new Lazy<Details>(() => BuildDetails());
_icon = new Lazy<IconInfo>(() =>
{
var t = FetchIcon(useThumbnails);
t.Wait();
return t.Result;
});
}
private void BuildDetails()
private Details BuildDetails()
{
var metadata = new List<DetailsElement>();
metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } });
@@ -34,7 +50,7 @@ internal sealed partial class AppListItem : ListItem
metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } });
}
Details = new Details()
return new Details()
{
Title = this.Title,
HeroImage = this.Icon ?? new IconInfo(string.Empty),
@@ -42,18 +58,22 @@ internal sealed partial class AppListItem : ListItem
};
}
public async Task FetchIcon(bool useThumbnails)
public async Task<IconInfo> FetchIcon(bool useThumbnails)
{
IconInfo? icon = null;
if (_app.IsPackaged)
{
Icon = new IconInfo(_app.IcoPath);
BuildDetails();
return;
icon = new IconInfo(_app.IcoPath);
if (_details.IsValueCreated)
{
_details.Value.HeroImage = icon;
}
return icon;
}
if (useThumbnails)
{
IconInfo? icon = null;
try
{
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath);
@@ -67,13 +87,18 @@ internal sealed partial class AppListItem : ListItem
{
}
Icon = icon ?? new IconInfo(_app.IcoPath);
icon = icon ?? new IconInfo(_app.IcoPath);
}
else
{
Icon = new IconInfo(_app.IcoPath);
icon = new IconInfo(_app.IcoPath);
}
BuildDetails();
if (_details.IsValueCreated)
{
_details.Value.HeroImage = icon;
}
return icon;
}
}

View File

@@ -17,6 +17,8 @@
<PackageReference Include="WyHash" />
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>

View File

@@ -5,8 +5,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
@@ -32,8 +30,6 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
Icon = Icons.FileExplorerSegoe;
Name = Resources.Indexer_Title;
PlaceholderText = Resources.Indexer_PlaceholderText;
Logger.InitializeLogger("\\CmdPal\\Indexer\\Logs");
}
public override void UpdateSearchText(string oldSearch, string newSearch)

View File

@@ -4,6 +4,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -85,7 +86,7 @@ public sealed partial class CommandPaletteHost : IExtensionHost
return Task.CompletedTask.AsAsyncAction();
}
Debug.WriteLine(message.Message);
Logger.LogDebug(message.Message);
_ = Task.Run(() =>
{

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
@@ -60,6 +60,8 @@ public sealed class CommandProviderWrapper
Icon.InitializeProperties();
Settings = new(provider.Settings, this, _taskScheduler);
Settings.InitializeProperties();
Logger.LogDebug($"Initialized command provider {ProviderId}");
}
public CommandProviderWrapper(IExtensionWrapper extension, TaskScheduler mainThread)
@@ -69,7 +71,7 @@ public sealed class CommandProviderWrapper
ExtensionHost = new CommandPaletteHost(extension);
if (!Extension.IsRunning())
{
throw new ArgumentException("You forgot to start the extension. This is a coding error - make sure to call StartExtensionAsync");
throw new ArgumentException("You forgot to start the extension. This is a CmdPal error - we need to make sure to call StartExtensionAsync");
}
var extensionImpl = extension.GetExtensionObject();
@@ -90,12 +92,14 @@ public sealed class CommandProviderWrapper
model.ItemsChanged += CommandProvider_ItemsChanged;
isValid = true;
Logger.LogDebug($"Initialized extension command provider {Extension.PackageFamilyName}:{Extension.ExtensionUniqueId}");
}
catch (Exception e)
{
Debug.WriteLine("Failed to initialize CommandProvider for extension.");
Debug.WriteLine($"Extension was {Extension!.PackageFamilyName}");
Debug.WriteLine(e);
Logger.LogError("Failed to initialize CommandProvider for extension.");
Logger.LogError($"Extension was {Extension!.PackageFamilyName}");
Logger.LogError(e.ToString());
}
isValid = true;
@@ -129,12 +133,14 @@ public sealed class CommandProviderWrapper
Settings = new(model.Settings, this, _taskScheduler);
Settings.InitializeProperties();
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
}
catch (Exception e)
{
Debug.WriteLine("Failed to load commands from extension");
Debug.WriteLine($"Extension was {Extension!.PackageFamilyName}");
Debug.WriteLine(e);
Logger.LogError("Failed to load commands from extension");
Logger.LogError($"Extension was {Extension!.PackageFamilyName}");
Logger.LogError(e.ToString());
}
if (commands != null)

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Specialized;
using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Ext.Apps;
using Microsoft.CmdPal.UI.ViewModels.Messages;
@@ -91,8 +90,6 @@ public partial class MainListPage : DynamicListPage,
public override void UpdateSearchText(string oldSearch, string newSearch)
{
// Handle changes to the filter text here
Debug.WriteLine($"UpdateSearchText '{oldSearch}' -> '{newSearch}'");
if (!string.IsNullOrEmpty(SearchText))
{
var aliases = _serviceProvider.GetService<AliasManager>()!;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -153,7 +153,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
// Cancel any ongoing search
if (_cancellationTokenSource != null)
{
Debug.WriteLine("Cancelling old initialize task");
_cancellationTokenSource.Cancel();
}

View File

@@ -6,6 +6,7 @@ using CommunityToolkit.Common;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.MainPage;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
@@ -85,7 +86,10 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
{
// TODO: Handle failure case
System.Diagnostics.Debug.WriteLine(viewModel.InitializeCommand.ExecutionTask.Exception);
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
{
Logger.LogError(ex.ToString());
}
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>

View File

@@ -3,10 +3,10 @@
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
@@ -241,7 +241,7 @@ public partial class TopLevelCommandManager : ObservableObject,
}
catch (Exception ex)
{
Debug.WriteLine(ex);
Logger.LogError(ex.ToString());
}
}
}

View File

@@ -77,7 +77,6 @@ public sealed partial class SearchBar : UserControl,
// _ = _queue.EnqueueAsync(() =>
_queue.TryEnqueue(new(() =>
{
Debug.WriteLine("Clear search");
this.FilterBox.Text = string.Empty;
if (CurrentPageViewModel != null)
@@ -232,8 +231,7 @@ public sealed partial class SearchBar : UserControl,
_debounceTimer.Debounce(
() =>
{
// TODO: Actually Plumb Filtering
Debug.WriteLine($"Filter: {FilterBox.Text}");
// Actually plumb Filtering to the viewmodel
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.Filter = FilterBox.Text;

View File

@@ -108,9 +108,6 @@ public sealed partial class ListPage : Page,
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
private void ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Debug.WriteLine("ItemsList_SelectionChanged");
// Debug.WriteLine($" +{e.AddedItems.Count} / -{e.RemovedItems.Count}");
// Debug.WriteLine($" selected='{ItemsList.SelectedItem}'");
if (ItemsList.SelectedItem is ListItemViewModel item)
{
var vm = ViewModel;

View File

@@ -5,6 +5,7 @@
using System.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using ManagedCommon;
using Microsoft.CmdPal.UI.Settings;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.MainPage;
@@ -134,6 +135,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
if (command is IPage page)
{
Logger.LogDebug($"Navigating to page");
// TODO GH #526 This needs more better locking too
_ = _queue.TryEnqueue(() =>
{
@@ -184,6 +187,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
else if (command is IInvokableCommand invokable)
{
Logger.LogDebug($"Invoking command");
HandleInvokeCommand(message, invokable);
}
}
@@ -299,6 +303,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
if (result != null)
{
var kind = result.Kind;
Logger.LogDebug($"handling {kind.ToString()}");
switch (kind)
{
case CommandResultKind.Dismiss:

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.Windows.AppLifecycle;
namespace Microsoft.CmdPal.UI;
@@ -25,6 +26,9 @@ internal sealed class Program
return 0;
}
Logger.InitializeLogger("\\CmdPal\\Logs\\");
Logger.LogDebug($"Starting at {DateTime.UtcNow}");
WinRT.ComWrappersSupport.InitializeComWrappers();
var isRedirect = DecideRedirection();
if (!isRedirect)

View File

@@ -27,6 +27,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
</ItemGroup>

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Management.Deployment;
@@ -40,7 +41,7 @@ public partial class InstallPackageListItem : ListItem
{description}
""";
IconInfo heroIcon = new IconInfo(string.Empty);
var heroIcon = new IconInfo(string.Empty);
var icons = metadata.Icons;
if (icons.Count > 0)
{
@@ -164,7 +165,7 @@ public partial class InstallPackageListItem : ListItem
{
if (!ApiInformation.IsApiContractPresent("Microsoft.Management.Deployment", 12))
{
Debug.WriteLine($"RefreshPackageCatalogAsync isn't available");
Logger.LogError($"RefreshPackageCatalogAsync isn't available");
e.FakeChangeStatus();
Command = e;
Icon = (IconInfo?)Command.Icon;
@@ -174,7 +175,7 @@ public partial class InstallPackageListItem : ListItem
_ = Task.Run(() =>
{
Stopwatch s = new();
Debug.WriteLine($"Starting RefreshPackageCatalogAsync");
Logger.LogDebug($"Starting RefreshPackageCatalogAsync");
s.Start();
var refs = WinGetStatics.AvailableCatalogs.ToArray();
@@ -185,12 +186,12 @@ public partial class InstallPackageListItem : ListItem
}
s.Stop();
Debug.WriteLine($" RefreshPackageCatalogAsync took {s.ElapsedMilliseconds}ms");
Logger.LogDebug($"RefreshPackageCatalogAsync took {s.ElapsedMilliseconds}ms");
}).ContinueWith((previous) =>
{
if (previous.IsCompletedSuccessfully)
{
Debug.WriteLine($"Updating InstalledStatus");
Logger.LogDebug($"Updating InstalledStatus");
UpdatedInstalledStatus();
}
});

View File

@@ -11,6 +11,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.WinGet.Pages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -104,7 +105,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
// Cancel any ongoing search
if (_cancellationTokenSource != null)
{
Debug.WriteLine("Cancelling old search");
Logger.LogDebug("Cancelling old search", memberName: nameof(DoUpdateSearchText));
_cancellationTokenSource.Cancel();
}
@@ -139,18 +140,18 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
catch (OperationCanceledException)
{
// Handle cancellation gracefully (e.g., log or ignore)
Debug.WriteLine($" Cancelled search for '{newSearch}'");
Logger.LogDebug($" Cancelled search for '{newSearch}'");
}
catch (Exception ex)
{
// Handle other exceptions
Console.WriteLine(ex.Message);
Logger.LogError(ex.Message);
}
}
private void UpdateWithResults(IEnumerable<CatalogPackage> results, string query)
{
Debug.WriteLine($"Completed search for '{query}'");
Logger.LogDebug($"Completed search for '{query}'");
lock (_resultsLock)
{
this._results = results;
@@ -174,7 +175,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
}
var searchDebugText = $"{query}{(HasTag ? "+" : string.Empty)}{_tag}";
Debug.WriteLine($"Starting search for '{searchDebugText}'");
Logger.LogDebug($"Starting search for '{searchDebugText}'");
var results = new HashSet<CatalogPackage>(new PackageIdCompare());
// Default selector: this is the way to do a `winget search <query>`
@@ -217,7 +218,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
// foreach (var catalog in connections)
{
Debug.WriteLine($" Searching {catalog.Info.Name} ({query})");
Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync));
ct.ThrowIfCancellationRequested();
@@ -234,7 +235,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
return [];
}
Debug.WriteLine($" got results for ({query})");
Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync));
foreach (var match in searchResults.Matches.ToArray())
{
ct.ThrowIfCancellationRequested();
@@ -245,12 +246,12 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
results.Add(package);
}
Debug.WriteLine($" ({searchDebugText}): count: {results.Count}");
Logger.LogDebug($" ({searchDebugText}): count: {results.Count}", memberName: nameof(DoSearchAsync));
}
stopwatch.Stop();
Debug.WriteLine($"Search \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms");
Logger.LogDebug($"Search \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync));
return results;
}