File size: 5,662 Bytes
6ee917b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import Foundation
import Vapor
import OmFileFormat

fileprivate extension String {
    func pad(_ n: Int) -> String {
        return padding(toLength: n, withPad: " ", startingAt: 0)
    }
    static func dash(_ n: Int) -> String {
        return "".padding(toLength: n, withPad: "-", startingAt: 0)
    }
}

final class BenchmarkCommand: Command {
    var help: String { "Benchmark Open-Meteo core functions like data manipulation and compression" }
    
    struct Signature: CommandSignature {
        @Option(name: "time", short: "t", help: "Time per test in seconds")
        var time: Int?
    }
    
    /// `swift run -c release openmeteo-api benchmark`
    func run(using context: CommandContext, signature: Signature) throws {
        let run = BenchmarkRun(timePerTest: signature.time ?? 5)
        
        print("Open-Meteo Benchmark")
        print("Apple M1 is used as baseline. Positive values = slower than M1.")
        print("Time per test \(run.timePerTest) seconds (See --help)")
        
        print("| \("Test".pad(80)) | \("Mean".pad(8)) | \("Min".pad(8)) | \("Max".pad(8)) | \("Runs".pad(8)) | \("Diff to Apple M1".pad(20)) |")
        print("|\(String.dash(80+2))|\(String.dash(8+2))|\(String.dash(8+2))|\(String.dash(8+2))|\(String.dash(8+2))|\(String.dash(20+2))|")
        
        run.measure("Solar Position Calculation for 50 years, hourly", 625) {
            let _ = SolarPositionAlgorithm.sunPosition(timerange: TimerangeDt(start: Timestamp(1950,1,1), to: Timestamp(2000,1,1), dtSeconds: 3600))
        }
                
        /*let sizeMb = 128
        let data = run.measure("Generating dummy temperature timeseries (\(sizeMb) MB)", 272) {
            return (0..<1024*1024/4*sizeMb).map({
                let x = Float($0)
                return sin(x * .pi / 24) * 10 + sin(x * .pi / 24 / 365.25) * 15 + sin(x * .pi / 7)
            })
        }
        
        try run.measure("Compression in memory, large chunks", 191) {
            try OmFileWriter(dim0: data.count / 1024, dim1: 1024, chunk0: 1024, chunk1: 1024).writeInMemory(compressionType: .pfor_delta2d_int16, scalefactor: 20, all: data)
        }*/
        
        /*let compressed = try run.measure("Compression in memory, small chunks", 199) {
            try OmFileWriter(dim0: data.count / 1024, dim1: 1024, chunk0: 8, chunk1: 128).writeInMemory(compressionType: .pfor_delta2d_int16, scalefactor: 20, all: data)
        }
        let sizeMbCompressed = (compressed.count/1024/1024)
        
        let compressedData = DataAsClass(data: compressed)
        _ = try run.measure("Decompress from memory, small chunks (\(sizeMbCompressed) MB)", 44) {
            try OmFileReader(fn: compressedData).readAll()
        }
        
        let file = "\(OpenMeteo.dataDirectory)test.om"
        try FileManager.default.createDirectory(atPath: OpenMeteo.dataDirectory, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItemIfExists(at: file)}
        try run.measure("Compress to file, small chunks", 202) {
            try OmFileWriter(dim0: data.count / 1024, dim1: 1024, chunk0: 8, chunk1: 128).write(file: file, compressionType: .pfor_delta2d_int16, scalefactor: 20, all: data, overwrite: true)
        }
        
        try run.measure("Decompress from file, small chunks", 46) {
            let read = try OmFileReader(file: file)
            return try read.readAll()
        }*/
        
        
        let exradTime = TimerangeDt(start: Timestamp(1900,1,1), to: Timestamp(2000,1,1), dtSeconds: 3600)
        let exrad = run.measure("Calculate extra terrestrial radiation (100 years, hourly)", 47) {
            return Zensun.extraTerrestrialRadiationBackwards(latitude: 52, longitude: 7, timerange: exradTime)
        }
        
        _ = run.measure("Interpolate radiation to 15 minutes", 246) {
            let dtNew = exradTime.dtSeconds / 4
            let timeNew = exradTime.range.add(-exradTime.dtSeconds + dtNew).range(dtSeconds: dtNew)
            return exrad.interpolate(type: .solar_backwards_averaged, timeOld: exradTime, timeNew: timeNew, latitude: 52, longitude: 7, scalefactor: 100)
        }
    }
}

struct BenchmarkRun {
    var timePerTest: Int
    
    @discardableResult
    func measure<T>(_ section: String, _ baseLineMeanMs: Double, fn: () throws -> T) rethrows -> T
    {
        print("| \(section.pad(80)) | ", terminator: "")
        // Do not measure first execution
        var result = try fn()
        
        let start = DispatchTime.now()
        let end = start.uptimeNanoseconds + UInt64(timePerTest) * 1_000_000_000
        var min = 100.0
        var max = 0.0
        var count = 0
        
        repeat {
            let s = DispatchTime.now()
            result = try fn()
            count += 1
            let elapsed = Double((DispatchTime.now().uptimeNanoseconds - s.uptimeNanoseconds)) / 1_000_000_000
            if elapsed < min {
                min = elapsed
            }
            if elapsed > max {
                max = elapsed
            }
        } while DispatchTime.now().uptimeNanoseconds <= end
        let elapsed = Double((DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds)) / 1_000_000_000
        let mean = elapsed / Double(count)
        let diff = mean - baseLineMeanMs / 1000
        let factor = round((mean)/(baseLineMeanMs / 1000)*100)/100
        let b = "\(diff>0 ? "+" : "")\(diff.asSecondsPrettyPrint) (x\(factor))"
        print("\(mean.asSecondsPrettyPrint.pad(8)) | \(min.asSecondsPrettyPrint.pad(8)) | \(max.asSecondsPrettyPrint.pad(8)) | \(String(count).pad(8)) | \(b.pad(20)) |")
        return result
    }
}