SwiftyJSON Performance Issues
Asked Answered
S

1

16

I'm having serious performance issues parsing JSON with SwiftyJson from our API and populating in core data.

The data is downloaded with Alamofire, which works nicely but parsing the JSON with SwiftyJson is painfully slow. To see if the library really was the problem I rewrote the json parsing in one of the many places the data is parsed. In the code below I am parsing the opening hours of one of about 400 tourist attractions.

See the difference in these screenshots, 7,7 sec to 185 ms:

enter image description here

enter image description here

The Swifty way:

    let openDescription:String = json["OpeningHours"]["OpeningHoursGenericExceptions"].string!
    let monOpen:[String]    = json["OpeningHours"]["Monday"]["From"].string!.componentsSeparatedByString(":")
    let monClose:[String]   = json["OpeningHours"]["Monday"]["To"].string!.componentsSeparatedByString(":")
    let tueOpen:[String]    = json["OpeningHours"]["Tuesday"]["From"].string!.componentsSeparatedByString(":")
    let tueClose:[String]   = json["OpeningHours"]["Tuesday"]["To"].string!.componentsSeparatedByString(":")
    let wedOpen:[String]    = json["OpeningHours"]["Wednesday"]["From"].string!.componentsSeparatedByString(":")
    let wedClose:[String]   = json["OpeningHours"]["Wednesday"]["To"].string!.componentsSeparatedByString(":")
    let thuOpen:[String]    = json["OpeningHours"]["Thursday"]["From"].string!.componentsSeparatedByString(":")
    let thuClose:[String]   = json["OpeningHours"]["Thursday"]["To"].string!.componentsSeparatedByString(":")
    let friOpen:[String]    = json["OpeningHours"]["Friday"]["From"].string!.componentsSeparatedByString(":")
    let friClose:[String]   = json["OpeningHours"]["Friday"]["To"].string!.componentsSeparatedByString(":")
    let satOpen:[String]    = json["OpeningHours"]["Saturday"]["From"].string!.componentsSeparatedByString(":")
    let satClose:[String]   = json["OpeningHours"]["Saturday"]["To"].string!.componentsSeparatedByString(":")
    let sunOpen:[String]    = json["OpeningHours"]["Sunday"]["From"].string!.componentsSeparatedByString(":")
    let sunClose:[String]   = json["OpeningHours"]["Sunday"]["To"].string!.componentsSeparatedByString(":")

The native way:

    var monOpen:[String] = []
    var monClose:[String] = []
    var tueOpen:[String] = []
    var tueClose:[String] = []
    var wedOpen:[String] = []
    var wedClose:[String] = []
    var thuOpen:[String] = []
    var thuClose:[String] = []
    var friOpen:[String] = []
    var friClose:[String] = []
    var satOpen:[String] = []
    var satClose:[String] = []
    var sunOpen:[String] = []
    var sunClose:[String] = []
    var openDescription:String = ""
    
    if let attractionsArray = orgJson as? NSArray{
        if let attraction = attractionsArray[0] as? NSDictionary{
            if let openHours = attraction["OpeningHours"] as? NSDictionary{
                if let day = openHours["Monday"] as? NSDictionary{
                    if let open = day["From"] as? String{
                        monOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"] as? String{
                        monClose = close.componentsSeparatedByString(":")
                    }
                }
                if let day = openHours["Tuesday"] as? NSDictionary{
                    if let open = day["From"] as? String{
                        tueOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"] as? String{
                        tueClose = close.componentsSeparatedByString(":")
                    }
                }
                if let day = openHours["Wednesday"] as? NSDictionary{
                    if let open = day["From"] as? String{
                        wedOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"] as? String{
                        wedClose = close.componentsSeparatedByString(":")
                    }
                }
                if let day = openHours["Thursday"] as? NSDictionary{
                    if let open = day["From"] as? String{
                        thuOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"] as? String{
                        thuClose = close.componentsSeparatedByString(":")
                    }
                }
                if let day = openHours["Friday"] as? NSDictionary{
                    if let open = day["From"] as? String{
                        friOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"] as? String{
                        friClose = close.componentsSeparatedByString(":")
                    }
                }
                if let day = openHours["Saturday"] as? NSDictionary{
                    if let open = day["From"] as? String{
                        satOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"] as? String{
                        satClose = close.componentsSeparatedByString(":")
                    }
                }
                if let day = openHours["Sunday"] as? NSDictionary{
                    if let open = day["From"] as? String{
                        sunOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"] as? String{
                        sunClose = close.componentsSeparatedByString(":")
                    }
                }
                if let desc = openHours["OpeningHoursGenericExceptions"] as? String{
                    openDescription = desc
                }
            }
        }
    }

