Skip to content

Instantly share code, notes, and snippets.

@crazytonyli
Created March 2, 2023 23:09
Show Gist options
  • Save crazytonyli/568fa36a1a5f8a9ac7468304083184d7 to your computer and use it in GitHub Desktop.
Save crazytonyli/568fa36a1a5f8a9ac7468304083184d7 to your computer and use it in GitHub Desktop.
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