From 0e4d1dc79fc035dc63453e463179d81a0994621d Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 21 Feb 2024 09:33:21 +0000 Subject: [PATCH] perf: improve performance of computing dock configuration (#688) --- .../workspace-platform-starter/CHANGELOG.md | 1 + .../client/src/framework/utils-img.ts | 11 +- .../client/src/framework/workspace/dock.ts | 207 +++++++++--------- 3 files changed, 116 insertions(+), 103 deletions(-) diff --git a/how-to/workspace-platform-starter/CHANGELOG.md b/how-to/workspace-platform-starter/CHANGELOG.md index 73c3492d87..588e0e4775 100644 --- a/how-to/workspace-platform-starter/CHANGELOG.md +++ b/how-to/workspace-platform-starter/CHANGELOG.md @@ -3,6 +3,7 @@ ## v17.2.0 - Improved performance of switching schemes +- Improved performance of computing dock configuration, especially on theme changes. ## v17.0.0 diff --git a/how-to/workspace-platform-starter/client/src/framework/utils-img.ts b/how-to/workspace-platform-starter/client/src/framework/utils-img.ts index fd721792bd..466cf155a9 100644 --- a/how-to/workspace-platform-starter/client/src/framework/utils-img.ts +++ b/how-to/workspace-platform-starter/client/src/framework/utils-img.ts @@ -1,6 +1,6 @@ import { isStringValue } from "./utils"; -const IMAGE_CACHE: { [key: string]: string } = {}; +const IMAGE_CACHE = new Map(); /** * Load an image to a data url containing base64 image data. @@ -18,8 +18,9 @@ export async function imageUrlToDataUrl( const key = `${url}_${dimensions}`; - if (IMAGE_CACHE[key]) { - return IMAGE_CACHE[key]; + const cachedValue = IMAGE_CACHE.get(key); + if (cachedValue) { + return cachedValue.dataUri; } return new Promise((resolve) => { @@ -41,17 +42,19 @@ export async function imageUrlToDataUrl( if (ctx) { ctx.drawImage(img, 0, 0, dimensions, dimensions); dataUri = canvas.toDataURL("image/png", 1); - IMAGE_CACHE[key] = dataUri; + IMAGE_CACHE.set(key, { dataUri }); } } catch {} resolve(dataUri); }); img.addEventListener("error", () => { + IMAGE_CACHE.set(key, { dataUri: undefined }); // eslint-disable-next-line unicorn/no-useless-undefined resolve(undefined); }); img.src = url; } catch { + IMAGE_CACHE.set(key, { dataUri: undefined }); // eslint-disable-next-line unicorn/no-useless-undefined resolve(undefined); } diff --git a/how-to/workspace-platform-starter/client/src/framework/workspace/dock.ts b/how-to/workspace-platform-starter/client/src/framework/workspace/dock.ts index 32bade77e3..3e48d192e7 100644 --- a/how-to/workspace-platform-starter/client/src/framework/workspace/dock.ts +++ b/how-to/workspace-platform-starter/client/src/framework/workspace/dock.ts @@ -216,51 +216,55 @@ async function buildButtons(): Promise { * @returns The dock buttons to display. */ async function buildButtonsFromEntries(entries: DockButtonTypes[]): Promise { - const buttons: DockButton[] = []; + const [iconFolder, colorSchemeMode] = await Promise.all([ + getCurrentIconFolder(), + getCurrentColorSchemeMode() + ]); - const iconFolder = await getCurrentIconFolder(); - const colorSchemeMode = await getCurrentColorSchemeMode(); const platform = getCurrentSync(); - for (const entry of entries) { + const buttonPromises = entries.map(async (entry) => { const visible = entry.visible ?? true; if (Array.isArray(entry.conditions)) { for (const c of entry.conditions) { usedConditions.add(c); } } - if ( - visible && - (await checkConditions(platform, entry.conditions, { callerType: "dock", customData: entry })) - ) { + + const conditionsMet = visible + ? await checkConditions(platform, entry.conditions, { callerType: "dock", customData: entry }) + : false; + + if (conditionsMet) { if ("appId" in entry) { - await addEntryAsApp(buttons, entry, iconFolder, colorSchemeMode); + return addEntryAsApp(entry, iconFolder, colorSchemeMode); } else if ("action" in entry) { - await addEntryAsAction(buttons, entry, iconFolder, colorSchemeMode); + return addEntryAsAction(entry, iconFolder, colorSchemeMode); } else if ("options" in entry) { - await addEntriesAsDropdown(buttons, entry, iconFolder, colorSchemeMode, platform); + return addEntriesAsDropdown(entry, iconFolder, colorSchemeMode, platform); } else if ("tags" in entry) { - await addEntriesByAppTag(buttons, entry, iconFolder, colorSchemeMode); + return addEntriesByAppTag(entry, iconFolder, colorSchemeMode); } } - } + }); - return buttons; + const buttons = await Promise.all(buttonPromises); + const buttonsFlat = buttons.flat(); + return buttonsFlat.filter((button): button is DockButton => !isEmpty(button)); } /** * Add an entry to the dock as an app. - * @param buttons The list of buttons to add to. * @param entry The entry details. * @param iconFolder The folder for icons. * @param colorSchemeMode The color scheme + * @returns The dock entry. */ async function addEntryAsApp( - buttons: DockButton[], entry: Omit & { id?: string }, iconFolder: string, colorSchemeMode: ColorSchemeMode -): Promise { +): Promise { // If the button has an appId we are going to launch that // but the config can override the tooltip or icon let tooltip = entry.tooltip; @@ -279,7 +283,7 @@ async function addEntryAsApp( } } - buttons.push({ + return { id: entry.id, type: DockButtonNames.ActionButton, tooltip: tooltip ?? "", @@ -291,149 +295,149 @@ async function addEntryAsApp( appId: entry.appId } } - }); + }; } /** * Add an entry to the dock as an action. - * @param buttons The list of buttons to add to. * @param entry The entry details. * @param iconFolder The folder for icons. * @param colorSchemeMode The color scheme + * @returns The dock entry. */ async function addEntryAsAction( - buttons: DockButton[], entry: Omit & { id?: string }, iconFolder: string, colorSchemeMode: ColorSchemeMode -): Promise { +): Promise { if (!isStringValue(entry.tooltip)) { logger.error("You must specify the tooltip for a DockButtonAction"); } else { - buttons.push({ + return { id: entry.id, type: DockButtonNames.ActionButton, tooltip: entry.tooltip, iconUrl: themeUrl(entry.iconUrl, iconFolder, colorSchemeMode), action: entry.action - }); + }; } } /** * Add an entry to the dock as an drop down. - * @param buttons The list of buttons to add to. * @param entry The entry details. * @param iconFolder The folder for icons. * @param colorSchemeMode The color scheme * @param platform The workspace platform for checking conditions. + * @returns The dock entry */ async function addEntriesAsDropdown( - buttons: DockButton[], entry: Omit & { id?: string }, iconFolder: string, colorSchemeMode: ColorSchemeMode, platform: WorkspacePlatformModule -): Promise { +): Promise { // Options are present so this is a drop down // The items in the drop down can be an appId or a custom action if (!isStringValue(entry.tooltip)) { logger.error("You must specify the tooltip for a DockButtonDropdown"); } else { - const opts: DockButton[] = []; - - for (const option of entry.options) { - if (Array.isArray(option.conditions)) { - for (const c of option.conditions) { - usedConditions.add(c); + const opts = entry.options.map( + async (option): Promise => { + if (Array.isArray(option.conditions)) { + for (const c of option.conditions) { + usedConditions.add(c); + } } - } - if ( - await checkConditions(platform, option.conditions, { - callerType: "dock", - customData: { ...option, id: "" } - }) - ) { - // If there are options this is a submenu - if ("options" in option) { - const subOptions = await buildButtonsFromEntries(option.options as DockButtonTypes[]); - - opts.push({ - type: DockButtonNames.DropdownButton, - tooltip: option.tooltip ?? "", - iconUrl: option.iconUrl, - options: subOptions - }); - } else if ("appId" in option) { - // If the options has an appId we are going to launch that - // otherwise we use the custom action. - - const app = await getApp(option.appId); - let iconUrl = option.iconUrl; - if (!isStringValue(option.iconUrl) && app) { - iconUrl = getAppIcon(app); - } + if ( + await checkConditions(platform, option.conditions, { + callerType: "dock", + customData: { ...option, id: "" } + }) + ) { + // If there are options this is a submenu + if ("options" in option) { + const subOptions = await buildButtonsFromEntries(option.options as DockButtonTypes[]); + + return { + type: DockButtonNames.DropdownButton, + tooltip: option.tooltip ?? "", + iconUrl: option.iconUrl, + options: subOptions + }; + } else if ("appId" in option) { + // If the options has an appId we are going to launch that + // otherwise we use the custom action. + + const app = await getApp(option.appId); + let iconUrl = option.iconUrl; + if (!isStringValue(option.iconUrl) && app) { + iconUrl = getAppIcon(app); + } - opts.push({ - type: DockButtonNames.ActionButton, - tooltip: option.tooltip ?? app?.title ?? "", - iconUrl, - action: { - id: PLATFORM_ACTION_IDS.launchApp, - customData: { - source: "dock", - appId: option.appId + return { + type: DockButtonNames.ActionButton, + tooltip: option.tooltip ?? app?.title ?? "", + iconUrl, + action: { + id: PLATFORM_ACTION_IDS.launchApp, + customData: { + source: "dock", + appId: option.appId + } } - } - }); - } else if ("tags" in option) { - await addEntriesByAppTag(opts, option, iconFolder, colorSchemeMode); - } else if ("action" in option) { - opts.push({ - type: DockButtonNames.ActionButton, - tooltip: option.tooltip ?? "", - iconUrl: option.iconUrl, - action: option.action - }); + }; + } else if ("tags" in option) { + return addEntriesByAppTag(option, iconFolder, colorSchemeMode); + } else if ("action" in option) { + return { + type: DockButtonNames.ActionButton, + tooltip: option.tooltip ?? "", + iconUrl: option.iconUrl, + action: option.action + }; + } } + return undefined; } - } + ); - if (opts.length === 0) { - opts.push({ + const optionPromises = await Promise.all(opts); + const optionsFlat = optionPromises.flat(); + const filteredOptions = optionsFlat.filter((o): o is DockButton => !isEmpty(o)); + + if (filteredOptions.length === 0) { + return { tooltip: entry.noEntries ?? "There are no entries", disabled: true, action: { id: "noop" } - }); + }; } - buttons.push( - await addDropdownOrMenu( - entry.id, - entry.tooltip ?? "", - themeUrl(entry.iconUrl, iconFolder, colorSchemeMode), - opts - ) + return addDropdownOrMenu( + entry.id, + entry.tooltip ?? "", + themeUrl(entry.iconUrl, iconFolder, colorSchemeMode), + filteredOptions ); } } /** * Add entries to the dock based on their app tags as either multiple buttons or a drop down. - * @param buttons The list of buttons to add to. * @param entry The entry details. * @param iconFolder The folder for icons. * @param colorSchemeMode The color scheme + * @returns The dock entry */ async function addEntriesByAppTag( - buttons: DockButton[], entry: Omit & { id?: string }, iconFolder: string, colorSchemeMode: ColorSchemeMode -): Promise { +): Promise<(DockButton | undefined)[] | undefined> { if (!Array.isArray(entry.tags)) { logger.error("You must specify an array for the tags parameter for an DockButtonAppsByTag"); } else { @@ -442,10 +446,11 @@ async function addEntriesByAppTag( const dockApps = await getAppsByTag(entry.tags, false, { private: false }); if (entry.display === "individual") { + const entries: DockButton[] = []; // Individual so show a button for each app for (const dockApp of dockApps) { const icon = entry.iconUrl ?? getAppIcon(dockApp); - buttons.push({ + entries.push({ id: `${entry.id}-${dockApp.appId}`, tooltip: entry.tooltip ?? dockApp.title, iconUrl: themeUrl(icon, iconFolder, colorSchemeMode), @@ -458,6 +463,8 @@ async function addEntriesByAppTag( } }); } + + return entries; } else if (entry.display === "group") { // Group display so show a drop down with all the entries in it if (!isStringValue(entry.tooltip)) { @@ -496,14 +503,14 @@ async function addEntriesByAppTag( }); } - buttons.push( + return [ await addDropdownOrMenu( entry.id, entry.tooltip ?? "", themeUrl(iconUrl, iconFolder, colorSchemeMode), opts ) - ); + ]; } } } @@ -722,9 +729,11 @@ async function addDropdownOrMenu( if (popupMenuStyle === "platform") { // Built-in native dock menus require the entry icons as base64, so convert them - for (const opt of options) { - opt.iconUrl = await imageUrlToDataUrl(opt.iconUrl, 20); - } + await Promise.all( + options.map(async (opt) => { + opt.iconUrl = await imageUrlToDataUrl(opt.iconUrl, 20); + }) + ); return { id, type: DockButtonNames.DropdownButton,