IkigaJSON icon indicating copy to clipboard operation
IkigaJSON copied to clipboard

Faster? I think not

Open gitjoost opened this issue 1 year ago • 9 comments

replaced JSONDecoder() with IkigaJSONDecoder for a geo-json (geometry data) file. The data is characterized by geometry structs each with some coordinate detail. Some of the structs have long coordinate arrays describing tracks or area boundaries. The JSON file size I used to test was 188K. Standard JSONDecoder took 0.024 seconds to decode, IkigaJSONDecoder was a factor 10 slower; 0.23 seconds. I can make data and structs available if you wish.

gitjoost avatar Feb 19 '24 19:02 gitjoost

@gitjoost did you build and run IkigaJSON on release compilation mode?

Joannis avatar Feb 19 '24 22:02 Joannis

Definitely happy to see some data that I can optimise for

Joannis avatar Feb 19 '24 22:02 Joannis

Attached you will find a json file containing a geometry data and a geojson.swift file with the decoder structs. Archive.zip

gitjoost avatar Feb 20 '24 17:02 gitjoost

My measured times with your code:

DEBUG build w/ IkigaJSON: 230ms DEBUG build w/ Foundation: 25ms RELEASE build w/ IkigaJSON: 17ms RELEASE build w/ Foundation: 12ms

But I'd also like to note that you're decoding a file into a String, then converting it into Data, which is not the most efficient solution. JSONNull could be a struct, and decoding the doubleArray or double could be improved. Fixing those; you can get faster times still:

IkigaJSON Release: 6ms Foundation Release: 5.5ms

You're right that IkigaJSON is not as fast (or faster) on this file (thanks for reporting, I can look into this). I've already found a mis-optimisation in parsing Doubles thanks to you! I think the remainder of performance gap is due to your use of arrays, I'm looking into it

Joannis avatar Feb 21 '24 09:02 Joannis

Hey that’s great to hear this is of help. The string normally comes from a database so file reads are not really my issue. And I definitely used the debug version as I see the same (roughly) factor 10 difference. I will take another look at my code but perhaps you’ll blow right passed whatever speeds I can come up with - will be using your component in that case.

gitjoost avatar Feb 21 '24 22:02 gitjoost

import Foundation
import IkigaJSON
import CoreData

// GeoJSON decoding structures. This is necessary for getting to a
// track's complete (lon,lat,alt,dts) data which is not supported
// by Apple's standard decoder


class GeoJSON : Codable {
    struct FeatureCollection: Codable, Sequence {
        var features: [Feature]
        let type: String

        func makeIterator() -> IndexingIterator<[Feature]> {
            return features.makeIterator()
        }
    }

    struct Feature: Codable {
        let geometry: Geometry?
        let type: FeatureType
        let id : String
        var properties: Properties
    }

    enum FeatureType: String, Codable {
        case feature = "Feature"
    }

    struct Geometry: Codable {
        let coordinates: [GeoCoordinate]?
        let type: GeometryType
    }


    enum GeoCoordinate: Codable {
        case double(Double)
        case doubleArray([Double])

        init(from decoder: Decoder) throws {
            do {
                var container = try decoder.unkeyedContainer()
                var array = [Double]()
                if let count = container.count {
                    array.reserveCapacity(count)
                }
                while !container.isAtEnd {
                    array.append(try container.decode(Double.self))
                }
                self = .doubleArray(array)
            } catch {
                let container = try decoder.singleValueContainer()
                let x = try container.decode(Double.self)
                self = .double(x)
            }
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .double(let x):
                try container.encode(x)
            case .doubleArray(let x):
                try container.encode(x)
            }
        }