This is only part of the data being parsed so the performance is really noticeable in the app.

I guess the question is, have i used SwiftyJSON incorrectly or is this to be expected?

Savanna answered 25/3, 2015 at 9:44 Comment(0)
P
25

First of all, your "native way" is not equivalent to "Swifty way".

SwiftyJSON version has 45 subscript accesses, but native way has only 23 subscript accesses.

To make "native way" equivalent, it should be something like:

let openDescription = attraction["OpeningHours"]!["OpeningHoursGenericExceptions"] as String
let monOpen  = (attraction["OpeningHours"]!["Monday"]!!["From"] as String).componentsSeparatedByString(":")
let monClose = (attraction["OpeningHours"]!["Monday"]!!["To"] as String).componentsSeparatedByString(":")
let tueOpen  = (attraction["OpeningHours"]!["Tuesday"]!!["From"] as String).componentsSeparatedByString(":")
let tueClose = (attraction["OpeningHours"]!["Tuesday"]!!["To"]! as String).componentsSeparatedByString(":")
// ...

or "Swifty way" should be like:

let openHours = json[0]["OpeningHours"]
var day:JSON

day = openHours["Monday"]
if let open = day["From"].string {
    monOpen = open.componentsSeparatedByString(":")
}
if let close = day["To"].string {
    monClose = close.componentsSeparatedByString(":")
}
day = openHours["Tuesday"]
if let open = day["From"].string {
    tueOpen = open.componentsSeparatedByString(":")
}
if let close = day["To"].string {
    tueClose = close.componentsSeparatedByString(":")
}
// ...

Anyway, Yes, SwiftyJSON is slow. Let's measure:

import Foundation
import XCTest

let orgJson:AnyObject = [
    [
        "OpeningHours": [
            "OpeningHoursGenericExceptions": "test",
            "Monday":    ["From":"1:2:3","To":"1:2:3"],
            "Tuesday":   ["From":"1:2:3","To":"1:2:3"],
            "Wednesday": ["From":"1:2:3","To":"1:2:3"],
            "Thursday":  ["From":"1:2:3","To":"1:2:3"],
            "Friday":    ["From":"1:2:3","To":"1:2:3"],
            "Saturday":  ["From":"1:2:3","To":"1:2:3"],
            "Sunday":    ["From":"1:2:3","To":"1:2:3"],
        ]
    ]
]
let json = JSON(orgJson)

class JSONTestTests: XCTestCase {

    func testNativeSubscript() {
        measureBlock { () -> Void in

            for _ in 0 ..< 400 {
                autoreleasepool {
                    if let attraction = orgJson[0] as? NSDictionary {
                        let openDescription = attraction["OpeningHours"]!["OpeningHoursGenericExceptions"] as String
                        let monOpen  = (attraction["OpeningHours"]!["Monday"]!!["From"] as String).componentsSeparatedByString(":")
                        let monClose = (attraction["OpeningHours"]!["Monday"]!!["To"] as String).componentsSeparatedByString(":")
                        let tueOpen  = (attraction["OpeningHours"]!["Tuesday"]!!["From"] as String).componentsSeparatedByString(":")
                        let tueClose = (attraction["OpeningHours"]!["Tuesday"]!!["To"] as String).componentsSeparatedByString(":")
                        let wedOpen  = (attraction["OpeningHours"]!["Wednesday"]!!["From"] as String).componentsSeparatedByString(":")
                        let wedClose = (attraction["OpeningHours"]!["Wednesday"]!!["To"] as String).componentsSeparatedByString(":")
                        let thuOpen  = (attraction["OpeningHours"]!["Thursday"]!!["From"] as String).componentsSeparatedByString(":")
                        let thuClose = (attraction["OpeningHours"]!["Thursday"]!!["To"] as String).componentsSeparatedByString(":")
                        let friOpen  = (attraction["OpeningHours"]!["Friday"]!!["From"] as String).componentsSeparatedByString(":")
                        let friClose = (attraction["OpeningHours"]!["Friday"]!!["To"] as String).componentsSeparatedByString(":")
                        let satOpen  = (attraction["OpeningHours"]!["Saturday"]!!["From"] as String).componentsSeparatedByString(":")
                        let satClose = (attraction["OpeningHours"]!["Saturday"]!!["To"] as String).componentsSeparatedByString(":")
                        let sunOpen  = (attraction["OpeningHours"]!["Sunday"]!!["From"] as String).componentsSeparatedByString(":")
                        let sunClose = (attraction["OpeningHours"]!["Sunday"]!!["To"] as String).componentsSeparatedByString(":")
                        XCTAssertEqual(monOpen, ["1","2","3"], "")
                        XCTAssertEqual(openDescription, "test")
                    }
                }
            }
        }
    }

