fast-cli/src/lib/http_latency_tester.zig

163 lines
5.1 KiB
Zig

const std = @import("std");
const http = std.http;
pub const HttpLatencyTester = struct {
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
_ = self;
}
/// Measure latency to multiple URLs using HEAD requests
/// Returns median latency in milliseconds, or null if all requests failed
pub fn measureLatency(self: *Self, urls: []const []const u8) !?f64 {
if (urls.len == 0) return null;
var latencies = std.ArrayList(f64).init(self.allocator);
defer latencies.deinit();
// Test each URL
for (urls) |url| {
if (self.measureSingleUrl(url)) |latency_ms| {
try latencies.append(latency_ms);
} else |_| {
// Ignore errors, continue with other URLs
continue;
}
}
if (latencies.items.len == 0) return null;
// Return median latency
return self.calculateMedian(latencies.items);
}
/// Measure latency to a single URL using connection reuse method
/// First request establishes HTTPS connection, second request measures pure RTT
fn measureSingleUrl(self: *Self, url: []const u8) !f64 {
var client = http.Client{ .allocator = self.allocator };
defer client.deinit();
// Parse URL
const uri = try std.Uri.parse(url);
// First request: Establish HTTPS connection (ignore timing)
{
const server_header_buffer = try self.allocator.alloc(u8, 4096);
defer self.allocator.free(server_header_buffer);
var req = try client.open(.HEAD, uri, .{
.server_header_buffer = server_header_buffer,
});
defer req.deinit();
try req.send();
try req.finish();
try req.wait();
}
// Second request: Reuse connection and measure pure HTTP RTT
const start_time = std.time.nanoTimestamp();
{
const server_header_buffer = try self.allocator.alloc(u8, 4096);
defer self.allocator.free(server_header_buffer);
var req = try client.open(.HEAD, uri, .{
.server_header_buffer = server_header_buffer,
});
defer req.deinit();
try req.send();
try req.finish();
try req.wait();
}
const end_time = std.time.nanoTimestamp();
// Convert to milliseconds
const latency_ns = end_time - start_time;
const latency_ms = @as(f64, @floatFromInt(latency_ns)) / std.time.ns_per_ms;
return latency_ms;
}
/// Calculate median from array of latencies
fn calculateMedian(self: *Self, latencies: []f64) f64 {
_ = self;
if (latencies.len == 0) return 0;
if (latencies.len == 1) return latencies[0];
// Sort latencies
std.mem.sort(f64, latencies, {}, std.sort.asc(f64));
const mid = latencies.len / 2;
if (latencies.len % 2 == 0) {
// Even number of elements - average of two middle values
return (latencies[mid - 1] + latencies[mid]) / 2.0;
} else {
// Odd number of elements - middle value
return latencies[mid];
}
}
};
const testing = std.testing;
test "HttpLatencyTester init/deinit" {
var tester = HttpLatencyTester.init(testing.allocator);
defer tester.deinit();
// Test with empty URLs
const result = try tester.measureLatency(&[_][]const u8{});
try testing.expect(result == null);
}
test "calculateMedian" {
var tester = HttpLatencyTester.init(testing.allocator);
defer tester.deinit();
// Test odd number of elements
var latencies_odd = [_]f64{ 10.0, 20.0, 30.0 };
const median_odd = tester.calculateMedian(&latencies_odd);
try testing.expectEqual(@as(f64, 20.0), median_odd);
// Test even number of elements
var latencies_even = [_]f64{ 10.0, 20.0, 30.0, 40.0 };
const median_even = tester.calculateMedian(&latencies_even);
try testing.expectEqual(@as(f64, 25.0), median_even);
// Test single element
var latencies_single = [_]f64{15.0};
const median_single = tester.calculateMedian(&latencies_single);
try testing.expectEqual(@as(f64, 15.0), median_single);
}
test "HttpLatencyTester integration with example.com" {
var tester = HttpLatencyTester.init(testing.allocator);
defer tester.deinit();
// Test with real HTTP endpoint
const urls = [_][]const u8{"https://example.com"};
const result = tester.measureLatency(&urls) catch |err| {
// Allow network errors in CI environments
std.log.warn("Network error in integration test (expected in CI): {}", .{err});
return;
};
if (result) |latency_ms| {
// Reasonable latency bounds (1ms to 5000ms)
try testing.expect(latency_ms >= 1.0);
try testing.expect(latency_ms <= 5000.0);
std.log.info("example.com latency: {d:.1}ms", .{latency_ms});
}
}