        func getValue() -> Any {
            switch self {
            case .double(let value):
                return value
            case .doubleArray(let values):
                return values
            }
        }
    }

    enum GeometryType: String, Codable {
        case lineString = "LineString"
        case point = "Point"
        case polygon = "Polygon"
    }


    struct Properties: Codable {
        let visible, labelVisible: Bool?
        let timestamp: Int?
        var title: String
        let propertiesClass: Class
        let updated: Int
        let creator: String
        let description, markerSize, markerSymbol: String?
        let markerRotation: JSONNull?
        let markerColor: String?
        let folderID: String?
        let strokeWidth: Int?
        let stroke: String?
        let weight: Int?
        let strokeOpacity: Double?
        let pattern, fill: String?
        let alias, number: String?
        let unresponsivePOD: String?
        let secondaryFrequency: String?
        let responsivePOD: String?
        let operationalPeriodID, previousEfforts, teamSize, timeAllocated: String?
        let cluePOD: String?
        let primaryFrequency: String?
        let status: String?
        let preparedBy, letter: String?
        let priority: String?
        let transportation: String?
        let resourceType: ResourceType?
        let fillOpacity: Double?

        enum CodingKeys: String, CodingKey {
            case visible, labelVisible, timestamp, title
            case propertiesClass = "class"
            case updated, creator, description
            case markerSize = "marker-size"
            case markerSymbol = "marker-symbol"
            case markerRotation = "marker-rotation"
            case markerColor = "marker-color"
            case folderID = "folderId"
            case strokeWidth = "stroke-width"
            case stroke, weight
            case strokeOpacity = "stroke-opacity"
            case pattern, fill, alias, number, unresponsivePOD, secondaryFrequency, responsivePOD
            case operationalPeriodID = "operationalPeriodId"
            case previousEfforts, teamSize, timeAllocated, cluePOD, primaryFrequency, status, preparedBy, letter, priority, transportation, resourceType
            case fillOpacity = "fill-opacity"
        }
    }
    enum Class: String, Codable {
        case appTrack = "AppTrack"
        case assignment = "Assignment"
        case configuredLayer = "ConfiguredLayer"
        case fieldWaypoint = "FieldWaypoint"
        case folder = "Folder"
        case marker = "Marker"
        case shape = "Shape"
    }

    enum ResourceType: String, Codable {
        case ground = "GROUND"
        case ground1 = "GROUND_1"
        case ground2 = "GROUND_2"
        case ground3 = "GROUND_3"
        case ohv = "OHV"
        case air = "AIR"
        case water = "WATER"
        case mounted = "MOUNTED"
        case dog = "DOG"
        case dogTrail = "DOG_TRAIL"
        case dogArea = "DOG_AREA"
        case dogHrd = "DOG_HRD"
    }

    struct JSONNull: Codable, Hashable {

        public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
            return true
        }

        func hash(into hasher: inout Hasher) {
            hasher.combine(0)
        }

        public init() {}

        public init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if !container.decodeNil() {
                throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
            }
        }

        public func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            try container.encodeNil()
        }
    }

    // -------------------------------------------------------------------------------

    var featureCollection : FeatureCollection?

    init() {}

    func decode(geoJsonStr : Data) -> Bool {
        if geoJsonStr.isEmpty { return false }

        do {
            self.featureCollection = try IkigaJSONDecoder().decode(FeatureCollection.self, from: geoJsonStr)
            return true
        }
        catch {
            print("ERROR GeoJSON decode(): \(error)")
            return false
        }
    }
}

Joannis avatar Feb 25 '24 17:02 Joannis

PR #40 was just merged and tagged.

Joannis avatar Feb 26 '24 09:02 Joannis

Updated my code and ran as release; JSONDecoder() and IkaJSONDecoder() are now the same speed; 0.0063 seconds. Nice improvement on your side though not the speed improvement I was hoping for. Notice that 0.0063 was measured on my M1Max, not the iPhone which is my target OS. Some of the GeoJSON files I need to process there are quite large so anything faster than JSONDecoder() is good.

gitjoost avatar Feb 26 '24 21:02 gitjoost

@gitjoost IkigaJSON should show it's strength in particular when it comes to larger files. I'm curious if you experience the same with your dataset.

Joannis avatar Feb 26 '24 22:02 Joannis