    func testJSONSubscript() {
        measureBlock { () -> Void in
            for _ in 0 ..< 400 {
                autoreleasepool {
                    let attraction = json[0]
                    let openDescription:String = attraction["OpeningHours"]["OpeningHoursGenericExceptions"].string!
                    let monOpen:[String]    = attraction["OpeningHours"]["Monday"]["From"].string!.componentsSeparatedByString(":")
                    let monClose:[String]   = attraction["OpeningHours"]["Monday"]["To"].string!.componentsSeparatedByString(":")
                    let tueOpen:[String]    = attraction["OpeningHours"]["Tuesday"]["From"].string!.componentsSeparatedByString(":")
                    let tueClose:[String]   = attraction["OpeningHours"]["Tuesday"]["To"].string!.componentsSeparatedByString(":")
                    let wedOpen:[String]    = attraction["OpeningHours"]["Wednesday"]["From"].string!.componentsSeparatedByString(":")
                    let wedClose:[String]   = attraction["OpeningHours"]["Wednesday"]["To"].string!.componentsSeparatedByString(":")
                    let thuOpen:[String]    = attraction["OpeningHours"]["Thursday"]["From"].string!.componentsSeparatedByString(":")
                    let thuClose:[String]   = attraction["OpeningHours"]["Thursday"]["To"].string!.componentsSeparatedByString(":")
                    let friOpen:[String]    = attraction["OpeningHours"]["Friday"]["From"].string!.componentsSeparatedByString(":")
                    let friClose:[String]   = attraction["OpeningHours"]["Friday"]["To"].string!.componentsSeparatedByString(":")
                    let satOpen:[String]    = attraction["OpeningHours"]["Saturday"]["From"].string!.componentsSeparatedByString(":")
                    let satClose:[String]   = attraction["OpeningHours"]["Saturday"]["To"].string!.componentsSeparatedByString(":")
                    let sunOpen:[String]    = attraction["OpeningHours"]["Sunday"]["From"].string!.componentsSeparatedByString(":")
                    let sunClose:[String]   = attraction["OpeningHours"]["Sunday"]["To"].string!.componentsSeparatedByString(":")
                    XCTAssertEqual(monOpen, ["1","2","3"], "")
                }
            }
        }
    }

