This is the mail archive of the gdb-patches@sourceware.org mailing list for the GDB project.


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]
Other format: [Raw text]

Re: [PATCH] Conditional tracepoints


Here's what I committed for conditional tracepoints. On to the next piece!

Stan

2009-07-14 Stan Shebs <stan@codesourcery.com>

   Conditional tracepoints.
   * ax-gdb.h (gen_eval_for_expr): Declare.
   * ax-gdb.c (gen_expr): Generate bytecodes for BINOP_EQUAL
   and other comparisons.
   (gen_eval_for_expr): New function.
   (agent_eval_command): New maintenance command.
   (_initialize_ax_gdb): Define the command.
   * remote.c (struct remote_state): New field cond_tracepoints.
   (PACKET_ConditionalTracepoints): New packet config type.
   (remote_cond_tracepoint_feature): New function.
   (remote_protocol_features): Add ConditionalTracepoints.
   (remote_supports_cond_tracepoints): New function.
   (_initialize_remote): Add ConditionalTracepoints.
   * tracepoint.c (download_tracepoint): Add conditional.
   * NEWS: Mention conditional tracepoints.

   * gdb.texinfo (Tracepoint Conditions): New section.
   (General Query Packets): Describe ConditionalTracepoints.
   (Tracepoint Packets): Describe condition field.
   (Maintenance Commands): Describe maint agent-eval.
   * agentexpr.texi (Using Agent Expressions): Mention eval usage.

* gdb.trace/tracecmd.exp: Add basic test of tracepoint conditions.

Index: NEWS
===================================================================
RCS file: /cvs/src/src/gdb/NEWS,v
retrieving revision 1.317
diff -p -r1.317 NEWS
*** NEWS	11 Jul 2009 14:04:23 -0000	1.317
--- NEWS	14 Jul 2009 21:14:15 -0000
***************
*** 3,8 ****
--- 3,14 ----
  
  *** Changes since GDB 6.8
  
+ * Tracepoints may now be conditional.  The syntax is as for
+ breakpoints; either an "if" clause appended to the "trace" command,
+ or the "condition" command is available.  GDB sends the condition to
+ the target for evaluation using the same bytecode format as is used
+ for tracepoint actions.
+ 
  * "disassemble" command with a /r modifier, print the raw instructions
  in hex as well as in symbolic form."
  
