fast-cli/src/lib/measurement_strategy.zig
Caeser af4d3971c9
Updated to zig 0.15.1 and zli 4.1.0
* Refactor: Update build process and improve memory management

- Commented out library tests in build.zig for clarity.
- Changed minimum Zig version to 0.15.1 in build.zig.zon.
- Modified root.zig to accept a Writer for output instead of using log.
- Updated bandwidth.zig tests to use std.Thread.sleep for consistency.
- Adjusted fast.zig to improve memory allocation handling.
- Enhanced http_latency_tester.zig to manage latencies with allocator.
- Refined speed_worker.zig to utilize std.Thread.sleep for delays.
- Improved measurement_strategy.zig to handle speed measurements with allocator.
- Updated main.zig to flush writer after command execution.

* Fix: Update zli dependency to use URL and hash instead of path

* Fix: Add newline to spinner output for better readability

* switch worksflows to using zig 0.15.1

---------

Co-authored-by: mikkelam <mikkelarm@gmail.com>
2025-11-17 13:42:35 +00:00

154 lines
5.8 KiB
Zig

const std = @import("std");
pub const StabilityCriteria = struct {
ramp_up_duration_seconds: u32 = 4,
max_duration_seconds: u32 = 25,
measurement_interval_ms: u64 = 750,
sliding_window_size: u32 = 6,
stability_threshold_cov: f64 = 0.15,
stable_checks_required: u32 = 2,
};
pub const DurationStrategy = struct {
target_duration_ns: u64,
progress_update_interval_ms: u64,
pub fn shouldContinue(self: DurationStrategy, current_time: u64) bool {
return current_time < self.target_duration_ns;
}
pub fn getSleepInterval(self: DurationStrategy) u64 {
return std.time.ns_per_ms * self.progress_update_interval_ms;
}
};
pub const StabilityStrategy = struct {
criteria: StabilityCriteria,
ramp_up_duration_ns: u64,
max_duration_ns: u64,
measurement_interval_ns: u64,
speed_measurements: std.ArrayList(f64), // Sliding window of recent speeds
last_sample_time: u64 = 0,
last_total_bytes: u64 = 0,
consecutive_stable_checks: u32 = 0,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, criteria: StabilityCriteria) StabilityStrategy {
return StabilityStrategy{
.criteria = criteria,
.ramp_up_duration_ns = @as(u64, criteria.ramp_up_duration_seconds) * std.time.ns_per_s,
.max_duration_ns = @as(u64, criteria.max_duration_seconds) * std.time.ns_per_s,
.measurement_interval_ns = criteria.measurement_interval_ms * std.time.ns_per_ms,
.speed_measurements = std.ArrayList(f64).empty,
.allocator = allocator,
};
}
pub fn deinit(self: *StabilityStrategy) void {
self.speed_measurements.deinit(self.allocator);
}
pub fn shouldContinue(self: StabilityStrategy, current_time: u64) bool {
return current_time < self.max_duration_ns;
}
pub fn getSleepInterval(self: StabilityStrategy) u64 {
return self.measurement_interval_ns / 3; // Sample more frequently than measurement interval
}
pub fn shouldSample(self: *StabilityStrategy, current_time: u64) bool {
return current_time - self.last_sample_time >= self.measurement_interval_ns;
}
pub fn addSample(self: *StabilityStrategy, current_time: u64, current_total_bytes: u64) !bool {
// Skip first sample to calculate speed
if (self.last_sample_time > 0) {
const bytes_diff = current_total_bytes - self.last_total_bytes;
const time_diff_ns = current_time - self.last_sample_time;
const time_diff_s = @as(f64, @floatFromInt(time_diff_ns)) / std.time.ns_per_s;
const interval_speed = @as(f64, @floatFromInt(bytes_diff)) / time_diff_s;
// Phase 1: Ramp-up - collect measurements but don't check stability
if (current_time < self.ramp_up_duration_ns) {
try self.speed_measurements.append(self.allocator, interval_speed);
// Keep sliding window size
if (self.speed_measurements.items.len > self.criteria.sliding_window_size) {
_ = self.speed_measurements.orderedRemove(0);
}
} else {
// Phase 2: Stabilization - check CoV for stability
try self.speed_measurements.append(self.allocator, interval_speed);
// Maintain sliding window
if (self.speed_measurements.items.len > self.criteria.sliding_window_size) {
_ = self.speed_measurements.orderedRemove(0);
}
// Check stability if we have enough measurements
if (self.speed_measurements.items.len >= self.criteria.sliding_window_size) {
const cov = calculateCoV(self.speed_measurements.items);
if (cov <= self.criteria.stability_threshold_cov) {
self.consecutive_stable_checks += 1;
if (self.consecutive_stable_checks >= self.criteria.stable_checks_required) {
return true; // Stable, can stop
}
} else {
self.consecutive_stable_checks = 0; // Reset counter
}
}
}
}
self.last_sample_time = current_time;
self.last_total_bytes = current_total_bytes;
return false; // Not stable yet
}
pub fn handleProgress(self: *StabilityStrategy, current_time: u64, current_bytes: u64) !bool {
if (self.shouldSample(current_time)) {
return try self.addSample(current_time, current_bytes);
}
return false;
}
};
/// Calculate Coefficient of Variation (standard deviation / mean) for stability detection
fn calculateCoV(speeds: []const f64) f64 {
if (speeds.len < 2) return 1.0; // Not enough data, assume unstable
// Calculate mean
var sum: f64 = 0;
for (speeds) |speed| {
sum += speed;
}
const mean = sum / @as(f64, @floatFromInt(speeds.len));
if (mean == 0) return 1.0; // Avoid division by zero
// Calculate variance
var variance: f64 = 0;
for (speeds) |speed| {
const diff = speed - mean;
variance += diff * diff;
}
variance = variance / @as(f64, @floatFromInt(speeds.len));
// Calculate CoV (coefficient of variation)
const std_dev = @sqrt(variance);
return std_dev / mean;
}
// Clean helper functions
pub fn createDurationStrategy(duration_seconds: u32, progress_update_interval_ms: u64) DurationStrategy {
return DurationStrategy{
.target_duration_ns = @as(u64, duration_seconds) * std.time.ns_per_s,
.progress_update_interval_ms = progress_update_interval_ms,
};
}
pub fn createStabilityStrategy(allocator: std.mem.Allocator, criteria: StabilityCriteria) StabilityStrategy {
return StabilityStrategy.init(allocator, criteria);
}