Use BT_SEEK_LAST
[lttv.git] / lttv / lttv / traceset.c
1 /* This file is part of the Linux Trace Toolkit viewer
2 * Copyright (C) 2003-2004 Michel Dagenais
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License Version 2 as
6 * published by the Free Software Foundation;
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program; if not, write to the Free Software
15 * Foundation, Inc., 59 Temple Place - Suite 330, Boston,
16 * MA 02111-1307, USA.
17 */
18
19 #ifdef HAVE_CONFIG_H
20 #include <config.h>
21 #endif
22
23 #include <lttv/traceset.h>
24 #include <lttv/iattribute.h>
25 #include <lttv/state.h>
26 #include <lttv/event.h>
27 #include <lttv/hook.h>
28 #include <stdio.h>
29 #include <babeltrace/babeltrace.h>
30 #include <babeltrace/context.h>
31 #include <babeltrace/ctf/iterator.h>
32 #include <babeltrace/ctf/events.h>
33
34 /* To traverse a tree recursively */
35 #include <fcntl.h>
36 #include <fts.h>
37 /* For the use of realpath*/
38 #include <limits.h>
39 #include <stdlib.h>
40 /* For strcpy*/
41 #include <string.h>
42
43 /* A trace is a sequence of events gathered in the same tracing session. The
44 events may be stored in several tracefiles in the same directory.
45 A trace set is defined when several traces are to be analyzed together,
46 possibly to study the interactions between events in the different traces.
47 */
48
49
50 LttvTraceset *lttv_traceset_new(void)
51 {
52 LttvTraceset *ts;
53 struct bt_iter_pos begin_pos;
54
55 ts = g_new(LttvTraceset, 1);
56 ts->filename = NULL;
57 ts->traces = g_ptr_array_new();
58 ts->context = bt_context_create();
59 ts->a = g_object_new(LTTV_ATTRIBUTE_TYPE, NULL);
60 //TODO remove this when we have really mecanism
61 //s->tmpState = g_new(LttvTraceState *, 1);
62 //lttv_trace_state_init(s->tmpState,0);
63
64 /*Initialize iterator to the beginning of the traces*/
65 begin_pos.type = BT_SEEK_BEGIN;
66 ts->iter = bt_ctf_iter_create(ts->context, &begin_pos, NULL);
67
68 ts->event_hooks = lttv_hooks_new();
69
70 ts->state_trace_handle_index = g_ptr_array_new();
71 ts->has_precomputed_states = FALSE;
72
73 ts->time_span.start_time = ltt_time_zero;
74 ts->time_span.end_time = ltt_time_zero;
75 lttv_traceset_get_time_span_real(ts);
76 return ts;
77 }
78
79 char * lttv_traceset_name(LttvTraceset * s)
80 {
81 return s->filename;
82 }
83
84 #ifdef BABEL_CLEANUP
85 LttvTrace *lttv_trace_new(LttTrace *t)
86 {
87 LttvTrace *new_trace;
88
89 new_trace = g_new(LttvTrace, 1);
90 new_trace->a = g_object_new(LTTV_ATTRIBUTE_TYPE, NULL);
91 new_trace->id = t;
92 new_trace->ref_count = 0;
93 return new_trace;
94 }
95 #endif
96 /*
97 * get_absolute_pathname : Return the unique pathname in the system
98 *
99 * pathname is the relative path.
100 *
101 * abs_pathname is being set to the absolute path.
102 *
103 */
104 void get_absolute_pathname(const gchar *pathname, gchar * abs_pathname)
105 {
106 abs_pathname[0] = '\0';
107
108 if (realpath(pathname, abs_pathname) != NULL)
109 return;
110 else
111 {
112 /* error, return the original path unmodified */
113 strcpy(abs_pathname, pathname);
114 return;
115 }
116 return;
117 }
118
119
120
121 /*
122 * lttv_trace_create : Create a trace from a path
123 *
124 * ts is the traceset in which will be contained the trace
125 *
126 * path is the path where to find a trace. It is not recursive.
127 *
128 * This function is static since a trace should always be contained in a
129 * traceset.
130 *
131 * return the created trace or NULL on failure
132 */
133 static LttvTrace *lttv_trace_create(LttvTraceset *ts, const char *path)
134 {
135 int id = bt_context_add_trace(lttv_traceset_get_context(ts),
136 path,
137 "ctf",
138 NULL,
139 NULL,
140 NULL);
141 if (id < 0) {
142 return NULL;
143 }
144 // Create the trace and save the trace handle id returned by babeltrace
145 LttvTrace *new_trace;
146
147 new_trace = g_new(LttvTrace, 1);
148 new_trace->a = g_object_new(LTTV_ATTRIBUTE_TYPE, NULL);
149 new_trace->id = id;
150 new_trace->ref_count = 0;
151 new_trace->traceset = ts;
152 new_trace->state = g_new(LttvTraceState,1);
153 lttv_trace_state_init(new_trace->state,new_trace);
154
155 /* Add the state to the trace_handle to state index */
156 g_ptr_array_set_size(ts->state_trace_handle_index,id+1);
157 g_ptr_array_index(ts->state_trace_handle_index,id) = new_trace->state;
158
159 return new_trace;
160 }
161
162 /*
163 * lttv_trace_create : Create and add a single trace to a traceset
164 *
165 * ts is the traceset in which will be contained the trace
166 *
167 * path is the path where to find a trace. It is not recursive.
168 *
169 * return a positive integer (>=0)on success or -1 on failure
170 */
171 static int lttv_traceset_create_trace(LttvTraceset *ts, const char *path)
172 {
173 LttvTrace *trace = lttv_trace_create(ts, path);
174 if (trace == NULL) {
175 return -1;
176 }
177 lttv_traceset_add(ts, trace);
178 return 0;
179 }
180
181 LttvTraceset *lttv_traceset_copy(LttvTraceset *s_orig)
182 {
183 guint i;
184 LttvTraceset *s;
185 LttvTrace * trace;
186
187 s = g_new(LttvTraceset, 1);
188 s->filename = NULL;
189 s->traces = g_ptr_array_new();
190 s->state_trace_handle_index = g_ptr_array_new();
191 for(i=0;i<s_orig->traces->len;i++)
192 {
193 trace = g_ptr_array_index(s_orig->traces, i);
194 trace->ref_count++;
195
196 /* WARNING: this is an alias, not a copy. */
197 g_ptr_array_add(s->traces, trace);
198
199 g_ptr_array_set_size(s->state_trace_handle_index,trace->id+1);
200 g_ptr_array_index(s->state_trace_handle_index,trace->id) = trace->state;
201
202 }
203 s->context = s_orig->context;
204 bt_context_get(s->context);
205 s->a = LTTV_ATTRIBUTE(lttv_iattribute_deep_copy(LTTV_IATTRIBUTE(s_orig->a)));
206 return s;
207 }
208
209
210 LttvTraceset *lttv_traceset_load(const gchar *filename)
211 {
212 LttvTraceset *s = g_new(LttvTraceset,1);
213 FILE *tf;
214
215 s->filename = g_strdup(filename);
216 tf = fopen(filename,"r");
217
218 g_critical("NOT IMPLEMENTED : load traceset data from a XML file");
219
220 fclose(tf);
221 return s;
222 }
223
224 gint lttv_traceset_save(LttvTraceset *s)
225 {
226 FILE *tf;
227
228 tf = fopen(s->filename, "w");
229
230 g_critical("NOT IMPLEMENTED : save traceset data in a XML file");
231
232 fclose(tf);
233 return 0;
234 }
235
236 void lttv_traceset_destroy(LttvTraceset *s)
237 {
238 guint i;
239
240 for(i=0;i<s->traces->len;i++) {
241 LttvTrace *trace = g_ptr_array_index(s->traces, i);
242 lttv_trace_unref(trace);
243 // todo mdenis 2012-03-27: uncomment when babeltrace gets fixed
244 //bt_context_remove_trace(lttv_traceset_get_context(s), trace->id);
245 if(lttv_trace_get_ref_number(trace) == 0)
246 lttv_trace_destroy(trace);
247 }
248 g_ptr_array_free(s->traces, TRUE);
249 bt_context_put(s->context);
250 g_object_unref(s->a);
251 g_free(s);
252 }
253
254 struct bt_context *lttv_traceset_get_context(LttvTraceset *s)
255 {
256 return s->context;
257 }
258
259 LttvTraceset *lttv_trace_get_traceset(LttvTrace *trace)
260 {
261 return trace->traceset;
262 }
263
264 LttvHooks *lttv_traceset_get_hooks(LttvTraceset *s)
265 {
266 return s->event_hooks;
267 }
268
269 void lttv_trace_destroy(LttvTrace *t)
270 {
271 g_object_unref(t->a);
272 g_free(t);
273 }
274
275 void lttv_traceset_add(LttvTraceset *s, LttvTrace *t)
276 {
277 t->ref_count++;
278 g_ptr_array_add(s->traces, t);
279 }
280
281 int lttv_traceset_add_path(LttvTraceset *ts, char *trace_path)
282 {
283 FTS *tree;
284 FTSENT *node;
285 char * const paths[2] = { trace_path, NULL };
286 int ret = -1;
287
288 gboolean metaFileFound = FALSE;
289
290 tree = fts_open(paths, FTS_NOCHDIR | FTS_LOGICAL, 0);
291 if (tree == NULL) {
292 g_warning("Cannot traverse \"%s\" for reading.\n",
293 trace_path);
294 return ret;
295 }
296
297 int dirfd, metafd;
298 while ((node = fts_read(tree))) {
299
300 if (!(node->fts_info & FTS_D))
301 continue;
302
303 dirfd = open(node->fts_accpath, 0);
304 if (dirfd < 0) {
305 g_warning("Unable to open trace "
306 "directory file descriptor : %s.", node->fts_accpath);
307 ret = dirfd;
308 goto error;
309 }
310
311 // Check if a metadata file exists in the current directory
312 metafd = openat(dirfd, "metadata", O_RDONLY);
313 if (metafd < 0) {
314 ret = close(dirfd);
315 if (ret < 0) {
316 g_warning("Unable to open metadata "
317 "file descriptor : %s.", node->fts_accpath);
318 goto error;
319 }
320 } else {
321 ret = close(metafd);
322 if (ret < 0) {
323 g_warning("Unable to close metadata "
324 "file descriptor : %s.", node->fts_accpath);
325 goto error;
326 }
327 ret = close(dirfd);
328 if (ret < 0) {
329 g_warning("Unable to close trace "
330 "directory file descriptor : %s.", node->fts_accpath);
331 goto error;
332 }
333
334 ret = lttv_traceset_create_trace(ts, node->fts_accpath);
335 if (ret < 0) {
336 g_warning("Opening trace \"%s\" from %s "
337 "for reading.", node->fts_accpath, trace_path);
338 goto error;
339 }
340 metaFileFound = TRUE;
341 }
342 }
343
344 error:
345 ret = fts_close(tree);
346 if (ret < 0) {
347 g_warning("Unable to close tree "
348 "file descriptor : %s.", trace_path);
349 }
350 if(metaFileFound)
351 return ret;
352 else
353 return -1;
354 }
355
356
357 unsigned lttv_traceset_number(LttvTraceset *s)
358 {
359 return s->traces->len;
360 }
361
362
363 LttvTrace *lttv_traceset_get(LttvTraceset *s, unsigned i)
364 {
365 g_assert(s->traces->len > i);
366 return ((LttvTrace *)s->traces->pdata[i]);
367 }
368
369
370 void lttv_traceset_remove(LttvTraceset *s, unsigned i)
371 {
372 LttvTrace * t;
373 g_assert(s->traces->len > i);
374 t = (LttvTrace *)s->traces->pdata[i];
375 t->ref_count--;
376 bt_context_remove_trace(lttv_traceset_get_context(s), t->id);
377 g_ptr_array_remove_index(s->traces, i);
378 }
379
380
381 /* A set of attributes is attached to each trace set, trace and tracefile
382 to store user defined data as needed. */
383
384 LttvAttribute *lttv_traceset_attribute(LttvTraceset *s)
385 {
386 return s->a;
387 }
388
389
390 LttvAttribute *lttv_trace_attribute(LttvTrace *t)
391 {
392 return t->a;
393 }
394
395
396 gint lttv_trace_get_id(LttvTrace *t)
397 {
398 return t->id;
399 }
400
401 guint lttv_trace_get_ref_number(LttvTrace * t)
402 {
403 // todo mdenis: adapt to babeltrace
404 return t->ref_count;
405 }
406
407 guint lttv_trace_ref(LttvTrace * t)
408 {
409 t->ref_count++;
410
411 return t->ref_count;
412 }
413
414 guint lttv_trace_unref(LttvTrace * t)
415 {
416 if(likely(t->ref_count > 0))
417 t->ref_count--;
418
419 return t->ref_count;
420 }
421
422 guint lttv_trace_get_num_cpu(LttvTrace *t)
423 {
424 #warning "TODO - Set the right number of CPU"
425 return 24;
426 }
427
428 LttvTracesetPosition *lttv_traceset_create_current_position(LttvTraceset *traceset)
429 {
430 LttvTracesetPosition *traceset_pos;
431
432 traceset_pos = g_new(LttvTracesetPosition, 1);
433
434 /* Check if the new passed */
435 if(traceset_pos == NULL) {
436 return NULL;
437 }
438
439 traceset_pos->iter = traceset->iter;
440 traceset_pos->bt_pos = bt_iter_get_pos(bt_ctf_get_iter(traceset->iter));
441 traceset_pos->timestamp = G_MAXUINT64;
442 traceset_pos->cpu_id = INT_MAX;
443
444 return traceset_pos;
445 }
446
447 LttvTracesetPosition *lttv_traceset_create_time_position(LttvTraceset *traceset,
448 LttTime timestamp)
449 {
450 LttvTracesetPosition *traceset_pos;
451
452 traceset_pos = g_new(LttvTracesetPosition, 1);
453
454 /* Check if the new passed */
455 if(traceset_pos == NULL) {
456 return NULL;
457 }
458
459 traceset_pos->iter = traceset->iter;
460 traceset_pos->bt_pos = bt_iter_create_time_pos(
461 bt_ctf_get_iter(traceset_pos->iter),
462 ltt_time_to_uint64(timestamp));
463 traceset_pos->timestamp = G_MAXUINT64;
464 traceset_pos->cpu_id = INT_MAX;
465 return traceset_pos;
466 }
467
468 void lttv_traceset_destroy_position(LttvTracesetPosition *traceset_pos)
469 {
470 bt_iter_free_pos(traceset_pos->bt_pos);
471 g_free(traceset_pos);
472 }
473
474 void lttv_traceset_seek_to_position(const LttvTracesetPosition *traceset_pos)
475 {
476 bt_iter_set_pos(bt_ctf_get_iter(traceset_pos->iter), traceset_pos->bt_pos);
477 }
478
479 guint lttv_traceset_get_cpuid_from_event(LttvEvent *event)
480 {
481 unsigned long timestamp;
482 unsigned int cpu_id;
483
484 struct bt_ctf_event *ctf_event = event->bt_event;
485 timestamp = bt_ctf_get_timestamp(ctf_event);
486 if (timestamp == -1ULL) {
487 return 0;
488 }
489 const struct definition *scope = bt_ctf_get_top_level_scope(ctf_event, BT_STREAM_PACKET_CONTEXT);
490 if (bt_ctf_field_get_error()) {
491 return 0;
492 }
493 cpu_id = bt_ctf_get_uint64(bt_ctf_get_field(ctf_event, scope, "cpu_id"));
494 if (bt_ctf_field_get_error()) {
495 return 0;
496 } else {
497 return cpu_id;
498 }
499 }
500
501 guint64 lttv_traceset_get_timestamp_first_event(LttvTraceset *ts)
502 {
503 LttvTracesetPosition begin_position;
504 struct bt_iter_pos pos;
505 begin_position.bt_pos = &pos;
506 begin_position.timestamp = G_MAXUINT64;
507 begin_position.cpu_id = INT_MAX;
508
509 /* Assign iterator to the beginning of the traces */
510 begin_position.bt_pos->type = BT_SEEK_BEGIN;
511 begin_position.iter = ts->iter;
512
513 return lttv_traceset_position_get_timestamp(&begin_position);
514 }
515
516 guint64 lttv_traceset_get_timestamp_last_event(LttvTraceset *ts)
517 {
518 LttvTracesetPosition last_position;
519 struct bt_iter_pos pos;
520 last_position.bt_pos = &pos;
521 last_position.timestamp = G_MAXUINT64;
522 last_position.cpu_id = INT_MAX;
523
524 /* Assign iterator to the last event of the traces */
525 last_position.bt_pos->type = BT_SEEK_LAST;
526 last_position.iter = ts->iter;
527
528 return lttv_traceset_position_get_timestamp(&last_position);
529 }
530
531 /*
532 * lttv_traceset_get_timestamp_begin : returns the minimum timestamp of
533 * all the traces in the traceset.
534 *
535 */
536 guint64 lttv_traceset_get_timestamp_begin(LttvTraceset *traceset)
537 {
538 struct bt_context *bt_ctx;
539 bt_ctx = lttv_traceset_get_context(traceset);
540 guint64 timestamp_min, timestamp_cur = 0;
541 int i;
542 int trace_count;
543 LttvTrace *currentTrace;
544 trace_count = traceset->traces->len;
545 if(trace_count == 0)
546 timestamp_min = 0;
547 else{
548 timestamp_min = G_MAXUINT64;
549 for(i = 0; i < trace_count;i++)
550 {
551 currentTrace = g_ptr_array_index(traceset->traces,i);
552 timestamp_cur = bt_trace_handle_get_timestamp_begin(bt_ctx,
553 currentTrace->id,
554 BT_CLOCK_REAL);
555 if(timestamp_cur < timestamp_min)
556 timestamp_min = timestamp_cur;
557 }
558 }
559 return timestamp_min;
560 }
561
562 /*
563 * lttv_traceset_get_timestamp_end: returns the maximum timestamp of
564 * all the traces in the traceset.
565 *
566 */
567 guint64 lttv_traceset_get_timestamp_end(LttvTraceset *traceset)
568 {
569 struct bt_context *bt_ctx;
570 bt_ctx = lttv_traceset_get_context(traceset);
571 guint64 timestamp_max, timestamp_cur = 0;
572 int i;
573 int trace_count;
574 LttvTrace *currentTrace;
575 trace_count = traceset->traces->len;
576
577 if(trace_count == 0){
578 timestamp_max = 1;
579 }
580 else{
581 timestamp_max = 0;
582 for(i =0; i < trace_count;i++)
583 {
584 currentTrace = g_ptr_array_index(traceset->traces,i);
585 timestamp_cur = bt_trace_handle_get_timestamp_end(bt_ctx,
586 currentTrace->id,
587 BT_CLOCK_REAL);
588 if(timestamp_cur > timestamp_max){
589 timestamp_max = timestamp_cur;
590 }
591 }
592 }
593 return timestamp_max;
594 }
595 /*
596 * lttv_traceset_get_time_span_real : return a TimeInterval representing the
597 * minimum timestamp and the maximum timestamp of the traceset.
598 *
599 */
600 TimeInterval lttv_traceset_get_time_span_real(LttvTraceset *ts)
601 {
602
603
604 if(ltt_time_compare(ts->time_span.start_time,
605 ltt_time_zero) == 0 && ts->traces->len > 0){
606 ts->time_span.start_time = ltt_time_from_uint64(
607 lttv_traceset_get_timestamp_first_event(ts));
608 ts->time_span.end_time = ltt_time_from_uint64(
609 lttv_traceset_get_timestamp_last_event(ts));
610 }
611 return ts->time_span;
612 }
613
614 /*
615 * lttv_traceset_get_time_span : return a TimeInterval representing the
616 * minimum timestamp and the maximum timestamp of the traceset.
617 *
618 */
619 TimeInterval lttv_traceset_get_time_span(LttvTraceset *ts)
620 {
621 if(ltt_time_compare(ts->time_span.start_time, ltt_time_zero) == 0){
622 ts->time_span.start_time =ltt_time_from_uint64(
623 lttv_traceset_get_timestamp_begin(ts));
624 ts->time_span.end_time = ltt_time_from_uint64(
625 lttv_traceset_get_timestamp_end(ts));
626 }
627 return ts->time_span;
628 }
629
630 const char *lttv_traceset_get_name_from_event(LttvEvent *event)
631 {
632 return bt_ctf_event_name(event->bt_event);
633 }
634
635 int set_values_position(const LttvTracesetPosition *pos)
636 {
637 LttvTracesetPosition previous_pos;
638 previous_pos.iter = pos->iter;
639 previous_pos.bt_pos = bt_iter_get_pos(bt_ctf_get_iter(pos->iter));
640 /* Seek to the new desired position */
641 lttv_traceset_seek_to_position(pos);
642 /*Read the event*/
643 struct bt_ctf_event *event = bt_ctf_iter_read_event(pos->iter);
644
645 if(event != NULL){
646 ((LttvTracesetPosition *)pos)->timestamp = bt_ctf_get_timestamp(event);
647
648 LttvEvent lttv_event;
649 lttv_event.bt_event = event;
650 ((LttvTracesetPosition *)pos)->cpu_id = lttv_traceset_get_cpuid_from_event(&lttv_event);
651 }
652 else {
653 /* The event is null */
654 return 0;
655 }
656
657 /* Reassign the previously saved position */
658 lttv_traceset_seek_to_position(&previous_pos);
659 /*We must desallocate because the function bt_iter_get_pos() does a g_new */
660 bt_iter_free_pos(previous_pos.bt_pos);
661 return 1;
662 }
663
664 guint64 lttv_traceset_position_get_timestamp(const LttvTracesetPosition *pos)
665 {
666 if(pos->timestamp == G_MAXUINT64){
667 if(set_values_position(pos) == 0){
668 return 0;
669 }
670 }
671
672 return pos->timestamp;
673 }
674
675 int lttv_traceset_position_get_cpuid(const LttvTracesetPosition *pos){
676 if(pos->cpu_id == INT_MAX ){
677 if(set_values_position(pos) == 0){
678 return 0;
679 }
680 }
681 return pos->cpu_id;
682 }
683
684 LttTime lttv_traceset_position_get_time(const LttvTracesetPosition *pos)
685 {
686 return ltt_time_from_uint64(lttv_traceset_position_get_timestamp(pos));
687 }
688
689
690
691 int lttv_traceset_position_compare(const LttvTracesetPosition *pos1, const LttvTracesetPosition *pos2)
692 {
693 #warning " TODO :Rename for lttv_traceset_position_equals && Must return COMPARAISON OF THE 2 POSITION && verify if it is the best way to compare position"
694 if(pos1 == NULL || pos2 == NULL){
695 return -1;
696 }
697
698 guint64 timeStampPos1,timeStampPos2;
699 guint cpuId1, cpuId2;
700
701 timeStampPos1 = lttv_traceset_position_get_timestamp(pos1);
702 timeStampPos2 = lttv_traceset_position_get_timestamp(pos2);
703
704
705 cpuId1 = lttv_traceset_position_get_cpuid(pos1);
706 cpuId2 = lttv_traceset_position_get_cpuid(pos2);
707
708 if(timeStampPos1 == timeStampPos2 && cpuId1 == cpuId2){
709 return 0;
710 }
711 else{
712 return 1;
713 }
714 }
715
716 int lttv_traceset_position_time_compare(const LttvTracesetPosition *pos1,
717 const LttvTracesetPosition *pos2)
718 {
719 guint64 timeStampPos1,timeStampPos2;
720
721 timeStampPos1 = lttv_traceset_position_get_timestamp(pos1);
722 timeStampPos2 = lttv_traceset_position_get_timestamp(pos2);
723
724 return timeStampPos1 - timeStampPos2;
725 }
726 int lttv_traceset_position_compare_current(const LttvTraceset *ts,
727 const LttvTracesetPosition *pos)
728 {
729 int result = 0;
730 LttvTracesetPosition *curPos = lttv_traceset_create_current_position(ts);
731
732 result = lttv_traceset_position_compare(curPos,pos);
733
734 lttv_traceset_destroy_position(curPos);
735
736 return result;
737 }
738
739 LttTime lttv_traceset_get_current_time(const LttvTraceset *ts)
740 {
741 LttvTracesetPosition *curPos = lttv_traceset_create_current_position(ts);
742 guint64 currentTimestamp = lttv_traceset_position_get_timestamp(curPos);
743 lttv_traceset_destroy_position(curPos);
744
745 return ltt_time_from_uint64(currentTimestamp);
746 }
This page took 0.054919 seconds and 5 git commands to generate.