snapraid/cmdline/thermal.c
Andrea Mazzoleni d452b109bc
Some checks are pending
Build / build-mac (push) Waiting to run
Build / build-linux (push) Waiting to run
Build / build-win (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
More regression tests
2025-10-28 12:15:50 +01:00

396 lines
11 KiB
C

/*
* Copyright (C) 2025 Andrea Mazzoleni
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "portable.h"
#include "thermal.h"
#include "state.h"
#include "io.h"
#include <math.h>
struct snapraid_thermal* thermal_alloc(uint64_t dev, const char* name)
{
struct snapraid_thermal* thermal = malloc_nofail(sizeof(struct snapraid_thermal));
thermal->device = dev;
thermal->latest_temperature = 0;
thermal->count = 0;
pathcpy(thermal->name, sizeof(thermal->name), name);
return thermal;
}
void thermal_free(struct snapraid_thermal* thermal)
{
free(thermal);
}
/*
* Fit exponential heating model to data using least squares
*/
struct snapraid_thermal_params fit_thermal_model(const struct snapraid_thermal_point* points, int n_points, double t_ambient)
{
struct snapraid_thermal_params model;
double t_steady_try;
double k_try;
memset(&model, 0, sizeof(model));
model.t_ambient = t_ambient;
/* at least four points to have a result */
if (n_points < 4)
return model;
double last_temp = points[n_points - 1].temperature;
/* iterative refinement to find best k_heat and t_steady */
double best_error = 1e10;
double best_k = 0;
double best_t_steady = 0;
/* grid search for parameters */
for (t_steady_try = last_temp + 2.0; t_steady_try <= last_temp + 25.0; t_steady_try += 0.5) {
for (k_try = 0.00001; k_try <= 0.001; k_try *= 1.2) {
double error = 0.0;
int i;
/* calculate error for this parameter set */
for (i = 0; i < n_points; i++) {
double t = points[i].time;
double t_predicted = t_steady_try - (t_steady_try - points[0].temperature) * exp(-k_try * t);
double diff = points[i].temperature - t_predicted;
error += diff * diff;
}
if (error < best_error) {
best_error = error;
best_k = k_try;
best_t_steady = t_steady_try;
}
}
}
model.k_heat = best_k;
model.t_steady = best_t_steady;
/* calculate quality metrics */
double sum_squared_residuals = 0.0;
double sum_total = 0.0;
double mean_temp = 0.0;
model.max_error = 0.0;
int i;
/* calculate mean temperature */
for (i = 0; i < n_points; i++)
mean_temp += points[i].temperature;
mean_temp /= n_points;
/* calculate R-squared and errors */
for (i = 0; i < n_points; i++) {
double t = points[i].time;
double t_predicted = model.t_steady - (model.t_steady - points[0].temperature) * exp(-model.k_heat * t);
double residual = points[i].temperature - t_predicted;
sum_squared_residuals += residual * residual;
sum_total += (points[i].temperature - mean_temp) * (points[i].temperature - mean_temp);
double abs_error = fabs(residual);
if (abs_error > model.max_error) {
model.max_error = abs_error;
}
}
model.rmse = sqrt(sum_squared_residuals / n_points);
model.r_squared = 1.0 - (sum_squared_residuals / sum_total);
return model;
}
static int smart_temp(devinfo_t* devinfo)
{
uint64_t t = devinfo->smart[SMART_TEMPERATURE_CELSIUS];
/* validate temperature */
if (t == SMART_UNASSIGNED)
return -1;
if (t == 0)
return -1;
if (t > 100)
return -1;
return t;
}
void state_thermal(struct snapraid_state* state, time_t now)
{
tommy_node* i;
unsigned j;
tommy_list high;
tommy_list low;
int ret;
if (state->thermal_temperature_limit == 0)
return;
tommy_list_init(&high);
tommy_list_init(&low);
/* for all disks */
for (i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
devinfo_t* entry;
entry = calloc_nofail(1, sizeof(devinfo_t));
entry->device = disk->device;
pathcpy(entry->name, sizeof(entry->name), disk->name);
pathcpy(entry->mount, sizeof(entry->mount), disk->dir);
pathcpy(entry->smartctl, sizeof(entry->smartctl), disk->smartctl);
memcpy(entry->smartignore, disk->smartignore, sizeof(entry->smartignore));
tommy_list_insert_tail(&high, &entry->node, entry);
}
/* for all parities */
for (j = 0; j < state->level; ++j) {
devinfo_t* entry;
unsigned s;
for (s = 0; s < state->parity[j].split_mac; ++s) {
entry = calloc_nofail(1, sizeof(devinfo_t));
entry->device = state->parity[j].split_map[s].device;
pathcpy(entry->name, sizeof(entry->name), lev_config_name(j));
pathcpy(entry->mount, sizeof(entry->mount), state->parity[j].split_map[s].path);
pathcpy(entry->smartctl, sizeof(entry->smartctl), state->parity[j].smartctl);
memcpy(entry->smartignore, state->parity[j].smartignore, sizeof(entry->smartignore));
pathcut(entry->mount); /* remove the parity file */
tommy_list_insert_tail(&high, &entry->node, entry);
}
}
if (state->opt.fake_device) {
ret = devtest(&high, &low, DEVICE_SMART);
} else {
ret = devquery(&high, &low, DEVICE_SMART, 0 /* only disks in the array */);
}
/* on error, just disable thermal gathering */
if (ret != 0)
return;
/* if the list is empty, it's not supported in this platform */
if (tommy_list_empty(&low))
return;
/* if ambient temperature is not set, set it now with the lowest HD temperature */
if (state->thermal_ambient_temperature == 0) {
state->thermal_ambient_temperature = ambient_temperature();
for (i = tommy_list_head(&low); i != 0; i = i->next) {
devinfo_t* devinfo = i->data;
int temp = smart_temp(devinfo);
if (temp < 0)
continue;
log_tag("thermal:system:candidate:%d\n", temp);
if (state->thermal_ambient_temperature == 0 || state->thermal_ambient_temperature > temp)
state->thermal_ambient_temperature = temp;
}
log_tag("thermal:system:final:%d\n", state->thermal_ambient_temperature);
}
int highest_temperature = 0;
for (i = tommy_list_head(&low); i != 0; i = i->next) {
tommy_node* t;
struct snapraid_thermal* found;
devinfo_t* devinfo = i->data;
unsigned k;
int temperature = smart_temp(devinfo);
if (temperature < 0)
continue;
/* search of the entry */
found = 0;
for (t = tommy_list_head(&state->thermallist); t != 0; t = t->next) {
struct snapraid_thermal* thermal = t->data;
if (thermal->device == devinfo->device) {
found = thermal;
break;
}
}
/* if not found, create it */
if (found == 0) {
found = thermal_alloc(devinfo->device, devinfo->name);
tommy_list_insert_tail(&state->thermallist, &found->node, found);
}
found->latest_temperature = temperature;
if (highest_temperature < temperature)
highest_temperature = temperature;
log_tag("thermal:current:%s:%" PRIu64 ":%d\n", devinfo->name, devinfo->device, temperature);
if (state->thermal_stop_gathering)
continue;
if (found->count + 1 >= THERMAL_MAX) /* keep one extra space at the end */
continue;
/* only monotone increasing temperature */
if (found->count > 0 && found->data[found->count - 1].temperature >= temperature)
continue;
/* insert the new data point */
found->data[found->count].temperature = temperature;
found->data[found->count].time = now - state->thermal_first;
++found->count;
if (state->opt.fake_device) {
/* fill with fake data */
found->data[0].time = 0;
found->data[0].temperature = 27;
found->data[1].time = 100;
found->data[1].temperature = 28;
found->data[2].time = 300;
found->data[2].temperature = 29;
found->data[3].time = 700;
found->data[3].temperature = 30;
found->data[4].time = 1500;
found->data[4].temperature = 31;
found->count = 5;
}
/* log the new data */
log_tag("thermal:heat:%s:%" PRIu64 ":%u:", devinfo->name, devinfo->device, found->count);
for (k = 0; k < found->count; ++k)
log_tag("%s%d/%d", k > 0 ? "," : "", (int)found->data[k].temperature, (int)found->data[k].time);
log_tag("\n");
/* estimate parameters */
found->params = fit_thermal_model(found->data, found->count, state->thermal_ambient_temperature);
log_tag("thermal:params:%s:%" PRIu64 ":%g:%g:%g:%g:%g:%g\n", devinfo->name, devinfo->device,
found->params.k_heat, found->params.t_ambient, found->params.t_steady,
found->params.rmse, found->params.r_squared, found->params.max_error);
}
/* always update the highest temperature */
state->thermal_highest_temperature = highest_temperature;
log_tag("thermal:highest:%d\n", highest_temperature);
log_flush();
tommy_list_foreach(&high, free);
tommy_list_foreach(&low, free);
}
int state_thermal_alarm(struct snapraid_state* state)
{
/* if no limit, there is no thermal support */
if (state->thermal_temperature_limit == 0)
return 0;
if (state->thermal_highest_temperature <= state->thermal_temperature_limit)
return 0;
return 1;
}
void state_thermal_cooldown(struct snapraid_state* state)
{
int sleep_time = state->thermal_cooldown_time;
if (sleep_time == 0)
sleep_time = 5 * 60; /* default sleep time */
if (sleep_time < 5 * 60)
sleep_time = 5 * 60; /* minimum sleep time */
/* from now on, stop any further data gathering as the heating is interrupted */
state->thermal_stop_gathering = 1;
log_tag("thermal:spindown\n");
state_device(state, DEVICE_DOWN, 0);
msg_progress("Cooldown...\n");
log_tag("thermal:cooldown:%d\n", sleep_time);
printf("Waiting for %d minutes...\n", sleep_time / 60);
log_flush();
/* every 30 seconds spin down any disk that was spunup */
while (sleep_time > 0) {
state_device(state, DEVICE_DOWNIFUP, 0);
sleep(30);
sleep_time -= 30;
}
if (!global_interrupt) { /* don't wake-up if we are interrupting */
log_tag("thermal:spinup\n");
/* spinup */
state_device(state, DEVICE_UP, 0);
/* log new thermal info */
state_thermal(state, 0);
}
}
int state_thermal_begin(struct snapraid_state* state, time_t now)
{
if (state->thermal_temperature_limit == 0)
return 1;
/* initial thermal measure */
state->thermal_first = now;
state->thermal_latest = now;
state_thermal(state, now);
if (state->thermal_ambient_temperature != 0) {
printf("System temperature is %u degrees\n", state->thermal_ambient_temperature);
if (state->thermal_temperature_limit != 0 && state->thermal_temperature_limit <= state->thermal_ambient_temperature) {
/* LCOV_EXCL_START */
log_fatal("DANGER! System temperature of %d degrees is higher than the temperature limit of %d degrees. Unable to proceeed!\n", state->thermal_ambient_temperature, state->thermal_temperature_limit);
log_flush();
return 0;
/* LCOV_EXCL_STOP */
}
}
if (state_thermal_alarm(state)) {
/* LCOV_EXCL_START */
log_fatal("DANGER! Hard disk temperature of %d degrees is already outside the operating range. Unable to proceeed!\n", state->thermal_highest_temperature);
log_flush();
return 0;
/* LCOV_EXCL_STOP */
}
return 1;
}