15355572cb90d16f67ee848c25f17986b8902de4
[vpp.git] / src / vlib / trace.c
1 /*
2  * Copyright (c) 2015 Cisco and/or its affiliates.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at:
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 /*
16  * trace.c: VLIB trace buffer.
17  *
18  * Copyright (c) 2008 Eliot Dresselhaus
19  *
20  * Permission is hereby granted, free of charge, to any person obtaining
21  * a copy of this software and associated documentation files (the
22  * "Software"), to deal in the Software without restriction, including
23  * without limitation the rights to use, copy, modify, merge, publish,
24  * distribute, sublicense, and/or sell copies of the Software, and to
25  * permit persons to whom the Software is furnished to do so, subject to
26  * the following conditions:
27  *
28  * The above copyright notice and this permission notice shall be
29  * included in all copies or substantial portions of the Software.
30  *
31  *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
32  *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
33  *  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
34  *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
35  *  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
36  *  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
37  *  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
38  */
39
40 #include <vlib/vlib.h>
41 #include <vlib/threads.h>
42 #include <vnet/classify/vnet_classify.h>
43
44 u8 *vnet_trace_placeholder;
45
46 /* Helper function for nodes which only trace buffer data. */
47 void
48 vlib_trace_frame_buffers_only (vlib_main_t * vm,
49                                vlib_node_runtime_t * node,
50                                u32 * buffers,
51                                uword n_buffers,
52                                uword next_buffer_stride,
53                                uword n_buffer_data_bytes_in_trace)
54 {
55   u32 n_left, *from;
56
57   n_left = n_buffers;
58   from = buffers;
59
60   while (n_left >= 4)
61     {
62       u32 bi0, bi1;
63       vlib_buffer_t *b0, *b1;
64       u8 *t0, *t1;
65
66       /* Prefetch next iteration. */
67       vlib_prefetch_buffer_with_index (vm, from[2], LOAD);
68       vlib_prefetch_buffer_with_index (vm, from[3], LOAD);
69
70       bi0 = from[0];
71       bi1 = from[1];
72
73       b0 = vlib_get_buffer (vm, bi0);
74       b1 = vlib_get_buffer (vm, bi1);
75
76       if (b0->flags & VLIB_BUFFER_IS_TRACED)
77         {
78           t0 = vlib_add_trace (vm, node, b0, n_buffer_data_bytes_in_trace);
79           clib_memcpy_fast (t0, b0->data + b0->current_data,
80                             n_buffer_data_bytes_in_trace);
81         }
82       if (b1->flags & VLIB_BUFFER_IS_TRACED)
83         {
84           t1 = vlib_add_trace (vm, node, b1, n_buffer_data_bytes_in_trace);
85           clib_memcpy_fast (t1, b1->data + b1->current_data,
86                             n_buffer_data_bytes_in_trace);
87         }
88       from += 2;
89       n_left -= 2;
90     }
91
92   while (n_left >= 1)
93     {
94       u32 bi0;
95       vlib_buffer_t *b0;
96       u8 *t0;
97
98       bi0 = from[0];
99
100       b0 = vlib_get_buffer (vm, bi0);
101
102       if (b0->flags & VLIB_BUFFER_IS_TRACED)
103         {
104           t0 = vlib_add_trace (vm, node, b0, n_buffer_data_bytes_in_trace);
105           clib_memcpy_fast (t0, b0->data + b0->current_data,
106                             n_buffer_data_bytes_in_trace);
107         }
108       from += 1;
109       n_left -= 1;
110     }
111 }
112
113 /* Free up all trace buffer memory. */
114 void
115 clear_trace_buffer (void)
116 {
117   int i;
118   vlib_trace_main_t *tm;
119
120   foreach_vlib_main ()
121     {
122       tm = &this_vlib_main->trace_main;
123
124       tm->trace_enable = 0;
125       vec_free (tm->nodes);
126     }
127
128   foreach_vlib_main ()
129     {
130       tm = &this_vlib_main->trace_main;
131
132       for (i = 0; i < vec_len (tm->trace_buffer_pool); i++)
133         if (!pool_is_free_index (tm->trace_buffer_pool, i))
134           vec_free (tm->trace_buffer_pool[i]);
135       pool_free (tm->trace_buffer_pool);
136     }
137 }
138
139 u8 *
140 format_vlib_trace (u8 * s, va_list * va)
141 {
142   vlib_main_t *vm = va_arg (*va, vlib_main_t *);
143   vlib_trace_header_t *h = va_arg (*va, vlib_trace_header_t *);
144   vlib_trace_header_t *e = vec_end (h);
145   vlib_node_t *node, *prev_node;
146   clib_time_t *ct = &vm->clib_time;
147   f64 t;
148
149   prev_node = 0;
150   while (h < e)
151     {
152       node = vlib_get_node (vm, h->node_index);
153
154       if (node != prev_node)
155         {
156           t =
157             (h->time - vm->cpu_time_main_loop_start) * ct->seconds_per_clock;
158           s =
159             format (s, "\n%U: %v", format_time_interval, "h:m:s:u", t,
160                     node->name);
161         }
162       prev_node = node;
163
164       if (node->format_trace)
165         s = format (s, "\n  %U", node->format_trace, vm, node, h->data);
166       else
167         s = format (s, "\n  %U", node->format_buffer, h->data);
168
169       h = vlib_trace_header_next (h);
170     }
171
172   return s;
173 }
174
175 /* Root of all trace cli commands. */
176 /* *INDENT-OFF* */
177 VLIB_CLI_COMMAND (trace_cli_command,static) = {
178   .path = "trace",
179   .short_help = "Packet tracer commands",
180 };
181 /* *INDENT-ON* */
182
183 int
184 trace_time_cmp (void *a1, void *a2)
185 {
186   vlib_trace_header_t **t1 = a1;
187   vlib_trace_header_t **t2 = a2;
188   i64 dt = t1[0]->time - t2[0]->time;
189   return dt < 0 ? -1 : (dt > 0 ? +1 : 0);
190 }
191
192 /*
193  * Return 1 if this packet passes the trace filter, or 0 otherwise
194  */
195 u32
196 filter_accept (vlib_trace_main_t * tm, vlib_trace_header_t * h)
197 {
198   vlib_trace_header_t *e = vec_end (h);
199
200   if (tm->filter_flag == 0)
201     return 1;
202
203   /*
204    * When capturing a post-mortem dispatch trace,
205    * toss all existing traces once per dispatch cycle.
206    * So we can trace 4 billion pkts without running out of
207    * memory...
208    */
209   if (tm->filter_flag == FILTER_FLAG_POST_MORTEM)
210     return 0;
211
212   if (tm->filter_flag == FILTER_FLAG_INCLUDE)
213     {
214       while (h < e)
215         {
216           if (h->node_index == tm->filter_node_index)
217             return 1;
218           h = vlib_trace_header_next (h);
219         }
220       return 0;
221     }
222   else                          /* FILTER_FLAG_EXCLUDE */
223     {
224       while (h < e)
225         {
226           if (h->node_index == tm->filter_node_index)
227             return 0;
228           h = vlib_trace_header_next (h);
229         }
230       return 1;
231     }
232
233   return 0;
234 }
235
236 /*
237  * Remove traces from the trace buffer pool that don't pass the filter
238  */
239 void
240 trace_apply_filter (vlib_main_t * vm)
241 {
242   vlib_trace_main_t *tm = &vm->trace_main;
243   vlib_trace_header_t **h;
244   vlib_trace_header_t ***traces_to_remove = 0;
245   u32 index;
246   u32 trace_index;
247   u32 n_accepted;
248
249   u32 accept;
250
251   if (tm->filter_flag == FILTER_FLAG_NONE)
252     return;
253
254   /*
255    * Ideally we would retain the first N traces that pass the filter instead
256    * of any N traces.
257    */
258   n_accepted = 0;
259   /* *INDENT-OFF* */
260   pool_foreach (h, tm->trace_buffer_pool)
261     {
262       accept = filter_accept(tm, h[0]);
263
264       if ((n_accepted == tm->filter_count) || !accept)
265           vec_add1 (traces_to_remove, h);
266       else
267           n_accepted++;
268   }
269   /* *INDENT-ON* */
270
271   /* remove all traces that we don't want to keep */
272   for (index = 0; index < vec_len (traces_to_remove); index++)
273     {
274       trace_index = traces_to_remove[index] - tm->trace_buffer_pool;
275       vec_set_len (tm->trace_buffer_pool[trace_index], 0);
276       pool_put_index (tm->trace_buffer_pool, trace_index);
277     }
278
279   vec_free (traces_to_remove);
280 }
281
282 static clib_error_t *
283 cli_show_trace_buffer (vlib_main_t * vm,
284                        unformat_input_t * input, vlib_cli_command_t * cmd)
285 {
286   vlib_trace_main_t *tm;
287   vlib_trace_header_t **h, **traces;
288   u32 i, index = 0;
289   char *fmt;
290   u8 *s = 0;
291   u32 max;
292
293   /*
294    * By default display only this many traces. To display more, explicitly
295    * specify a max. This prevents unexpectedly huge outputs.
296    */
297   max = 50;
298   while (unformat_check_input (input) != (uword) UNFORMAT_END_OF_INPUT)
299     {
300       if (unformat (input, "max %d", &max))
301         ;
302       else
303         return clib_error_create ("expected 'max COUNT', got `%U'",
304                                   format_unformat_error, input);
305     }
306
307
308   /* Get active traces from pool. */
309
310   foreach_vlib_main ()
311     {
312       fmt = "------------------- Start of thread %d %s -------------------\n";
313       s = format (s, fmt, index, vlib_worker_threads[index].name);
314
315       tm = &this_vlib_main->trace_main;
316
317       trace_apply_filter (this_vlib_main);
318
319       traces = 0;
320       pool_foreach (h, tm->trace_buffer_pool)
321         {
322           vec_add1 (traces, h[0]);
323         }
324
325       if (vec_len (traces) == 0)
326         {
327           s = format (s, "No packets in trace buffer\n");
328           goto done;
329         }
330
331       /* Sort them by increasing time. */
332       vec_sort_with_function (traces, trace_time_cmp);
333
334       for (i = 0; i < vec_len (traces); i++)
335         {
336           if (i == max)
337             {
338               char *warn = "Limiting display to %d packets."
339                            " To display more specify max.";
340               vlib_cli_output (vm, warn, max);
341               s = format (s, warn, max);
342               goto done;
343             }
344
345           s = format (s, "Packet %d\n%U\n\n", i + 1, format_vlib_trace, vm,
346                       traces[i]);
347         }
348
349     done:
350       vec_free (traces);
351
352       index++;
353     }
354
355   vlib_cli_output (vm, "%v", s);
356   vec_free (s);
357   return 0;
358 }
359
360 /* *INDENT-OFF* */
361 VLIB_CLI_COMMAND (show_trace_cli,static) = {
362   .path = "show trace",
363   .short_help = "Show trace buffer [max COUNT]",
364   .function = cli_show_trace_buffer,
365 };
366 /* *INDENT-ON* */
367
368 int vlib_enable_disable_pkt_trace_filter (int enable) __attribute__ ((weak));
369
370 int
371 vlib_enable_disable_pkt_trace_filter (int enable)
372 {
373   return 0;
374 }
375
376 void
377 vlib_trace_stop_and_clear (void)
378 {
379   vlib_enable_disable_pkt_trace_filter (0);     /* disble tracing */
380   clear_trace_buffer ();
381 }
382
383
384 void
385 trace_update_capture_options (u32 add, u32 node_index, u32 filter, u8 verbose)
386 {
387   vlib_trace_main_t *tm;
388   vlib_trace_node_t *tn;
389
390   if (add == ~0)
391     add = 50;
392
393   foreach_vlib_main ()
394     {
395       tm = &this_vlib_main->trace_main;
396       tm->verbose = verbose;
397       vec_validate (tm->nodes, node_index);
398       tn = tm->nodes + node_index;
399
400       /*
401        * Adding 0 makes no real sense, and there wa no other way
402        * to explicilty zero-out the limits and count, so make
403        * an "add 0" request really be "set to 0".
404        */
405       if (add == 0)
406           tn->limit = tn->count = 0;
407       else
408           tn->limit += add;
409     }
410
411   foreach_vlib_main ()
412     {
413       tm = &this_vlib_main->trace_main;
414       tm->trace_enable = 1;
415     }
416
417   vlib_enable_disable_pkt_trace_filter (! !filter);
418 }
419
420 static clib_error_t *
421 cli_add_trace_buffer (vlib_main_t * vm,
422                       unformat_input_t * input, vlib_cli_command_t * cmd)
423 {
424   unformat_input_t _line_input, *line_input = &_line_input;
425   vlib_node_t *node;
426   u32 node_index, add;
427   u8 verbose = 0;
428   int filter = 0;
429   clib_error_t *error = 0;
430
431   if (!unformat_user (input, unformat_line_input, line_input))
432     return 0;
433
434   if (vnet_trace_placeholder == 0)
435     vec_validate_aligned (vnet_trace_placeholder, 2048,
436                           CLIB_CACHE_LINE_BYTES);
437
438   while (unformat_check_input (line_input) != (uword) UNFORMAT_END_OF_INPUT)
439     {
440       if (unformat (line_input, "%U %d",
441                     unformat_vlib_node, vm, &node_index, &add))
442         ;
443       else if (unformat (line_input, "verbose"))
444         verbose = 1;
445       else if (unformat (line_input, "filter"))
446         filter = 1;
447       else
448         {
449           error = clib_error_create ("expected NODE COUNT, got `%U'",
450                                      format_unformat_error, line_input);
451           goto done;
452         }
453     }
454
455   node = vlib_get_node (vm, node_index);
456
457   if ((node->flags & VLIB_NODE_FLAG_TRACE_SUPPORTED) == 0)
458     {
459       error = clib_error_create ("node '%U' doesn't support per-node "
460                                  "tracing. There may be another way to "
461                                  "initiate trace on this node.",
462                                  format_vlib_node_name, vm, node_index);
463       goto done;
464     }
465
466   trace_update_capture_options (add, node_index, filter, verbose);
467
468 done:
469   unformat_free (line_input);
470
471   return error;
472 }
473
474 /* *INDENT-OFF* */
475 VLIB_CLI_COMMAND (add_trace_cli,static) = {
476   .path = "trace add",
477   .short_help = "trace add <input-graph-node> <add'l-pkts-for-node-> [filter] [verbose]",
478   .function = cli_add_trace_buffer,
479 };
480 /* *INDENT-ON* */
481
482 /*
483  * Configure a filter for packet traces.
484  *
485  * This supplements the packet trace feature so that only packets matching
486  * the filter are included in the trace. Currently the only filter is to
487  * keep packets that include a certain node in the trace or exclude a certain
488  * node in the trace.
489  *
490  * The count of traced packets in the "trace add" command is still used to
491  * create a certain number of traces. The "trace filter" command specifies
492  * how many of those packets should be retained in the trace.
493  *
494  * For example, 1Mpps of traffic is arriving and one of those packets is being
495  * dropped. To capture the trace for only that dropped packet, you can do:
496  *     trace filter include error-drop 1
497  *     trace add dpdk-input 1000000
498  *     <wait one second>
499  *     show trace
500  *
501  * Note that the filter could be implemented by capturing all traces and just
502  * reducing traces displayed by the "show trace" function. But that would
503  * require a lot of memory for storing the traces, making that infeasible.
504  *
505  * To remove traces from the trace pool that do not include a certain node
506  * requires that the trace be "complete" before applying the filter. To
507  * accomplish this, the trace pool is filtered upon each iteraction of the
508  * main vlib loop. Doing so keeps the number of allocated traces down to a
509  * reasonably low number. This requires that tracing for a buffer is not
510  * performed after the vlib main loop interation completes. i.e. you can't
511  * save away a buffer temporarily then inject it back into the graph and
512  * expect that the trace_index is still valid (such as a traffic manager might
513  * do). A new trace buffer should be allocated for those types of packets.
514  *
515  * The filter can be extended to support multiple nodes and other match
516  * criteria (e.g. input sw_if_index, mac address) but for now just checks if
517  * a specified node is in the trace or not in the trace.
518  */
519
520 void
521 trace_filter_set (u32 node_index, u32 flag, u32 count)
522 {
523   foreach_vlib_main ()
524     {
525       vlib_trace_main_t *tm;
526
527       tm = &this_vlib_main->trace_main;
528       tm->filter_node_index = node_index;
529       tm->filter_flag = flag;
530       tm->filter_count = count;
531
532       /*
533        * Clear the trace limits to stop any in-progress tracing
534        * Prevents runaway trace allocations when the filter changes
535        * (or is removed)
536        */
537       vec_free (tm->nodes);
538     }
539 }
540
541
542 static clib_error_t *
543 cli_filter_trace (vlib_main_t * vm,
544                   unformat_input_t * input, vlib_cli_command_t * cmd)
545 {
546   u32 filter_node_index;
547   u32 filter_flag;
548   u32 filter_count;
549
550   if (unformat (input, "include %U %d",
551                 unformat_vlib_node, vm, &filter_node_index, &filter_count))
552     {
553       filter_flag = FILTER_FLAG_INCLUDE;
554     }
555   else if (unformat (input, "exclude %U %d",
556                      unformat_vlib_node, vm, &filter_node_index,
557                      &filter_count))
558     {
559       filter_flag = FILTER_FLAG_EXCLUDE;
560     }
561   else if (unformat (input, "none"))
562     {
563       filter_flag = FILTER_FLAG_NONE;
564       filter_node_index = 0;
565       filter_count = 0;
566     }
567   else
568     return
569       clib_error_create
570       ("expected 'include NODE COUNT' or 'exclude NODE COUNT' or 'none', got `%U'",
571        format_unformat_error, input);
572
573   trace_filter_set (filter_node_index, filter_flag, filter_count);
574
575   return 0;
576 }
577
578 /* *INDENT-OFF* */
579 VLIB_CLI_COMMAND (filter_trace_cli,static) = {
580   .path = "trace filter",
581   .short_help = "trace filter none | [include|exclude] NODE COUNT",
582   .function = cli_filter_trace,
583 };
584 /* *INDENT-ON* */
585
586 static clib_error_t *
587 cli_clear_trace_buffer (vlib_main_t * vm,
588                         unformat_input_t * input, vlib_cli_command_t * cmd)
589 {
590   vlib_trace_stop_and_clear ();
591   return 0;
592 }
593
594 /* *INDENT-OFF* */
595 VLIB_CLI_COMMAND (clear_trace_cli,static) = {
596   .path = "clear trace",
597   .short_help = "Clear trace buffer and free memory",
598   .function = cli_clear_trace_buffer,
599 };
600 /* *INDENT-ON* */
601
602 /* Placeholder function to get us linked in. */
603 void
604 vlib_trace_cli_reference (void)
605 {
606 }
607
608 void *
609 vlib_add_trace (vlib_main_t * vm,
610                 vlib_node_runtime_t * r, vlib_buffer_t * b, u32 n_data_bytes)
611 {
612   return vlib_add_trace_inline (vm, r, b, n_data_bytes);
613 }
614
615 vlib_is_packet_traced_fn_t *
616 vlib_is_packet_traced_function_from_name (const char *name)
617 {
618   vlib_trace_filter_function_registration_t *reg =
619     vlib_trace_filter_main.trace_filter_registration;
620   while (reg)
621     {
622       if (clib_strcmp (reg->name, name) == 0)
623         break;
624       reg = reg->next;
625     }
626   if (!reg)
627     return 0;
628   return reg->function;
629 }
630
631 vlib_is_packet_traced_fn_t *
632 vlib_is_packet_traced_default_function ()
633 {
634   vlib_trace_filter_function_registration_t *reg =
635     vlib_trace_filter_main.trace_filter_registration;
636   vlib_trace_filter_function_registration_t *tmp_reg = reg;
637   while (reg)
638     {
639       if (reg->priority > tmp_reg->priority)
640         tmp_reg = reg;
641       reg = reg->next;
642     }
643   return tmp_reg->function;
644 }
645
646 static clib_error_t *
647 vlib_trace_filter_function_init (vlib_main_t *vm)
648 {
649   vlib_is_packet_traced_fn_t *default_fn =
650     vlib_is_packet_traced_default_function ();
651   foreach_vlib_main ()
652     {
653       vlib_trace_main_t *tm = &this_vlib_main->trace_main;
654       tm->current_trace_filter_function = default_fn;
655     }
656   return 0;
657 }
658
659 vlib_trace_filter_main_t vlib_trace_filter_main;
660
661 VLIB_INIT_FUNCTION (vlib_trace_filter_function_init);
662
663 static clib_error_t *
664 show_trace_filter_function (vlib_main_t *vm, unformat_input_t *input,
665                             vlib_cli_command_t *cmd)
666 {
667   vlib_trace_filter_main_t *tfm = &vlib_trace_filter_main;
668   vlib_trace_main_t *tm = &vm->trace_main;
669   vlib_is_packet_traced_fn_t *current_trace_filter_fn =
670     tm->current_trace_filter_function;
671   vlib_trace_filter_function_registration_t *reg =
672     tfm->trace_filter_registration;
673
674   while (reg)
675     {
676       vlib_cli_output (vm, "%sname:%s description: %s priority: %u",
677                        reg->function == current_trace_filter_fn ? "(*) " : "",
678                        reg->name, reg->description, reg->priority);
679       reg = reg->next;
680     }
681   return 0;
682 }
683
684 VLIB_CLI_COMMAND (show_trace_filter_function_cli, static) = {
685   .path = "show trace filter function",
686   .short_help = "show trace filter function",
687   .function = show_trace_filter_function,
688 };
689
690 uword
691 unformat_vlib_trace_filter_function (unformat_input_t *input, va_list *args)
692 {
693   vlib_is_packet_traced_fn_t **res =
694     va_arg (*args, vlib_is_packet_traced_fn_t **);
695   vlib_trace_filter_main_t *tfm = &vlib_trace_filter_main;
696
697   vlib_trace_filter_function_registration_t *reg =
698     tfm->trace_filter_registration;
699   while (reg)
700     {
701       if (unformat (input, reg->name))
702         {
703           *res = reg->function;
704           return 1;
705         }
706       reg = reg->next;
707     }
708   return 0;
709 }
710
711 void
712 vlib_set_trace_filter_function (vlib_is_packet_traced_fn_t *x)
713 {
714   foreach_vlib_main ()
715     {
716       this_vlib_main->trace_main.current_trace_filter_function = x;
717     }
718 }
719
720 static clib_error_t *
721 set_trace_filter_function (vlib_main_t *vm, unformat_input_t *input,
722                            vlib_cli_command_t *cmd)
723 {
724   unformat_input_t _line_input, *line_input = &_line_input;
725   vlib_is_packet_traced_fn_t *res = 0;
726   clib_error_t *error = 0;
727
728   if (!unformat_user (input, unformat_line_input, line_input))
729     return 0;
730
731   while (unformat_check_input (line_input) != (uword) UNFORMAT_END_OF_INPUT)
732     {
733       if (unformat (line_input, "%U", unformat_vlib_trace_filter_function,
734                     &res))
735         ;
736       else
737         {
738           error = clib_error_create (
739             "expected valid trace filter function, got `%U'",
740             format_unformat_error, line_input);
741           goto done;
742         }
743     }
744   vlib_set_trace_filter_function (res);
745
746 done:
747   unformat_free (line_input);
748
749   return error;
750 }
751
752 VLIB_CLI_COMMAND (set_trace_filter_function_cli, static) = {
753   .path = "set trace filter function",
754   .short_help = "set trace filter function <func_name>",
755   .function = set_trace_filter_function,
756 };
757 /*
758  * fd.io coding-style-patch-verification: ON
759  *
760  * Local Variables:
761  * eval: (c-set-style "gnu")
762  * End:
763  */