Created
March 2, 2023 23:09
-
-
Save crazytonyli/568fa36a1a5f8a9ac7468304083184d7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import SwiftUI | |
import Charts | |
import os | |
struct DataPoint: Identifiable { | |
var id: UUID | |
var interval: Double | |
var iteration: Int | |
} | |
struct ContentView: View { | |
static let websites = [ | |
"https://www.apple.com/", | |
"https://www.google.com/", | |
"https://www.swift.org/", | |
"https://theuselessweb.com/", | |
"https://public-api.wordpress.com/", | |
] | |
@State var oneSessionData = [DataPoint]() | |
@State var multiSessionData = [DataPoint]() | |
@State var website = websites[0] | |
@State var started = false | |
let iterations = 30 | |
let concurrentRequestsPerIteration = 10 | |
var body: some View { | |
VStack(spacing: 32) { | |
Chart { | |
ForEach( | |
[ | |
(id: "One URLSession instance", data: oneSessionData), | |
(id: "Multiple URLSession instances", data: multiSessionData) | |
], | |
id: \.id | |
) { (id, dataPoints) in | |
ForEach(dataPoints) { point in | |
LineMark( | |
x: .value("Iteration", point.iteration), | |
y: .value("Duration", point.interval) | |
) | |
} | |
.foregroundStyle(by: .value("Type", id)) | |
} | |
} | |
Picker("Choose a website", selection: $website) { | |
ForEach(Self.websites, id: \.self) { | |
Text($0) | |
} | |
} | |
.disabled(started) | |
} | |
.padding() | |
.onChange(of: website) { newValue in | |
Task { | |
await start() | |
} | |
} | |
.onAppear { | |
Task { | |
await start() | |
} | |
} | |
} | |
func start() async { | |
print("🟢") | |
await MainActor.run { | |
started = true | |
oneSessionData = [] | |
multiSessionData = [] | |
} | |
_ = await sendHTTPRequest(using: URLSession.shared) | |
var head = URLRequest(url: URL(string: website)!) | |
head.httpMethod = "HEAD" | |
if let (_, response) = try? await URLSession.shared.data(for: head) { | |
print("HEAD \(website)") | |
let httpResponse = response as! HTTPURLResponse | |
for case let header as String in httpResponse.allHeaderFields.keys { | |
print("\(header): \(httpResponse.value(forHTTPHeaderField: header) ?? "<empty>")") | |
} | |
} | |
for iter in 1...iterations { | |
let duration = await multipleSession() | |
await MainActor.run { | |
multiSessionData.append(DataPoint(id: UUID(), interval: duration.asMilliseconds(), iteration: iter)) | |
} | |
} | |
for iter in 1...iterations { | |
let duration = await oneSession() | |
await MainActor.run { | |
oneSessionData.append(DataPoint(id: UUID(), interval: duration.asMilliseconds(), iteration: iter)) | |
} | |
} | |
await MainActor.run { | |
started = false | |
} | |
print("🔴") | |
} | |
func configuration() -> URLSessionConfiguration { | |
let configuration = URLSessionConfiguration.ephemeral | |
configuration.urlCache = nil | |
return configuration | |
} | |
func multipleSession() async -> DispatchTimeInterval { | |
await benchmark(label: "Multiple Sessions", { URLSession(configuration: self.configuration()) }) | |
} | |
func oneSession() async -> DispatchTimeInterval { | |
let session = URLSession(configuration: self.configuration()) | |
return await benchmark(label: "One Session", { session }) | |
} | |
func sendHTTPRequest(using urlSession: URLSession) async -> Bool { | |
let request = URLRequest(url: URL(string: website)!) | |
guard let _ = try? await urlSession.data(for: request) else { | |
print("🛑 HTTP request to \(website) failed") | |
return false | |
} | |
return true | |
} | |
func benchmark(label: StaticString, _ session: @escaping () -> URLSession) async -> DispatchTimeInterval { | |
let start = DispatchTime.now() | |
await withThrowingTaskGroup(of: Bool.self) { group in | |
for _ in (1...concurrentRequestsPerIteration) { | |
group.addTask { | |
await self.sendHTTPRequest(using: session()) | |
} | |
} | |
} | |
let end = DispatchTime.now() | |
return start.distance(to: end) | |
} | |
} | |
extension DispatchTimeInterval { | |
func asMilliseconds() -> Double { | |
var interval: Measurement<UnitDuration> | |
switch self { | |
case let .seconds(value): | |
interval = Measurement(value: Double(value), unit: .seconds) | |
case let.milliseconds(value): | |
interval = Measurement(value: Double(value), unit: .milliseconds) | |
case let .microseconds(value): | |
interval = Measurement(value: Double(value), unit: .microseconds) | |
case let .nanoseconds(value): | |
interval = Measurement(value: Double(value), unit: .nanoseconds) | |
case .never: | |
fatalError() | |
@unknown default: | |
fatalError() | |
} | |
interval.convert(to: UnitDuration.milliseconds) | |
return interval.value | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment