| 1 | |
| 2 | |
| 3 | * Copyright 2023 Linaro Limited
|
| 4 | *
|
| 5 | * Author: Daniel Lezcano <[email protected]>
|
| 6 | *
|
| 7 | * Thermal subsystem debug support
|
| 8 |
|
| 9 | #include <linux/debugfs.h>
|
| 10 | #include <linux/ktime.h>
|
| 11 | #include <linux/list.h>
|
| 12 | #include <linux/minmax.h>
|
| 13 | #include <linux/mutex.h>
|
| 14 | #include <linux/thermal.h>
|
| 15 |
|
| 16 | #include "thermal_core.h"
|
| 17 |
|
| 18 | static struct dentry *d_root;
|
| 19 | static struct dentry *d_cdev;
|
| 20 | static struct dentry *d_tz;
|
| 21 |
|
| 22 | |
| 23 | * Length of the string containing the thermal zone id or the cooling
|
| 24 | * device id, including the ending nul character. We can reasonably
|
| 25 | * assume there won't be more than 256 thermal zones as the maximum
|
| 26 | * observed today is around 32.
|
| 27 |
|
| 28 | #define IDSLENGTH 4
|
| 29 |
|
| 30 | |
| 31 | * The cooling device transition list is stored in a hash table where
|
| 32 | * the size is CDEVSTATS_HASH_SIZE. The majority of cooling devices
|
| 33 | * have dozen of states but some can have much more, so a hash table
|
| 34 | * is more adequate in this case, because the cost of browsing the entire
|
| 35 | * list when storing the transitions may not be negligible.
|
| 36 |
|
| 37 | #define CDEVSTATS_HASH_SIZE 16
|
| 38 |
|
| 39 | |
| 40 | * struct cdev_debugfs - per cooling device statistics structure
|
| 41 | * A cooling device can have a high number of states. Showing the
|
| 42 | * transitions on a matrix based representation can be overkill given
|
| 43 | * most of the transitions won't happen and we end up with a matrix
|
| 44 | * filled with zero. Instead, we show the transitions which actually
|
| 45 | * happened.
|
| 46 | *
|
| 47 | * Every transition updates the current_state and the timestamp. The
|
| 48 | * transitions and the durations are stored in lists.
|
| 49 | *
|
| 50 | * @total: the number of transitions for this cooling device
|
| 51 | * @current_state: the current cooling device state
|
| 52 | * @timestamp: the state change timestamp
|
| 53 | * @transitions: an array of lists containing the state transitions
|
| 54 | * @durations: an array of lists containing the residencies of each state
|
| 55 |
|
| 56 | struct cdev_debugfs {
|
| 57 | u32 total;
|
| 58 | int current_state;
|
| 59 | ktime_t timestamp;
|
| 60 | struct list_head transitions[CDEVSTATS_HASH_SIZE];
|
| 61 | struct list_head durations[CDEVSTATS_HASH_SIZE];
|
| 62 | };
|
| 63 |
|
| 64 | |
| 65 | * struct cdev_record - Common structure for cooling device entry
|
| 66 | *
|
| 67 | * The following common structure allows to store the information
|
| 68 | * related to the transitions and to the state residencies. They are
|
| 69 | * identified with a id which is associated to a value. It is used as
|
| 70 | * nodes for the "transitions" and "durations" above.
|
| 71 | *
|
| 72 | * @node: node to insert the structure in a list
|
| 73 | * @id: identifier of the value which can be a state or a transition
|
| 74 | * @residency: a ktime_t representing a state residency duration
|
| 75 | * @count: a number of occurrences
|
| 76 |
|
| 77 | struct cdev_record {
|
| 78 | struct list_head node;
|
| 79 | int id;
|
| 80 | union {
|
| 81 | ktime_t residency;
|
| 82 | u64 count;
|
| 83 | };
|
| 84 | };
|
| 85 |
|
| 86 | |
| 87 | * struct trip_stats - Thermal trip statistics
|
| 88 | *
|
| 89 | * The trip_stats structure has the relevant information to show the
|
| 90 | * statistics related to temperature going above a trip point.
|
| 91 | *
|
| 92 | * @timestamp: the trip crossing timestamp
|
| 93 | * @duration: total time when the zone temperature was above the trip point
|
| 94 | * @trip_temp: trip temperature at mitigation start
|
| 95 | * @trip_hyst: trip hysteresis at mitigation start
|
| 96 | * @count: the number of times the zone temperature was above the trip point
|
| 97 | * @min: minimum recorded temperature above the trip point
|
| 98 | * @avg: average temperature above the trip point
|
| 99 |
|
| 100 | struct trip_stats {
|
| 101 | ktime_t timestamp;
|
| 102 | ktime_t duration;
|
| 103 | int trip_temp;
|
| 104 | int trip_hyst;
|
| 105 | int count;
|
| 106 | int min;
|
| 107 | int avg;
|
| 108 | };
|
| 109 |
|
| 110 | |
| 111 | * struct tz_episode - A mitigation episode information
|
| 112 | *
|
| 113 | * The tz_episode structure describes a mitigation episode. A
|
| 114 | * mitigation episode begins the trip point with the lower temperature
|
| 115 | * is crossed the way up and ends when it is crossed the way
|
| 116 | * down. During this episode we can have multiple trip points crossed
|
| 117 | * the way up and down if there are multiple trip described in the
|
| 118 | * firmware after the lowest temperature trip point.
|
| 119 | *
|
| 120 | * @timestamp: first trip point crossed the way up
|
| 121 | * @duration: total duration of the mitigation episode
|
| 122 | * @node: a list element to be added to the list of tz events
|
| 123 | * @max_temp: maximum zone temperature during this episode
|
| 124 | * @trip_stats: per trip point statistics, flexible array
|
| 125 |
|
| 126 | struct tz_episode {
|
| 127 | ktime_t timestamp;
|
| 128 | ktime_t duration;
|
| 129 | struct list_head node;
|
| 130 | int max_temp;
|
| 131 | struct trip_stats trip_stats[];
|
| 132 | };
|
| 133 |
|
| 134 | |
| 135 | * struct tz_debugfs - Store all mitigation episodes for a thermal zone
|
| 136 | *
|
| 137 | * The tz_debugfs structure contains the list of the mitigation
|
| 138 | * episodes and has to track which trip point has been crossed in
|
| 139 | * order to handle correctly nested trip point mitigation episodes.
|
| 140 | *
|
| 141 | * We keep the history of the trip point crossed in an array and as we
|
| 142 | * can go back and forth inside this history, eg. trip 0,1,2,1,2,1,0,
|
| 143 | * we keep track of the current position in the history array.
|
| 144 | *
|
| 145 | * @tz_episodes: a list of thermal mitigation episodes
|
| 146 | * @tz: thermal zone this object belongs to
|
| 147 | * @trips_crossed: an array of trip points crossed by id
|
| 148 | * @nr_trips: the number of trip points currently being crossed
|
| 149 |
|
| 150 | struct tz_debugfs {
|
| 151 | struct list_head tz_episodes;
|
| 152 | struct thermal_zone_device *tz;
|
| 153 | int *trips_crossed;
|
| 154 | int nr_trips;
|
| 155 | };
|
| 156 |
|
| 157 | |
| 158 | * struct thermal_debugfs - High level structure for a thermal object in debugfs
|
| 159 | *
|
| 160 | * The thermal_debugfs structure is the common structure used by the
|
| 161 | * cooling device or the thermal zone to store the statistics.
|
| 162 | *
|
| 163 | * @d_top: top directory of the thermal object directory
|
| 164 | * @lock: per object lock to protect the internals
|
| 165 | *
|
| 166 | * @cdev_dbg: a cooling device debug structure
|
| 167 | * @tz_dbg: a thermal zone debug structure
|
| 168 |
|
| 169 | struct thermal_debugfs {
|
| 170 | struct dentry *d_top;
|
| 171 | struct mutex lock;
|
| 172 | union {
|
| 173 | struct cdev_debugfs cdev_dbg;
|
| 174 | struct tz_debugfs tz_dbg;
|
| 175 | };
|
| 176 | };
|
| 177 |
|
| 178 | void thermal_debug_init(void)
|
| 179 | {
|
| 180 | d_root = debugfs_create_dir("thermal", NULL);
|
| 181 | if (IS_ERR(d_root))
|
| 182 | return;
|
| 183 |
|
| 184 | d_cdev = debugfs_create_dir("cooling_devices", d_root);
|
| 185 | if (IS_ERR(d_cdev))
|
| 186 | return;
|
| 187 |
|
| 188 | d_tz = debugfs_create_dir("thermal_zones", d_root);
|
| 189 | }
|
| 190 |
|
| 191 | static struct thermal_debugfs *thermal_debugfs_add_id(struct dentry *d, int id)
|
| 192 | {
|
| 193 | struct thermal_debugfs *thermal_dbg;
|
| 194 | char ids[IDSLENGTH];
|
| 195 |
|
| 196 | thermal_dbg = kzalloc_obj(*thermal_dbg);
|
| 197 | if (!thermal_dbg)
|
| 198 | return NULL;
|
| 199 |
|
| 200 | mutex_init(&thermal_dbg->lock);
|
| 201 |
|
| 202 | snprintf(ids, IDSLENGTH, "%d", id);
|
| 203 |
|
| 204 | thermal_dbg->d_top = debugfs_create_dir(ids, d);
|
| 205 | if (IS_ERR(thermal_dbg->d_top)) {
|
| 206 | kfree(thermal_dbg);
|
| 207 | return NULL;
|
| 208 | }
|
| 209 |
|
| 210 | return thermal_dbg;
|
| 211 | }
|
| 212 |
|
| 213 | static void thermal_debugfs_remove_id(struct thermal_debugfs *thermal_dbg)
|
| 214 | {
|
| 215 | if (!thermal_dbg)
|
| 216 | return;
|
| 217 |
|
| 218 | debugfs_remove(thermal_dbg->d_top);
|
| 219 |
|
| 220 | kfree(thermal_dbg);
|
| 221 | }
|
| 222 |
|
| 223 | static struct cdev_record *
|
| 224 | thermal_debugfs_cdev_record_alloc(struct thermal_debugfs *thermal_dbg,
|
| 225 | struct list_head *lists, int id)
|
| 226 | {
|
| 227 | struct cdev_record *cdev_record;
|
| 228 |
|
| 229 | cdev_record = kzalloc_obj(*cdev_record);
|
| 230 | if (!cdev_record)
|
| 231 | return NULL;
|
| 232 |
|
| 233 | cdev_record->id = id;
|
| 234 | INIT_LIST_HEAD(&cdev_record->node);
|
| 235 | list_add_tail(&cdev_record->node,
|
| 236 | &lists[cdev_record->id % CDEVSTATS_HASH_SIZE]);
|
| 237 |
|
| 238 | return cdev_record;
|
| 239 | }
|
| 240 |
|
| 241 | static struct cdev_record *
|
| 242 | thermal_debugfs_cdev_record_find(struct thermal_debugfs *thermal_dbg,
|
| 243 | struct list_head *lists, int id)
|
| 244 | {
|
| 245 | struct cdev_record *entry;
|
| 246 |
|
| 247 | list_for_each_entry(entry, &lists[id % CDEVSTATS_HASH_SIZE], node)
|
| 248 | if (entry->id == id)
|
| 249 | return entry;
|
| 250 |
|
| 251 | return NULL;
|
| 252 | }
|
| 253 |
|
| 254 | static struct cdev_record *
|
| 255 | thermal_debugfs_cdev_record_get(struct thermal_debugfs *thermal_dbg,
|
| 256 | struct list_head *lists, int id)
|
| 257 | {
|
| 258 | struct cdev_record *cdev_record;
|
| 259 |
|
| 260 | cdev_record = thermal_debugfs_cdev_record_find(thermal_dbg, lists, id);
|
| 261 | if (cdev_record)
|
| 262 | return cdev_record;
|
| 263 |
|
| 264 | return thermal_debugfs_cdev_record_alloc(thermal_dbg, lists, id);
|
| 265 | }
|
| 266 |
|
| 267 | static void thermal_debugfs_cdev_clear(struct cdev_debugfs *cdev_dbg)
|
| 268 | {
|
| 269 | int i;
|
| 270 | struct cdev_record *entry, *tmp;
|
| 271 |
|
| 272 | for (i = 0; i < CDEVSTATS_HASH_SIZE; i++) {
|
| 273 |
|
| 274 | list_for_each_entry_safe(entry, tmp,
|
| 275 | &cdev_dbg->transitions[i], node) {
|
| 276 | list_del(&entry->node);
|
| 277 | kfree(entry);
|
| 278 | }
|
| 279 |
|
| 280 | list_for_each_entry_safe(entry, tmp,
|
| 281 | &cdev_dbg->durations[i], node) {
|
| 282 | list_del(&entry->node);
|
| 283 | kfree(entry);
|
| 284 | }
|
| 285 | }
|
| 286 |
|
| 287 | cdev_dbg->total = 0;
|
| 288 | }
|
| 289 |
|
| 290 | static void *cdev_seq_start(struct seq_file *s, loff_t *pos)
|
| 291 | {
|
| 292 | struct thermal_debugfs *thermal_dbg = s->private;
|
| 293 |
|
| 294 | mutex_lock(&thermal_dbg->lock);
|
| 295 |
|
| 296 | return (*pos < CDEVSTATS_HASH_SIZE) ? pos : NULL;
|
| 297 | }
|
| 298 |
|
| 299 | static void *cdev_seq_next(struct seq_file *s, void *v, loff_t *pos)
|
| 300 | {
|
| 301 | (*pos)++;
|
| 302 |
|
| 303 | return (*pos < CDEVSTATS_HASH_SIZE) ? pos : NULL;
|
| 304 | }
|
| 305 |
|
| 306 | static void cdev_seq_stop(struct seq_file *s, void *v)
|
| 307 | {
|
| 308 | struct thermal_debugfs *thermal_dbg = s->private;
|
| 309 |
|
| 310 | mutex_unlock(&thermal_dbg->lock);
|
| 311 | }
|
| 312 |
|
| 313 | static int cdev_tt_seq_show(struct seq_file *s, void *v)
|
| 314 | {
|
| 315 | struct thermal_debugfs *thermal_dbg = s->private;
|
| 316 | struct cdev_debugfs *cdev_dbg = &thermal_dbg->cdev_dbg;
|
| 317 | struct list_head *transitions = cdev_dbg->transitions;
|
| 318 | struct cdev_record *entry;
|
| 319 | int i = *(loff_t *)v;
|
| 320 |
|
| 321 | if (!i)
|
| 322 | seq_puts(s, "Transition\tOccurrences\n");
|
| 323 |
|
| 324 | list_for_each_entry(entry, &transitions[i], node) {
|
| 325 | |
| 326 | * Assuming maximum cdev states is 1024, the longer
|
| 327 | * string for a transition would be "1024->1024\0"
|
| 328 |
|
| 329 | char buffer[11];
|
| 330 |
|
| 331 | snprintf(buffer, ARRAY_SIZE(buffer), "%d->%d",
|
| 332 | entry->id >> 16, entry->id & 0xFFFF);
|
| 333 |
|
| 334 | seq_printf(s, "%-10s\t%-10llu\n", buffer, entry->count);
|
| 335 | }
|
| 336 |
|
| 337 | return 0;
|
| 338 | }
|
| 339 |
|
| 340 | static const struct seq_operations tt_sops = {
|
| 341 | .start = cdev_seq_start,
|
| 342 | .next = cdev_seq_next,
|
| 343 | .stop = cdev_seq_stop,
|
| 344 | .show = cdev_tt_seq_show,
|
| 345 | };
|
| 346 |
|
| 347 | DEFINE_SEQ_ATTRIBUTE(tt);
|
| 348 |
|
| 349 | static int cdev_dt_seq_show(struct seq_file *s, void *v)
|
| 350 | {
|
| 351 | struct thermal_debugfs *thermal_dbg = s->private;
|
| 352 | struct cdev_debugfs *cdev_dbg = &thermal_dbg->cdev_dbg;
|
| 353 | struct list_head *durations = cdev_dbg->durations;
|
| 354 | struct cdev_record *entry;
|
| 355 | int i = *(loff_t *)v;
|
| 356 |
|
| 357 | if (!i)
|
| 358 | seq_puts(s, "State\tResidency\n");
|
| 359 |
|
| 360 | list_for_each_entry(entry, &durations[i], node) {
|
| 361 | s64 duration = ktime_to_ms(entry->residency);
|
| 362 |
|
| 363 | if (entry->id == cdev_dbg->current_state)
|
| 364 | duration += ktime_ms_delta(ktime_get(),
|
| 365 | cdev_dbg->timestamp);
|
| 366 |
|
| 367 | seq_printf(s, "%-5d\t%-10llu\n", entry->id, duration);
|
| 368 | }
|
| 369 |
|
| 370 | return 0;
|
| 371 | }
|
| 372 |
|
| 373 | static const struct seq_operations dt_sops = {
|
| 374 | .start = cdev_seq_start,
|
| 375 | .next = cdev_seq_next,
|
| 376 | .stop = cdev_seq_stop,
|
| 377 | .show = cdev_dt_seq_show,
|
| 378 | };
|
| 379 |
|
| 380 | DEFINE_SEQ_ATTRIBUTE(dt);
|
| 381 |
|
| 382 | static int cdev_clear_set(void *data, u64 val)
|
| 383 | {
|
| 384 | struct thermal_debugfs *thermal_dbg = data;
|
| 385 |
|
| 386 | if (!val)
|
| 387 | return -EINVAL;
|
| 388 |
|
| 389 | mutex_lock(&thermal_dbg->lock);
|
| 390 |
|
| 391 | thermal_debugfs_cdev_clear(&thermal_dbg->cdev_dbg);
|
| 392 |
|
| 393 | mutex_unlock(&thermal_dbg->lock);
|
| 394 |
|
| 395 | return 0;
|
| 396 | }
|
| 397 |
|
| 398 | DEFINE_DEBUGFS_ATTRIBUTE(cdev_clear_fops, NULL, cdev_clear_set, "%llu\n");
|
| 399 |
|
| 400 | |
| 401 | * thermal_debug_cdev_state_update - Update a cooling device state change
|
| 402 | *
|
| 403 | * Computes a transition and the duration of the previous state residency.
|
| 404 | *
|
| 405 | * @cdev : a pointer to a cooling device
|
| 406 | * @new_state: an integer corresponding to the new cooling device state
|
| 407 |
|
| 408 | void thermal_debug_cdev_state_update(const struct thermal_cooling_device *cdev,
|
| 409 | int new_state)
|
| 410 | {
|
| 411 | struct thermal_debugfs *thermal_dbg = cdev->debugfs;
|
| 412 | struct cdev_debugfs *cdev_dbg;
|
| 413 | struct cdev_record *cdev_record;
|
| 414 | int transition, old_state;
|
| 415 |
|
| 416 | if (!thermal_dbg || (thermal_dbg->cdev_dbg.current_state == new_state))
|
| 417 | return;
|
| 418 |
|
| 419 | mutex_lock(&thermal_dbg->lock);
|
| 420 |
|
| 421 | cdev_dbg = &thermal_dbg->cdev_dbg;
|
| 422 |
|
| 423 | old_state = cdev_dbg->current_state;
|
| 424 |
|
| 425 | |
| 426 | * Get the old state information in the durations list. If
|
| 427 | * this one does not exist, a new allocated one will be
|
| 428 | * returned. Recompute the total duration in the old state and
|
| 429 | * get a new timestamp for the new state.
|
| 430 |
|
| 431 | cdev_record = thermal_debugfs_cdev_record_get(thermal_dbg,
|
| 432 | cdev_dbg->durations,
|
| 433 | old_state);
|
| 434 | if (cdev_record) {
|
| 435 | ktime_t now = ktime_get();
|
| 436 | ktime_t delta = ktime_sub(now, cdev_dbg->timestamp);
|
| 437 | cdev_record->residency = ktime_add(cdev_record->residency, delta);
|
| 438 | cdev_dbg->timestamp = now;
|
| 439 | }
|
| 440 |
|
| 441 | cdev_dbg->current_state = new_state;
|
| 442 |
|
| 443 | |
| 444 | * Create a record for the new state if it is not there, so its
|
| 445 | * duration will be printed by cdev_dt_seq_show() as expected if it
|
| 446 | * runs before the next state transition.
|
| 447 |
|
| 448 | thermal_debugfs_cdev_record_get(thermal_dbg, cdev_dbg->durations, new_state);
|
| 449 |
|
| 450 | transition = (old_state << 16) | new_state;
|
| 451 |
|
| 452 | |
| 453 | * Get the transition in the transitions list. If this one
|
| 454 | * does not exist, a new allocated one will be returned.
|
| 455 | * Increment the occurrence of this transition which is stored
|
| 456 | * in the value field.
|
| 457 |
|
| 458 | cdev_record = thermal_debugfs_cdev_record_get(thermal_dbg,
|
| 459 | cdev_dbg->transitions,
|
| 460 | transition);
|
| 461 | if (cdev_record)
|
| 462 | cdev_record->count++;
|
| 463 |
|
| 464 | cdev_dbg->total++;
|
| 465 |
|
| 466 | mutex_unlock(&thermal_dbg->lock);
|
| 467 | }
|
| 468 |
|
| 469 | |
| 470 | * thermal_debug_cdev_add - Add a cooling device debugfs entry
|
| 471 | *
|
| 472 | * Allocates a cooling device object for debug, initializes the
|
| 473 | * statistics and create the entries in sysfs.
|
| 474 | * @cdev: a pointer to a cooling device
|
| 475 | * @state: current state of the cooling device
|
| 476 |
|
| 477 | void thermal_debug_cdev_add(struct thermal_cooling_device *cdev, int state)
|
| 478 | {
|
| 479 | struct thermal_debugfs *thermal_dbg;
|
| 480 | struct cdev_debugfs *cdev_dbg;
|
| 481 | int i;
|
| 482 |
|
| 483 | thermal_dbg = thermal_debugfs_add_id(d_cdev, cdev->id);
|
| 484 | if (!thermal_dbg)
|
| 485 | return;
|
| 486 |
|
| 487 | cdev_dbg = &thermal_dbg->cdev_dbg;
|
| 488 |
|
| 489 | for (i = 0; i < CDEVSTATS_HASH_SIZE; i++) {
|
| 490 | INIT_LIST_HEAD(&cdev_dbg->transitions[i]);
|
| 491 | INIT_LIST_HEAD(&cdev_dbg->durations[i]);
|
| 492 | }
|
| 493 |
|
| 494 | cdev_dbg->current_state = state;
|
| 495 | cdev_dbg->timestamp = ktime_get();
|
| 496 |
|
| 497 | |
| 498 | * Create a record for the initial cooling device state, so its
|
| 499 | * duration will be printed by cdev_dt_seq_show() as expected if it
|
| 500 | * runs before the first state transition.
|
| 501 |
|
| 502 | thermal_debugfs_cdev_record_get(thermal_dbg, cdev_dbg->durations, state);
|
| 503 |
|
| 504 | debugfs_create_file("trans_table", 0400, thermal_dbg->d_top,
|
| 505 | thermal_dbg, &tt_fops);
|
| 506 |
|
| 507 | debugfs_create_file("time_in_state_ms", 0400, thermal_dbg->d_top,
|
| 508 | thermal_dbg, &dt_fops);
|
| 509 |
|
| 510 | debugfs_create_file("clear", 0200, thermal_dbg->d_top,
|
| 511 | thermal_dbg, &cdev_clear_fops);
|
| 512 |
|
| 513 | debugfs_create_u32("total_trans", 0400, thermal_dbg->d_top,
|
| 514 | &cdev_dbg->total);
|
| 515 |
|
| 516 | cdev->debugfs = thermal_dbg;
|
| 517 | }
|
| 518 |
|
| 519 | static struct thermal_debugfs *thermal_debug_cdev_clear(struct thermal_cooling_device *cdev)
|
| 520 | {
|
| 521 | struct thermal_debugfs *thermal_dbg;
|
| 522 |
|
| 523 | guard(cooling_dev)(cdev);
|
| 524 |
|
| 525 | thermal_dbg = cdev->debugfs;
|
| 526 | if (thermal_dbg)
|
| 527 | cdev->debugfs = NULL;
|
| 528 |
|
| 529 | return thermal_dbg;
|
| 530 | }
|
| 531 |
|
| 532 | |
| 533 | * thermal_debug_cdev_remove - Remove a cooling device debugfs entry
|
| 534 | *
|
| 535 | * Frees the statistics memory data and remove the debugfs entry
|
| 536 | *
|
| 537 | * @cdev: a pointer to a cooling device
|
| 538 |
|
| 539 | void thermal_debug_cdev_remove(struct thermal_cooling_device *cdev)
|
| 540 | {
|
| 541 | struct thermal_debugfs *thermal_dbg;
|
| 542 |
|
| 543 | thermal_dbg = thermal_debug_cdev_clear(cdev);
|
| 544 | if (!thermal_dbg)
|
| 545 | return;
|
| 546 |
|
| 547 | mutex_lock(&thermal_dbg->lock);
|
| 548 |
|
| 549 | thermal_debugfs_cdev_clear(&thermal_dbg->cdev_dbg);
|
| 550 |
|
| 551 | mutex_unlock(&thermal_dbg->lock);
|
| 552 |
|
| 553 | thermal_debugfs_remove_id(thermal_dbg);
|
| 554 | }
|
| 555 |
|
| 556 | static struct tz_episode *thermal_debugfs_tz_event_alloc(struct thermal_zone_device *tz,
|
| 557 | ktime_t now)
|
| 558 | {
|
| 559 | struct tz_episode *tze;
|
| 560 | int i;
|
| 561 |
|
| 562 | tze = kzalloc_flex(*tze, trip_stats, tz->num_trips);
|
| 563 | if (!tze)
|
| 564 | return NULL;
|
| 565 |
|
| 566 | INIT_LIST_HEAD(&tze->node);
|
| 567 | tze->timestamp = now;
|
| 568 | tze->duration = KTIME_MIN;
|
| 569 | tze->max_temp = INT_MIN;
|
| 570 |
|
| 571 | for (i = 0; i < tz->num_trips; i++) {
|
| 572 | tze->trip_stats[i].trip_temp = THERMAL_TEMP_INVALID;
|
| 573 | tze->trip_stats[i].min = INT_MAX;
|
| 574 | }
|
| 575 |
|
| 576 | return tze;
|
| 577 | }
|
| 578 |
|
| 579 | void thermal_debug_tz_trip_up(struct thermal_zone_device *tz,
|
| 580 | const struct thermal_trip *trip)
|
| 581 | {
|
| 582 | struct thermal_debugfs *thermal_dbg = tz->debugfs;
|
| 583 | int trip_id = thermal_zone_trip_id(tz, trip);
|
| 584 | ktime_t now = ktime_get();
|
| 585 | struct trip_stats *trip_stats;
|
| 586 | struct tz_debugfs *tz_dbg;
|
| 587 | struct tz_episode *tze;
|
| 588 |
|
| 589 | if (!thermal_dbg)
|
| 590 | return;
|
| 591 |
|
| 592 | tz_dbg = &thermal_dbg->tz_dbg;
|
| 593 |
|
| 594 | mutex_lock(&thermal_dbg->lock);
|
| 595 |
|
| 596 | |
| 597 | * The mitigation is starting. A mitigation can contain
|
| 598 | * several episodes where each of them is related to a
|
| 599 | * temperature crossing a trip point. The episodes are
|
| 600 | * nested. That means when the temperature is crossing the
|
| 601 | * first trip point, the duration begins to be measured. If
|
| 602 | * the temperature continues to increase and reaches the
|
| 603 | * second trip point, the duration of the first trip must be
|
| 604 | * also accumulated.
|
| 605 | *
|
| 606 | * eg.
|
| 607 | *
|
| 608 | * temp
|
| 609 | * ^
|
| 610 | * | --------
|
| 611 | * trip 2 / \ ------
|
| 612 | * | /| |\ /| |\
|
| 613 | * trip 1 / | | `---- | | \
|
| 614 | * | /| | | | | |\
|
| 615 | * trip 0 / | | | | | | \
|
| 616 | * | /| | | | | | | |\
|
| 617 | * | / | | | | | | | | `--
|
| 618 | * | / | | | | | | | |
|
| 619 | * |----- | | | | | | | |
|
| 620 | * | | | | | | | | |
|
| 621 | * --------|-|-|--------|--------|------|-|-|------------------> time
|
| 622 | * | | |<--t2-->| |<-t2'>| | |
|
| 623 | * | | | |
|
| 624 | * | |<------------t1------------>| |
|
| 625 | * | |
|
| 626 | * |<-------------t0--------------->|
|
| 627 | *
|
| 628 |
|
| 629 | if (!tz_dbg->nr_trips) {
|
| 630 | tze = thermal_debugfs_tz_event_alloc(tz, now);
|
| 631 | if (!tze)
|
| 632 | goto unlock;
|
| 633 |
|
| 634 | list_add(&tze->node, &tz_dbg->tz_episodes);
|
| 635 | }
|
| 636 |
|
| 637 | |
| 638 | * Each time a trip point is crossed the way up, the trip_id
|
| 639 | * is stored in the trip_crossed array and the nr_trips is
|
| 640 | * incremented. A nr_trips equal to zero means we are entering
|
| 641 | * a mitigation episode.
|
| 642 | *
|
| 643 | * The trip ids may not be in the ascending order but the
|
| 644 | * result in the array trips_crossed will be in the ascending
|
| 645 | * temperature order. The function detecting when a trip point
|
| 646 | * is crossed the way down will handle the very rare case when
|
| 647 | * the trip points may have been reordered during this
|
| 648 | * mitigation episode.
|
| 649 |
|
| 650 | tz_dbg->trips_crossed[tz_dbg->nr_trips++] = trip_id;
|
| 651 |
|
| 652 | tze = list_first_entry(&tz_dbg->tz_episodes, struct tz_episode, node);
|
| 653 | trip_stats = &tze->trip_stats[trip_id];
|
| 654 | trip_stats->trip_temp = trip->temperature;
|
| 655 | trip_stats->trip_hyst = trip->hysteresis;
|
| 656 | trip_stats->timestamp = now;
|
| 657 |
|
| 658 | unlock:
|
| 659 | mutex_unlock(&thermal_dbg->lock);
|
| 660 | }
|
| 661 |
|
| 662 | static void tz_episode_close_trip(struct tz_episode *tze, int trip_id, ktime_t now)
|
| 663 | {
|
| 664 | struct trip_stats *trip_stats = &tze->trip_stats[trip_id];
|
| 665 | ktime_t delta = ktime_sub(now, trip_stats->timestamp);
|
| 666 |
|
| 667 | trip_stats->duration = ktime_add(delta, trip_stats->duration);
|
| 668 |
|
| 669 | trip_stats->timestamp = KTIME_MAX;
|
| 670 | }
|
| 671 |
|
| 672 | void thermal_debug_tz_trip_down(struct thermal_zone_device *tz,
|
| 673 | const struct thermal_trip *trip)
|
| 674 | {
|
| 675 | struct thermal_debugfs *thermal_dbg = tz->debugfs;
|
| 676 | int trip_id = thermal_zone_trip_id(tz, trip);
|
| 677 | ktime_t now = ktime_get();
|
| 678 | struct tz_episode *tze;
|
| 679 | struct tz_debugfs *tz_dbg;
|
| 680 | int i;
|
| 681 |
|
| 682 | if (!thermal_dbg)
|
| 683 | return;
|
| 684 |
|
| 685 | tz_dbg = &thermal_dbg->tz_dbg;
|
| 686 |
|
| 687 | mutex_lock(&thermal_dbg->lock);
|
| 688 |
|
| 689 | |
| 690 | * The temperature crosses the way down but there was not
|
| 691 | * mitigation detected before. That may happen when the
|
| 692 | * temperature is greater than a trip point when registering a
|
| 693 | * thermal zone, which is a common use case as the kernel has
|
| 694 | * no mitigation mechanism yet at boot time.
|
| 695 |
|
| 696 | if (!tz_dbg->nr_trips)
|
| 697 | goto out;
|
| 698 |
|
| 699 | for (i = tz_dbg->nr_trips - 1; i >= 0; i--) {
|
| 700 | if (tz_dbg->trips_crossed[i] == trip_id)
|
| 701 | break;
|
| 702 | }
|
| 703 |
|
| 704 | if (i < 0)
|
| 705 | goto out;
|
| 706 |
|
| 707 | tz_dbg->nr_trips--;
|
| 708 |
|
| 709 | if (i < tz_dbg->nr_trips)
|
| 710 | tz_dbg->trips_crossed[i] = tz_dbg->trips_crossed[tz_dbg->nr_trips];
|
| 711 |
|
| 712 | tze = list_first_entry(&tz_dbg->tz_episodes, struct tz_episode, node);
|
| 713 |
|
| 714 | tz_episode_close_trip(tze, trip_id, now);
|
| 715 |
|
| 716 | |
| 717 | * This event closes the mitigation as we are crossing the
|
| 718 | * last trip point the way down.
|
| 719 |
|
| 720 | if (!tz_dbg->nr_trips)
|
| 721 | tze->duration = ktime_sub(now, tze->timestamp);
|
| 722 |
|
| 723 | out:
|
| 724 | mutex_unlock(&thermal_dbg->lock);
|
| 725 | }
|
| 726 |
|
| 727 | void thermal_debug_update_trip_stats(struct thermal_zone_device *tz)
|
| 728 | {
|
| 729 | struct thermal_debugfs *thermal_dbg = tz->debugfs;
|
| 730 | struct tz_debugfs *tz_dbg;
|
| 731 | struct tz_episode *tze;
|
| 732 | int i;
|
| 733 |
|
| 734 | if (!thermal_dbg)
|
| 735 | return;
|
| 736 |
|
| 737 | tz_dbg = &thermal_dbg->tz_dbg;
|
| 738 |
|
| 739 | mutex_lock(&thermal_dbg->lock);
|
| 740 |
|
| 741 | if (!tz_dbg->nr_trips)
|
| 742 | goto out;
|
| 743 |
|
| 744 | tze = list_first_entry(&tz_dbg->tz_episodes, struct tz_episode, node);
|
| 745 |
|
| 746 | if (tz->temperature > tze->max_temp)
|
| 747 | tze->max_temp = tz->temperature;
|
| 748 |
|
| 749 | for (i = 0; i < tz_dbg->nr_trips; i++) {
|
| 750 | int trip_id = tz_dbg->trips_crossed[i];
|
| 751 | struct trip_stats *trip_stats = &tze->trip_stats[trip_id];
|
| 752 |
|
| 753 | trip_stats->min = min(trip_stats->min, tz->temperature);
|
| 754 | trip_stats->avg += (tz->temperature - trip_stats->avg) /
|
| 755 | ++trip_stats->count;
|
| 756 | }
|
| 757 | out:
|
| 758 | mutex_unlock(&thermal_dbg->lock);
|
| 759 | }
|
| 760 |
|
| 761 | static void *tze_seq_start(struct seq_file *s, loff_t *pos)
|
| 762 | {
|
| 763 | struct thermal_debugfs *thermal_dbg = s->private;
|
| 764 | struct tz_debugfs *tz_dbg = &thermal_dbg->tz_dbg;
|
| 765 |
|
| 766 | mutex_lock(&thermal_dbg->lock);
|
| 767 |
|
| 768 | return seq_list_start(&tz_dbg->tz_episodes, *pos);
|
| 769 | }
|
| 770 |
|
| 771 | static void *tze_seq_next(struct seq_file *s, void *v, loff_t *pos)
|
| 772 | {
|
| 773 | struct thermal_debugfs *thermal_dbg = s->private;
|
| 774 | struct tz_debugfs *tz_dbg = &thermal_dbg->tz_dbg;
|
| 775 |
|
| 776 | return seq_list_next(v, &tz_dbg->tz_episodes, pos);
|
| 777 | }
|
| 778 |
|
| 779 | static void tze_seq_stop(struct seq_file *s, void *v)
|
| 780 | {
|
| 781 | struct thermal_debugfs *thermal_dbg = s->private;
|
| 782 |
|
| 783 | mutex_unlock(&thermal_dbg->lock);
|
| 784 | }
|
| 785 |
|
| 786 | static int tze_seq_show(struct seq_file *s, void *v)
|
| 787 | {
|
| 788 | struct thermal_debugfs *thermal_dbg = s->private;
|
| 789 | struct thermal_zone_device *tz = thermal_dbg->tz_dbg.tz;
|
| 790 | struct thermal_trip_desc *td;
|
| 791 | struct tz_episode *tze;
|
| 792 | u64 duration_ms;
|
| 793 | int trip_id;
|
| 794 | char c;
|
| 795 |
|
| 796 | tze = list_entry((struct list_head *)v, struct tz_episode, node);
|
| 797 |
|
| 798 | if (tze->duration == KTIME_MIN) {
|
| 799 |
|
| 800 | duration_ms = ktime_to_ms(ktime_sub(ktime_get(), tze->timestamp));
|
| 801 | c = '>';
|
| 802 | } else {
|
| 803 | duration_ms = ktime_to_ms(tze->duration);
|
| 804 | c = '=';
|
| 805 | }
|
| 806 |
|
| 807 | seq_printf(s, ",-Mitigation at %llums, duration%c%llums, max. temp=%dm°C\n",
|
| 808 | ktime_to_ms(tze->timestamp), c, duration_ms, tze->max_temp);
|
| 809 |
|
| 810 | seq_puts(s, "| trip | type | temp(m°C) | hyst(m°C) | duration(ms) | avg(m°C) | min(m°C) |\n");
|
| 811 |
|
| 812 | for_each_trip_desc(tz, td) {
|
| 813 | const struct thermal_trip *trip = &td->trip;
|
| 814 | struct trip_stats *trip_stats;
|
| 815 |
|
| 816 | |
| 817 | * There is no possible mitigation happening at the
|
| 818 | * critical trip point, so the stats will be always
|
| 819 | * zero, skip this trip point
|
| 820 |
|
| 821 | if (trip->type == THERMAL_TRIP_CRITICAL)
|
| 822 | continue;
|
| 823 |
|
| 824 | trip_id = thermal_zone_trip_id(tz, trip);
|
| 825 | trip_stats = &tze->trip_stats[trip_id];
|
| 826 |
|
| 827 |
|
| 828 | if (trip_stats->trip_temp == THERMAL_TEMP_INVALID)
|
| 829 | continue;
|
| 830 |
|
| 831 | if (trip_stats->timestamp != KTIME_MAX) {
|
| 832 |
|
| 833 | ktime_t delta = ktime_sub(ktime_get(),
|
| 834 | trip_stats->timestamp);
|
| 835 |
|
| 836 | delta = ktime_add(delta, trip_stats->duration);
|
| 837 | duration_ms = ktime_to_ms(delta);
|
| 838 | c = '>';
|
| 839 | } else {
|
| 840 | duration_ms = ktime_to_ms(trip_stats->duration);
|
| 841 | c = ' ';
|
| 842 | }
|
| 843 |
|
| 844 | seq_printf(s, "| %*d | %*s | %*d | %*d | %c%*lld | %*d | %*d |\n",
|
| 845 | 4 , trip_id,
|
| 846 | 8, thermal_trip_type_name(trip->type),
|
| 847 | 9, trip_stats->trip_temp,
|
| 848 | 9, trip_stats->trip_hyst,
|
| 849 | c, 11, duration_ms,
|
| 850 | 9, trip_stats->avg,
|
| 851 | 9, trip_stats->min);
|
| 852 | }
|
| 853 |
|
| 854 | return 0;
|
| 855 | }
|
| 856 |
|
| 857 | static const struct seq_operations tze_sops = {
|
| 858 | .start = tze_seq_start,
|
| 859 | .next = tze_seq_next,
|
| 860 | .stop = tze_seq_stop,
|
| 861 | .show = tze_seq_show,
|
| 862 | };
|
| 863 |
|
| 864 | DEFINE_SEQ_ATTRIBUTE(tze);
|
| 865 |
|
| 866 | void thermal_debug_tz_add(struct thermal_zone_device *tz)
|
| 867 | {
|
| 868 | struct thermal_debugfs *thermal_dbg;
|
| 869 | struct tz_debugfs *tz_dbg;
|
| 870 |
|
| 871 | thermal_dbg = thermal_debugfs_add_id(d_tz, tz->id);
|
| 872 | if (!thermal_dbg)
|
| 873 | return;
|
| 874 |
|
| 875 | tz_dbg = &thermal_dbg->tz_dbg;
|
| 876 |
|
| 877 | tz_dbg->tz = tz;
|
| 878 |
|
| 879 | tz_dbg->trips_crossed = kzalloc_objs(int, tz->num_trips);
|
| 880 | if (!tz_dbg->trips_crossed) {
|
| 881 | thermal_debugfs_remove_id(thermal_dbg);
|
| 882 | return;
|
| 883 | }
|
| 884 |
|
| 885 | INIT_LIST_HEAD(&tz_dbg->tz_episodes);
|
| 886 |
|
| 887 | debugfs_create_file("mitigations", 0400, thermal_dbg->d_top,
|
| 888 | thermal_dbg, &tze_fops);
|
| 889 |
|
| 890 | tz->debugfs = thermal_dbg;
|
| 891 | }
|
| 892 |
|
| 893 | static struct thermal_debugfs *thermal_debug_tz_clear(struct thermal_zone_device *tz)
|
| 894 | {
|
| 895 | struct thermal_debugfs *thermal_dbg;
|
| 896 |
|
| 897 | guard(thermal_zone)(tz);
|
| 898 |
|
| 899 | thermal_dbg = tz->debugfs;
|
| 900 | if (thermal_dbg)
|
| 901 | tz->debugfs = NULL;
|
| 902 |
|
| 903 | return thermal_dbg;
|
| 904 | }
|
| 905 |
|
| 906 | void thermal_debug_tz_remove(struct thermal_zone_device *tz)
|
| 907 | {
|
| 908 | struct thermal_debugfs *thermal_dbg;
|
| 909 | struct tz_episode *tze, *tmp;
|
| 910 | struct tz_debugfs *tz_dbg;
|
| 911 | int *trips_crossed;
|
| 912 |
|
| 913 | thermal_dbg = thermal_debug_tz_clear(tz);
|
| 914 | if (!thermal_dbg)
|
| 915 | return;
|
| 916 |
|
| 917 | tz_dbg = &thermal_dbg->tz_dbg;
|
| 918 |
|
| 919 | mutex_lock(&thermal_dbg->lock);
|
| 920 |
|
| 921 | trips_crossed = tz_dbg->trips_crossed;
|
| 922 |
|
| 923 | list_for_each_entry_safe(tze, tmp, &tz_dbg->tz_episodes, node) {
|
| 924 | list_del(&tze->node);
|
| 925 | kfree(tze);
|
| 926 | }
|
| 927 |
|
| 928 | mutex_unlock(&thermal_dbg->lock);
|
| 929 |
|
| 930 | thermal_debugfs_remove_id(thermal_dbg);
|
| 931 | kfree(trips_crossed);
|
| 932 | }
|
| 933 |
|
| 934 | void thermal_debug_tz_resume(struct thermal_zone_device *tz)
|
| 935 | {
|
| 936 | struct thermal_debugfs *thermal_dbg = tz->debugfs;
|
| 937 | ktime_t now = ktime_get();
|
| 938 | struct tz_debugfs *tz_dbg;
|
| 939 | struct tz_episode *tze;
|
| 940 | int i;
|
| 941 |
|
| 942 | if (!thermal_dbg)
|
| 943 | return;
|
| 944 |
|
| 945 | mutex_lock(&thermal_dbg->lock);
|
| 946 |
|
| 947 | tz_dbg = &thermal_dbg->tz_dbg;
|
| 948 |
|
| 949 | if (!tz_dbg->nr_trips)
|
| 950 | goto out;
|
| 951 |
|
| 952 | |
| 953 | * A mitigation episode was in progress before the preceding system
|
| 954 | * suspend transition, so close it because the zone handling is starting
|
| 955 | * over from scratch.
|
| 956 |
|
| 957 | tze = list_first_entry(&tz_dbg->tz_episodes, struct tz_episode, node);
|
| 958 |
|
| 959 | for (i = 0; i < tz_dbg->nr_trips; i++)
|
| 960 | tz_episode_close_trip(tze, tz_dbg->trips_crossed[i], now);
|
| 961 |
|
| 962 | tze->duration = ktime_sub(now, tze->timestamp);
|
| 963 |
|
| 964 | tz_dbg->nr_trips = 0;
|
| 965 |
|
| 966 | out:
|
| 967 | mutex_unlock(&thermal_dbg->lock);
|
| 968 | }
|