Index: ax-gdb.c
===================================================================
RCS file: /cvs/src/src/gdb/ax-gdb.c,v
retrieving revision 1.52
diff -p -r1.52 ax-gdb.c
*** ax-gdb.c	2 Jul 2009 12:20:17 -0000	1.52
--- ax-gdb.c	14 Jul 2009 21:14:15 -0000
*************** gen_expr (struct expression *exp, union 
*** 1468,1473 ****
--- 1468,1479 ----
      case BINOP_BITWISE_AND:
      case BINOP_BITWISE_IOR:
      case BINOP_BITWISE_XOR:
+     case BINOP_EQUAL:
+     case BINOP_NOTEQUAL:
+     case BINOP_LESS:
+     case BINOP_GTR:
+     case BINOP_LEQ:
+     case BINOP_GEQ:
        (*pc)++;
        gen_expr (exp, pc, ax, &value1);
        gen_usual_unary (exp, ax, &value1);
*************** gen_expr (struct expression *exp, union 
*** 1537,1542 ****
--- 1543,1589 ----
  		     aop_bit_xor, aop_bit_xor, 0, "bitwise exclusive-or");
  	  break;
  
+ 	case BINOP_EQUAL:
+ 	  gen_binop (ax, value, &value1, &value2,
+ 		     aop_equal, aop_equal, 0, "equal");
+ 	  break;
+ 
+ 	case BINOP_NOTEQUAL:
+ 	  gen_binop (ax, value, &value1, &value2,
+ 		     aop_equal, aop_equal, 0, "equal");
+ 	  gen_logical_not (ax, value,
+ 			   language_bool_type (exp->language_defn,
+ 					       exp->gdbarch));
+ 	  break;
+ 
+ 	case BINOP_LESS:
+ 	  gen_binop (ax, value, &value1, &value2,
+ 		     aop_less_signed, aop_less_unsigned, 0, "less than");
+ 	  break;
+ 
+ 	case BINOP_GTR:
+ 	  ax_simple (ax, aop_swap);
+ 	  gen_binop (ax, value, &value1, &value2,
+ 		     aop_less_signed, aop_less_unsigned, 0, "less than");
+ 	  break;
+ 
+ 	case BINOP_LEQ:
+ 	  ax_simple (ax, aop_swap);
+ 	  gen_binop (ax, value, &value1, &value2,
+ 		     aop_less_signed, aop_less_unsigned, 0, "less than");
+ 	  gen_logical_not (ax, value,
+ 			   language_bool_type (exp->language_defn,
+ 					       exp->gdbarch));
+ 	  break;
+ 
+ 	case BINOP_GEQ:
+ 	  gen_binop (ax, value, &value1, &value2,
+ 		     aop_less_signed, aop_less_unsigned, 0, "less than");
+ 	  gen_logical_not (ax, value,
+ 			   language_bool_type (exp->language_defn,
+ 					       exp->gdbarch));
+ 	  break;
+ 
  	default:
  	  /* We should only list operators in the outer case statement
  	     that we actually handle in the inner case statement.  */
*************** gen_trace_for_expr (CORE_ADDR scope, str
*** 1756,1761 ****
--- 1803,1839 ----
    return ax;
  }
  
+ /* Given a GDB expression EXPR, return a bytecode sequence that will
+    evaluate and return a result.  The bytecodes will do a direct
+    evaluation, using the current data on the target, rather than
+    recording blocks of memory and registers for later use, as
+    gen_trace_for_expr does.  The generated bytecode sequence leaves
+    the result of expression evaluation on the top of the stack.  */
+ 
+ struct agent_expr *
+ gen_eval_for_expr (CORE_ADDR scope, struct expression *expr)
+ {
+   struct cleanup *old_chain = 0;
+   struct agent_expr *ax = new_agent_expr (scope);
+   union exp_element *pc;
+   struct axs_value value;
+ 
+   old_chain = make_cleanup_free_agent_expr (ax);
+ 
+   pc = expr->elts;
+   trace_kludge = 0;
+   gen_expr (expr, &pc, ax, &value);
+ 
+   /* Oh, and terminate.  */
+   ax_simple (ax, aop_end);
+ 
+   /* We have successfully built the agent expr, so cancel the cleanup
+      request.  If we add more cleanups that we always want done, this
+      will have to get more complicated.  */
+   discard_cleanups (old_chain);
+   return ax;
+ }
+ 
  static void
  agent_command (char *exp, int from_tty)
  {
*************** agent_command (char *exp, int from_tty)
*** 1786,1791 ****
--- 1864,1904 ----
    do_cleanups (old_chain);
    dont_repeat ();
  }
+ 
+ /* Parse the given expression, compile it into an agent expression
+    that does direct evaluation, and display the resulting
+    expression.  */
+ 
+ static void
+ agent_eval_command (char *exp, int from_tty)
+ {
+   struct cleanup *old_chain = 0;
+   struct expression *expr;
+   struct agent_expr *agent;
+   struct frame_info *fi = get_current_frame ();	/* need current scope */
+ 
+   /* We don't deal with overlay debugging at the moment.  We need to
+      think more carefully about this.  If you copy this code into
+      another command, change the error message; the user shouldn't
+      have to know anything about agent expressions.  */
+   if (overlay_debugging)
+     error (_("GDB can't do agent expression translation with overlays."));
+ 
+   if (exp == 0)
+     error_no_arg (_("expression to translate"));
+ 
+   expr = parse_expression (exp);
+   old_chain = make_cleanup (free_current_contents, &expr);
+   agent = gen_eval_for_expr (get_frame_pc (fi), expr);
+   make_cleanup_free_agent_expr (agent);
+   ax_print (gdb_stdout, agent);
+ 
+   /* It would be nice to call ax_reqs here to gather some general info
+      about the expression, and then print out the result.  */
+ 
+   do_cleanups (old_chain);
+   dont_repeat ();
+ }
  
  
  /* Initialization code.  */
*************** void
*** 1795,1800 ****
  _initialize_ax_gdb (void)
  {
    add_cmd ("agent", class_maintenance, agent_command,
! 	   _("Translate an expression into remote agent bytecode."),
  	   &maintenancelist);
  }
--- 1908,1917 ----
  _initialize_ax_gdb (void)
  {
    add_cmd ("agent", class_maintenance, agent_command,
! 	   _("Translate an expression into remote agent bytecode for tracing."),
! 	   &maintenancelist);
! 
!   add_cmd ("agent-eval", class_maintenance, agent_eval_command,
! 	   _("Translate an expression into remote agent bytecode for evaluation."),
  	   &maintenancelist);
  }
Index: ax-gdb.h
===================================================================
RCS file: /cvs/src/src/gdb/ax-gdb.h,v
retrieving revision 1.11
diff -p -r1.11 ax-gdb.h
*** ax-gdb.h	3 Jan 2009 05:57:50 -0000	1.11
--- ax-gdb.h	14 Jul 2009 21:14:15 -0000
*************** struct axs_value
*** 99,102 ****
--- 99,104 ----
     function to discover which registers the expression uses.  */
  extern struct agent_expr *gen_trace_for_expr (CORE_ADDR, struct expression *);
  
+ extern struct agent_expr *gen_eval_for_expr (CORE_ADDR, struct expression *);
+ 
  #endif /* AX_GDB_H */
Index: remote.c
===================================================================
RCS file: /cvs/src/src/gdb/remote.c,v
retrieving revision 1.363
diff -p -r1.363 remote.c
*** remote.c	14 Jul 2009 14:53:41 -0000	1.363
--- remote.c	14 Jul 2009 21:14:16 -0000
*************** struct remote_state
*** 294,299 ****
--- 294,302 ----
  
    /* True if the stub reports support for vCont;t.  */
    int support_vCont_t;
+ 
+   /* True if the stub reports support for conditional tracepoints.  */
+   int cond_tracepoints;
  };
  
  /* Returns true if the multi-process extensions are in effect.  */
