Skip to content

Commit

Permalink
XmlParser: reduce CPU load when in background
Browse files Browse the repository at this point in the history
  • Loading branch information
BLeeEZ committed Apr 15, 2024
1 parent e36c560 commit 2071065
Show file tree
Hide file tree
Showing 55 changed files with 211 additions and 142 deletions.
2 changes: 2 additions & 0 deletions Amperfy/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
os_log("sceneWillEnterForeground", log: self.log, type: .info)
AmperKit.shared.isInForeground = true
}

func sceneDidEnterBackground(_ scene: UIScene) {
Expand All @@ -109,6 +110,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Save changes in the application's managed object context when the application transitions to the background.
os_log("sceneDidEnterBackground", log: self.log, type: .info)
self.appDelegate.scheduleAppRefresh()
AmperKit.shared.isInForeground = false
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
Expand Down
10 changes: 9 additions & 1 deletion AmperfyKit/AmperfyKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public class AmperKit {
return inst!
}

public var isInForeground = true

public lazy var log = {
return OSLog(subsystem: "Amperfy", category: "AppDelegate")
}()
Expand All @@ -60,7 +62,7 @@ public class AmperKit {
return EventLogger(storage: storage)
}()
public lazy var backendApi: BackendProxy = {
return BackendProxy(eventLogger: eventLogger)
return BackendProxy(performanceMonitor: self, eventLogger: eventLogger)
}()
public lazy var notificationHandler: EventNotificationHandler = {
return EventNotificationHandler()
Expand Down Expand Up @@ -164,3 +166,9 @@ public class AmperKit {
}

}

extension AmperKit: ThreadPerformanceMonitor {
public var shouldSlowDownExecution: Bool {
return !isInForeground
}
}
6 changes: 4 additions & 2 deletions AmperfyKit/Api/Ampache/AmpacheApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ import PromiseKit
class AmpacheApi: BackendApi {

private let ampacheXmlServerApi: AmpacheXmlServerApi
private let performanceMonitor: ThreadPerformanceMonitor
private let eventLogger: EventLogger

init(ampacheXmlServerApi: AmpacheXmlServerApi, eventLogger: EventLogger) {
init(ampacheXmlServerApi: AmpacheXmlServerApi, performanceMonitor: ThreadPerformanceMonitor, eventLogger: EventLogger) {
self.ampacheXmlServerApi = ampacheXmlServerApi
self.performanceMonitor = performanceMonitor
self.eventLogger = eventLogger
}

Expand Down Expand Up @@ -65,7 +67,7 @@ class AmpacheApi: BackendApi {
}

func createLibrarySyncer(storage: PersistentStorage) -> LibrarySyncer {
return AmpacheLibrarySyncer(ampacheXmlServerApi: ampacheXmlServerApi, storage: storage, eventLogger: eventLogger)
return AmpacheLibrarySyncer(ampacheXmlServerApi: ampacheXmlServerApi, performanceMonitor: self.performanceMonitor, storage: storage, eventLogger: eventLogger)
}

func createArtworkArtworkDownloadDelegate() -> DownloadManagerDelegate {
Expand Down
64 changes: 33 additions & 31 deletions AmperfyKit/Api/Ampache/AmpacheLibrarySyncer.swift

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions AmperfyKit/Api/Ampache/AmpacheXmlParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ class AmpacheNotifiableXmlParser: AmpacheXmlParser {

var parseNotifier: ParsedObjectNotifiable?

init(parseNotifier: ParsedObjectNotifiable? = nil) {
init(performanceMonitor: ThreadPerformanceMonitor, parseNotifier: ParsedObjectNotifiable? = nil) {
self.parseNotifier = parseNotifier
super.init(performanceMonitor: performanceMonitor)
}

}
Expand All @@ -67,9 +68,9 @@ class AmpacheXmlLibParser: AmpacheNotifiableXmlParser {

var library: LibraryStorage

init(library: LibraryStorage, parseNotifier: ParsedObjectNotifiable? = nil) {
init(performanceMonitor: ThreadPerformanceMonitor, library: LibraryStorage, parseNotifier: ParsedObjectNotifiable? = nil) {
self.library = library
super.init(parseNotifier: parseNotifier)
super.init(performanceMonitor: performanceMonitor, parseNotifier: parseNotifier)
}

func parseArtwork(urlString: String) -> Artwork? {
Expand Down
8 changes: 5 additions & 3 deletions AmperfyKit/Api/Ampache/AmpacheXmlServerApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class AmpacheXmlServerApi: URLCleanser {
let clientApiVersion = "500000"

private let log = OSLog(subsystem: "Amperfy", category: "Ampache")
private let performanceMonitor: ThreadPerformanceMonitor
let eventLogger: EventLogger
private var credentials: LoginCredentials?
private var authHandshake: AuthentificationHandshake?
Expand All @@ -92,7 +93,8 @@ class AmpacheXmlServerApi: URLCleanser {
}
}

init(eventLogger: EventLogger) {
init(performanceMonitor: ThreadPerformanceMonitor,eventLogger: EventLogger) {
self.performanceMonitor = performanceMonitor
self.eventLogger = eventLogger
}

Expand Down Expand Up @@ -214,7 +216,7 @@ class AmpacheXmlServerApi: URLCleanser {
private func parseAuthResult(response: APIDataResponse) -> Promise<AuthentificationHandshake> {
return Promise<AuthentificationHandshake> { seal in
let parser = XMLParser(data: response.data)
let curDelegate = AuthParserDelegate()
let curDelegate = AuthParserDelegate(performanceMonitor: self.performanceMonitor)
parser.delegate = curDelegate
let success = parser.parse()
if let serverApiVersion = curDelegate.serverApiVersion {
Expand Down Expand Up @@ -697,7 +699,7 @@ class AmpacheXmlServerApi: URLCleanser {
}

func checkForErrorResponse(response: APIDataResponse) -> ResponseError? {
let errorParser = AmpacheXmlParser()
let errorParser = AmpacheXmlParser(performanceMonitor: self.performanceMonitor)
let parser = XMLParser(data: response.data)
parser.delegate = errorParser
parser.parse()
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Ampache/CatalogParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ class CatalogParserDelegate: AmpacheXmlLibParser {
var musicFoldersParsed = Set<MusicFolder>()
var musicFolderBuffer: MusicFolder?

init(library: LibraryStorage) {
init(performanceMonitor: ThreadPerformanceMonitor, library: LibraryStorage) {
musicFoldersBeforeFetch = Set(library.getMusicFolders())
super.init(library: library)
super.init(performanceMonitor: performanceMonitor, library: library)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Ampache/PlaylistParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ class PlaylistParserDelegate: AmpacheNotifiableXmlParser {
private var parsedPlaylists: Set<Playlist>
private var library: LibraryStorage

init(library: LibraryStorage, parseNotifier: ParsedObjectNotifiable?, playlistToValidate: Playlist? = nil) {
init(performanceMonitor: ThreadPerformanceMonitor, library: LibraryStorage, parseNotifier: ParsedObjectNotifiable?, playlistToValidate: Playlist? = nil) {
self.library = library
self.playlist = playlistToValidate
self.playlistToValidate = playlistToValidate
oldPlaylists = Set(library.getPlaylists())
parsedPlaylists = Set<Playlist>()
super.init(parseNotifier: parseNotifier)
super.init(performanceMonitor: performanceMonitor, parseNotifier: parseNotifier)
}

private func resetPlaylistInCaseOfError() {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Ampache/PlaylistSongsParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ class PlaylistSongsParserDelegate: SongParserDelegate {
let playlist: Playlist
var items: [PlaylistItem]

init(playlist: Playlist, library: LibraryStorage) {
init(performanceMonitor: ThreadPerformanceMonitor, playlist: Playlist, library: LibraryStorage) {
self.playlist = playlist
self.items = playlist.items
super.init(library: library, parseNotifier: nil)
super.init(performanceMonitor: performanceMonitor, library: library, parseNotifier: nil)
}

override func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Ampache/PodcastEpisodeParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ class PodcastEpisodeParserDelegate: PlayableParserDelegate {
var episodeBuffer: PodcastEpisode?
var parsedEpisodes = [PodcastEpisode]()

init(podcast: Podcast, library: LibraryStorage) {
init(performanceMonitor: ThreadPerformanceMonitor, podcast: Podcast, library: LibraryStorage) {
self.podcast = podcast
super.init(library: library, parseNotifier: nil)
super.init(performanceMonitor: performanceMonitor, library: library, parseNotifier: nil)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Ampache/PodcastParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ class PodcastParserDelegate: AmpacheXmlLibParser {
private var podcastBuffer: Podcast?
var rating: Int = 0

override init(library: LibraryStorage, parseNotifier: ParsedObjectNotifiable? = nil) {
override init(performanceMonitor: ThreadPerformanceMonitor, library: LibraryStorage, parseNotifier: ParsedObjectNotifiable? = nil) {
parsedPodcasts = Set<Podcast>()
super.init(library: library, parseNotifier: parseNotifier)
super.init(performanceMonitor: performanceMonitor, library: library, parseNotifier: parseNotifier)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
4 changes: 4 additions & 0 deletions AmperfyKit/Api/BackendApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public protocol SyncCallbacks: ParsedObjectNotifiable {
func notifySyncStarted(ofType parsedObjectType: ParsedObjectType, totalCount: Int)
}

public protocol ThreadPerformanceMonitor {
var shouldSlowDownExecution: Bool { get }
}

public class APIDataResponse {
public var data: Data
public var url: URL?
Expand Down
10 changes: 6 additions & 4 deletions AmperfyKit/Api/BackendProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public class ResourceNotAvailableResponseError: ResponseError {
public class BackendProxy {

private let log = OSLog(subsystem: "Amperfy", category: "BackendProxy")
private let performanceMonitor: ThreadPerformanceMonitor
private let eventLogger: EventLogger
private var activeApiType = BackenApiType.ampache
public var selectedApi: BackenApiType {
Expand All @@ -172,20 +173,21 @@ public class BackendProxy {
}

private lazy var ampacheApi: BackendApi = {
return AmpacheApi(ampacheXmlServerApi: AmpacheXmlServerApi(eventLogger: eventLogger), eventLogger: eventLogger)
return AmpacheApi(ampacheXmlServerApi: AmpacheXmlServerApi(performanceMonitor: self.performanceMonitor, eventLogger: eventLogger), performanceMonitor: performanceMonitor, eventLogger: eventLogger)
}()
private lazy var subsonicApi: BackendApi = {
let api = SubsonicApi(subsonicServerApi: SubsonicServerApi(eventLogger: eventLogger), eventLogger: eventLogger)
let api = SubsonicApi(subsonicServerApi: SubsonicServerApi(performanceMonitor: self.performanceMonitor, eventLogger: eventLogger), performanceMonitor: performanceMonitor, eventLogger: eventLogger)
api.authType = .autoDetect
return api
}()
private lazy var subsonicLegacyApi: BackendApi = {
let api = SubsonicApi(subsonicServerApi: SubsonicServerApi(eventLogger: eventLogger), eventLogger: eventLogger)
let api = SubsonicApi(subsonicServerApi: SubsonicServerApi(performanceMonitor: self.performanceMonitor, eventLogger: eventLogger), performanceMonitor: performanceMonitor, eventLogger: eventLogger)
api.authType = .legacy
return api
}()

init(eventLogger: EventLogger) {
init(performanceMonitor: ThreadPerformanceMonitor, eventLogger: EventLogger) {
self.performanceMonitor = performanceMonitor
self.eventLogger = eventLogger
}

Expand Down
36 changes: 35 additions & 1 deletion AmperfyKit/Api/GenericXmlParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,31 @@ import os.log
class GenericXmlParser: NSObject, XMLParserDelegate {

static var debugPrint = false

/// Background tasks that produce to much CPU load will be terminated by iOS:
/// Event: cpu usage
/// Action taken: Process killed
/// CPU: 48 seconds cpu time over 51 seconds (93% cpu average), exceeding limit of 80% cpu over 60 seconds
/// When big albums/playlists are requested by Amperfy the following happens:
/// - The XML file gets generated on the server and will be send to Amperfy as answer. This process creates no/small CPU load
/// - The response is parsed by an API XML Parser.
/// Depending on the library size (songs need to be fetched from local database with lots of entries) and the response file size, the parsing process can take very long.
/// The parse time will be monitored and after a certain threshold the ThreadPerformanceMonitor is asked if a slow down is requered.
/// If the App is in Foreground now slow down is needed the CPU load can be 100% for any amount of time.
/// If the App gets send to Background the callback returns true and the CPU will be send to slepp for a certain amount of time to reduce the CPU load.
/// With this sleep aproach the sync process can go on even in background.
static let activeTimeDurationNanoSec: UInt32 = 20_000_000 // 20 milliseconds
static let sleepTimeDurationMicroSec: UInt32 = (4*activeTimeDurationNanoSec)/1000

let log = OSLog(subsystem: "Amperfy", category: "Parser")
let performanceMonitor: ThreadPerformanceMonitor
var buffer = ""
var parsedCount = 0
var startTime: DispatchTime

init(performanceMonitor: ThreadPerformanceMonitor) {
self.performanceMonitor = performanceMonitor
startTime = DispatchTime.now()
}

func parser(_ parser: XMLParser, foundCharacters string: String) {
buffer.append(string)
Expand All @@ -53,6 +74,19 @@ class GenericXmlParser: NSObject, XMLParserDelegate {
os_log("</%s>", log: log, type: .debug, elementName)
}
buffer = ""
let elapsedTime = DispatchTime.now().uptimeNanoseconds - self.startTime.uptimeNanoseconds
if elapsedTime > Self.activeTimeDurationNanoSec {
if Self.debugPrint {
os_log("Parsing took longer then %f milliseconds", log: log, type: .debug, Double(Double(Self.activeTimeDurationNanoSec)/1_000_000))
}
if performanceMonitor.shouldSlowDownExecution {
if Self.debugPrint {
os_log("Parsing took long: sleep for %f milliseconds to reduce CPU load", log: log, type: .debug, Double(Double(Self.sleepTimeDurationMicroSec)/1_000))
}
usleep(Self.sleepTimeDurationMicroSec)
}
startTime = DispatchTime.now()
}
}

}
8 changes: 4 additions & 4 deletions AmperfyKit/Api/Subsonic/SsDirectoryParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,20 @@ class SsDirectoryParserDelegate: SsSongParserDelegate {
let songsBeforeFetch: Set<Song>
var songsParsed = Set<Song>()

init(directory: Directory, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator) {
init(performanceMonitor: ThreadPerformanceMonitor, directory: Directory, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator) {
self.directory = directory
self.musicFolder = nil
directoriesBeforeFetch = Set(directory.subdirectories)
songsBeforeFetch = Set(directory.songs)
super.init(library: library, subsonicUrlCreator: subsonicUrlCreator)
super.init(performanceMonitor: performanceMonitor, library: library, subsonicUrlCreator: subsonicUrlCreator)
}

init(musicFolder: MusicFolder, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator) {
init(performanceMonitor: ThreadPerformanceMonitor, musicFolder: MusicFolder, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator) {
self.directory = nil
self.musicFolder = musicFolder
directoriesBeforeFetch = Set(musicFolder.directories)
songsBeforeFetch = Set(musicFolder.songs)
super.init(library: library, subsonicUrlCreator: subsonicUrlCreator)
super.init(performanceMonitor: performanceMonitor, library: library, subsonicUrlCreator: subsonicUrlCreator)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Subsonic/SsMusicFolderParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ class SsMusicFolderParserDelegate: SsXmlLibParser {
let musicFoldersBeforeFetch: Set<MusicFolder>
var musicFoldersParsed = Set<MusicFolder>()

init(library: LibraryStorage) {
init(performanceMonitor: ThreadPerformanceMonitor, library: LibraryStorage) {
musicFoldersBeforeFetch = Set(library.getMusicFolders())
super.init(library: library)
super.init(performanceMonitor: performanceMonitor, library: library)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Subsonic/SsPlaylistParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ class SsPlaylistParserDelegate: SsXmlParser {
private var parsedPlaylists: Set<Playlist>
private let library: LibraryStorage

init(library: LibraryStorage) {
init(performanceMonitor: ThreadPerformanceMonitor, library: LibraryStorage) {
self.library = library
oldPlaylists = Set(library.getPlaylists())
parsedPlaylists = Set<Playlist>()
super.init()
super.init(performanceMonitor: performanceMonitor)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Subsonic/SsPlaylistSongsParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ class SsPlaylistSongsParserDelegate: SsSongParserDelegate {
var items: [PlaylistItem]
public private(set) var playlistHasBeenDetected = false

init(playlist: Playlist, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator) {
init(performanceMonitor: ThreadPerformanceMonitor, playlist: Playlist, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator) {
self.playlist = playlist
self.items = playlist.items
super.init(library: library, subsonicUrlCreator: subsonicUrlCreator)
super.init(performanceMonitor: performanceMonitor, library: library, subsonicUrlCreator: subsonicUrlCreator)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Subsonic/SsPodcastEpisodeParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ class SsPodcastEpisodeParserDelegate: SsPlayableParserDelegate {
var episodeBuffer: PodcastEpisode?
var parsedEpisodes = [PodcastEpisode]()

init(podcast: Podcast?, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator) {
init(performanceMonitor: ThreadPerformanceMonitor, podcast: Podcast?, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator) {
self.podcast = podcast
super.init(library: library, subsonicUrlCreator: subsonicUrlCreator)
super.init(performanceMonitor: performanceMonitor, library: library, subsonicUrlCreator: subsonicUrlCreator)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
4 changes: 2 additions & 2 deletions AmperfyKit/Api/Subsonic/SsPodcastParserDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ class SsPodcastParserDelegate: SsXmlLibWithArtworkParser {
var parsedPodcasts: Set<Podcast>
private var podcastBuffer: Podcast?

override init(library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator, parseNotifier: ParsedObjectNotifiable? = nil) {
override init(performanceMonitor: ThreadPerformanceMonitor, library: LibraryStorage, subsonicUrlCreator: SubsonicUrlCreator, parseNotifier: ParsedObjectNotifiable? = nil) {
parsedPodcasts = Set<Podcast>()
super.init(library: library, subsonicUrlCreator: subsonicUrlCreator, parseNotifier: parseNotifier)
super.init(performanceMonitor: performanceMonitor, library: library, subsonicUrlCreator: subsonicUrlCreator, parseNotifier: parseNotifier)
}

override func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
Expand Down
Loading

0 comments on commit 2071065

Please sign in to comment.