source: mainline/uspace/app/top/top.c@ f682f5a

lfn serial ticket/834-toolchain-update topic/msim-upgrade topic/simplify-dev-export
Last change on this file since f682f5a was f682f5a, checked in by Sean Bartell <wingedtachikoma@…>, 14 years ago

top: use generic handling for tables

  • each table has a name, columns, and fields
  • each column has a name, width, and shortcut key
  • each field has a type and value
  • this will help make top more configurable in the future
  • Property mode set to 100644
File size: 17.8 KB
Line 
1/*
2 * Copyright (c) 2010 Stanislav Kozina
3 * Copyright (c) 2010 Martin Decky
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 *
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * - Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 * - The name of the author may not be used to endorse or promote products
16 * derived from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30/** @addtogroup top
31 * @brief Top utility.
32 * @{
33 */
34/**
35 * @file
36 */
37
38#include <stdio.h>
39#include <stdlib.h>
40#include <unistd.h>
41#include <task.h>
42#include <thread.h>
43#include <sys/time.h>
44#include <errno.h>
45#include <sort.h>
46#include "screen.h"
47#include "top.h"
48
49#define NAME "top"
50
51#define UPDATE_INTERVAL 1
52
53#define DAY 86400
54#define HOUR 3600
55#define MINUTE 60
56
57typedef enum {
58 OP_TASKS,
59 OP_IPC,
60 OP_EXCS,
61} op_mode_t;
62
63screen_mode_t screen_mode = SCREEN_TABLE;
64static op_mode_t op_mode = OP_TASKS;
65sort_mode_t sort_mode = SORT_TASK_CYCLES;
66static bool excs_all = false;
67
68static const column_t task_columns[] = {
69 {"taskid", 't', 8},
70 {"thrds", 'h', 7},
71 {"resident", 'r', 10},
72 {"%resi", 'R', 7},
73 {"virtual", 'v', 9},
74 {"%virt", 'V', 7},
75 {"%user", 'U', 7},
76 {"%kern", 'K', 7},
77 {"name", 'd', 0},
78};
79
80enum {
81 TASK_COL_ID = 0,
82 TASK_COL_NUM_THREADS,
83 TASK_COL_RESIDENT,
84 TASK_COL_PERCENT_RESIDENT,
85 TASK_COL_VIRTUAL,
86 TASK_COL_PERCENT_VIRTUAL,
87 TASK_COL_PERCENT_USER,
88 TASK_COL_PERCENT_KERNEL,
89 TASK_COL_NAME,
90 TASK_NUM_COLUMNS,
91};
92
93static const column_t ipc_columns[] = {
94 {"taskid", 't', 8},
95 {"cls snt", 'c', 9},
96 {"cls rcv", 'C', 9},
97 {"ans snt", 'a', 9},
98 {"ans rcv", 'A', 9},
99 {"forward", 'f', 9},
100 {"name", 'd', 0},
101};
102
103enum {
104 IPC_COL_TASKID = 0,
105 IPC_COL_CLS_SNT,
106 IPC_COL_CLS_RCV,
107 IPC_COL_ANS_SNT,
108 IPC_COL_ANS_RCV,
109 IPC_COL_FORWARD,
110 IPC_COL_NAME,
111 IPC_NUM_COLUMNS,
112};
113
114static const column_t exception_columns[] = {
115 {"exc", 'e', 8},
116 {"count", 'n', 10},
117 {"%count", 'N', 8},
118 {"cycles", 'c', 10},
119 {"%cycles", 'C', 9},
120 {"description", 'd', 0},
121};
122
123enum {
124 EXCEPTION_COL_ID = 0,
125 EXCEPTION_COL_COUNT,
126 EXCEPTION_COL_PERCENT_COUNT,
127 EXCEPTION_COL_CYCLES,
128 EXCEPTION_COL_PERCENT_CYCLES,
129 EXCEPTION_COL_DESCRIPTION,
130 EXCEPTION_NUM_COLUMNS,
131};
132
133static const char *read_data(data_t *target)
134{
135 /* Initialize data */
136 target->load = NULL;
137 target->cpus = NULL;
138 target->cpus_perc = NULL;
139 target->tasks = NULL;
140 target->tasks_perc = NULL;
141 target->tasks_map = NULL;
142 target->threads = NULL;
143 target->exceptions = NULL;
144 target->exceptions_perc = NULL;
145 target->physmem = NULL;
146 target->ucycles_diff = NULL;
147 target->kcycles_diff = NULL;
148 target->ecycles_diff = NULL;
149 target->ecount_diff = NULL;
150 target->table.name = NULL;
151 target->table.num_columns = 0;
152 target->table.columns = NULL;
153 target->table.num_fields = 0;
154 target->table.fields = NULL;
155
156 /* Get current time */
157 struct timeval time;
158 if (gettimeofday(&time, NULL) != EOK)
159 return "Cannot get time of day";
160
161 target->hours = (time.tv_sec % DAY) / HOUR;
162 target->minutes = (time.tv_sec % HOUR) / MINUTE;
163 target->seconds = time.tv_sec % MINUTE;
164
165 /* Get uptime */
166 sysarg_t uptime = stats_get_uptime();
167 target->udays = uptime / DAY;
168 target->uhours = (uptime % DAY) / HOUR;
169 target->uminutes = (uptime % HOUR) / MINUTE;
170 target->useconds = uptime % MINUTE;
171
172 /* Get load */
173 target->load = stats_get_load(&(target->load_count));
174 if (target->load == NULL)
175 return "Cannot get system load";
176
177 /* Get CPUs */
178 target->cpus = stats_get_cpus(&(target->cpus_count));
179 if (target->cpus == NULL)
180 return "Cannot get CPUs";
181
182 target->cpus_perc =
183 (perc_cpu_t *) calloc(target->cpus_count, sizeof(perc_cpu_t));
184 if (target->cpus_perc == NULL)
185 return "Not enough memory for CPU utilization";
186
187 /* Get tasks */
188 target->tasks = stats_get_tasks(&(target->tasks_count));
189 if (target->tasks == NULL)
190 return "Cannot get tasks";
191
192 target->tasks_perc =
193 (perc_task_t *) calloc(target->tasks_count, sizeof(perc_task_t));
194 if (target->tasks_perc == NULL)
195 return "Not enough memory for task utilization";
196
197 target->tasks_map =
198 (size_t *) calloc(target->tasks_count, sizeof(size_t));
199 if (target->tasks_map == NULL)
200 return "Not enough memory for task map";
201
202 /* Get threads */
203 target->threads = stats_get_threads(&(target->threads_count));
204 if (target->threads == NULL)
205 return "Cannot get threads";
206
207 /* Get Exceptions */
208 target->exceptions = stats_get_exceptions(&(target->exceptions_count));
209 if (target->exceptions == NULL)
210 return "Cannot get exceptions";
211
212 target->exceptions_perc =
213 (perc_exc_t *) calloc(target->exceptions_count, sizeof(perc_exc_t));
214 if (target->exceptions_perc == NULL)
215 return "Not enough memory for exception utilization";
216
217 /* Get physical memory */
218 target->physmem = stats_get_physmem();
219 if (target->physmem == NULL)
220 return "Cannot get physical memory";
221
222 target->ucycles_diff = calloc(target->tasks_count,
223 sizeof(uint64_t));
224 if (target->ucycles_diff == NULL)
225 return "Not enough memory for user utilization";
226
227 /* Allocate memory for computed values */
228 target->kcycles_diff = calloc(target->tasks_count,
229 sizeof(uint64_t));
230 if (target->kcycles_diff == NULL)
231 return "Not enough memory for kernel utilization";
232
233 target->ecycles_diff = calloc(target->exceptions_count,
234 sizeof(uint64_t));
235 if (target->ecycles_diff == NULL)
236 return "Not enough memory for exception cycles utilization";
237
238 target->ecount_diff = calloc(target->exceptions_count,
239 sizeof(uint64_t));
240 if (target->ecount_diff == NULL)
241 return "Not enough memory for exception count utilization";
242
243 return NULL;
244}
245
246/** Computes percentage differencies from old_data to new_data
247 *
248 * @param old_data Pointer to old data strucutre.
249 * @param new_data Pointer to actual data where percetages are stored.
250 *
251 */
252static void compute_percentages(data_t *old_data, data_t *new_data)
253{
254 /* For each CPU: Compute total cycles and divide it between
255 user and kernel */
256
257 size_t i;
258 for (i = 0; i < new_data->cpus_count; i++) {
259 uint64_t idle =
260 new_data->cpus[i].idle_cycles - old_data->cpus[i].idle_cycles;
261 uint64_t busy =
262 new_data->cpus[i].busy_cycles - old_data->cpus[i].busy_cycles;
263 uint64_t sum = idle + busy;
264
265 FRACTION_TO_FLOAT(new_data->cpus_perc[i].idle, idle * 100, sum);
266 FRACTION_TO_FLOAT(new_data->cpus_perc[i].busy, busy * 100, sum);
267 }
268
269 /* For all tasks compute sum and differencies of all cycles */
270
271 uint64_t virtmem_total = 0;
272 uint64_t resmem_total = 0;
273 uint64_t ucycles_total = 0;
274 uint64_t kcycles_total = 0;
275
276 for (i = 0; i < new_data->tasks_count; i++) {
277 /* Match task with the previous instance */
278
279 bool found = false;
280 size_t j;
281 for (j = 0; j < old_data->tasks_count; j++) {
282 if (new_data->tasks[i].task_id == old_data->tasks[j].task_id) {
283 found = true;
284 break;
285 }
286 }
287
288 if (!found) {
289 /* This is newly borned task, ignore it */
290 new_data->ucycles_diff[i] = 0;
291 new_data->kcycles_diff[i] = 0;
292 continue;
293 }
294
295 new_data->ucycles_diff[i] =
296 new_data->tasks[i].ucycles - old_data->tasks[j].ucycles;
297 new_data->kcycles_diff[i] =
298 new_data->tasks[i].kcycles - old_data->tasks[j].kcycles;
299
300 virtmem_total += new_data->tasks[i].virtmem;
301 resmem_total += new_data->tasks[i].resmem;
302 ucycles_total += new_data->ucycles_diff[i];
303 kcycles_total += new_data->kcycles_diff[i];
304 }
305
306 /* For each task compute percential change */
307
308 for (i = 0; i < new_data->tasks_count; i++) {
309 FRACTION_TO_FLOAT(new_data->tasks_perc[i].virtmem,
310 new_data->tasks[i].virtmem * 100, virtmem_total);
311 FRACTION_TO_FLOAT(new_data->tasks_perc[i].resmem,
312 new_data->tasks[i].resmem * 100, resmem_total);
313 FRACTION_TO_FLOAT(new_data->tasks_perc[i].ucycles,
314 new_data->ucycles_diff[i] * 100, ucycles_total);
315 FRACTION_TO_FLOAT(new_data->tasks_perc[i].kcycles,
316 new_data->kcycles_diff[i] * 100, kcycles_total);
317 }
318
319 /* For all exceptions compute sum and differencies of cycles */
320
321 uint64_t ecycles_total = 0;
322 uint64_t ecount_total = 0;
323
324 for (i = 0; i < new_data->exceptions_count; i++) {
325 /*
326 * March exception with the previous instance.
327 * This is quite paranoid since exceptions do not
328 * usually disappear, but it does not hurt.
329 */
330
331 bool found = false;
332 size_t j;
333 for (j = 0; j < old_data->exceptions_count; j++) {
334 if (new_data->exceptions[i].id == old_data->exceptions[j].id) {
335 found = true;
336 break;
337 }
338 }
339
340 if (!found) {
341 /* This is a new exception, ignore it */
342 new_data->ecycles_diff[i] = 0;
343 new_data->ecount_diff[i] = 0;
344 continue;
345 }
346
347 new_data->ecycles_diff[i] =
348 new_data->exceptions[i].cycles - old_data->exceptions[j].cycles;
349 new_data->ecount_diff[i] =
350 new_data->exceptions[i].count - old_data->exceptions[i].count;
351
352 ecycles_total += new_data->ecycles_diff[i];
353 ecount_total += new_data->ecount_diff[i];
354 }
355
356 /* For each exception compute percential change */
357
358 for (i = 0; i < new_data->exceptions_count; i++) {
359 FRACTION_TO_FLOAT(new_data->exceptions_perc[i].cycles,
360 new_data->ecycles_diff[i] * 100, ecycles_total);
361 FRACTION_TO_FLOAT(new_data->exceptions_perc[i].count,
362 new_data->ecount_diff[i] * 100, ecount_total);
363 }
364}
365
366static int cmp_data(void *a, void *b, void *arg)
367{
368 size_t ia = *((size_t *) a);
369 size_t ib = *((size_t *) b);
370 data_t *data = (data_t *) arg;
371
372 uint64_t acycles = data->ucycles_diff[ia] + data->kcycles_diff[ia];
373 uint64_t bcycles = data->ucycles_diff[ib] + data->kcycles_diff[ib];
374
375 if (acycles > bcycles)
376 return -1;
377
378 if (acycles < bcycles)
379 return 1;
380
381 return 0;
382}
383
384static void sort_data(data_t *data)
385{
386 size_t i;
387
388 for (i = 0; i < data->tasks_count; i++)
389 data->tasks_map[i] = i;
390
391 qsort((void *) data->tasks_map, data->tasks_count,
392 sizeof(size_t), cmp_data, (void *) data);
393}
394
395static const char *fill_task_table(data_t *data)
396{
397 data->table.name = "Tasks";
398 data->table.num_columns = TASK_NUM_COLUMNS;
399 data->table.columns = task_columns;
400 data->table.num_fields = data->tasks_count * TASK_NUM_COLUMNS;
401 data->table.fields = calloc(data->table.num_fields,
402 sizeof(field_t));
403 if (data->table.fields == NULL)
404 return "Not enough memory for table fields";
405
406 field_t *field = data->table.fields;
407 for (size_t i = 0; i < data->tasks_count; i++) {
408 stats_task_t *task = data->tasks + data->tasks_map[i];
409 perc_task_t *perc = data->tasks_perc + data->tasks_map[i];
410 field[TASK_COL_ID].type = FIELD_UINT;
411 field[TASK_COL_ID].uint = task->task_id;
412 field[TASK_COL_NUM_THREADS].type = FIELD_UINT;
413 field[TASK_COL_NUM_THREADS].uint = task->threads;
414 field[TASK_COL_RESIDENT].type = FIELD_UINT_SUFFIX_BIN;
415 field[TASK_COL_RESIDENT].uint = task->resmem;
416 field[TASK_COL_PERCENT_RESIDENT].type = FIELD_PERCENT;
417 field[TASK_COL_PERCENT_RESIDENT].fixed = perc->resmem;
418 field[TASK_COL_VIRTUAL].type = FIELD_UINT_SUFFIX_BIN;
419 field[TASK_COL_VIRTUAL].uint = task->virtmem;
420 field[TASK_COL_PERCENT_VIRTUAL].type = FIELD_PERCENT;
421 field[TASK_COL_PERCENT_VIRTUAL].fixed = perc->virtmem;
422 field[TASK_COL_PERCENT_USER].type = FIELD_PERCENT;
423 field[TASK_COL_PERCENT_USER].fixed = perc->ucycles;
424 field[TASK_COL_PERCENT_KERNEL].type = FIELD_PERCENT;
425 field[TASK_COL_PERCENT_KERNEL].fixed = perc->kcycles;
426 field[TASK_COL_NAME].type = FIELD_STRING;
427 field[TASK_COL_NAME].string = task->name;
428 field += TASK_NUM_COLUMNS;
429 }
430
431 return NULL;
432}
433
434static const char *fill_ipc_table(data_t *data)
435{
436 data->table.name = "IPC";
437 data->table.num_columns = IPC_NUM_COLUMNS;
438 data->table.columns = ipc_columns;
439 data->table.num_fields = data->tasks_count * IPC_NUM_COLUMNS;
440 data->table.fields = calloc(data->table.num_fields,
441 sizeof(field_t));
442 if (data->table.fields == NULL)
443 return "Not enough memory for table fields";
444
445 field_t *field = data->table.fields;
446 for (size_t i = 0; i < data->tasks_count; i++) {
447 field[IPC_COL_TASKID].type = FIELD_UINT;
448 field[IPC_COL_TASKID].uint = data->tasks[i].task_id;
449 field[IPC_COL_CLS_SNT].type = FIELD_UINT_SUFFIX_DEC;
450 field[IPC_COL_CLS_SNT].uint = data->tasks[i].ipc_info.call_sent;
451 field[IPC_COL_CLS_RCV].type = FIELD_UINT_SUFFIX_DEC;
452 field[IPC_COL_CLS_RCV].uint = data->tasks[i].ipc_info.call_received;
453 field[IPC_COL_ANS_SNT].type = FIELD_UINT_SUFFIX_DEC;
454 field[IPC_COL_ANS_SNT].uint = data->tasks[i].ipc_info.answer_sent;
455 field[IPC_COL_ANS_RCV].type = FIELD_UINT_SUFFIX_DEC;
456 field[IPC_COL_ANS_RCV].uint = data->tasks[i].ipc_info.answer_received;
457 field[IPC_COL_FORWARD].type = FIELD_UINT_SUFFIX_DEC;
458 field[IPC_COL_FORWARD].uint = data->tasks[i].ipc_info.forwarded;
459 field[IPC_COL_NAME].type = FIELD_STRING;
460 field[IPC_COL_NAME].string = data->tasks[i].name;
461 field += IPC_NUM_COLUMNS;
462 }
463
464 return NULL;
465}
466
467static const char *fill_exception_table(data_t *data)
468{
469 data->table.name = "Exceptions";
470 data->table.num_columns = EXCEPTION_NUM_COLUMNS;
471 data->table.columns = exception_columns;
472 data->table.num_fields = data->exceptions_count *
473 EXCEPTION_NUM_COLUMNS;
474 data->table.fields = calloc(data->table.num_fields, sizeof(field_t));
475 if (data->table.fields == NULL)
476 return "Not enough memory for table fields";
477
478 field_t *field = data->table.fields;
479 for (size_t i = 0; i < data->exceptions_count; i++) {
480 if (!excs_all && !data->exceptions[i].hot)
481 continue;
482 field[EXCEPTION_COL_ID].type = FIELD_UINT;
483 field[EXCEPTION_COL_ID].uint = data->exceptions[i].id;
484 field[EXCEPTION_COL_COUNT].type = FIELD_UINT_SUFFIX_DEC;
485 field[EXCEPTION_COL_COUNT].uint = data->exceptions[i].count;
486 field[EXCEPTION_COL_PERCENT_COUNT].type = FIELD_PERCENT;
487 field[EXCEPTION_COL_PERCENT_COUNT].fixed = data->exceptions_perc[i].count;
488 field[EXCEPTION_COL_CYCLES].type = FIELD_UINT_SUFFIX_DEC;
489 field[EXCEPTION_COL_CYCLES].uint = data->exceptions[i].cycles;
490 field[EXCEPTION_COL_PERCENT_CYCLES].type = FIELD_PERCENT;
491 field[EXCEPTION_COL_PERCENT_CYCLES].fixed = data->exceptions_perc[i].cycles;
492 field[EXCEPTION_COL_DESCRIPTION].type = FIELD_STRING;
493 field[EXCEPTION_COL_DESCRIPTION].string = data->exceptions[i].desc;
494 field += EXCEPTION_NUM_COLUMNS;
495 }
496
497 /* in case any cold exceptions were ignored */
498 data->table.num_fields = field - data->table.fields;
499
500 return NULL;
501}
502
503static const char *fill_table(data_t *data)
504{
505 if (data->table.fields != NULL) {
506 free(data->table.fields);
507 data->table.fields = NULL;
508 }
509
510 switch (op_mode) {
511 case OP_TASKS:
512 return fill_task_table(data);
513 case OP_IPC:
514 return fill_ipc_table(data);
515 case OP_EXCS:
516 return fill_exception_table(data);
517 }
518 return NULL;
519}
520
521static void free_data(data_t *target)
522{
523 if (target->load != NULL)
524 free(target->load);
525
526 if (target->cpus != NULL)
527 free(target->cpus);
528
529 if (target->cpus_perc != NULL)
530 free(target->cpus_perc);
531
532 if (target->tasks != NULL)
533 free(target->tasks);
534
535 if (target->tasks_perc != NULL)
536 free(target->tasks_perc);
537
538 if (target->threads != NULL)
539 free(target->threads);
540
541 if (target->exceptions != NULL)
542 free(target->exceptions);
543
544 if (target->exceptions_perc != NULL)
545 free(target->exceptions_perc);
546
547 if (target->physmem != NULL)
548 free(target->physmem);
549
550 if (target->ucycles_diff != NULL)
551 free(target->ucycles_diff);
552
553 if (target->kcycles_diff != NULL)
554 free(target->kcycles_diff);
555
556 if (target->ecycles_diff != NULL)
557 free(target->ecycles_diff);
558
559 if (target->ecount_diff != NULL)
560 free(target->ecount_diff);
561
562 if (target->table.fields != NULL)
563 free(target->table.fields);
564}
565
566int main(int argc, char *argv[])
567{
568 data_t data;
569 data_t data_prev;
570 const char *ret = NULL;
571
572 screen_init();
573 printf("Reading initial data...\n");
574
575 if ((ret = read_data(&data)) != NULL)
576 goto out;
577
578 /* Compute some rubbish to have initialised values */
579 compute_percentages(&data, &data);
580
581 /* And paint screen until death */
582 while (true) {
583 int c = tgetchar(UPDATE_INTERVAL);
584
585 switch (c) {
586 case -1: /* timeout */
587 data_prev = data;
588 if ((ret = read_data(&data)) != NULL) {
589 free_data(&data_prev);
590 goto out;
591 }
592
593 compute_percentages(&data_prev, &data);
594 free_data(&data_prev);
595 break;
596 case 't':
597 screen_mode = SCREEN_TABLE;
598 op_mode = OP_TASKS;
599 break;
600 case 'i':
601 screen_mode = SCREEN_TABLE;
602 op_mode = OP_IPC;
603 break;
604 case 'e':
605 screen_mode = SCREEN_TABLE;
606 op_mode = OP_EXCS;
607 break;
608 case 'h':
609 case '?':
610 if (screen_mode == SCREEN_HELP)
611 screen_mode = SCREEN_TABLE;
612 else
613 screen_mode = SCREEN_HELP;
614 break;
615 case 'q':
616 goto out;
617 case 'a':
618 if (op_mode == OP_EXCS) {
619 screen_mode = SCREEN_TABLE;
620 excs_all = !excs_all;
621 if (excs_all)
622 show_warning("Showing all exceptions");
623 else
624 show_warning("Showing only hot exceptions");
625 break;
626 }
627 /* fallthrough */
628 default:
629 show_warning("Unknown command \"%c\", use \"h\" for help", c);
630 continue; /* don't redraw */
631 }
632
633 sort_data(&data);
634 if ((ret = fill_table(&data)) != NULL) {
635 goto out;
636 }
637 print_data(&data);
638 }
639
640out:
641 screen_done();
642 free_data(&data);
643
644 if (ret != NULL) {
645 fprintf(stderr, "%s: %s\n", NAME, ret);
646 return 1;
647 }
648
649 return 0;
650}
651
652/** @}
653 */
Note: See TracBrowser for help on using the repository browser.