Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftyJSON Performance Issues

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?

like image 966
Simpa Avatar asked Mar 25 '15 09:03

Simpa


1 Answers

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 :)

like image 164
rintaro Avatar answered Oct 21 '22 11:10

rintaro