mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-12 02:10:54 +00:00
433 lines
12 KiB
C
433 lines
12 KiB
C
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 Andrea Mazzoleni
|
|
|
|
#include "os/portable.h"
|
|
|
|
#include "thermal.h"
|
|
#include "state.h"
|
|
#include "io.h"
|
|
|
|
#include <math.h>
|
|
#include <float.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;
|
|
memset(&thermal->params, 0, sizeof(thermal->params));
|
|
pathcpy(thermal->name, sizeof(thermal->name), name);
|
|
|
|
return thermal;
|
|
}
|
|
|
|
void thermal_free(void* void_thermal)
|
|
{
|
|
struct snapraid_thermal* thermal = void_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);
|
|
|
|
/* avoid sum_total underflow and division by zero */
|
|
if (sum_total > FLT_EPSILON) {
|
|
model.r_squared = 1.0 - (sum_squared_residuals / sum_total);
|
|
} else {
|
|
model.r_squared = 1.0;
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
int 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 0;
|
|
|
|
/* reset the current highest temperature to ensure fail-open on measurement failure */
|
|
state->thermal_highest_temperature = 0;
|
|
|
|
state_devmap(state);
|
|
|
|
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->mount_device;
|
|
device_name_set(entry, disk->name, 0);
|
|
entry->is_array = 1;
|
|
pathcpy(entry->mount, sizeof(entry->mount), disk->mount_point);
|
|
pathcpy(entry->smartctl, sizeof(entry->smartctl), disk->smartctl);
|
|
pathcpy(entry->smartctl_info, sizeof(entry->smartctl_info), disk->smartctl_info);
|
|
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;
|
|
device_name_set(entry, lev_config_name(j), s);
|
|
entry->is_array = 1;
|
|
pathcpy(entry->mount, sizeof(entry->mount), state->parity[j].split_map[s].path);
|
|
pathcpy(entry->smartctl, sizeof(entry->smartctl), state->parity[j].smartctl);
|
|
pathcpy(entry->smartctl_info, sizeof(entry->smartctl_info), state->parity[j].smartctl_info);
|
|
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);
|
|
}
|
|
}
|
|
|
|
/* with a GUI always gives time reference */
|
|
if (state->opt.gui)
|
|
log_tag("unixtime:%" PRIi64 "\n", (int64_t)now);
|
|
|
|
if (state->opt.fake_device) {
|
|
ret = devtest(&high, &low, DEVICE_SMART);
|
|
} else {
|
|
ret = devquery(&high, &low);
|
|
if (ret == 0 && !tommy_list_empty(&low))
|
|
ret = devrun(&low, DEVICE_SMART);
|
|
}
|
|
|
|
/* on error, just disable thermal gathering */
|
|
if (ret != 0)
|
|
goto bail_error;
|
|
|
|
/* if the list is empty, it's not supported in this platform */
|
|
if (tommy_list_empty(&low))
|
|
goto bail_error;
|
|
|
|
/* report new attribute */
|
|
state_attr(state, &low);
|
|
|
|
/* if ambient temperature is not set, get the system temperature or fallback to the lowest HD temperature */
|
|
if (state->thermal_ambient_temperature == 0) {
|
|
state->thermal_system_temperature = ambient_temperature();
|
|
state->thermal_ambient_temperature = state->thermal_system_temperature;
|
|
|
|
if (state->thermal_ambient_temperature == 0) {
|
|
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 count = 0;
|
|
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;
|
|
|
|
++count;
|
|
|
|
/* 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 = state->thermal_latest - 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);
|
|
}
|
|
|
|
/* if no valid temperature was collected, report failure */
|
|
if (count == 0)
|
|
goto bail_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);
|
|
return 0;
|
|
|
|
bail_error:
|
|
tommy_list_foreach(&high, free);
|
|
tommy_list_foreach(&low, free);
|
|
return -1;
|
|
}
|
|
|
|
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 cooldown_time = state->thermal_cooldown_time;
|
|
|
|
if (cooldown_time == 0)
|
|
cooldown_time = 5 * 60; /* default sleep time */
|
|
if (cooldown_time < 3 * 60)
|
|
cooldown_time = 3 * 60; /* minimum sleep time */
|
|
|
|
/* from now on, stop any further data gathering as the heating is interrupted */
|
|
state->thermal_stop_gathering = 1;
|
|
|
|
for (;;) {
|
|
int sleep_time = cooldown_time;
|
|
|
|
log_tag("thermal:spindown\n");
|
|
|
|
/*
|
|
* Power management is best effort.
|
|
*
|
|
* Some devices may not support explicit spindown/spinup operations, or an
|
|
* operation may fail only for a subset of the devices. This must not make the
|
|
* whole thermal cooldown fail.
|
|
*
|
|
* Thermal safety is enforced by measuring the temperatures again after the
|
|
* cooldown. If any disk is still above the configured limit, the cooldown is
|
|
* repeated. Therefore failure to spin down a device only makes cooling less
|
|
* effective; it does not allow the operation to resume while a measurable disk
|
|
* is still above the temperature limit.
|
|
*/
|
|
state_device_force(state, DEVICE_DOWN, 0, 1);
|
|
|
|
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 && !os_signal_interrupt()) {
|
|
state_device_force(state, DEVICE_DOWNIFUP, 0, 1);
|
|
|
|
sleep(30);
|
|
sleep_time -= 30;
|
|
}
|
|
|
|
if (os_signal_interrupt())
|
|
break; /* don't wake-up if we are interrupting */
|
|
|
|
log_tag("thermal:spinup\n");
|
|
|
|
/* spinup */
|
|
state_device(state, DEVICE_UP, 0);
|
|
|
|
/* log new thermal info */
|
|
if (state_thermal(state, time(0)) != 0)
|
|
break;
|
|
|
|
/* repeat the cooldown if the temperature is still outside the operating range */
|
|
if (!state_thermal_alarm(state))
|
|
break;
|
|
}
|
|
}
|
|
|
|
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 = os_tick_sec();
|
|
state->thermal_latest = state->thermal_first;
|
|
state_thermal(state, now);
|
|
|
|
if (state->thermal_system_temperature != 0) {
|
|
printf("System temperature is %u degrees\n", state->thermal_system_temperature);
|
|
|
|
if (state->thermal_temperature_limit != 0 && state->thermal_temperature_limit <= state->thermal_system_temperature) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EENVIRONMENT, "DANGER! System temperature of %d degrees is higher than the temperature limit of %d degrees. Unable to proceed!\n", state->thermal_system_temperature, state->thermal_temperature_limit);
|
|
log_flush();
|
|
return 0;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
if (state_thermal_alarm(state)) {
|
|
msg_progress("Hard disk temperature of %d degrees is already outside the operating range. Cooling down before starting...\n", state->thermal_highest_temperature);
|
|
state_thermal_cooldown(state);
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|