    func testNativeBinding() {
        measureBlock { () -> Void in
            for _ in 0 ..< 400 {
                autoreleasepool {
                    var monOpen:[String] = []
                    var monClose:[String] = []
                    var tueOpen:[String] = []
                    var tueClose:[String] = []
                    var wedOpen:[String] = []
                    var wedClose:[String] = []
                    var thuOpen:[String] = []
                    var thuClose:[String] = []
                    var friOpen:[String] = []
                    var friClose:[String] = []
                    var satOpen:[String] = []
                    var satClose:[String] = []
                    var sunOpen:[String] = []
                    var sunClose:[String] = []
                    var openDescription:String = ""

                    if let attractionsArray = orgJson as? NSArray{
                        if let attraction = attractionsArray[0] as? NSDictionary{
                            if let openHours = attraction["OpeningHours"] as? NSDictionary{
                                if let day = openHours["Monday"] as? NSDictionary{
                                    if let open = day["From"] as? String{
                                        monOpen = open.componentsSeparatedByString(":")
                                    }
                                    if let close = day["To"] as? String{
                                        monClose = close.componentsSeparatedByString(":")
                                    }
                                }
                                if let day = openHours["Tuesday"] as? NSDictionary{
                                    if let open = day["From"] as? String{
                                        tueOpen = open.componentsSeparatedByString(":")
                                    }
                                    if let close = day["To"] as? String{
                                        tueClose = close.componentsSeparatedByString(":")
                                    }
                                }
                                if let day = openHours["Wednesday"] as? NSDictionary{
                                    if let open = day["From"] as? String{
                                        wedOpen = open.componentsSeparatedByString(":")
                                    }
                                    if let close = day["To"] as? String{
                                        wedClose = close.componentsSeparatedByString(":")
                                    }
                                }
                                if let day = openHours["Thursday"] as? NSDictionary{
                                    if let open = day["From"] as? String{
                                        thuOpen = open.componentsSeparatedByString(":")
                                    }
                                    if let close = day["To"] as? String{
                                        thuClose = close.componentsSeparatedByString(":")
                                    }
                                }
                                if let day = openHours["Friday"] as? NSDictionary{
                                    if let open = day["From"] as? String{
                                        friOpen = open.componentsSeparatedByString(":")
                                    }
                                    if let close = day["To"] as? String{
                                        friClose = close.componentsSeparatedByString(":")
                                    }
                                }
                                if let day = openHours["Saturday"] as? NSDictionary{
                                    if let open = day["From"] as? String{
                                        satOpen = open.componentsSeparatedByString(":")
                                    }
                                    if let close = day["To"] as? String{
                                        satClose = close.componentsSeparatedByString(":")
                                    }
                                }
                                if let day = openHours["Sunday"] as? NSDictionary{
                                    if let open = day["From"] as? String{
                                        sunOpen = open.componentsSeparatedByString(":")
                                    }
                                    if let close = day["To"] as? String{
                                        sunClose = close.componentsSeparatedByString(":")
                                    }
                                }
                                if let desc = openHours["OpeningHoursGenericExceptions"] as? String{
                                    openDescription = desc
                                }
                            }
                        }
                    }
                    XCTAssertEqual(monOpen, ["1","2","3"], "")
                }
            }
        }
    }
    func testJSONBinding() {
        measureBlock { () -> Void in
            for _ in 0 ..< 400 {
                autoreleasepool {
                    var monOpen:[String] = []
                    var monClose:[String] = []
                    var tueOpen:[String] = []
                    var tueClose:[String] = []
                    var wedOpen:[String] = []
                    var wedClose:[String] = []
                    var thuOpen:[String] = []
                    var thuClose:[String] = []
                    var friOpen:[String] = []
                    var friClose:[String] = []
                    var satOpen:[String] = []
                    var satClose:[String] = []
                    var sunOpen:[String] = []
                    var sunClose:[String] = []
                    var openDescription:String = ""

                    let openHours = json[0]["OpeningHours"]
                    var day:JSON

                    day = openHours["Monday"]
                    if let open = day["From"].string {
                        monOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"].string {
                        monClose
                            = close.componentsSeparatedByString(":")
                    }
                    day = openHours["Tuesday"]
                    if let open = day["From"].string {
                        tueOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"].string {
                        tueClose = close.componentsSeparatedByString(":")
                    }
                    day = openHours["WednesDay"]
                    if let open = day["From"].string {
                        wedOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"].string {
                        wedClose = close.componentsSeparatedByString(":")
                    }
                    day = openHours["Thursday"]
                    if let open = day["From"].string {
                        thuOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"].string {
                        thuClose = close.componentsSeparatedByString(":")
                    }
                    day = openHours["Friday"]
                    if let open = day["From"].string {
                        friOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"].string {
                        friClose = close.componentsSeparatedByString(":")
                    }
                    day = openHours["Saturday"]
                    if let open = day["From"].string {
                        satOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"].string {
                        satClose = close.componentsSeparatedByString(":")
                    }
                    day = openHours["Sunday"]
                    if let open = day["From"].string {
                        sunOpen = open.componentsSeparatedByString(":")
                    }
                    if let close = day["To"].string {
                        sunClose = close.componentsSeparatedByString(":")
                    }
                    XCTAssertEqual(monOpen, ["1","2","3"], "")
                }
            }
        }
    }

}

Outputs:

