Made launch performance *much* faster for large (thousands of feeds) subscriptions list. Also: split container.children in container.topLevelFeeds and container.folders. This simplifies a bunch of things, and makes some things faster.

This commit is contained in:
Brent Simmons 2018-09-16 17:54:42 -07:00
parent f88c58a130
commit a914b3949b
8 changed files with 252 additions and 216 deletions

View File

@ -37,6 +37,7 @@ public enum AccountType: Int {
public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
public struct UserInfoKey {
public static let newArticles = "newArticles" // AccountDidDownloadArticles
public static let updatedArticles = "updatedArticles" // AccountDidDownloadArticles
@ -48,8 +49,20 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
public let accountID: String
public let type: AccountType
public var nameForDisplay = ""
public var children = [AnyObject]()
var idToFeedDictionary = [String: Feed]()
public var topLevelFeeds = Set<Feed>()
public var folders: Set<Folder>? = Set<Folder>()
private var feedDictionaryNeedsUpdate = true
private var _idToFeedDictionary = [String: Feed]()
var idToFeedDictionary: [String: Feed] {
if feedDictionaryNeedsUpdate {
rebuildFeedDictionaries()
}
return _idToFeedDictionary
}
private var fetchingAllUnreadCounts = false
let settingsFile: String
let dataFolder: String
let database: ArticlesDatabase
@ -65,6 +78,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
private var unreadCounts = [String: Int]() // [feedID: Int]
private let opmlFilePath: String
private var _flattenedFeeds = Set<Feed>()
private var flattenedFeedsNeedUpdate = true
private struct SettingsKey {
static let unreadCount = "unreadCount"
}
@ -204,7 +220,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
}
let folder = Folder(account: self, name: name)
children += [folder]
folders!.insert(folder)
dirty = true
postChildrenDidChangeNotification()
@ -244,21 +260,30 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
didAddFeed = folder.addFeed(feed)
}
else {
if !topLevelObjectsContainsFeed(feed) {
children += [feed]
if !topLevelFeeds.contains(feed) {
topLevelFeeds.insert(feed)
postChildrenDidChangeNotification()
didAddFeed = true
}
didAddFeed = true
}
if didAddFeed {
addToFeedDictionaries(feed)
dirty = true
structureDidChange()
}
return didAddFeed
}
public func addFeeds(_ feeds: Set<Feed>, to folder: Folder?) {
if let folder = folder {
folder.addFeeds(feeds)
}
else {
topLevelFeeds.formUnion(feeds)
}
structureDidChange()
}
public func createFeed(with name: String?, editedName: String?, url: String) -> Feed? {
// For syncing, this may need to be an async method with a callback,
@ -285,13 +310,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
// TODO: support subfolders, maybe, some day, if one of the sync systems
// supports subfolders. But, for now, parentFolder is ignored.
if objectIsChild(folder) {
if folders!.contains(folder) {
return true
}
children += [folder]
folders!.insert(folder)
postChildrenDidChangeNotification()
rebuildFeedDictionaries()
structureDidChange()
return true
}
@ -300,9 +324,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
guard let children = opmlDocument.children else {
return
}
rebuildFeedDictionaries()
importOPMLItems(children, parentFolder: nil)
saveToDisk()
structureDidChange()
DispatchQueue.main.async {
self.refreshAll()
@ -450,6 +473,35 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
unreadCounts[feed.feedID] = unreadCount
}
public func structureDidChange() {
// Feeds were added or deleted. Or folders added or deleted.
// Or feeds inside folders were added or deleted.
dirty = true
flattenedFeedsNeedUpdate = true
feedDictionaryNeedsUpdate = true
}
// MARK: - Container
public func flattenedFeeds() -> Set<Feed> {
if flattenedFeedsNeedUpdate {
updateFlattenedFeeds()
}
return _flattenedFeeds
}
public func deleteFeed(_ feed: Feed) {
topLevelFeeds.remove(feed)
structureDidChange()
postChildrenDidChangeNotification()
}
public func deleteFolder(_ folder: Folder) {
folders?.remove(folder)
structureDidChange()
postChildrenDidChangeNotification()
}
// MARK: - Debug
public func debugDropConditionalGetInfo() {
@ -482,6 +534,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
@objc func batchUpdateDidPerform(_ note: Notification) {
flattenedFeedsNeedUpdate = true
rebuildFeedDictionaries()
updateUnreadCount()
}
@ -492,17 +545,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return
}
if let account = object as? Account, account === self {
dirty = true
structureDidChange()
}
if let folder = object as? Folder, folder.account === self {
dirty = true
structureDidChange()
}
}
@objc func displayNameDidChange(_ note: Notification) {
if let folder = note.object as? Folder, folder.account === self {
dirty = true
structureDidChange()
}
}
@ -582,11 +635,20 @@ private extension Account {
guard let childrenArray = d[Key.children] as? [[String: Any]] else {
return
}
children = objects(with: childrenArray)
rebuildFeedDictionaries()
let userInfo = d[Key.userInfo] as? NSDictionary
delegate.update(account: self, withUserInfo: userInfo)
let children = objects(with: childrenArray)
var feeds = Set<Feed>()
var folders = Set<Folder>()
for oneChild in children {
if let feed = oneChild as? Feed {
feeds.insert(feed)
}
else if let folder = oneChild as? Folder {
folders.insert(folder)
}
}
self.topLevelFeeds = feeds
self.folders = folders
structureDidChange()
// Rename plist file so we dont see it next time.
let renamedFilePath = (dataFolder as NSString).appendingPathComponent("AccountData-old.plist")
@ -624,11 +686,13 @@ private extension Account {
NSApplication.shared.presentError(error)
return
}
guard let parsedOPML = opmlDocument else {
guard let parsedOPML = opmlDocument, let children = parsedOPML.children else {
return
}
importOPML(parsedOPML)
BatchUpdate.shared.perform {
importOPMLItems(children, parentFolder: nil)
}
}
func saveToDisk() {
@ -650,50 +714,47 @@ private extension Account {
private extension Account {
func updateFlattenedFeeds() {
var feeds = Set<Feed>()
feeds.formUnion(topLevelFeeds)
for folder in folders! {
feeds.formUnion(folder.flattenedFeeds())
}
_flattenedFeeds = feeds
flattenedFeedsNeedUpdate = false
}
func rebuildFeedDictionaries() {
var urlDictionary = [String: Feed]()
var idDictionary = [String: Feed]()
flattenedFeeds().forEach { (feed) in
urlDictionary[feed.url] = feed
idDictionary[feed.feedID] = feed
}
idToFeedDictionary = idDictionary
}
func addToFeedDictionaries(_ feed: Feed) {
idToFeedDictionary[feed.feedID] = feed
}
func topLevelObjectsContainsFeed(_ feed: Feed) -> Bool {
return children.contains(where: { (object) -> Bool in
if let oneFeed = object as? Feed {
if oneFeed.feedID == feed.feedID {
return true
}
}
return false
})
_idToFeedDictionary = idDictionary
feedDictionaryNeedsUpdate = false
}
func createFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed {
let feed = Feed(account: self, url: opmlFeedSpecifier.feedURL, feedID: opmlFeedSpecifier.feedURL)
feed.editedName = opmlFeedSpecifier.title
if let feedTitle = opmlFeedSpecifier.title, feed.editedName == nil {
feed.editedName = feedTitle
}
return feed
}
func importOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) {
var feedsToAdd = Set<Feed>()
items.forEach { (item) in
if let feedSpecifier = item.feedSpecifier {
let feed = createFeed(with: feedSpecifier)
addFeed(feed, to: parentFolder)
feedsToAdd.insert(feed)
return
}
@ -713,11 +774,21 @@ private extension Account {
importOPMLItems(itemChildren, parentFolder: folder)
}
}
if !feedsToAdd.isEmpty {
addFeeds(feedsToAdd, to: parentFolder)
}
}
func updateUnreadCount() {
unreadCount = calculateUnreadCount(flattenedFeeds())
if fetchingAllUnreadCounts {
return
}
var updatedUnreadCount = 0
for feed in flattenedFeeds() {
updatedUnreadCount += feed.unreadCount
}
unreadCount = updatedUnreadCount
}
func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
@ -734,6 +805,7 @@ private extension Account {
func fetchAllUnreadCounts() {
fetchingAllUnreadCounts = true
database.fetchAllNonZeroUnreadCounts { (unreadCountDictionary) in
if unreadCountDictionary.isEmpty {
@ -751,6 +823,7 @@ private extension Account {
feed.unreadCount = 0
}
}
self.fetchingAllUnreadCounts = false
self.updateUnreadCount()
}
}
@ -764,6 +837,7 @@ extension Account {
return idToFeedDictionary[feedID]
}
}
// MARK: - OPMLRepresentable
@ -773,10 +847,11 @@ extension Account: OPMLRepresentable {
public func OPMLString(indentLevel: Int) -> String {
var s = ""
for oneObject in children {
if let oneOPMLObject = oneObject as? OPMLRepresentable {
s += oneOPMLObject.OPMLString(indentLevel: indentLevel + 1)
}
for feed in topLevelFeeds {
s += feed.OPMLString(indentLevel: indentLevel + 1)
}
for folder in folders! {
s += folder.OPMLString(indentLevel: indentLevel + 1)
}
return s
}

View File

@ -18,7 +18,8 @@ extension Notification.Name {
public protocol Container: class {
var children: [AnyObject] { get set }
var topLevelFeeds: Set<Feed> { get set }
var folders: Set<Folder>? { get set }
func hasAtLeastOneFeed() -> Bool
func objectIsChild(_ object: AnyObject) -> Bool
@ -29,7 +30,7 @@ public protocol Container: class {
func deleteFeed(_ feed: Feed)
func deleteFolder(_ folder: Folder)
//Recursive
//Recursive  checks subfolders
func flattenedFeeds() -> Set<Feed>
func hasFeed(with feedID: String) -> Bool
func hasFeed(withURL url: String) -> Bool
@ -44,43 +45,31 @@ public protocol Container: class {
public extension Container {
func hasAtLeastOneFeed() -> Bool {
for child in children {
if child is Feed {
return true
}
if let folder = child as? Folder {
if folder.hasAtLeastOneFeed() {
return true
}
}
}
return false
return topLevelFeeds.count > 0
}
func hasChildFolder(with name: String) -> Bool {
return childFolder(with: name) != nil
}
func childFolder(with name: String) -> Folder? {
for child in children {
if let folder = child as? Folder, folder.name == name {
guard let folders = folders else {
return nil
}
for folder in folders {
if folder.name == name {
return folder
}
}
return nil
}
func objectIsChild(_ object: AnyObject) -> Bool {
for child in children {
if object === child {
return true
}
if let feed = object as? Feed {
return topLevelFeeds.contains(feed)
}
if let folder = object as? Folder {
return folders?.contains(folder) ?? false
}
return false
}
@ -88,123 +77,74 @@ public extension Container {
func flattenedFeeds() -> Set<Feed> {
var feeds = Set<Feed>()
for object in children {
if let feed = object as? Feed {
feeds.insert(feed)
}
else if let container = object as? Container {
feeds.formUnion(container.flattenedFeeds())
feeds.formUnion(topLevelFeeds)
if let folders = folders {
for folder in folders {
feeds.formUnion(folder.flattenedFeeds())
}
}
return feeds
}
func hasFeed(with feedID: String) -> Bool {
return existingFeed(with: feedID) != nil
}
func hasFeed(withURL url: String) -> Bool {
return existingFeed(withURL: url) != nil
}
func existingFeed(with feedID: String) -> Feed? {
for child in children {
if let feed = child as? Feed, feed.feedID == feedID {
return feed
}
if let container = child as? Container, let feed = container.existingFeed(with: feedID) {
for feed in flattenedFeeds() {
if feed.feedID == feedID {
return feed
}
}
return nil
}
func existingFeed(withURL url: String) -> Feed? {
for child in children {
if let feed = child as? Feed, feed.url == url {
return feed
}
if let container = child as? Container, let feed = container.existingFeed(withURL: url) {
for feed in flattenedFeeds() {
if feed.url == url {
return feed
}
}
return nil
}
func existingFolder(with name: String) -> Folder? {
for child in children {
if let folder = child as? Folder {
if folder.name == name {
return folder
}
if let subFolder = folder.existingFolder(with: name) {
return subFolder
}
}
guard let folders = folders else {
return nil
}
for folder in folders {
if folder.name == name {
return folder
}
if let subFolder = folder.existingFolder(with: name) {
return subFolder
}
}
return nil
}
func existingFolder(withID folderID: Int) -> Folder? {
for child in children {
if let folder = child as? Folder {
if folder.folderID == folderID {
return folder
}
if let subFolder = folder.existingFolder(withID: folderID) {
return subFolder
}
}
guard let folders = folders else {
return nil
}
for folder in folders {
if folder.folderID == folderID {
return folder
}
if let subFolder = folder.existingFolder(withID: folderID) {
return subFolder
}
}
return nil
}
func indexOf<T: Equatable>(_ object: T) -> Int? {
return children.index(where: { (child) -> Bool in
if let oneObject = child as? T {
return oneObject == object
}
return false
})
}
func delete<T: Equatable>(_ object: T) {
if let index = indexOf(object) {
children.remove(at: index)
postChildrenDidChangeNotification()
}
}
func deleteFeed(_ feed: Feed) {
return delete(feed)
}
func deleteFolder(_ folder: Folder) {
return delete(folder)
}
func postChildrenDidChangeNotification() {
NotificationCenter.default.post(name: .ChildrenDidChange, object: self)
}
}

View File

@ -10,11 +10,13 @@ import Foundation
import Articles
import RSCore
public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, Hashable {
public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, Hashable {
public weak var account: Account?
public var children = [AnyObject]()
public var topLevelFeeds: Set<Feed> = Set<Feed>()
public var folders: Set<Folder>? = nil // subfolders are not supported, so this is always nil
public var name: String? {
didSet {
postDisplayNameDidChangeNotification()
@ -69,21 +71,28 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider,
self.init(account: account, name: name)
if let childrenArray = dictionary[Key.children] as? [[String: Any]] {
self.children = Folder.objects(with: childrenArray, account: account)
self.topLevelFeeds = Folder.feedsOnly(with: childrenArray, account: account)
}
}
// MARK: Feeds
/// Add a single feed. Return true if number of feeds in folder changes.
func addFeed(_ feed: Feed) -> Bool {
// Return true in the case where the feed is already a child.
if !childrenContain(feed) {
children += [feed]
return addFeeds(Set([feed]))
}
/// Add one or more feeds. Return true if number of feeds in folder changes.
@discardableResult
func addFeeds(_ feedsToAdd: Set<Feed>) -> Bool {
let feedCount = topLevelFeeds.count
topLevelFeeds.formUnion(feedsToAdd)
if feedCount != topLevelFeeds.count {
postChildrenDidChangeNotification()
return true
}
return true
return false
}
// MARK: - Notifications
@ -102,6 +111,30 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider,
updateUnreadCount()
}
// MARK: Container
public func flattenedFeeds() -> Set<Feed> {
// Since sub-folders are not supported, its always the top-level feeds.
return topLevelFeeds
}
public func objectIsChild(_ object: AnyObject) -> Bool {
// Folders contain Feed objects only, at least for now.
guard let feed = object as? Feed else {
return false
}
return topLevelFeeds.contains(feed)
}
public func deleteFeed(_ feed: Feed) {
topLevelFeeds.remove(feed)
postChildrenDidChangeNotification()
}
public func deleteFolder(_ folder: Folder) {
// Nothing to do
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
@ -121,17 +154,15 @@ public final class Folder: DisplayNameProvider, Container, UnreadCountProvider,
private extension Folder {
func updateUnreadCount() {
unreadCount = calculateUnreadCount(children)
var updatedUnreadCount = 0
for feed in topLevelFeeds {
updatedUnreadCount += feed.unreadCount
}
unreadCount = updatedUnreadCount
}
func childrenContain(_ feed: Feed) -> Bool {
return children.contains(where: { (object) -> Bool in
if let oneFeed = object as? Feed {
return oneFeed == feed
}
return false
})
return topLevelFeeds.contains(feed)
}
}
@ -139,17 +170,6 @@ private extension Folder {
private extension Folder {
static func objects(with diskObjects: [[String: Any]], account: Account) -> [AnyObject] {
if account.supportsSubFolders {
return account.objects(with: diskObjects)
}
else {
let flattenedFeeds = feedsOnly(with: diskObjects, account: account)
return Array(flattenedFeeds) as [AnyObject]
}
}
static func feedsOnly(with diskObjects: [[String: Any]], account: Account) -> Set<Feed> {
// This Folder doesnt support subfolders, but they might exist on disk.
@ -187,11 +207,9 @@ extension Folder: OPMLRepresentable {
var hasAtLeastOneChild = false
for child in children {
if let opmlObject = child as? OPMLRepresentable {
s += opmlObject.OPMLString(indentLevel: indentLevel + 1)
hasAtLeastOneChild = true
}
for feed in topLevelFeeds {
s += feed.OPMLString(indentLevel: indentLevel + 1)
hasAtLeastOneChild = true
}
if !hasAtLeastOneChild {

View File

@ -27,15 +27,10 @@ private extension FolderTreeControllerDelegate {
// Root node is Top Level and children are folders. Folders cant have subfolders.
// This will have to be revised later.
var folderNodes = [Node]()
for oneRepresentedObject in AccountManager.shared.localAccount.children {
if let folder = oneRepresentedObject as? Folder {
folderNodes += [createNode(folder, parent: node)]
}
guard let folders = AccountManager.shared.localAccount.folders else {
return nil
}
let folderNodes = folders.map { createNode($0, parent: node) }
return folderNodes.sortedAlphabetically()
}

View File

@ -51,9 +51,15 @@ private extension SidebarTreeControllerDelegate {
let container = containerNode.representedObject as! Container
var children = [AnyObject]()
children.append(contentsOf: Array(container.topLevelFeeds))
if let folders = container.folders {
children.append(contentsOf: Array(folders))
}
var updatedChildNodes = [Node]()
container.children.forEach { (representedObject) in
children.forEach { (representedObject) in
if let existingNode = containerNode.childNodeRepresentingObject(representedObject) {
if !updatedChildNodes.contains(existingNode) {

View File

@ -68,34 +68,35 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
@objc(feeds)
var feeds:NSArray {
let feeds = account.children.compactMap { $0 as? Feed }
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
return account.topLevelFeeds.map { ScriptableFeed($0, container:self) } as NSArray
}
@objc(valueInFeedsWithUniqueID:)
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? {
let feeds = account.children.compactMap { $0 as? Feed }
let feeds = Array(account.topLevelFeeds)
guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil }
return ScriptableFeed(feed, container:self)
}
@objc(valueInFeedsWithName:)
func valueInFeeds(withName name:String) -> ScriptableFeed? {
let feeds = account.children.compactMap { $0 as? Feed }
let feeds = Array(account.topLevelFeeds)
guard let feed = feeds.first(where:{$0.name == name}) else { return nil }
return ScriptableFeed(feed, container:self)
}
@objc(folders)
var folders:NSArray {
let folders = account.children.compactMap { $0 as? Folder }
return folders.map { ScriptableFolder($0, container:self) } as NSArray
let foldersSet = account.folders ?? Set<Folder>()
let folders = Array(foldersSet)
return folders.map { ScriptableFolder($0, container:self) } as NSArray
}
@objc(valueInFoldersWithUniqueID:)
func valueInFolders(withUniqueID id:NSNumber) -> ScriptableFolder? {
let folderId = id.intValue
let folders = account.children.compactMap { $0 as? Folder }
let foldersSet = account.folders ?? Set<Folder>()
let folders = Array(foldersSet)
guard let folder = folders.first(where:{$0.folderID == folderId}) else { return nil }
return ScriptableFolder(folder, container:self)
}
@ -105,14 +106,15 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
@objc(contents)
var contents:NSArray {
var contentsArray:[AnyObject] = []
for child in account.children {
if let aFeed = child as? Feed {
contentsArray.append(ScriptableFeed(aFeed, container:self))
} else if let aFolder = child as? Folder {
contentsArray.append(ScriptableFolder(aFolder, container:self))
}
}
return contentsArray as NSArray
for feed in account.topLevelFeeds {
contentsArray.append(ScriptableFeed(feed, container: self))
}
if let folders = account.folders {
for folder in folders {
contentsArray.append(ScriptableFolder(folder, container:self))
}
}
return contentsArray as NSArray
}
@objc(opmlRepresentation)

View File

@ -92,7 +92,7 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
@objc(feeds)
var feeds:NSArray {
let feeds = folder.children.compactMap { $0 as? Feed }
let feeds = Array(folder.topLevelFeeds)
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
}

View File

@ -77,7 +77,7 @@ extension NSApplication : ScriptingObjectContainer {
let accounts = AccountManager.shared.accounts
let emptyFeeds:[Feed] = []
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in
let accountFeeds = nthAccount.children.compactMap { $0 as? Feed }
let accountFeeds = Array(nthAccount.topLevelFeeds)
return result + accountFeeds
}
}