IkigaJSON
IkigaJSON copied to clipboard
Faster? I think not
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 did you build and run IkigaJSON on release compilation mode?
Definitely happy to see some data that I can optimise for
Attached you will find a json file containing a geometry data and a geojson.swift file with the decoder structs. Archive.zip
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
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.
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
}
}
}
PR #40 was just merged and tagged.
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 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.