*************** enum {
*** 993,998 ****
--- 996,1002 ----
    PACKET_qXfer_siginfo_read,
    PACKET_qXfer_siginfo_write,
    PACKET_qAttached,
+   PACKET_ConditionalTracepoints,
    PACKET_MAX
  };
  
*************** remote_non_stop_feature (const struct pr
*** 3015,3020 ****
--- 3019,3033 ----
    rs->non_stop_aware = (support == PACKET_ENABLE);
  }
  
+ static void
+ remote_cond_tracepoint_feature (const struct protocol_feature *feature,
+ 				       enum packet_support support,
+ 				       const char *value)
+ {
+   struct remote_state *rs = get_remote_state ();
+   rs->cond_tracepoints = (support == PACKET_ENABLE);
+ }
+ 
  static struct protocol_feature remote_protocol_features[] = {
    { "PacketSize", PACKET_DISABLE, remote_packet_size, -1 },
    { "qXfer:auxv:read", PACKET_DISABLE, remote_supported_packet,
*************** static struct protocol_feature remote_pr
*** 3041,3046 ****
--- 3054,3061 ----
      PACKET_qXfer_siginfo_read },
    { "qXfer:siginfo:write", PACKET_DISABLE, remote_supported_packet,
      PACKET_qXfer_siginfo_write },
+   { "ConditionalTracepoints", PACKET_DISABLE, remote_cond_tracepoint_feature,
+     PACKET_ConditionalTracepoints },
  };
  
  static void
*************** remote_supports_multi_process (void)
*** 8740,8745 ****
--- 8755,8767 ----
    return remote_multi_process_p (rs);
  }
  
