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