From 5611d08df1c7ca2a799dc7f115881d0ff3a84df4 Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Wed, 16 Feb 2022 21:38:43 -0800 Subject: [PATCH] Reduce memory usage of adding tracks Significantly reduce the memory footprint of adding tracks to the playlist, by coalescing the NSString and NSData objects in the info dictionaries as they are being loaded in the background, into a common data set which will then be discarded when the whole job is completed. Signed-off-by: Christopher Snowhill --- Cog.xcodeproj/project.pbxproj | 6 +++ Playlist/PlaylistLoader.m | 5 +++ Utils/RedundantPlaylistDataStore.h | 27 ++++++++++++ Utils/RedundantPlaylistDataStore.m | 68 ++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 Utils/RedundantPlaylistDataStore.h create mode 100644 Utils/RedundantPlaylistDataStore.m diff --git a/Cog.xcodeproj/project.pbxproj b/Cog.xcodeproj/project.pbxproj index c3cfdb2f6..348bc558a 100644 --- a/Cog.xcodeproj/project.pbxproj +++ b/Cog.xcodeproj/project.pbxproj @@ -164,6 +164,7 @@ 838F851C256B4AC400C3E614 /* icon_blank.icns in Resources */ = {isa = PBXBuildFile; fileRef = 838F851B256B4AC400C3E614 /* icon_blank.icns */; }; 838F851E256B4E5E00C3E614 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 838F851D256B4E5E00C3E614 /* Sparkle.framework */; }; 838F851F256B4E8B00C3E614 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 838F851D256B4E5E00C3E614 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 83988F0E27BE0A5900A0E89A /* RedundantPlaylistDataStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 83988F0D27BE0A5900A0E89A /* RedundantPlaylistDataStore.m */; }; 8399D4E21805A55000B503B1 /* XmlContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 8399D4E01805A55000B503B1 /* XmlContainer.m */; }; 839DA7CF274A2D4C001B18E5 /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 839DA7CE274A2D4C001B18E5 /* NSDictionary+Merge.m */; }; 83A360B220E4E81D00192DAB /* Flac.bundle in CopyFiles */ = {isa = PBXBuildFile; fileRef = 8303A30C20E4E3D000951EF8 /* Flac.bundle */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -974,6 +975,8 @@ 838F84FF25687C5C00C3E614 /* Cog-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Cog-Bridging-Header.h"; sourceTree = ""; }; 838F851B256B4AC400C3E614 /* icon_blank.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = icon_blank.icns; sourceTree = ""; }; 838F851D256B4E5E00C3E614 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = ThirdParty/Frameworks/Sparkle.framework; sourceTree = ""; }; + 83988F0C27BE0A5900A0E89A /* RedundantPlaylistDataStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RedundantPlaylistDataStore.h; sourceTree = ""; }; + 83988F0D27BE0A5900A0E89A /* RedundantPlaylistDataStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RedundantPlaylistDataStore.m; sourceTree = ""; }; 8399D4E01805A55000B503B1 /* XmlContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XmlContainer.m; sourceTree = ""; }; 8399D4E11805A55000B503B1 /* XmlContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XmlContainer.h; sourceTree = ""; }; 839DA7CB274A2D4C001B18E5 /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Merge.h"; sourceTree = ""; }; @@ -1204,6 +1207,8 @@ 177EC01D0B8BC2CF0000BC8C /* TrackingSlider.m */, 8370D739277419D200245CE0 /* SQLiteStore.h */, 8370D73C277419F700245CE0 /* SQLiteStore.m */, + 83988F0C27BE0A5900A0E89A /* RedundantPlaylistDataStore.h */, + 83988F0D27BE0A5900A0E89A /* RedundantPlaylistDataStore.m */, ); path = Utils; sourceTree = ""; @@ -2511,6 +2516,7 @@ 179D03200E0CB2500064A77A /* DirectoryNode.m in Sources */, 179D03210E0CB2500064A77A /* FileIconCell.m in Sources */, 179D03220E0CB2500064A77A /* FileNode.m in Sources */, + 83988F0E27BE0A5900A0E89A /* RedundantPlaylistDataStore.m in Sources */, 179D03230E0CB2500064A77A /* FileTreeDataSource.m in Sources */, 179D03240E0CB2500064A77A /* FileTreeController.m in Sources */, 8370D73D277419F700245CE0 /* SQLiteStore.m in Sources */, diff --git a/Playlist/PlaylistLoader.m b/Playlist/PlaylistLoader.m index d4ac0ea13..2a0d65e3f 100644 --- a/Playlist/PlaylistLoader.m +++ b/Playlist/PlaylistLoader.m @@ -34,6 +34,8 @@ #import "NSDictionary+Merge.h" +#import "RedundantPlaylistDataStore.h" + @implementation PlaylistLoader - (id)init { @@ -540,9 +542,11 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc NSLock *outLock = [[NSLock alloc] init]; NSMutableArray *outArray = [[NSMutableArray alloc] init]; + RedundantPlaylistDataStore *dataStore = [[RedundantPlaylistDataStore alloc] init]; __block NSLock *weakLock = outLock; __block NSMutableArray *weakArray = outArray; + __block RedundantPlaylistDataStore *weakDataStore = dataStore; { [load_info_indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *_Nonnull stop) { @@ -566,6 +570,7 @@ static inline void dispatch_sync_reentrant(dispatch_queue_t queue, dispatch_bloc NSDictionary *entryInfo = [NSDictionary dictionaryByMerging:entryProperties with:entryMetadata]; [weakLock lock]; + entryInfo = [weakDataStore coalesceEntryInfo:entryInfo]; [weakArray addObject:weakPe]; [weakArray addObject:entryInfo]; [self setProgressBarStatus:progress]; diff --git a/Utils/RedundantPlaylistDataStore.h b/Utils/RedundantPlaylistDataStore.h new file mode 100644 index 000000000..e31b49cfd --- /dev/null +++ b/Utils/RedundantPlaylistDataStore.h @@ -0,0 +1,27 @@ +// +// RedundantPlaylistDataStore.h +// Cog +// +// Created by Christopher Snowhill on 2/16/22. +// + +// This is designed primarily for the PlaylistEntry info loader, to prevent +// memory overrun due to redundant blobs of data being duplicated repeatedly +// until the list is fully loaded. This instance will be discarded after the +// info is loaded, freeing up hopefully way less memory afterward than now. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RedundantPlaylistDataStore : NSObject { + NSMutableArray *stringStore; + NSMutableArray *artStore; +} + +- (id)init; +- (NSDictionary *)coalesceEntryInfo:(NSDictionary *)pe; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Utils/RedundantPlaylistDataStore.m b/Utils/RedundantPlaylistDataStore.m new file mode 100644 index 000000000..8118eb3e9 --- /dev/null +++ b/Utils/RedundantPlaylistDataStore.m @@ -0,0 +1,68 @@ +// +// RedundantPlaylistDataStore.m +// Cog +// +// Created by Christopher Snowhill on 2/16/22. +// + +// Coalesce an entryInfo dictionary from tag loading into a common data dictionary, to +// reduce the memory footprint of adding a lot of tracks to the playlist. + +#import "RedundantPlaylistDataStore.h" + +@implementation RedundantPlaylistDataStore + +- (id)init { + self = [super init]; + + if(self) { + stringStore = [[NSMutableArray alloc] init]; + artStore = [[NSMutableArray alloc] init]; + } + + return self; +} + +- (NSString *)coalesceString:(NSString *)in { + if(in == nil) return in; + + NSUInteger index = [stringStore indexOfObject:in]; + if(index == NSNotFound) { + [stringStore addObject:in]; + return in; + } else { + return [stringStore objectAtIndex:index]; + } +} + +- (NSData *)coalesceArt:(NSData *)in { + if(in == nil) return in; + + NSUInteger index = [artStore indexOfObject:in]; + if(index == NSNotFound) { + [artStore addObject:in]; + return in; + } else { + return [artStore objectAtIndex:index]; + } +} + +- (NSDictionary *)coalesceEntryInfo:(NSDictionary *)entryInfo { + __block NSMutableDictionary *ret = [[NSMutableDictionary alloc] initWithCapacity:[entryInfo count]]; + + [entryInfo enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) { + if([[obj class] isSubclassOfClass:[NSString class]]) { + NSString *stringObj = (NSString *)obj; + [ret setObject:[self coalesceString:stringObj] forKey:key]; + } else if([[obj class] isSubclassOfClass:[NSData class]]) { + NSData *dataObj = (NSData *)obj; + [ret setObject:[self coalesceArt:dataObj] forKey:key]; + } else { + [ret setObject:obj forKey:key]; + } + }]; + + return [NSDictionary dictionaryWithDictionary:ret]; +} + +@end