From a3c9d70a81a1f4f9d55fbe60146e35034bdf23b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Wed, 10 Jan 2024 17:30:24 +0200 Subject: [PATCH] Use caching to improve performance of large profiles When the local mod list is browsed vue seems to render the whole list again on some user actions. This causes three methods of the component to get called repeatedly: - getMissingDependencies, which uses modifiableModList property, which is based on the mod list stored in VueX store. modifiableModList is used by other parts of the component as well, so I thought better not touch it at this time since figuring out unintended side effects would be a lot of work - getThunderstoreModFromMod, which used the Thunderstore mod list stored in VueX, but now uses the list stored in the ThunderstorePackages. As far as I can tell both are updated in the Splash view and by the scheduled background process in UtilityMixin. Ergo this shouldn't break things, but this is the most significant functional change in this commit, and therefore most likely culprit should problems arise - isLatestVersion, which did and still does use ThunderstorePackages for its shenanigans So while this commit doesn't reduce the incessant function calls, it caches the results to a simple object to reduce required calculations. Effects were tested with a profile containing a mod pack with 109 mods. Completing the following tasks were timed (roughly and manually), with the accompanying results (original vs. cached): - Initial rendering of the local mod list when moving from profile selection view: 7.0s vs. 5.4s - Opening modal to disable a mod with two dependants: 4.4s vs. 1,2s - Closing the modal without disabling the mod: 4.4s vs. 1.2s - Opening modal to uninstall the same mod: 4.5s vs. 1.0s - Uninstalling the mod: 15.8s vs 6.0s (There might be further changes for optimizing the uninstall process, since it seems some stuff is done after each dependant is uninstalled, while it MIGHT be enough to do it just once in the end.) For a small profile with 3 mods there's no noticeable difference between the performance of the old and new implementation. --- src/components/views/LocalModList.vue | 6 +-- src/model/VersionNumber.ts | 4 ++ src/r2mm/mods/ModBridge.ts | 54 ++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/components/views/LocalModList.vue b/src/components/views/LocalModList.vue index 63c8a05d9..b97a3dd38 100644 --- a/src/components/views/LocalModList.vue +++ b/src/components/views/LocalModList.vue @@ -327,7 +327,7 @@ import SearchUtils from '../../utils/SearchUtils'; } getThunderstoreModFromMod(mod: ManifestV2) { - return ModBridge.getThunderstoreModFromMod(mod, this.thunderstorePackages); + return ModBridge.getCachedThunderstoreModFromMod(mod); } async moveUp(vueMod: any) { @@ -360,8 +360,8 @@ import SearchUtils from '../../utils/SearchUtils'; this.filterModList(); } - isLatest(vueMod: any): boolean { - return ModBridge.isLatestVersion(vueMod); + isLatest(mod: ManifestV2): boolean { + return ModBridge.isCachedLatestVersion(mod); } getMissingDependencies(vueMod: any): string[] { diff --git a/src/model/VersionNumber.ts b/src/model/VersionNumber.ts index c95218682..ccf263b54 100644 --- a/src/model/VersionNumber.ts +++ b/src/model/VersionNumber.ts @@ -64,4 +64,8 @@ export default class VersionNumber implements ReactiveObjectConverterInterface { const patchCompare = Math.sign(this.patch - version.patch); return (majorCompare === 0 && minorCompare === 0 && patchCompare === 0); } + + public isEqualOrNewerThan(version: VersionNumber): boolean { + return this.isEqualTo(version) || this.isNewerThan(version); + } } diff --git a/src/r2mm/mods/ModBridge.ts b/src/r2mm/mods/ModBridge.ts index cd1cafaec..d3f591246 100644 --- a/src/r2mm/mods/ModBridge.ts +++ b/src/r2mm/mods/ModBridge.ts @@ -3,20 +3,25 @@ import ThunderstoreVersion from '../../model/ThunderstoreVersion'; import ManifestV2 from '../../model/ManifestV2'; import ThunderstorePackages from '../data/ThunderstorePackages'; +interface CachedMod { + tsMod: ThunderstoreMod | undefined; + isLatest: boolean; +} + +interface ModCache { + [key: string]: CachedMod; +} + export default class ModBridge { + private static CACHE: ModCache = {} - public static getLatestVersion(mod: ManifestV2, modList: ThunderstoreMod[]): ThunderstoreVersion | void { + public static getLatestVersion(mod: ManifestV2, modList: ThunderstoreMod[]): ThunderstoreVersion | undefined { const matchingMod: ThunderstoreMod | undefined = modList.find((tsMod: ThunderstoreMod) => tsMod.getFullName() === mod.getName()); if (matchingMod === undefined) { return; } // Compare version numbers and reduce. - return matchingMod.getVersions().reduce((v1: ThunderstoreVersion, v2: ThunderstoreVersion) => { - if (v1.getVersionNumber().isNewerThan(v2.getVersionNumber())) { - return v1; - } - return v2; - }); + return matchingMod.getVersions().reduce(reduceToNewestVersion); } public static getThunderstoreModFromMod(mod: ManifestV2, modList: ThunderstoreMod[]): ThunderstoreMod | undefined { @@ -27,10 +32,41 @@ export default class ModBridge { const mod: ManifestV2 = new ManifestV2().fromReactive(vueMod); const latestVersion: ThunderstoreVersion | void = ModBridge.getLatestVersion(mod, ThunderstorePackages.PACKAGES); if (latestVersion instanceof ThunderstoreVersion) { - return mod.getVersionNumber() - .isEqualTo(latestVersion.getVersionNumber()) || mod.getVersionNumber().isNewerThan(latestVersion.getVersionNumber()); + return mod.getVersionNumber().isEqualOrNewerThan(latestVersion.getVersionNumber()); } return true; } + private static getCached(mod: ManifestV2): CachedMod { + const cacheKey = `${mod.getName()}-${mod.getVersionNumber()}`; + + if (ModBridge.CACHE[cacheKey] === undefined) { + const tsMod = ThunderstorePackages.PACKAGES.find((tsMod) => tsMod.getFullName() === mod.getName()); + + if (tsMod === undefined) { + ModBridge.CACHE[cacheKey] = { tsMod: undefined, isLatest: true }; + } else { + const latestVersion = tsMod.getVersions().reduce(reduceToNewestVersion); + const isLatest = mod.getVersionNumber().isEqualOrNewerThan(latestVersion.getVersionNumber()); + ModBridge.CACHE[cacheKey] = { tsMod, isLatest }; + } + } + + return ModBridge.CACHE[cacheKey]; + } + + public static getCachedThunderstoreModFromMod(mod: ManifestV2): ThunderstoreMod | undefined { + return ModBridge.getCached(mod).tsMod; + } + + public static isCachedLatestVersion(mod: ManifestV2): boolean { + return ModBridge.getCached(mod).isLatest; + } } + +const reduceToNewestVersion = (v1: ThunderstoreVersion, v2: ThunderstoreVersion) => { + if (v1.getVersionNumber().isNewerThan(v2.getVersionNumber())) { + return v1; + } + return v2; +};