1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Copyright 2023 Linaro Limited |
4 | * |
5 | * Author: Daniel Lezcano <daniel.lezcano@linaro.org> |
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 | * @count: the number of times the zone temperature was above the trip point |
95 | * @max: maximum recorded temperature above the trip point |
96 | * @min: minimum recorded temperature above the trip point |
97 | * @avg: average temperature above the trip point |
98 | */ |
99 | struct trip_stats { |
100 | ktime_t timestamp; |
101 | ktime_t duration; |
102 | int count; |
103 | int max; |
104 | int min; |
105 | int avg; |
106 | }; |
107 | |
108 | /** |
109 | * struct tz_episode - A mitigation episode information |
110 | * |
111 | * The tz_episode structure describes a mitigation episode. A |
112 | * mitigation episode begins the trip point with the lower temperature |
113 | * is crossed the way up and ends when it is crossed the way |
114 | * down. During this episode we can have multiple trip points crossed |
115 | * the way up and down if there are multiple trip described in the |
116 | * firmware after the lowest temperature trip point. |
117 | * |
118 | * @timestamp: first trip point crossed the way up |
119 | * @duration: total duration of the mitigation episode |
120 | * @node: a list element to be added to the list of tz events |
121 | * @trip_stats: per trip point statistics, flexible array |
122 | */ |
123 | struct tz_episode { |
124 | ktime_t timestamp; |
125 | ktime_t duration; |
126 | struct list_head node; |
127 | struct trip_stats trip_stats[]; |
128 | }; |
129 | |
130 | /** |
131 | * struct tz_debugfs - Store all mitigation episodes for a thermal zone |
132 | * |
133 | * The tz_debugfs structure contains the list of the mitigation |
134 | * episodes and has to track which trip point has been crossed in |
135 | * order to handle correctly nested trip point mitigation episodes. |
136 | * |
137 | * We keep the history of the trip point crossed in an array and as we |
138 | * can go back and forth inside this history, eg. trip 0,1,2,1,2,1,0, |
139 | * we keep track of the current position in the history array. |
140 | * |
141 | * @tz_episodes: a list of thermal mitigation episodes |
142 | * @trips_crossed: an array of trip points crossed by id |
143 | * @nr_trips: the number of trip points currently being crossed |
144 | */ |
145 | struct tz_debugfs { |
146 | struct list_head tz_episodes; |
147 | int *trips_crossed; |
148 | int nr_trips; |
149 | }; |
150 | |
151 | /** |
152 | * struct thermal_debugfs - High level structure for a thermal object in debugfs |
153 | * |
154 | * The thermal_debugfs structure is the common structure used by the |
155 | * cooling device or the thermal zone to store the statistics. |
156 | * |
157 | * @d_top: top directory of the thermal object directory |
158 | * @lock: per object lock to protect the internals |
159 | * |
160 | * @cdev_dbg: a cooling device debug structure |
161 | * @tz_dbg: a thermal zone debug structure |
162 | */ |
163 | struct thermal_debugfs { |
164 | struct dentry *d_top; |
165 | struct mutex lock; |
166 | union { |
167 | struct cdev_debugfs cdev_dbg; |
168 | struct tz_debugfs tz_dbg; |
169 | }; |
170 | }; |
171 | |
172 | void thermal_debug_init(void) |
173 | { |
174 | d_root = debugfs_create_dir(name: "thermal" , NULL); |
175 | if (!d_root) |
176 | return; |
177 | |
178 | d_cdev = debugfs_create_dir(name: "cooling_devices" , parent: d_root); |
179 | if (!d_cdev) |
180 | return; |
181 | |
182 | d_tz = debugfs_create_dir(name: "thermal_zones" , parent: d_root); |
183 | } |
184 | |
185 | static struct thermal_debugfs *thermal_debugfs_add_id(struct dentry *d, int id) |
186 | { |
187 | struct thermal_debugfs *thermal_dbg; |
188 | char ids[IDSLENGTH]; |
189 | |
190 | thermal_dbg = kzalloc(size: sizeof(*thermal_dbg), GFP_KERNEL); |
191 | if (!thermal_dbg) |
192 | return NULL; |
193 | |
194 | mutex_init(&thermal_dbg->lock); |
195 | |
196 | snprintf(buf: ids, IDSLENGTH, fmt: "%d" , id); |
197 | |
198 | thermal_dbg->d_top = debugfs_create_dir(name: ids, parent: d); |
199 | if (!thermal_dbg->d_top) { |
200 | kfree(objp: thermal_dbg); |
201 | return NULL; |
202 | } |
203 | |
204 | return thermal_dbg; |
205 | } |
206 | |
207 | static void thermal_debugfs_remove_id(struct thermal_debugfs *thermal_dbg) |
208 | { |
209 | if (!thermal_dbg) |
210 | return; |
211 | |
212 | debugfs_remove(dentry: thermal_dbg->d_top); |
213 | |
214 | kfree(objp: thermal_dbg); |
215 | } |
216 | |
217 | static struct cdev_record * |
218 | thermal_debugfs_cdev_record_alloc(struct thermal_debugfs *thermal_dbg, |
219 | struct list_head *lists, int id) |
220 | { |
221 | struct cdev_record *cdev_record; |
222 | |
223 | cdev_record = kzalloc(size: sizeof(*cdev_record), GFP_KERNEL); |
224 | if (!cdev_record) |
225 | return NULL; |
226 | |
227 | cdev_record->id = id; |
228 | INIT_LIST_HEAD(list: &cdev_record->node); |
229 | list_add_tail(new: &cdev_record->node, |
230 | head: &lists[cdev_record->id % CDEVSTATS_HASH_SIZE]); |
231 | |
232 | return cdev_record; |
233 | } |
234 | |
235 | static struct cdev_record * |
236 | thermal_debugfs_cdev_record_find(struct thermal_debugfs *thermal_dbg, |
237 | struct list_head *lists, int id) |
238 | { |
239 | struct cdev_record *entry; |
240 | |
241 | list_for_each_entry(entry, &lists[id % CDEVSTATS_HASH_SIZE], node) |
242 | if (entry->id == id) |
243 | return entry; |
244 | |
245 | return NULL; |
246 | } |
247 | |
248 | static struct cdev_record * |
249 | thermal_debugfs_cdev_record_get(struct thermal_debugfs *thermal_dbg, |
250 | struct list_head *lists, int id) |
251 | { |
252 | struct cdev_record *cdev_record; |
253 | |
254 | cdev_record = thermal_debugfs_cdev_record_find(thermal_dbg, lists, id); |
255 | if (cdev_record) |
256 | return cdev_record; |
257 | |
258 | return thermal_debugfs_cdev_record_alloc(thermal_dbg, lists, id); |
259 | } |
260 | |
261 | static void thermal_debugfs_cdev_clear(struct cdev_debugfs *cdev_dbg) |
262 | { |
263 | int i; |
264 | struct cdev_record *entry, *tmp; |
265 | |
266 | for (i = 0; i < CDEVSTATS_HASH_SIZE; i++) { |
267 | |
268 | list_for_each_entry_safe(entry, tmp, |
269 | &cdev_dbg->transitions[i], node) { |
270 | list_del(entry: &entry->node); |
271 | kfree(objp: entry); |
272 | } |
273 | |
274 | list_for_each_entry_safe(entry, tmp, |
275 | &cdev_dbg->durations[i], node) { |
276 | list_del(entry: &entry->node); |
277 | kfree(objp: entry); |
278 | } |
279 | } |
280 | |
281 | cdev_dbg->total = 0; |
282 | } |
283 | |
284 | static void *cdev_seq_start(struct seq_file *s, loff_t *pos) |
285 | { |
286 | struct thermal_debugfs *thermal_dbg = s->private; |
287 | |
288 | mutex_lock(&thermal_dbg->lock); |
289 | |
290 | return (*pos < CDEVSTATS_HASH_SIZE) ? pos : NULL; |
291 | } |
292 | |
293 | static void *cdev_seq_next(struct seq_file *s, void *v, loff_t *pos) |
294 | { |
295 | (*pos)++; |
296 | |
297 | return (*pos < CDEVSTATS_HASH_SIZE) ? pos : NULL; |
298 | } |
299 | |
300 | static void cdev_seq_stop(struct seq_file *s, void *v) |
301 | { |
302 | struct thermal_debugfs *thermal_dbg = s->private; |
303 | |
304 | mutex_unlock(lock: &thermal_dbg->lock); |
305 | } |
306 | |
307 | static int cdev_tt_seq_show(struct seq_file *s, void *v) |
308 | { |
309 | struct thermal_debugfs *thermal_dbg = s->private; |
310 | struct cdev_debugfs *cdev_dbg = &thermal_dbg->cdev_dbg; |
311 | struct list_head *transitions = cdev_dbg->transitions; |
312 | struct cdev_record *entry; |
313 | int i = *(loff_t *)v; |
314 | |
315 | if (!i) |
316 | seq_puts(m: s, s: "Transition\tOccurences\n" ); |
317 | |
318 | list_for_each_entry(entry, &transitions[i], node) { |
319 | /* |
320 | * Assuming maximum cdev states is 1024, the longer |
321 | * string for a transition would be "1024->1024\0" |
322 | */ |
323 | char buffer[11]; |
324 | |
325 | snprintf(buf: buffer, ARRAY_SIZE(buffer), fmt: "%d->%d" , |
326 | entry->id >> 16, entry->id & 0xFFFF); |
327 | |
328 | seq_printf(m: s, fmt: "%-10s\t%-10llu\n" , buffer, entry->count); |
329 | } |
330 | |
331 | return 0; |
332 | } |
333 | |
334 | static const struct seq_operations tt_sops = { |
335 | .start = cdev_seq_start, |
336 | .next = cdev_seq_next, |
337 | .stop = cdev_seq_stop, |
338 | .show = cdev_tt_seq_show, |
339 | }; |
340 | |
341 | DEFINE_SEQ_ATTRIBUTE(tt); |
342 | |
343 | static int cdev_dt_seq_show(struct seq_file *s, void *v) |
344 | { |
345 | struct thermal_debugfs *thermal_dbg = s->private; |
346 | struct cdev_debugfs *cdev_dbg = &thermal_dbg->cdev_dbg; |
347 | struct list_head *durations = cdev_dbg->durations; |
348 | struct cdev_record *entry; |
349 | int i = *(loff_t *)v; |
350 | |
351 | if (!i) |
352 | seq_puts(m: s, s: "State\tResidency\n" ); |
353 | |
354 | list_for_each_entry(entry, &durations[i], node) { |
355 | s64 duration = ktime_to_ms(kt: entry->residency); |
356 | |
357 | if (entry->id == cdev_dbg->current_state) |
358 | duration += ktime_ms_delta(later: ktime_get(), |
359 | earlier: cdev_dbg->timestamp); |
360 | |
361 | seq_printf(m: s, fmt: "%-5d\t%-10llu\n" , entry->id, duration); |
362 | } |
363 | |
364 | return 0; |
365 | } |
366 | |
367 | static const struct seq_operations dt_sops = { |
368 | .start = cdev_seq_start, |
369 | .next = cdev_seq_next, |
370 | .stop = cdev_seq_stop, |
371 | .show = cdev_dt_seq_show, |
372 | }; |
373 | |
374 | DEFINE_SEQ_ATTRIBUTE(dt); |
375 | |
376 | static int cdev_clear_set(void *data, u64 val) |
377 | { |
378 | struct thermal_debugfs *thermal_dbg = data; |
379 | |
380 | if (!val) |
381 | return -EINVAL; |
382 | |
383 | mutex_lock(&thermal_dbg->lock); |
384 | |
385 | thermal_debugfs_cdev_clear(cdev_dbg: &thermal_dbg->cdev_dbg); |
386 | |
387 | mutex_unlock(lock: &thermal_dbg->lock); |
388 | |
389 | return 0; |
390 | } |
391 | |
392 | DEFINE_DEBUGFS_ATTRIBUTE(cdev_clear_fops, NULL, cdev_clear_set, "%llu\n" ); |
393 | |
394 | /** |
395 | * thermal_debug_cdev_state_update - Update a cooling device state change |
396 | * |
397 | * Computes a transition and the duration of the previous state residency. |
398 | * |
399 | * @cdev : a pointer to a cooling device |
400 | * @new_state: an integer corresponding to the new cooling device state |
401 | */ |
402 | void thermal_debug_cdev_state_update(const struct thermal_cooling_device *cdev, |
403 | int new_state) |
404 | { |
405 | struct thermal_debugfs *thermal_dbg = cdev->debugfs; |
406 | struct cdev_debugfs *cdev_dbg; |
407 | struct cdev_record *cdev_record; |
408 | int transition, old_state; |
409 | |
410 | if (!thermal_dbg || (thermal_dbg->cdev_dbg.current_state == new_state)) |
411 | return; |
412 | |
413 | mutex_lock(&thermal_dbg->lock); |
414 | |
415 | cdev_dbg = &thermal_dbg->cdev_dbg; |
416 | |
417 | old_state = cdev_dbg->current_state; |
418 | |
419 | /* |
420 | * Get the old state information in the durations list. If |
421 | * this one does not exist, a new allocated one will be |
422 | * returned. Recompute the total duration in the old state and |
423 | * get a new timestamp for the new state. |
424 | */ |
425 | cdev_record = thermal_debugfs_cdev_record_get(thermal_dbg, |
426 | lists: cdev_dbg->durations, |
427 | id: old_state); |
428 | if (cdev_record) { |
429 | ktime_t now = ktime_get(); |
430 | ktime_t delta = ktime_sub(now, cdev_dbg->timestamp); |
431 | cdev_record->residency = ktime_add(cdev_record->residency, delta); |
432 | cdev_dbg->timestamp = now; |
433 | } |
434 | |
435 | cdev_dbg->current_state = new_state; |
436 | transition = (old_state << 16) | new_state; |
437 | |
438 | /* |
439 | * Get the transition in the transitions list. If this one |
440 | * does not exist, a new allocated one will be returned. |
441 | * Increment the occurrence of this transition which is stored |
442 | * in the value field. |
443 | */ |
444 | cdev_record = thermal_debugfs_cdev_record_get(thermal_dbg, |
445 | lists: cdev_dbg->transitions, |
446 | id: transition); |
447 | if (cdev_record) |
448 | cdev_record->count++; |
449 | |
450 | cdev_dbg->total++; |
451 | |
452 | mutex_unlock(lock: &thermal_dbg->lock); |
453 | } |
454 | |
455 | /** |
456 | * thermal_debug_cdev_add - Add a cooling device debugfs entry |
457 | * |
458 | * Allocates a cooling device object for debug, initializes the |
459 | * statistics and create the entries in sysfs. |
460 | * @cdev: a pointer to a cooling device |
461 | */ |
462 | void thermal_debug_cdev_add(struct thermal_cooling_device *cdev) |
463 | { |
464 | struct thermal_debugfs *thermal_dbg; |
465 | struct cdev_debugfs *cdev_dbg; |
466 | int i; |
467 | |
468 | thermal_dbg = thermal_debugfs_add_id(d: d_cdev, id: cdev->id); |
469 | if (!thermal_dbg) |
470 | return; |
471 | |
472 | cdev_dbg = &thermal_dbg->cdev_dbg; |
473 | |
474 | for (i = 0; i < CDEVSTATS_HASH_SIZE; i++) { |
475 | INIT_LIST_HEAD(list: &cdev_dbg->transitions[i]); |
476 | INIT_LIST_HEAD(list: &cdev_dbg->durations[i]); |
477 | } |
478 | |
479 | cdev_dbg->current_state = 0; |
480 | cdev_dbg->timestamp = ktime_get(); |
481 | |
482 | debugfs_create_file(name: "trans_table" , mode: 0400, parent: thermal_dbg->d_top, |
483 | data: thermal_dbg, fops: &tt_fops); |
484 | |
485 | debugfs_create_file(name: "time_in_state_ms" , mode: 0400, parent: thermal_dbg->d_top, |
486 | data: thermal_dbg, fops: &dt_fops); |
487 | |
488 | debugfs_create_file(name: "clear" , mode: 0200, parent: thermal_dbg->d_top, |
489 | data: thermal_dbg, fops: &cdev_clear_fops); |
490 | |
491 | debugfs_create_u32(name: "total_trans" , mode: 0400, parent: thermal_dbg->d_top, |
492 | value: &cdev_dbg->total); |
493 | |
494 | cdev->debugfs = thermal_dbg; |
495 | } |
496 | |
497 | /** |
498 | * thermal_debug_cdev_remove - Remove a cooling device debugfs entry |
499 | * |
500 | * Frees the statistics memory data and remove the debugfs entry |
501 | * |
502 | * @cdev: a pointer to a cooling device |
503 | */ |
504 | void thermal_debug_cdev_remove(struct thermal_cooling_device *cdev) |
505 | { |
506 | struct thermal_debugfs *thermal_dbg = cdev->debugfs; |
507 | |
508 | if (!thermal_dbg) |
509 | return; |
510 | |
511 | mutex_lock(&thermal_dbg->lock); |
512 | |
513 | thermal_debugfs_cdev_clear(cdev_dbg: &thermal_dbg->cdev_dbg); |
514 | cdev->debugfs = NULL; |
515 | |
516 | mutex_unlock(lock: &thermal_dbg->lock); |
517 | |
518 | thermal_debugfs_remove_id(thermal_dbg); |
519 | } |
520 | |
521 | static struct tz_episode *thermal_debugfs_tz_event_alloc(struct thermal_zone_device *tz, |
522 | ktime_t now) |
523 | { |
524 | struct tz_episode *tze; |
525 | int i; |
526 | |
527 | tze = kzalloc(struct_size(tze, trip_stats, tz->num_trips), GFP_KERNEL); |
528 | if (!tze) |
529 | return NULL; |
530 | |
531 | INIT_LIST_HEAD(list: &tze->node); |
532 | tze->timestamp = now; |
533 | |
534 | for (i = 0; i < tz->num_trips; i++) { |
535 | tze->trip_stats[i].min = INT_MAX; |
536 | tze->trip_stats[i].max = INT_MIN; |
537 | } |
538 | |
539 | return tze; |
540 | } |
541 | |
542 | void thermal_debug_tz_trip_up(struct thermal_zone_device *tz, |
543 | const struct thermal_trip *trip) |
544 | { |
545 | struct tz_episode *tze; |
546 | struct tz_debugfs *tz_dbg; |
547 | struct thermal_debugfs *thermal_dbg = tz->debugfs; |
548 | int temperature = tz->temperature; |
549 | int trip_id = thermal_zone_trip_id(tz, trip); |
550 | ktime_t now = ktime_get(); |
551 | |
552 | if (!thermal_dbg) |
553 | return; |
554 | |
555 | mutex_lock(&thermal_dbg->lock); |
556 | |
557 | tz_dbg = &thermal_dbg->tz_dbg; |
558 | |
559 | /* |
560 | * The mitigation is starting. A mitigation can contain |
561 | * several episodes where each of them is related to a |
562 | * temperature crossing a trip point. The episodes are |
563 | * nested. That means when the temperature is crossing the |
564 | * first trip point, the duration begins to be measured. If |
565 | * the temperature continues to increase and reaches the |
566 | * second trip point, the duration of the first trip must be |
567 | * also accumulated. |
568 | * |
569 | * eg. |
570 | * |
571 | * temp |
572 | * ^ |
573 | * | -------- |
574 | * trip 2 / \ ------ |
575 | * | /| |\ /| |\ |
576 | * trip 1 / | | `---- | | \ |
577 | * | /| | | | | |\ |
578 | * trip 0 / | | | | | | \ |
579 | * | /| | | | | | | |\ |
580 | * | / | | | | | | | | `-- |
581 | * | / | | | | | | | | |
582 | * |----- | | | | | | | | |
583 | * | | | | | | | | | |
584 | * --------|-|-|--------|--------|------|-|-|------------------> time |
585 | * | | |<--t2-->| |<-t2'>| | | |
586 | * | | | | |
587 | * | |<------------t1------------>| | |
588 | * | | |
589 | * |<-------------t0--------------->| |
590 | * |
591 | */ |
592 | if (!tz_dbg->nr_trips) { |
593 | tze = thermal_debugfs_tz_event_alloc(tz, now); |
594 | if (!tze) |
595 | goto unlock; |
596 | |
597 | list_add(new: &tze->node, head: &tz_dbg->tz_episodes); |
598 | } |
599 | |
600 | /* |
601 | * Each time a trip point is crossed the way up, the trip_id |
602 | * is stored in the trip_crossed array and the nr_trips is |
603 | * incremented. A nr_trips equal to zero means we are entering |
604 | * a mitigation episode. |
605 | * |
606 | * The trip ids may not be in the ascending order but the |
607 | * result in the array trips_crossed will be in the ascending |
608 | * temperature order. The function detecting when a trip point |
609 | * is crossed the way down will handle the very rare case when |
610 | * the trip points may have been reordered during this |
611 | * mitigation episode. |
612 | */ |
613 | tz_dbg->trips_crossed[tz_dbg->nr_trips++] = trip_id; |
614 | |
615 | tze = list_first_entry(&tz_dbg->tz_episodes, struct tz_episode, node); |
616 | tze->trip_stats[trip_id].timestamp = now; |
617 | tze->trip_stats[trip_id].max = max(tze->trip_stats[trip_id].max, temperature); |
618 | tze->trip_stats[trip_id].min = min(tze->trip_stats[trip_id].min, temperature); |
619 | tze->trip_stats[trip_id].count++; |
620 | tze->trip_stats[trip_id].avg = tze->trip_stats[trip_id].avg + |
621 | (temperature - tze->trip_stats[trip_id].avg) / |
622 | tze->trip_stats[trip_id].count; |
623 | |
624 | unlock: |
625 | mutex_unlock(lock: &thermal_dbg->lock); |
626 | } |
627 | |
628 | void thermal_debug_tz_trip_down(struct thermal_zone_device *tz, |
629 | const struct thermal_trip *trip) |
630 | { |
631 | struct thermal_debugfs *thermal_dbg = tz->debugfs; |
632 | struct tz_episode *tze; |
633 | struct tz_debugfs *tz_dbg; |
634 | ktime_t delta, now = ktime_get(); |
635 | int trip_id = thermal_zone_trip_id(tz, trip); |
636 | int i; |
637 | |
638 | if (!thermal_dbg) |
639 | return; |
640 | |
641 | mutex_lock(&thermal_dbg->lock); |
642 | |
643 | tz_dbg = &thermal_dbg->tz_dbg; |
644 | |
645 | /* |
646 | * The temperature crosses the way down but there was not |
647 | * mitigation detected before. That may happen when the |
648 | * temperature is greater than a trip point when registering a |
649 | * thermal zone, which is a common use case as the kernel has |
650 | * no mitigation mechanism yet at boot time. |
651 | */ |
652 | if (!tz_dbg->nr_trips) |
653 | goto out; |
654 | |
655 | for (i = tz_dbg->nr_trips - 1; i >= 0; i--) { |
656 | if (tz_dbg->trips_crossed[i] == trip_id) |
657 | break; |
658 | } |
659 | |
660 | if (i < 0) |
661 | goto out; |
662 | |
663 | tz_dbg->nr_trips--; |
664 | |
665 | if (i < tz_dbg->nr_trips) |
666 | tz_dbg->trips_crossed[i] = tz_dbg->trips_crossed[tz_dbg->nr_trips]; |
667 | |
668 | tze = list_first_entry(&tz_dbg->tz_episodes, struct tz_episode, node); |
669 | |
670 | delta = ktime_sub(now, tze->trip_stats[trip_id].timestamp); |
671 | |
672 | tze->trip_stats[trip_id].duration = |
673 | ktime_add(delta, tze->trip_stats[trip_id].duration); |
674 | |
675 | /* |
676 | * This event closes the mitigation as we are crossing the |
677 | * last trip point the way down. |
678 | */ |
679 | if (!tz_dbg->nr_trips) |
680 | tze->duration = ktime_sub(now, tze->timestamp); |
681 | |
682 | out: |
683 | mutex_unlock(lock: &thermal_dbg->lock); |
684 | } |
685 | |
686 | void thermal_debug_update_temp(struct thermal_zone_device *tz) |
687 | { |
688 | struct thermal_debugfs *thermal_dbg = tz->debugfs; |
689 | struct tz_episode *tze; |
690 | struct tz_debugfs *tz_dbg; |
691 | int trip_id, i; |
692 | |
693 | if (!thermal_dbg) |
694 | return; |
695 | |
696 | mutex_lock(&thermal_dbg->lock); |
697 | |
698 | tz_dbg = &thermal_dbg->tz_dbg; |
699 | |
700 | if (!tz_dbg->nr_trips) |
701 | goto out; |
702 | |
703 | for (i = 0; i < tz_dbg->nr_trips; i++) { |
704 | trip_id = tz_dbg->trips_crossed[i]; |
705 | tze = list_first_entry(&tz_dbg->tz_episodes, struct tz_episode, node); |
706 | tze->trip_stats[trip_id].count++; |
707 | tze->trip_stats[trip_id].max = max(tze->trip_stats[trip_id].max, tz->temperature); |
708 | tze->trip_stats[trip_id].min = min(tze->trip_stats[trip_id].min, tz->temperature); |
709 | tze->trip_stats[trip_id].avg = tze->trip_stats[trip_id].avg + |
710 | (tz->temperature - tze->trip_stats[trip_id].avg) / |
711 | tze->trip_stats[trip_id].count; |
712 | } |
713 | out: |
714 | mutex_unlock(lock: &thermal_dbg->lock); |
715 | } |
716 | |
717 | static void *tze_seq_start(struct seq_file *s, loff_t *pos) |
718 | { |
719 | struct thermal_zone_device *tz = s->private; |
720 | struct thermal_debugfs *thermal_dbg = tz->debugfs; |
721 | struct tz_debugfs *tz_dbg = &thermal_dbg->tz_dbg; |
722 | |
723 | mutex_lock(&thermal_dbg->lock); |
724 | |
725 | return seq_list_start(head: &tz_dbg->tz_episodes, pos: *pos); |
726 | } |
727 | |
728 | static void *tze_seq_next(struct seq_file *s, void *v, loff_t *pos) |
729 | { |
730 | struct thermal_zone_device *tz = s->private; |
731 | struct thermal_debugfs *thermal_dbg = tz->debugfs; |
732 | struct tz_debugfs *tz_dbg = &thermal_dbg->tz_dbg; |
733 | |
734 | return seq_list_next(v, head: &tz_dbg->tz_episodes, ppos: pos); |
735 | } |
736 | |
737 | static void tze_seq_stop(struct seq_file *s, void *v) |
738 | { |
739 | struct thermal_zone_device *tz = s->private; |
740 | struct thermal_debugfs *thermal_dbg = tz->debugfs; |
741 | |
742 | mutex_unlock(lock: &thermal_dbg->lock); |
743 | } |
744 | |
745 | static int tze_seq_show(struct seq_file *s, void *v) |
746 | { |
747 | struct thermal_zone_device *tz = s->private; |
748 | struct thermal_trip *trip; |
749 | struct tz_episode *tze; |
750 | const char *type; |
751 | int trip_id; |
752 | |
753 | tze = list_entry((struct list_head *)v, struct tz_episode, node); |
754 | |
755 | seq_printf(m: s, fmt: ",-Mitigation at %lluus, duration=%llums\n" , |
756 | ktime_to_us(kt: tze->timestamp), |
757 | ktime_to_ms(kt: tze->duration)); |
758 | |
759 | seq_printf(m: s, fmt: "| trip | type | temp(°mC) | hyst(°mC) | duration | avg(°mC) | min(°mC) | max(°mC) |\n" ); |
760 | |
761 | for_each_trip(tz, trip) { |
762 | /* |
763 | * There is no possible mitigation happening at the |
764 | * critical trip point, so the stats will be always |
765 | * zero, skip this trip point |
766 | */ |
767 | if (trip->type == THERMAL_TRIP_CRITICAL) |
768 | continue; |
769 | |
770 | if (trip->type == THERMAL_TRIP_PASSIVE) |
771 | type = "passive" ; |
772 | else if (trip->type == THERMAL_TRIP_ACTIVE) |
773 | type = "active" ; |
774 | else |
775 | type = "hot" ; |
776 | |
777 | trip_id = thermal_zone_trip_id(tz, trip); |
778 | |
779 | seq_printf(m: s, fmt: "| %*d | %*s | %*d | %*d | %*lld | %*d | %*d | %*d |\n" , |
780 | 4 , trip_id, |
781 | 8, type, |
782 | 9, trip->temperature, |
783 | 9, trip->hysteresis, |
784 | 10, ktime_to_ms(kt: tze->trip_stats[trip_id].duration), |
785 | 9, tze->trip_stats[trip_id].avg, |
786 | 9, tze->trip_stats[trip_id].min, |
787 | 9, tze->trip_stats[trip_id].max); |
788 | } |
789 | |
790 | return 0; |
791 | } |
792 | |
793 | static const struct seq_operations tze_sops = { |
794 | .start = tze_seq_start, |
795 | .next = tze_seq_next, |
796 | .stop = tze_seq_stop, |
797 | .show = tze_seq_show, |
798 | }; |
799 | |
800 | DEFINE_SEQ_ATTRIBUTE(tze); |
801 | |
802 | void thermal_debug_tz_add(struct thermal_zone_device *tz) |
803 | { |
804 | struct thermal_debugfs *thermal_dbg; |
805 | struct tz_debugfs *tz_dbg; |
806 | |
807 | thermal_dbg = thermal_debugfs_add_id(d: d_tz, id: tz->id); |
808 | if (!thermal_dbg) |
809 | return; |
810 | |
811 | tz_dbg = &thermal_dbg->tz_dbg; |
812 | |
813 | tz_dbg->trips_crossed = kzalloc(size: sizeof(int) * tz->num_trips, GFP_KERNEL); |
814 | if (!tz_dbg->trips_crossed) { |
815 | thermal_debugfs_remove_id(thermal_dbg); |
816 | return; |
817 | } |
818 | |
819 | INIT_LIST_HEAD(list: &tz_dbg->tz_episodes); |
820 | |
821 | debugfs_create_file(name: "mitigations" , mode: 0400, parent: thermal_dbg->d_top, data: tz, fops: &tze_fops); |
822 | |
823 | tz->debugfs = thermal_dbg; |
824 | } |
825 | |
826 | void thermal_debug_tz_remove(struct thermal_zone_device *tz) |
827 | { |
828 | struct thermal_debugfs *thermal_dbg = tz->debugfs; |
829 | |
830 | if (!thermal_dbg) |
831 | return; |
832 | |
833 | mutex_lock(&thermal_dbg->lock); |
834 | |
835 | tz->debugfs = NULL; |
836 | |
837 | mutex_unlock(lock: &thermal_dbg->lock); |
838 | |
839 | thermal_debugfs_remove_id(thermal_dbg); |
840 | } |
841 | |