<unknown>:0: Test Case '-[JSONTestTests.JSONTestTests testJSONBinding]' measured [Time, seconds] average: 0.804, relative standard deviation: 5.592%, values: [0.835687, 0.814827, 0.819685, 0.841900, 0.764961, 0.845202, 0.691442, 0.779255, 0.818213, 0.830698], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
<unknown>:0: Test Case '-[JSONTestTests.JSONTestTests testJSONSubscript]' measured [Time, seconds] average: 4.247, relative standard deviation: 3.496%, values: [4.019640, 4.004123, 4.146146, 4.194535, 4.487171, 4.300971, 4.310613, 4.408405, 4.318354, 4.279362], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
<unknown>:0: Test Case '-[JSONTestTests.JSONTestTests testNativeBinding]' measured [Time, seconds] average: 0.223, relative standard deviation: 2.773%, values: [0.221099, 0.227395, 0.218860, 0.225989, 0.227128, 0.222370, 0.229956, 0.214535, 0.210818, 0.229868], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
<unknown>:0: Test Case '-[JSONTestTests.JSONTestTests testNativeSubscript]' measured [Time, seconds] average: 0.362, relative standard deviation: 17.528%, values: [0.346285, 0.316185, 0.333650, 0.339416, 0.330243, 0.354034, 0.378730, 0.269519, 0.486904, 0.467607], performanceMetricID:com.apple.XCTPerformanceMetric_WallClockTime, baselineName: "", baselineAverage: , maxPercentRegression: 10.000%, maxPercentRelativeStandardDeviation: 10.000%, maxRegression: 0.100, maxStandardDeviation: 0.100
  • Your SwiftyJSON: 4.247
  • Your Native: 0.223
  • My SwiftyJSON: 0.804
  • My Native: 0.362

By the way, if I were you, I would do something like:

if let hours = orgJson[0]?["OpeningHours"] as? NSDictionary {
    let openDescription = hours["OpeningHoursGenericExceptions"] as? String ?? ""
    let monOpen  = hours["Monday"]?["From"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let monClose = hours["Monday"]?["To"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let tueOpen  = hours["Tuesday"]?["From"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let tueClose = hours["Tuesday"]?["To"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let wedOpen  = hours["Wednesday"]?["From"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let wedClose = hours["Wednesday"]?["To"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let thuOpen  = hours["Thursday"]?["From"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let thuClose = hours["Thursday"]?["To"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let friOpen  = hours["Friday"]?["From"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let friClose = hours["Friday"]?["To"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let satOpen  = hours["Saturday"]?["From"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let satClose = hours["Saturday"]?["To"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let sunOpen  = hours["Sunday"]?["From"]??.componentsSeparatedByString?(":") as? [String] ?? []
    let sunClose = hours["Sunday"]?["To"]??.componentsSeparatedByString?(":") as? [String] ?? []

    // ...
}

It's reasonably fast, safe, not so complicated.


Why SwiftyJSON is slow?

On subscript access, SwiftyJSON does:

  1. check the key is String
  2. subscript again with self[key:sub]
  3. check the underlying object is NSDictionary
  4. subscript underlying NSDictionary with the supplied key
  5. construct JSON object with the result
  6. return

Maybe the compiler optimizes some steps, but "slower than native" is somewhat unavoidable :)

Penology answered 27/3, 2015 at 12:28 Comment(4)
Thank you for a very thorough answer! And with benchmarks! Since I am very new at Swift (I guess al lot of people are :) a lot of the coding is first-try-trial-and-error and I did the JSON parsing as suggested in the SwiftyJSON README file. Looking at the different examples of parsing methods I don't regret pulling out swifty from my project, the complexity of "native" parsing is not really an issue and adding the performance difference I don't see swifty as an option.Savanna
I mean no offense to the creator of Swifty, I think it can have great use for a lot of developers. But googling 'swift parse json' almost only gives results using swifty gave me the impression that was the way to go. But maybe it's not for everyone? :)Savanna
@Savanna in the Swift world, a lot of hits in Google cannot be trusted. One would get the impression that Objective-C is dead, Cocoapods is accepted as a standard, etc. etc... in point of fact, I think a lot of these appearances are problematic. However, is SwiftyJSON too slow to use? That's another question, but if you had gotten these results originally, you might not have noticed.Compressed
@DanRosenstark Working with Android right now, it's even worse there. Apis change but the search engine results don't. Some simple tasks require to much trial and error for my taste. I do notice if there is a performance error in my apps because I always profile them before release to see if anything is badly written: And a lot of times there is room for improvement.Savanna

© 2022 - 2024 — McMap. All rights reserved.