+ int
+ remote_supports_cond_tracepoints (void)
+ {
+   struct remote_state *rs = get_remote_state ();
+   return rs->cond_tracepoints;
+ }
+ 
  static void
  init_remote_ops (void)
  {
*************** Show the maximum size of the address (in
*** 9183,9188 ****
--- 9205,9213 ----
    add_packet_config_cmd (&remote_protocol_packets[PACKET_qAttached],
  			 "qAttached", "query-attached", 0);
  
+   add_packet_config_cmd (&remote_protocol_packets[PACKET_ConditionalTracepoints],
+ 			 "ConditionalTracepoints", "conditional-tracepoints", 0);
+ 
    /* Keep the old ``set remote Z-packet ...'' working.  Each individual
       Z sub-packet has its own set and show commands, but users may
       have sets to this variable in their .gdbinit files (or in their
Index: tracepoint.c
===================================================================
RCS file: /cvs/src/src/gdb/tracepoint.c,v
retrieving revision 1.125
diff -p -r1.125 tracepoint.c
*** tracepoint.c	2 Jul 2009 17:21:07 -0000	1.125
--- tracepoint.c	14 Jul 2009 21:14:16 -0000
***************
*** 33,38 ****
--- 33,39 ----
  #include "breakpoint.h"
  #include "tracepoint.h"
  #include "remote.h"
+ extern int remote_supports_cond_tracepoints (void);
  #include "linespec.h"
  #include "regcache.h"
  #include "completer.h"
*************** download_tracepoint (struct breakpoint *
*** 1311,1322 ****
--- 1312,1342 ----
    char **stepping_actions;
    int ndx;
    struct cleanup *old_chain = NULL;
+   struct agent_expr *aexpr;
+   struct cleanup *aexpr_chain = NULL;
  
    sprintf_vma (tmp, (t->loc ? t->loc->address : 0));
    sprintf (buf, "QTDP:%x:%s:%c:%lx:%x", t->number, 
  	   tmp, /* address */
  	   (t->enable_state == bp_enabled ? 'E' : 'D'),
  	   t->step_count, t->pass_count);
+   /* If the tracepoint has a conditional, make it into an agent
+      expression and append to the definition.  */
+   if (t->loc->cond)
+     {
+       /* Only test support at download time, we may not know target
+ 	 capabilities at definition time.  */
+       if (remote_supports_cond_tracepoints ())
+ 	{
+ 	  aexpr = gen_eval_for_expr (t->loc->address, t->loc->cond);
+ 	  aexpr_chain = make_cleanup_free_agent_expr (aexpr);
+ 	  sprintf (buf + strlen (buf), ":X%x,", aexpr->len);
+ 	  mem2hex (aexpr->buf, buf + strlen (buf), aexpr->len);
+ 	  do_cleanups (aexpr_chain);
+ 	}
+       else
+ 	warning (_("Target does not support conditional tracepoints, ignoring tp %d cond"), t->number);
+     }
  
    if (t->actions)
      strcat (buf, "-");
Index: doc/agentexpr.texi
===================================================================
RCS file: /cvs/src/src/gdb/doc/agentexpr.texi,v
retrieving revision 1.7
diff -p -r1.7 agentexpr.texi
*** doc/agentexpr.texi	20 Jan 2007 16:51:56 -0000	1.7
--- doc/agentexpr.texi	14 Jul 2009 21:14:17 -0000
***************
*** 7,19 ****
  
  @c This file is part of the GDB manual.
  @c
! @c Copyright (C) 2003, 2004, 2005, 2006
  @c               Free Software Foundation, Inc.
  @c
  @c See the file gdb.texinfo for copying conditions.
  
- @c Revision: $Id: agentexpr.texi,v 1.2 1998/12/09 21:23:46 jimb Exp $
- 
  @node Agent Expressions
  @appendix The GDB Agent Expression Mechanism
  
--- 7,17 ----
  
  @c This file is part of the GDB manual.
  @c
! @c Copyright (C) 2003, 2004, 2005, 2006, 2009
  @c               Free Software Foundation, Inc.
  @c
  @c See the file gdb.texinfo for copying conditions.
  
  @node Agent Expressions
  @appendix The GDB Agent Expression Mechanism
  
*************** address, and the top of the stack is the
*** 473,480 ****
  @node Using Agent Expressions
  @section Using Agent Expressions
  
! Here is a sketch of a full non-stop debugging cycle, showing how agent
! expressions fit into the process.
  
  @itemize @bullet
  
--- 471,490 ----
  @node Using Agent Expressions
  @section Using Agent Expressions
  
! Agent expressions can be used in several different ways by @value{GDBN},
! and the debugger can generate different bytecode sequences as appropriate.
! 
! One possibility is to do expression evaluation on the target rather
! than the host, such as for the conditional of a conditional
! tracepoint.  In such a case, @value{GDBN} compiles the source
! expression into a bytecode sequence that simply gets values from
! registers or memory, does arithmetic, and returns a result.
! 
! Another way to use agent expressions is for tracepoint data
! collection.  @value{GDBN} generates a different bytecode sequence for
! collection; in addition to bytecodes that do the calculation,
! @value{GDBN} adds @code{trace} bytecodes to save the pieces of
! memory that were used.
  
  @itemize @bullet
  
Index: doc/gdb.texinfo
===================================================================
RCS file: /cvs/src/src/gdb/doc/gdb.texinfo,v
retrieving revision 1.606
diff -p -r1.606 gdb.texinfo
*** doc/gdb.texinfo	11 Jul 2009 14:09:16 -0000	1.606
--- doc/gdb.texinfo	14 Jul 2009 21:14:21 -0000
*************** conditions and actions.
*** 8932,8937 ****
--- 8932,8938 ----
  * Create and Delete Tracepoints::
  * Enable and Disable Tracepoints::
  * Tracepoint Passcounts::
+ * Tracepoint Conditions::
  * Tracepoint Actions::
  * Listing Tracepoints::
  * Starting and Stopping Trace Experiments::
*************** Here are some examples of using the @cod
*** 8971,8976 ****
--- 8972,8984 ----
  @noindent
  You can abbreviate @code{trace} as @code{tr}.
  
+ @item trace @var{location} if @var{cond}
+ Set a tracepoint with condition @var{cond}; evaluate the expression
+ @var{cond} each time the tracepoint is reached, and collect data only
+ if the value is nonzero---that is, if @var{cond} evaluates as true.
+ @xref{Tracepoint Conditions, ,Tracepoint Conditions}, for more
+ information on tracepoint conditions.
+ 
  @vindex $tpnum
  @cindex last tracepoint number
  @cindex recent tracepoint number
*************** Examples:
*** 9053,9058 ****
--- 9061,9104 ----
  @end smallexample
  @end table
  
+ @node Tracepoint Conditions
+ @subsection Tracepoint Conditions
+ @cindex conditional tracepoints
+ @cindex tracepoint conditions
+ 
+ The simplest sort of tracepoint collects data every time your program
+ reaches a specified place.  You can also specify a @dfn{condition} for
+ a tracepoint.  A condition is just a Boolean expression in your
+ programming language (@pxref{Expressions, ,Expressions}).  A
+ tracepoint with a condition evaluates the expression each time your
+ program reaches it, and data collection happens only if the condition
+ is true.
+ 
+ Tracepoint conditions can be specified when a tracepoint is set, by
+ using @samp{if} in the arguments to the @code{trace} command.
+ @xref{Create and Delete Tracepoints, ,Setting Tracepoints}.  They can
+ also be set or changed at any time with the @code{condition} command,
+ just as with breakpoints.
+ 
+ Unlike breakpoint conditions, @value{GDBN} does not actually evaluate
+ the conditional expression itself.  Instead, @value{GDBN} encodes the
+ expression into an agent expression (@pxref{Agent Expressions}
+ suitable for execution on the target, independently of @value{GDBN}.
+ Global variables become raw memory locations, locals become stack
+ accesses, and so forth.
+ 
+ For instance, suppose you have a function that is usually called
+ frequently, but should not be called after an error has occurred.  You
+ could use the following tracepoint command to collect data about calls
+ of that function that happen while the error code is propagating
+ through the program; an unconditional tracepoint could end up
+ collecting thousands of useless trace frames that you would have to
+ search through.
+ 
+ @smallexample
+ (@value{GDBP}) @kbd{trace normal_operation if errcode > 0}
+ @end smallexample
+ 
  @node Tracepoint Actions
  @subsection Tracepoint Action Lists
  
*************** messages, see @ref{Debugging Output}.)
*** 26534,26543 ****
  
  @table @code
  @kindex maint agent
  @item maint agent @var{expression}
  Translate the given @var{expression} into remote agent bytecodes.
  This command is useful for debugging the Agent Expression mechanism
! (@pxref{Agent Expressions}).
  
  @kindex maint info breakpoints
  @item @anchor{maint info breakpoints}maint info breakpoints
--- 26580,26598 ----
  
  @table @code
  @kindex maint agent
+ @kindex maint agent-eval
  @item maint agent @var{expression}
+ @itemx maint agent-eval @var{expression}
  Translate the given @var{expression} into remote agent bytecodes.
  This command is useful for debugging the Agent Expression mechanism
! (@pxref{Agent Expressions}).  The @samp{agent} version produces an
! expression useful for data collection, such as by tracepoints, while
! @samp{maint agent-eval} produces an expression that evaluates directly
! to a result.  For instance, a collection expression for @code{globa +
! globb} will include bytecodes to record four bytes of memory at each
! of the addresses of @code{globa} and @code{globb}, while discarding
! the result of the addition, while an evaluation expression will do the
! addition and return the sum.
  
  @kindex maint info breakpoints
  @item @anchor{maint info breakpoints}maint info breakpoints
*************** These are the currently defined stub fea
*** 28415,28420 ****
--- 28470,28480 ----
  @tab @samp{-}
  @tab No
  
+ @item @samp{ConditionalTracepoints}
+ @tab No
+ @tab @samp{-}
+ @tab No
+ 
  @end multitable
  
  These are the currently defined stub features, in more detail:
*************** indicated it supports them in its @samp{
*** 28492,28497 ****
--- 28552,28561 ----
  The remote stub understands the @samp{qXfer:osdata:read} packet
  ((@pxref{qXfer osdata read}).
  
+ @item ConditionalTracepoints
+ The remote stub accepts and implements conditional expressions defined
+ for tracepoints (@pxref{Tracepoint Conditions}).
+ 
  @end table
  
  @item qSymbol::
*************** tracepoints (@pxref{Tracepoints}).
*** 28804,28814 ****
  
  @table @samp
  
! @item QTDP:@var{n}:@var{addr}:@var{ena}:@var{step}:@var{pass}@r{[}-@r{]}
  Create a new tracepoint, number @var{n}, at @var{addr}.  If @var{ena}
  is @samp{E}, then the tracepoint is enabled; if it is @samp{D}, then
  the tracepoint is disabled.  @var{step} is the tracepoint's step
! count, and @var{pass} is its pass count.  If the trailing @samp{-} is
  present, further @samp{QTDP} packets will follow to specify this
  tracepoint's actions.
  
--- 28868,28881 ----
  
  @table @samp
  
! @item QTDP:@var{n}:@var{addr}:@var{ena}:@var{step}:@var{pass}[:X@var{len},@var{bytes}]@r{[}-@r{]}
  Create a new tracepoint, number @var{n}, at @var{addr}.  If @var{ena}
  is @samp{E}, then the tracepoint is enabled; if it is @samp{D}, then
  the tracepoint is disabled.  @var{step} is the tracepoint's step
! count, and @var{pass} is its pass count.  If an @samp{X} is present,
! it introduces a tracepoint condition, which consists of a hexadecimal
! length, followed by a comma and hex-encoded bytes, in a manner similar
! to action encodings as described below.  If the trailing @samp{-} is
  present, further @samp{QTDP} packets will follow to specify this
  tracepoint's actions.
  
Index: testsuite/gdb.trace/tracecmd.exp
===================================================================
RCS file: /cvs/src/src/gdb/testsuite/gdb.trace/tracecmd.exp,v
retrieving revision 1.12
diff -p -r1.12 tracecmd.exp
*** testsuite/gdb.trace/tracecmd.exp	9 Jun 2009 17:12:43 -0000	1.12
--- testsuite/gdb.trace/tracecmd.exp	14 Jul 2009 21:14:23 -0000
*************** gdb_test "trace" "No default breakpoint 
*** 153,159 ****
  # deferred to limits test module
  
  # 1.11 tracepoint conditions
! # conditions on tracepoints not implemented
  
  # 1.12 set tracepoint in prologue
  # [see tfind.exp]
--- 153,164 ----
  # deferred to limits test module
  
  # 1.11 tracepoint conditions
! gdb_delete_tracepoints
! gdb_test "trace gdb_recursion_test if q1 > 0" \
! 	"Tracepoint $decimal at $hex: file.*$srcfile, line $testline1." \
! 	"1.11a: conditional tracepoint"
! gdb_test "info trace" "in gdb_recursion_test.*$srcfile:$testline1.*trace only if q1 > 0" \
! 	"1.11b: verify conditional tracepoint"
  
  # 1.12 set tracepoint in prologue
  # [see tfind.exp]

Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]