@curl-runner/cli 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ import type { ProfileStats } from '../types/config';
2
+
3
+ /**
4
+ * Calculate percentile from sorted array.
5
+ * Uses linear interpolation for non-integer indices.
6
+ */
7
+ export function calculatePercentile(sorted: number[], percentile: number): number {
8
+ if (sorted.length === 0) {
9
+ return 0;
10
+ }
11
+ if (sorted.length === 1) {
12
+ return sorted[0];
13
+ }
14
+
15
+ const index = (percentile / 100) * (sorted.length - 1);
16
+ const lower = Math.floor(index);
17
+ const upper = Math.ceil(index);
18
+ const fraction = index - lower;
19
+
20
+ if (lower === upper) {
21
+ return sorted[lower];
22
+ }
23
+ return sorted[lower] * (1 - fraction) + sorted[upper] * fraction;
24
+ }
25
+
26
+ /**
27
+ * Calculate arithmetic mean.
28
+ */
29
+ export function calculateMean(values: number[]): number {
30
+ if (values.length === 0) {
31
+ return 0;
32
+ }
33
+ return values.reduce((sum, v) => sum + v, 0) / values.length;
34
+ }
35
+
36
+ /**
37
+ * Calculate standard deviation.
38
+ */
39
+ export function calculateStdDev(values: number[], mean: number): number {
40
+ if (values.length <= 1) {
41
+ return 0;
42
+ }
43
+ const squaredDiffs = values.map((v) => (v - mean) ** 2);
44
+ const variance = squaredDiffs.reduce((sum, v) => sum + v, 0) / values.length;
45
+ return Math.sqrt(variance);
46
+ }
47
+
48
+ /**
49
+ * Calculate profile statistics from raw timings.
50
+ */
51
+ export function calculateProfileStats(
52
+ timings: number[],
53
+ warmup: number,
54
+ failures: number,
55
+ ): ProfileStats {
56
+ // Exclude warmup iterations
57
+ const effectiveTimings = timings.slice(warmup);
58
+ const sorted = [...effectiveTimings].sort((a, b) => a - b);
59
+
60
+ const mean = calculateMean(sorted);
61
+ const totalIterations = timings.length;
62
+ const effectiveIterations = effectiveTimings.length;
63
+
64
+ return {
65
+ iterations: effectiveIterations,
66
+ warmup,
67
+ min: sorted.length > 0 ? sorted[0] : 0,
68
+ max: sorted.length > 0 ? sorted[sorted.length - 1] : 0,
69
+ mean: Math.round(mean * 100) / 100,
70
+ median: Math.round(calculatePercentile(sorted, 50) * 100) / 100,
71
+ p50: Math.round(calculatePercentile(sorted, 50) * 100) / 100,
72
+ p95: Math.round(calculatePercentile(sorted, 95) * 100) / 100,
73
+ p99: Math.round(calculatePercentile(sorted, 99) * 100) / 100,
74
+ stdDev: Math.round(calculateStdDev(sorted, mean) * 100) / 100,
75
+ failures,
76
+ failureRate: totalIterations > 0 ? Math.round((failures / totalIterations) * 10000) / 100 : 0,
77
+ timings: effectiveTimings,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Generate ASCII histogram for latency distribution.
83
+ */
84
+ export function generateHistogram(timings: number[], buckets = 10, width = 40): string[] {
85
+ if (timings.length === 0) {
86
+ return ['No data'];
87
+ }
88
+
89
+ const min = Math.min(...timings);
90
+ const max = Math.max(...timings);
91
+ const range = max - min || 1;
92
+ const bucketSize = range / buckets;
93
+
94
+ // Count values per bucket
95
+ const counts = new Array(buckets).fill(0);
96
+ for (const t of timings) {
97
+ const bucket = Math.min(Math.floor((t - min) / bucketSize), buckets - 1);
98
+ counts[bucket]++;
99
+ }
100
+
101
+ const maxCount = Math.max(...counts);
102
+ const lines: string[] = [];
103
+
104
+ for (let i = 0; i < buckets; i++) {
105
+ const bucketMin = min + i * bucketSize;
106
+ const bucketMax = min + (i + 1) * bucketSize;
107
+ const barLength = maxCount > 0 ? Math.round((counts[i] / maxCount) * width) : 0;
108
+ const bar = '█'.repeat(barLength);
109
+ const label = `${bucketMin.toFixed(0).padStart(6)}ms - ${bucketMax.toFixed(0).padStart(6)}ms`;
110
+ lines.push(`${label} │${bar} ${counts[i]}`);
111
+ }
112
+
113
+ return lines;
114
+ }
115
+
116
+ /**
117
+ * Export stats to CSV format.
118
+ */
119
+ export function exportToCSV(stats: ProfileStats, _requestName: string): string {
120
+ const headers = ['iteration', 'latency_ms'];
121
+ const rows = stats.timings.map((t, i) => `${i + 1},${t}`);
122
+ return [headers.join(','), ...rows].join('\n');
123
+ }
124
+
125
+ /**
126
+ * Export stats to JSON format.
127
+ */
128
+ export function exportToJSON(stats: ProfileStats, requestName: string): string {
129
+ return JSON.stringify(
130
+ {
131
+ request: requestName,
132
+ summary: {
133
+ iterations: stats.iterations,
134
+ warmup: stats.warmup,
135
+ failures: stats.failures,
136
+ failureRate: stats.failureRate,
137
+ min: stats.min,
138
+ max: stats.max,
139
+ mean: stats.mean,
140
+ median: stats.median,
141
+ p50: stats.p50,
142
+ p95: stats.p95,
143
+ p99: stats.p99,
144
+ stdDev: stats.stdDev,
145
+ },
146
+ timings: stats.timings,
147
+ },
148
+ null,
149
+ 2,
150
+ );
151
+ }