Skip to content

Commit 4f585ee

Browse files
committed
Change tests according to new gradient behavior
1 parent b394afb commit 4f585ee

File tree

2 files changed

+142
-70
lines changed

2 files changed

+142
-70
lines changed

server/fan-control/index.test.ts

Lines changed: 97 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// TODO: upgrade packages so that eslint recognizes vitest globals out of the box
22
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3-
import { getCall, setCall } from "../native/index.js";
3+
import { getCall, setCall } from "../native";
44
import { state } from "../state/index.js";
55
import {
66
fanControl,
77
fanPercentToSpeed,
88
CYCLE_DURATION,
99
WAIT_RAMP_UP_CYCLES,
10+
WAIT_RAMP_DOWN_CYCLES,
1011
} from "./index.js";
1112

1213
vi.mock("../native", () => ({
@@ -32,21 +33,29 @@ function mockTemperatures(cpu: number, gpu: number) {
3233
}
3334

3435
async function waitUntilFanPercent(fanPercent: number) {
35-
let elapsed = 0;
36-
const interval = CYCLE_DURATION;
37-
const timeout = interval * 20;
38-
39-
mockedSetCall.mockClear();
40-
while (
41-
elapsed < timeout &&
42-
mockedSetCall.mock.calls[0]?.[2].Data !== fanPercentToSpeed(fanPercent)
43-
) {
44-
await vi.advanceTimersByTimeAsync(interval);
45-
elapsed += interval;
46-
}
36+
let advancedTime = 0;
37+
38+
while (true) {
39+
try {
40+
expect(mockedSetCall).toHaveBeenLastCalledWith(
41+
expect.any(String),
42+
expect.any(String),
43+
{
44+
Data: fanPercentToSpeed(fanPercent),
45+
},
46+
);
47+
return advancedTime / CYCLE_DURATION;
48+
} catch (_) {
49+
// console.log(
50+
// `Last call: ${
51+
// mockedSetCall.mock.calls[mockedSetCall.mock.calls.length - 1][2]
52+
// .Data
53+
// } (Expected: ${fanPercentToSpeed(fanPercent)})`,
54+
// );
55+
}
4756

48-
if (elapsed >= timeout) {
49-
throw new Error("Timeout - function never returned true.");
57+
await vi.advanceTimersByTimeAsync(10);
58+
advancedTime += 10;
5059
}
5160
}
5261

@@ -126,20 +135,47 @@ describe("fan-control", () => {
126135
]
127136
`);
128137

129-
// High CPU temperature
138+
// High CPU temperature => 50% fan speed
130139
mockTemperatures(90, 30);
131-
await waitUntilFanPercent(
132-
state.cpuFanTable[state.cpuFanTable.length - 1][1],
140+
await vi.advanceTimersByTimeAsync(
141+
3 * WAIT_RAMP_UP_CYCLES * CYCLE_DURATION + 1000,
142+
);
143+
expect(mockedSetCall).toHaveBeenLastCalledWith(
144+
expect.any(String),
145+
expect.any(String),
146+
{
147+
Data: fanPercentToSpeed(
148+
state.cpuFanTable[state.cpuFanTable.length - 1][1],
149+
),
150+
},
133151
);
134152

135-
// Cool CPU
153+
// Cool CPU => 15% fan speed
136154
mockTemperatures(30, 30);
137-
await waitUntilFanPercent(state.cpuFanTable[0][1]);
155+
await vi.advanceTimersByTimeAsync(
156+
3 * WAIT_RAMP_DOWN_CYCLES * CYCLE_DURATION + 1000,
157+
);
158+
expect(mockedSetCall).toHaveBeenLastCalledWith(
159+
expect.any(String),
160+
expect.any(String),
161+
{
162+
Data: fanPercentToSpeed(state.cpuFanTable[0][1]),
163+
},
164+
);
138165

139-
// High GPU temperature
166+
// High GPU temperature => 100% fan speed
140167
mockTemperatures(30, 90);
141-
await waitUntilFanPercent(
142-
state.gpuFanTable[state.gpuFanTable.length - 1][1],
168+
await vi.advanceTimersByTimeAsync(
169+
5 * WAIT_RAMP_UP_CYCLES * CYCLE_DURATION + 1000,
170+
);
171+
expect(mockedSetCall).toHaveBeenLastCalledWith(
172+
expect.any(String),
173+
expect.any(String),
174+
{
175+
Data: fanPercentToSpeed(
176+
state.gpuFanTable[state.gpuFanTable.length - 1][1],
177+
),
178+
},
143179
);
144180
});
145181

@@ -149,26 +185,51 @@ describe("fan-control", () => {
149185
state.doFixedSpeed = true;
150186
state.fixedPercentage = 75;
151187

152-
await waitUntilFanPercent(state.fixedPercentage);
188+
await vi.advanceTimersByTimeAsync(CYCLE_DURATION);
189+
expect(mockedSetCall).toHaveBeenLastCalledWith(
190+
expect.any(String),
191+
expect.any(String),
192+
{
193+
Data: fanPercentToSpeed(state.fixedPercentage),
194+
},
195+
);
153196
});
154197

155-
it("fan speed adjusts after X cycles", async () => {
198+
it("fan speed adjusts gradually after temperature change", async () => {
199+
// We can't just repeatedly advance the timer in this test because execution times aren't perfect and when allowing for a bit of leeway, we would cut into the time for the next gradient step.
200+
156201
fanControl();
157-
// Make sure steady state is reached
158-
await vi.advanceTimersByTimeAsync(CYCLE_DURATION * 10);
202+
// Make sure steady state is reached at initial temperature
203+
await waitUntilFanPercent(state.cpuFanTable[0][1]);
159204

205+
// Change to high CPU temperature
160206
mockTemperatures(90, 30);
161207
vi.clearAllMocks();
162-
// Cycle 1: Likely mix of temperatures
163-
// Cycle 2: Temp has become stable => target will be changed
164-
// Cycles 3+: WAIT_RAMP_UP_CYCLES until target will actually be applied
165-
await vi.advanceTimersByTimeAsync(
166-
CYCLE_DURATION * (WAIT_RAMP_UP_CYCLES + 2),
167-
);
168208

169-
expect(mockedSetCall.mock.calls[0][2].Data).toEqual(
170-
fanPercentToSpeed(state.cpuFanTable[state.cpuFanTable.length - 1][1]),
171-
);
209+
const initialPercentage = state.cpuFanTable[0][1];
210+
const targetPercentage = state.cpuFanTable[state.cpuFanTable.length - 1][1];
211+
212+
// First gradient step
213+
let currentPercentage = initialPercentage;
214+
let expectedPercentage =
215+
currentPercentage +
216+
Math.round((targetPercentage - currentPercentage) / 2);
217+
let cycles = await waitUntilFanPercent(expectedPercentage);
218+
expect(cycles - WAIT_RAMP_UP_CYCLES).toBeLessThan(1);
219+
220+
// Second gradient step
221+
currentPercentage = expectedPercentage;
222+
expectedPercentage =
223+
currentPercentage +
224+
Math.round((targetPercentage - currentPercentage) / 2);
225+
cycles = await waitUntilFanPercent(expectedPercentage);
226+
expect(cycles - WAIT_RAMP_UP_CYCLES).toBeLessThan(1);
227+
228+
// Reaching target
229+
currentPercentage = expectedPercentage;
230+
expectedPercentage = targetPercentage;
231+
cycles = await waitUntilFanPercent(expectedPercentage);
232+
expect(cycles - WAIT_RAMP_UP_CYCLES).toBeLessThan(1);
172233
});
173234

174235
it("should handle fan table changes", async () => {

server/fan-control/index.ts

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import {
33
FanTable,
44
FanControlActivity,
55
} from "../../common/types.js";
6-
import { getCall, setCall } from "../native/index.js";
6+
import { getCall, setCall } from "../native";
77
import { state } from "../state/index.js";
88

9-
const WAIT_RAMP_DOWN_CYCLES = 10;
10-
export const WAIT_RAMP_UP_CYCLES = 2;
9+
export const WAIT_RAMP_DOWN_CYCLES = 10;
10+
export const WAIT_RAMP_UP_CYCLES = 3;
1111
export const CYCLE_DURATION = 1000;
1212
const TEMP_POLL_INTERVAL = 200;
1313

@@ -82,26 +82,30 @@ export function fanControl() {
8282
return highestMatch;
8383
}
8484

85-
function getGradientTarget(lastAppliedSpeed: number, targetSpeed: number) {
86-
let gradientTarget = targetSpeed;
87-
if (targetSpeed > lastAppliedSpeed) {
85+
function getGradientTarget(
86+
lastAppliedPercentage: number,
87+
targetPercentage: number,
88+
) {
89+
let gradientTarget = targetPercentage;
90+
91+
if (targetPercentage > lastAppliedPercentage) {
8892
gradientTarget =
89-
lastAppliedSpeed + Math.round((targetSpeed - lastAppliedSpeed) / 2);
90-
} else if (targetSpeed < lastAppliedSpeed) {
93+
lastAppliedPercentage +
94+
Math.round((targetPercentage - lastAppliedPercentage) / 2);
95+
} else if (targetPercentage < lastAppliedPercentage) {
9196
gradientTarget =
92-
lastAppliedSpeed - Math.round((lastAppliedSpeed - targetSpeed) / 2);
93-
}
94-
95-
if (Math.abs(targetSpeed - gradientTarget) < 5) {
96-
gradientTarget = targetSpeed;
97+
lastAppliedPercentage -
98+
Math.round((lastAppliedPercentage - targetPercentage) / 2);
9799
}
98100

99-
return gradientTarget;
101+
return Math.abs(targetPercentage - gradientTarget) < 5
102+
? targetPercentage
103+
: gradientTarget;
100104
}
101105

102-
let appliedSpeed = -1;
103-
let currRampDownCycle = 0;
104-
let currRampUpCycle = 0;
106+
let appliedPercentage = -1;
107+
let currRampDownCycle = 1;
108+
let currRampUpCycle = 1;
105109
let prevCPUFanTable = state.cpuFanTable;
106110
let prevGPUFanTable = state.gpuFanTable;
107111
const autoFanInterval = setInterval(async () => {
@@ -126,7 +130,7 @@ export function fanControl() {
126130
const currGPUTemp2 = await getCallInt("0xe3", "getGpuTemp2");
127131
const currGPUTemp = Math.max(currGPUTemp1, currGPUTemp2);
128132
// console.log(
129-
// `CPU and GPU1/GPU2 temperatures: ${currCPUTemp} ${currGPUTemp1}/${currGPUTemp2}`
133+
// `CPU and GPU1/GPU2 temperatures: ${currCPUTemp} ${currGPUTemp1}/${currGPUTemp2}`,
130134
// );
131135

132136
CPUTemps.push(currCPUTemp);
@@ -163,44 +167,51 @@ export function fanControl() {
163167
) {
164168
// When tables change, do nothing in this cycle but reset fans to the
165169
// lowest percentage currently in state.
166-
appliedSpeed = resetFanSpeed();
170+
appliedPercentage = resetFanSpeed();
167171
prevCPUFanTable = state.cpuFanTable;
168172
prevGPUFanTable = state.gpuFanTable;
169-
currRampDownCycle = 0;
170-
currRampUpCycle = 0;
171-
} else if (appliedSpeed < target) {
173+
currRampDownCycle = 1;
174+
currRampUpCycle = 1;
175+
} else if (appliedPercentage < target) {
172176
if (currRampUpCycle === WAIT_RAMP_UP_CYCLES) {
173-
gradientTarget = getGradientTarget(appliedSpeed, target);
177+
gradientTarget = getGradientTarget(
178+
appliedPercentage === -1
179+
? state.cpuFanTable[0][1]
180+
: appliedPercentage,
181+
target,
182+
);
183+
// console.log(`Ramping up to ${fanPercentToSpeed(gradientTarget)}`);
174184
setFixedFan(gradientTarget);
175185

176-
currRampDownCycle = 0;
177-
currRampUpCycle = 0;
178-
appliedSpeed = gradientTarget;
186+
currRampDownCycle = 1;
187+
currRampUpCycle = 1;
188+
appliedPercentage = gradientTarget;
179189
} else {
180190
currRampUpCycle++;
181191
}
182-
} else if (target < appliedSpeed) {
192+
} else if (target < appliedPercentage) {
183193
// Make fan behavior less erratic by waiting a few cycles until we
184194
// ramp down.
185195
if (currRampDownCycle === WAIT_RAMP_DOWN_CYCLES) {
186-
gradientTarget = getGradientTarget(appliedSpeed, target);
196+
gradientTarget = getGradientTarget(appliedPercentage, target);
197+
// console.log(`Ramping down to ${fanPercentToSpeed(gradientTarget)}`);
187198
setFixedFan(gradientTarget);
188199

189-
currRampDownCycle = 0;
190-
currRampUpCycle = 0;
191-
appliedSpeed = gradientTarget;
200+
currRampDownCycle = 1;
201+
currRampUpCycle = 1;
202+
appliedPercentage = gradientTarget;
192203
} else {
193204
currRampDownCycle++;
194205
}
195206
} else {
196207
// Need to reset if e.g. ramp down phase is
197208
// interrupted by CPU getting hot again or getting cold again.
198-
currRampDownCycle = 0;
199-
currRampUpCycle = 0;
209+
currRampDownCycle = 1;
210+
currRampUpCycle = 1;
200211
}
201212

202213
sendActivity({
203-
appliedSpeed: appliedSpeed === -1 ? null : appliedSpeed,
214+
appliedSpeed: appliedPercentage === -1 ? null : appliedPercentage,
204215
avgCPUTemp,
205216
avgGPUTemp,
206217
target,

0 commit comments

Comments
 (0)