Skip to content

Commit 4c2659e

Browse files
committed
fix: correctly handle zero median and even samples
1 parent 7728f94 commit 4c2659e

File tree

2 files changed

+101
-9
lines changed

2 files changed

+101
-9
lines changed

packages/core/src/v3/runEngineWorker/supervisor/queueMetricsProcessor.test.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,53 @@ describe("QueueMetricsProcessor", () => {
6161
expect(() => processor.addSample(0)).not.toThrow();
6262
expect(processor.getCurrentSampleCount()).toBe(1);
6363
});
64+
65+
it("should handle empty queue with all zero samples", () => {
66+
processor.addSample(0);
67+
processor.addSample(0);
68+
processor.addSample(0);
69+
70+
const result = processor.processBatch();
71+
expect(result).not.toBeNull();
72+
expect(result!.median).toBe(0);
73+
expect(result!.smoothedValue).toBe(0);
74+
expect(processor.getSmoothedValue()).toBe(0);
75+
});
76+
77+
it("should properly transition from zero to non-zero queue", () => {
78+
// Start with empty queue
79+
processor.addSample(0);
80+
processor.addSample(0);
81+
let result = processor.processBatch();
82+
expect(result!.median).toBe(0);
83+
expect(result!.smoothedValue).toBe(0);
84+
85+
// Queue starts filling
86+
processor.addSample(10);
87+
processor.addSample(15);
88+
result = processor.processBatch();
89+
expect(result!.median).toBeGreaterThan(0);
90+
// EWMA: 0.3 * median + 0.7 * 0
91+
expect(result!.smoothedValue).toBeGreaterThan(0);
92+
});
93+
94+
it("should properly transition from non-zero to zero queue", () => {
95+
// Start with non-empty queue
96+
processor.addSample(10);
97+
processor.addSample(15);
98+
let result = processor.processBatch();
99+
const initialSmoothed = result!.smoothedValue;
100+
expect(initialSmoothed).toBeGreaterThan(0);
101+
102+
// Queue becomes empty
103+
processor.addSample(0);
104+
processor.addSample(0);
105+
processor.addSample(0);
106+
result = processor.processBatch();
107+
expect(result!.median).toBe(0);
108+
// EWMA should gradually decrease: 0.3 * 0 + 0.7 * initialSmoothed
109+
expect(result!.smoothedValue).toBe(0.7 * initialSmoothed);
110+
});
64111
});
65112

66113
describe("Batch processing timing", () => {
@@ -146,6 +193,15 @@ describe("QueueMetricsProcessor", () => {
146193
processor = new QueueMetricsProcessor({ ewmaAlpha: 0.3, batchWindowMs: 1000 });
147194
});
148195

196+
it("should calculate median of single sample", () => {
197+
processor.addSample(42);
198+
199+
const result = processor.processBatch();
200+
expect(result!.median).toBe(42);
201+
expect(result!.sampleCount).toBe(1);
202+
expect(result!.smoothedValue).toBe(42); // First batch initializes to median
203+
});
204+
149205
it("should calculate median of odd number of samples", () => {
150206
processor.addSample(1);
151207
processor.addSample(10);
@@ -162,9 +218,9 @@ describe("QueueMetricsProcessor", () => {
162218
processor.addSample(8);
163219

164220
const result = processor.processBatch();
165-
// With even count, we take the lower middle value (index 1)
166-
// Sorted: [1, 5, 8, 10], median index = floor(4/2) = 2, so median = 8
167-
expect(result!.median).toBe(8);
221+
// With even count, we average the two middle values
222+
// Sorted: [1, 5, 8, 10], median = (5 + 8) / 2 = 6.5
223+
expect(result!.median).toBe(6.5);
168224
});
169225

170226
it("should filter outliers using median", () => {

packages/core/src/v3/runEngineWorker/supervisor/queueMetricsProcessor.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,44 @@ export class QueueMetricsProcessor {
8282
return currentTime - this.lastBatchTime >= this.batchWindowMs;
8383
}
8484

85+
private calculateMedian(samples: number[]): number | null {
86+
const sortedSamples = [...this.samples].sort((a, b) => a - b);
87+
const mid = Math.floor(sortedSamples.length / 2);
88+
89+
if (sortedSamples.length % 2 === 1) {
90+
// Odd length: use middle value
91+
const median = sortedSamples[mid];
92+
93+
if (median === undefined) {
94+
console.error("Invalid median calculated from odd samples", {
95+
sortedSamples,
96+
mid,
97+
median,
98+
});
99+
return null;
100+
}
101+
102+
return median;
103+
} else {
104+
// Even length: average two middle values
105+
const lowMid = sortedSamples[mid - 1];
106+
const highMid = sortedSamples[mid];
107+
108+
if (lowMid === undefined || highMid === undefined) {
109+
console.error("Invalid median calculated from even samples", {
110+
sortedSamples,
111+
mid,
112+
lowMid,
113+
highMid,
114+
});
115+
return null;
116+
}
117+
118+
const median = (lowMid + highMid) / 2;
119+
return median;
120+
}
121+
}
122+
85123
/**
86124
* Processes the current batch of samples and returns the result.
87125
* Clears the samples array and updates the smoothed value.
@@ -90,16 +128,14 @@ export class QueueMetricsProcessor {
90128
*/
91129
processBatch(currentTime: number = Date.now()): BatchProcessingResult | null {
92130
if (this.samples.length === 0) {
131+
// No samples to process
93132
return null;
94133
}
95134

96135
// Calculate median of samples to filter outliers
97-
const sortedSamples = [...this.samples].sort((a, b) => a - b);
98-
const medianIndex = Math.floor(sortedSamples.length / 2);
99-
const median = sortedSamples[medianIndex];
100-
101-
if (!median) {
102-
console.warn("Median should exist for n > 0 samples");
136+
const median = this.calculateMedian(this.samples);
137+
if (median === null) {
138+
// We already logged a more specific error message
103139
return null;
104140
}
105141

0 commit comments

Comments
 (0)