001/*
002 * Copyright 2008-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2017 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.IOException;
026import java.io.OutputStream;
027import java.io.Serializable;
028import java.text.ParseException;
029import java.util.ArrayList;
030import java.util.LinkedHashMap;
031import java.util.LinkedHashSet;
032import java.util.List;
033import java.util.StringTokenizer;
034import java.util.concurrent.CyclicBarrier;
035import java.util.concurrent.Semaphore;
036import java.util.concurrent.atomic.AtomicBoolean;
037import java.util.concurrent.atomic.AtomicLong;
038
039import com.unboundid.ldap.sdk.Control;
040import com.unboundid.ldap.sdk.LDAPConnection;
041import com.unboundid.ldap.sdk.LDAPConnectionOptions;
042import com.unboundid.ldap.sdk.LDAPException;
043import com.unboundid.ldap.sdk.ResultCode;
044import com.unboundid.ldap.sdk.SearchScope;
045import com.unboundid.ldap.sdk.Version;
046import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
047import com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl;
048import com.unboundid.ldap.sdk.controls.SortKey;
049import com.unboundid.util.ColumnFormatter;
050import com.unboundid.util.FixedRateBarrier;
051import com.unboundid.util.FormattableColumn;
052import com.unboundid.util.HorizontalAlignment;
053import com.unboundid.util.LDAPCommandLineTool;
054import com.unboundid.util.ObjectPair;
055import com.unboundid.util.OutputFormat;
056import com.unboundid.util.RateAdjustor;
057import com.unboundid.util.ResultCodeCounter;
058import com.unboundid.util.ThreadSafety;
059import com.unboundid.util.ThreadSafetyLevel;
060import com.unboundid.util.WakeableSleeper;
061import com.unboundid.util.ValuePattern;
062import com.unboundid.util.args.ArgumentException;
063import com.unboundid.util.args.ArgumentParser;
064import com.unboundid.util.args.BooleanArgument;
065import com.unboundid.util.args.ControlArgument;
066import com.unboundid.util.args.FileArgument;
067import com.unboundid.util.args.FilterArgument;
068import com.unboundid.util.args.IntegerArgument;
069import com.unboundid.util.args.ScopeArgument;
070import com.unboundid.util.args.StringArgument;
071
072import static com.unboundid.util.Debug.*;
073import static com.unboundid.util.StaticUtils.*;
074
075
076
077/**
078 * This class provides a tool that can be used to search an LDAP directory
079 * server repeatedly using multiple threads.  It can help provide an estimate of
080 * the search performance that a directory server is able to achieve.  Either or
081 * both of the base DN and the search filter may be a value pattern as
082 * described in the {@link ValuePattern} class.  This makes it possible to
083 * search over a range of entries rather than repeatedly performing searches
084 * with the same base DN and filter.
085 * <BR><BR>
086 * Some of the APIs demonstrated by this example include:
087 * <UL>
088 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
089 *       package)</LI>
090 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
091 *       package)</LI>
092 *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
093 *       package)</LI>
094 *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
095 * </UL>
096 * <BR><BR>
097 * All of the necessary information is provided using command line arguments.
098 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
099 * class, as well as the following additional arguments:
100 * <UL>
101 *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
102 *       for the searches.  This must be provided.  It may be a simple DN, or it
103 *       may be a value pattern to express a range of base DNs.</LI>
104 *   <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
105 *       search.  The scope value should be one of "base", "one", "sub", or
106 *       "subord".  If this isn't specified, then a scope of "sub" will be
107 *       used.</LI>
108 *   <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
109 *       the searches.  This must be provided.  It may be a simple filter, or it
110 *       may be a value pattern to express a range of filters.</LI>
111 *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
112 *       attribute that should be included in entries returned from the server.
113 *       If this is not provided, then all user attributes will be requested.
114 *       This may include special tokens that the server may interpret, like
115 *       "1.1" to indicate that no attributes should be returned, "*", for all
116 *       user attributes, or "+" for all operational attributes.  Multiple
117 *       attributes may be requested with multiple instances of this
118 *       argument.</LI>
119 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
120 *       concurrent threads to use when performing the searches.  If this is not
121 *       provided, then a default of one thread will be used.</LI>
122 *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
123 *       time in seconds between lines out output.  If this is not provided,
124 *       then a default interval duration of five seconds will be used.</LI>
125 *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
126 *       intervals for which to run.  If this is not provided, then it will
127 *       run forever.</LI>
128 *   <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of search
129 *       iterations that should be performed on a connection before that
130 *       connection is closed and replaced with a newly-established (and
131 *       authenticated, if appropriate) connection.</LI>
132 *   <LI>"-r {searches-per-second}" or "--ratePerSecond {searches-per-second}"
133 *       -- specifies the target number of searches to perform per second.  It
134 *       is still necessary to specify a sufficient number of threads for
135 *       achieving this rate.  If this option is not provided, then the tool
136 *       will run at the maximum rate for the specified number of threads.</LI>
137 *   <LI>"--variableRateData {path}" -- specifies the path to a file containing
138 *       information needed to allow the tool to vary the target rate over time.
139 *       If this option is not provided, then the tool will either use a fixed
140 *       target rate as specified by the "--ratePerSecond" argument, or it will
141 *       run at the maximum rate.</LI>
142 *   <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to
143 *       which sample data will be written illustrating and describing the
144 *       format of the file expected to be used in conjunction with the
145 *       "--variableRateData" argument.</LI>
146 *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
147 *       complete before beginning overall statistics collection.</LI>
148 *   <LI>"--timestampFormat {format}" -- specifies the format to use for
149 *       timestamps included before each output line.  The format may be one of
150 *       "none" (for no timestamps), "with-date" (to include both the date and
151 *       the time), or "without-date" (to include only time time).</LI>
152 *   <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied
153 *       authorization v2 control to request that the operation be processed
154 *       using an alternate authorization identity.  In this case, the bind DN
155 *       should be that of a user that has permission to use this control.  The
156 *       authorization identity may be a value pattern.</LI>
157 *   <LI>"-a" or "--asynchronous" -- Indicates that searches should be performed
158 *       in asynchronous mode, in which the client will not wait for a response
159 *       to a previous request before sending the next request.  Either the
160 *       "--ratePerSecond" or "--maxOutstandingRequests" arguments must be
161 *       provided to limit the number of outstanding requests.</LI>
162 *   <LI>"-O {num}" or "--maxOutstandingRequests {num}" -- Specifies the maximum
163 *       number of outstanding requests that will be allowed in asynchronous
164 *       mode.</LI>
165 *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
166 *       result codes for failed operations should not be displayed.</LI>
167 *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
168 *       display-friendly format.</LI>
169 * </UL>
170 */
171@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
172public final class SearchRate
173       extends LDAPCommandLineTool
174       implements Serializable
175{
176  /**
177   * The serial version UID for this serializable class.
178   */
179  private static final long serialVersionUID = 3345838530404592182L;
180
181
182
183  // Indicates whether a request has been made to stop running.
184  private final AtomicBoolean stopRequested;
185
186  // The argument used to indicate whether to operate in asynchronous mode.
187  private BooleanArgument asynchronousMode;
188
189  // The argument used to indicate whether to generate output in CSV format.
190  private BooleanArgument csvFormat;
191
192  // The argument used to indicate whether to suppress information about error
193  // result codes.
194  private BooleanArgument suppressErrors;
195
196  // The argument used to indicate that a generic control should be included in
197  // the request.
198  private ControlArgument control;
199
200  // The argument used to specify a variable rate file.
201  private FileArgument sampleRateFile;
202
203  // The argument used to specify a variable rate file.
204  private FileArgument variableRateData;
205
206  // Indicates that search requests should include the assertion request control
207  // with the specified filter.
208  private FilterArgument assertionFilter;
209
210  // The argument used to specify the collection interval.
211  private IntegerArgument collectionInterval;
212
213  // The argument used to specify the number of search iterations on a
214  // connection before it is closed and re-established.
215  private IntegerArgument iterationsBeforeReconnect;
216
217  // The argument used to specify the maximum number of outstanding asynchronous
218  // requests.
219  private IntegerArgument maxOutstandingRequests;
220
221  // The argument used to specify the number of intervals.
222  private IntegerArgument numIntervals;
223
224  // The argument used to specify the number of threads.
225  private IntegerArgument numThreads;
226
227  // The argument used to specify the seed to use for the random number
228  // generator.
229  private IntegerArgument randomSeed;
230
231  // The target rate of searches per second.
232  private IntegerArgument ratePerSecond;
233
234  // The argument used to indicate that the search should use the simple paged
235  // results control with the specified page size.
236  private IntegerArgument simplePageSize;
237
238  // The number of warm-up intervals to perform.
239  private IntegerArgument warmUpIntervals;
240
241  // The argument used to specify the scope for the searches.
242  private ScopeArgument scopeArg;
243
244  // The argument used to specify the attributes to return.
245  private StringArgument attributes;
246
247  // The argument used to specify the base DNs for the searches.
248  private StringArgument baseDN;
249
250  // The argument used to specify the filters for the searches.
251  private StringArgument filter;
252
253  // The argument used to specify the proxied authorization identity.
254  private StringArgument proxyAs;
255
256  // The argument used to request that the server sort the results with the
257  // specified order.
258  private StringArgument sortOrder;
259
260  // The argument used to specify the timestamp format.
261  private StringArgument timestampFormat;
262
263  // The thread currently being used to run the searchrate tool.
264  private volatile Thread runningThread;
265
266  // A wakeable sleeper that will be used to sleep between reporting intervals.
267  private final WakeableSleeper sleeper;
268
269
270
271  /**
272   * Parse the provided command line arguments and make the appropriate set of
273   * changes.
274   *
275   * @param  args  The command line arguments provided to this program.
276   */
277  public static void main(final String[] args)
278  {
279    final ResultCode resultCode = main(args, System.out, System.err);
280    if (resultCode != ResultCode.SUCCESS)
281    {
282      System.exit(resultCode.intValue());
283    }
284  }
285
286
287
288  /**
289   * Parse the provided command line arguments and make the appropriate set of
290   * changes.
291   *
292   * @param  args       The command line arguments provided to this program.
293   * @param  outStream  The output stream to which standard out should be
294   *                    written.  It may be {@code null} if output should be
295   *                    suppressed.
296   * @param  errStream  The output stream to which standard error should be
297   *                    written.  It may be {@code null} if error messages
298   *                    should be suppressed.
299   *
300   * @return  A result code indicating whether the processing was successful.
301   */
302  public static ResultCode main(final String[] args,
303                                final OutputStream outStream,
304                                final OutputStream errStream)
305  {
306    final SearchRate searchRate = new SearchRate(outStream, errStream);
307    return searchRate.runTool(args);
308  }
309
310
311
312  /**
313   * Creates a new instance of this tool.
314   *
315   * @param  outStream  The output stream to which standard out should be
316   *                    written.  It may be {@code null} if output should be
317   *                    suppressed.
318   * @param  errStream  The output stream to which standard error should be
319   *                    written.  It may be {@code null} if error messages
320   *                    should be suppressed.
321   */
322  public SearchRate(final OutputStream outStream, final OutputStream errStream)
323  {
324    super(outStream, errStream);
325
326    stopRequested = new AtomicBoolean(false);
327    sleeper = new WakeableSleeper();
328  }
329
330
331
332  /**
333   * Retrieves the name for this tool.
334   *
335   * @return  The name for this tool.
336   */
337  @Override()
338  public String getToolName()
339  {
340    return "searchrate";
341  }
342
343
344
345  /**
346   * Retrieves the description for this tool.
347   *
348   * @return  The description for this tool.
349   */
350  @Override()
351  public String getToolDescription()
352  {
353    return "Perform repeated searches against an " +
354           "LDAP directory server.";
355  }
356
357
358
359  /**
360   * Retrieves the version string for this tool.
361   *
362   * @return  The version string for this tool.
363   */
364  @Override()
365  public String getToolVersion()
366  {
367    return Version.NUMERIC_VERSION_STRING;
368  }
369
370
371
372  /**
373   * Indicates whether this tool should provide support for an interactive mode,
374   * in which the tool offers a mode in which the arguments can be provided in
375   * a text-driven menu rather than requiring them to be given on the command
376   * line.  If interactive mode is supported, it may be invoked using the
377   * "--interactive" argument.  Alternately, if interactive mode is supported
378   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
379   * interactive mode may be invoked by simply launching the tool without any
380   * arguments.
381   *
382   * @return  {@code true} if this tool supports interactive mode, or
383   *          {@code false} if not.
384   */
385  @Override()
386  public boolean supportsInteractiveMode()
387  {
388    return true;
389  }
390
391
392
393  /**
394   * Indicates whether this tool defaults to launching in interactive mode if
395   * the tool is invoked without any command-line arguments.  This will only be
396   * used if {@link #supportsInteractiveMode()} returns {@code true}.
397   *
398   * @return  {@code true} if this tool defaults to using interactive mode if
399   *          launched without any command-line arguments, or {@code false} if
400   *          not.
401   */
402  @Override()
403  public boolean defaultsToInteractiveMode()
404  {
405    return true;
406  }
407
408
409
410  /**
411   * Indicates whether this tool should provide arguments for redirecting output
412   * to a file.  If this method returns {@code true}, then the tool will offer
413   * an "--outputFile" argument that will specify the path to a file to which
414   * all standard output and standard error content will be written, and it will
415   * also offer a "--teeToStandardOut" argument that can only be used if the
416   * "--outputFile" argument is present and will cause all output to be written
417   * to both the specified output file and to standard output.
418   *
419   * @return  {@code true} if this tool should provide arguments for redirecting
420   *          output to a file, or {@code false} if not.
421   */
422  @Override()
423  protected boolean supportsOutputFile()
424  {
425    return true;
426  }
427
428
429
430  /**
431   * Indicates whether this tool should default to interactively prompting for
432   * the bind password if a password is required but no argument was provided
433   * to indicate how to get the password.
434   *
435   * @return  {@code true} if this tool should default to interactively
436   *          prompting for the bind password, or {@code false} if not.
437   */
438  @Override()
439  protected boolean defaultToPromptForBindPassword()
440  {
441    return true;
442  }
443
444
445
446  /**
447   * Indicates whether this tool supports the use of a properties file for
448   * specifying default values for arguments that aren't specified on the
449   * command line.
450   *
451   * @return  {@code true} if this tool supports the use of a properties file
452   *          for specifying default values for arguments that aren't specified
453   *          on the command line, or {@code false} if not.
454   */
455  @Override()
456  public boolean supportsPropertiesFile()
457  {
458    return true;
459  }
460
461
462
463  /**
464   * Indicates whether the LDAP-specific arguments should include alternate
465   * versions of all long identifiers that consist of multiple words so that
466   * they are available in both camelCase and dash-separated versions.
467   *
468   * @return  {@code true} if this tool should provide multiple versions of
469   *          long identifiers for LDAP-specific arguments, or {@code false} if
470   *          not.
471   */
472  @Override()
473  protected boolean includeAlternateLongIdentifiers()
474  {
475    return true;
476  }
477
478
479
480  /**
481   * Adds the arguments used by this program that aren't already provided by the
482   * generic {@code LDAPCommandLineTool} framework.
483   *
484   * @param  parser  The argument parser to which the arguments should be added.
485   *
486   * @throws  ArgumentException  If a problem occurs while adding the arguments.
487   */
488  @Override()
489  public void addNonLDAPArguments(final ArgumentParser parser)
490         throws ArgumentException
491  {
492    String description = "The base DN to use for the searches.  It may be a " +
493         "simple DN or a value pattern to specify a range of DNs (e.g., " +
494         "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  See " +
495         ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " +
496         "value pattern syntax.  This must be provided.";
497    baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description);
498    baseDN.setArgumentGroupName("Search Arguments");
499    baseDN.addLongIdentifier("base-dn");
500    parser.addArgument(baseDN);
501
502
503    description = "The scope to use for the searches.  It should be 'base', " +
504                  "'one', 'sub', or 'subord'.  If this is not provided, then " +
505                  "a default scope of 'sub' will be used.";
506    scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
507                                 SearchScope.SUB);
508    scopeArg.setArgumentGroupName("Search Arguments");
509    parser.addArgument(scopeArg);
510
511
512    description = "The filter to use for the searches.  It may be a simple " +
513                  "filter or a value pattern to specify a range of filters " +
514                  "(e.g., \"(uid=user.[1-1000])\").  See " +
515                  ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " +
516                  "about the value pattern syntax.  This must be provided.";
517    filter = new StringArgument('f', "filter", true, 1, "{filter}",
518                                description);
519    filter.setArgumentGroupName("Search Arguments");
520    parser.addArgument(filter);
521
522
523    description = "The name of an attribute to include in entries returned " +
524                  "from the searches.  Multiple attributes may be requested " +
525                  "by providing this argument multiple times.  If no request " +
526                  "attributes are provided, then the entries returned will " +
527                  "include all user attributes.";
528    attributes = new StringArgument('A', "attribute", false, 0, "{name}",
529                                    description);
530    attributes.setArgumentGroupName("Search Arguments");
531    parser.addArgument(attributes);
532
533
534    description = "Indicates that search requests should include the " +
535                  "assertion request control with the specified filter.";
536    assertionFilter = new FilterArgument(null, "assertionFilter", false, 1,
537                                         "{filter}", description);
538    assertionFilter.setArgumentGroupName("Request Control Arguments");
539    assertionFilter.addLongIdentifier("assertion-filter");
540    parser.addArgument(assertionFilter);
541
542
543    description = "Indicates that search requests should include the simple " +
544                  "paged results control with the specified page size.";
545    simplePageSize = new IntegerArgument(null, "simplePageSize", false, 1,
546                                         "{size}", description, 1,
547                                         Integer.MAX_VALUE);
548    simplePageSize.setArgumentGroupName("Request Control Arguments");
549    simplePageSize.addLongIdentifier("simple-page-size");
550    parser.addArgument(simplePageSize);
551
552
553    description = "Indicates that search requests should include the " +
554                  "server-side sort request control with the specified sort " +
555                  "order. This should be a comma-delimited list in which " +
556                  "each item is an attribute name, optionally preceded by a " +
557                  "plus or minus sign (to indicate ascending or descending " +
558                  "order; where ascending order is the default), and " +
559                  "optionally followed by a colon and the name or OID of " +
560                  "the desired ordering matching rule (if this is not " +
561                  "provided, the the attribute type's default ordering " +
562                  "rule will be used).";
563    sortOrder = new StringArgument(null, "sortOrder", false, 1, "{sortOrder}",
564                                   description);
565    sortOrder.setArgumentGroupName("Request Control Arguments");
566    sortOrder.addLongIdentifier("sort-order");
567    parser.addArgument(sortOrder);
568
569
570    description = "Indicates that the proxied authorization control (as " +
571                  "defined in RFC 4370) should be used to request that " +
572                  "operations be processed using an alternate authorization " +
573                  "identity.  This may be a simple authorization ID or it " +
574                  "may be a value pattern to specify a range of " +
575                  "identities.  See " + ValuePattern.PUBLIC_JAVADOC_URL +
576                  " for complete details about the value pattern syntax.";
577    proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}",
578                                 description);
579    proxyAs.setArgumentGroupName("Request Control Arguments");
580    proxyAs.addLongIdentifier("proxy-as");
581    parser.addArgument(proxyAs);
582
583
584    description = "Indicates that search requests should include the " +
585                  "specified request control.  This may be provided multiple " +
586                  "times to include multiple request controls.";
587    control = new ControlArgument('J', "control", false, 0, null, description);
588    control.setArgumentGroupName("Request Control Arguments");
589    parser.addArgument(control);
590
591
592    description = "The number of threads to use to perform the searches.  If " +
593                  "this is not provided, then a default of one thread will " +
594                  "be used.";
595    numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
596                                     description, 1, Integer.MAX_VALUE, 1);
597    numThreads.setArgumentGroupName("Rate Management Arguments");
598    numThreads.addLongIdentifier("num-threads");
599    parser.addArgument(numThreads);
600
601
602    description = "The length of time in seconds between output lines.  If " +
603                  "this is not provided, then a default interval of five " +
604                  "seconds will be used.";
605    collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
606                                             "{num}", description, 1,
607                                             Integer.MAX_VALUE, 5);
608    collectionInterval.setArgumentGroupName("Rate Management Arguments");
609    collectionInterval.addLongIdentifier("interval-duration");
610    parser.addArgument(collectionInterval);
611
612
613    description = "The maximum number of intervals for which to run.  If " +
614                  "this is not provided, then the tool will run until it is " +
615                  "interrupted.";
616    numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
617                                       description, 1, Integer.MAX_VALUE,
618                                       Integer.MAX_VALUE);
619    numIntervals.setArgumentGroupName("Rate Management Arguments");
620    numIntervals.addLongIdentifier("num-intervals");
621    parser.addArgument(numIntervals);
622
623    description = "The number of search iterations that should be processed " +
624                  "on a connection before that connection is closed and " +
625                  "replaced with a newly-established (and authenticated, if " +
626                  "appropriate) connection.  If this is not provided, then " +
627                  "connections will not be periodically closed and " +
628                  "re-established.";
629    iterationsBeforeReconnect = new IntegerArgument(null,
630         "iterationsBeforeReconnect", false, 1, "{num}", description, 0);
631    iterationsBeforeReconnect.setArgumentGroupName("Rate Management Arguments");
632    iterationsBeforeReconnect.addLongIdentifier("iterations-before-reconnect");
633    parser.addArgument(iterationsBeforeReconnect);
634
635    description = "The target number of searches to perform per second.  It " +
636                  "is still necessary to specify a sufficient number of " +
637                  "threads for achieving this rate.  If neither this option " +
638                  "nor --variableRateData is provided, then the tool will " +
639                  "run at the maximum rate for the specified number of " +
640                  "threads.";
641    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
642                                        "{searches-per-second}", description,
643                                        1, Integer.MAX_VALUE);
644    ratePerSecond.setArgumentGroupName("Rate Management Arguments");
645    ratePerSecond.addLongIdentifier("rate-per-second");
646    parser.addArgument(ratePerSecond);
647
648    final String variableRateDataArgName = "variableRateData";
649    final String generateSampleRateFileArgName = "generateSampleRateFile";
650    description = RateAdjustor.getVariableRateDataArgumentDescription(
651         generateSampleRateFileArgName);
652    variableRateData = new FileArgument(null, variableRateDataArgName, false, 1,
653                                        "{path}", description, true, true, true,
654                                        false);
655    variableRateData.setArgumentGroupName("Rate Management Arguments");
656    variableRateData.addLongIdentifier("variable-rate-data");
657    parser.addArgument(variableRateData);
658
659    description = RateAdjustor.getGenerateSampleVariableRateFileDescription(
660         variableRateDataArgName);
661    sampleRateFile = new FileArgument(null, generateSampleRateFileArgName,
662                                      false, 1, "{path}", description, false,
663                                      true, true, false);
664    sampleRateFile.setArgumentGroupName("Rate Management Arguments");
665    sampleRateFile.addLongIdentifier("generate-sample-rate-file");
666    sampleRateFile.setUsageArgument(true);
667    parser.addArgument(sampleRateFile);
668    parser.addExclusiveArgumentSet(variableRateData, sampleRateFile);
669
670    description = "The number of intervals to complete before beginning " +
671                  "overall statistics collection.  Specifying a nonzero " +
672                  "number of warm-up intervals gives the client and server " +
673                  "a chance to warm up without skewing performance results.";
674    warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
675         "{num}", description, 0, Integer.MAX_VALUE, 0);
676    warmUpIntervals.setArgumentGroupName("Rate Management Arguments");
677    warmUpIntervals.addLongIdentifier("warm-up-intervals");
678    parser.addArgument(warmUpIntervals);
679
680    description = "Indicates the format to use for timestamps included in " +
681                  "the output.  A value of 'none' indicates that no " +
682                  "timestamps should be included.  A value of 'with-date' " +
683                  "indicates that both the date and the time should be " +
684                  "included.  A value of 'without-date' indicates that only " +
685                  "the time should be included.";
686    final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
687    allowedFormats.add("none");
688    allowedFormats.add("with-date");
689    allowedFormats.add("without-date");
690    timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
691         "{format}", description, allowedFormats, "none");
692    timestampFormat.addLongIdentifier("timestamp-format");
693    parser.addArgument(timestampFormat);
694
695    description = "Indicates that the client should operate in asynchronous " +
696                  "mode, in which it will not be necessary to wait for a " +
697                  "response to a previous request before sending the next " +
698                  "request.  Either the '--ratePerSecond' or the " +
699                  "'--maxOutstandingRequests' argument must be provided to " +
700                  "limit the number of outstanding requests.";
701    asynchronousMode = new BooleanArgument('a', "asynchronous", description);
702    parser.addArgument(asynchronousMode);
703
704    description = "Specifies the maximum number of outstanding requests " +
705                  "that should be allowed when operating in asynchronous mode.";
706    maxOutstandingRequests = new IntegerArgument('O', "maxOutstandingRequests",
707         false, 1, "{num}", description, 1, Integer.MAX_VALUE, (Integer) null);
708    maxOutstandingRequests.addLongIdentifier("max-outstanding-requests");
709    parser.addArgument(maxOutstandingRequests);
710
711    description = "Indicates that information about the result codes for " +
712                  "failed operations should not be displayed.";
713    suppressErrors = new BooleanArgument(null,
714         "suppressErrorResultCodes", 1, description);
715    suppressErrors.addLongIdentifier("suppress-error-result-codes");
716    parser.addArgument(suppressErrors);
717
718    description = "Generate output in CSV format rather than a " +
719                  "display-friendly format";
720    csvFormat = new BooleanArgument('c', "csv", 1, description);
721    parser.addArgument(csvFormat);
722
723    description = "Specifies the seed to use for the random number generator.";
724    randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
725         description);
726    randomSeed.addLongIdentifier("random-seed");
727    parser.addArgument(randomSeed);
728
729
730    parser.addDependentArgumentSet(asynchronousMode, ratePerSecond,
731         maxOutstandingRequests);
732    parser.addDependentArgumentSet(maxOutstandingRequests, asynchronousMode);
733
734    parser.addExclusiveArgumentSet(asynchronousMode, simplePageSize);
735  }
736
737
738
739  /**
740   * Indicates whether this tool supports creating connections to multiple
741   * servers.  If it is to support multiple servers, then the "--hostname" and
742   * "--port" arguments will be allowed to be provided multiple times, and
743   * will be required to be provided the same number of times.  The same type of
744   * communication security and bind credentials will be used for all servers.
745   *
746   * @return  {@code true} if this tool supports creating connections to
747   *          multiple servers, or {@code false} if not.
748   */
749  @Override()
750  protected boolean supportsMultipleServers()
751  {
752    return true;
753  }
754
755
756
757  /**
758   * Retrieves the connection options that should be used for connections
759   * created for use with this tool.
760   *
761   * @return  The connection options that should be used for connections created
762   *          for use with this tool.
763   */
764  @Override()
765  public LDAPConnectionOptions getConnectionOptions()
766  {
767    final LDAPConnectionOptions options = new LDAPConnectionOptions();
768    options.setUseSynchronousMode(! asynchronousMode.isPresent());
769    return options;
770  }
771
772
773
774  /**
775   * Performs the actual processing for this tool.  In this case, it gets a
776   * connection to the directory server and uses it to perform the requested
777   * searches.
778   *
779   * @return  The result code for the processing that was performed.
780   */
781  @Override()
782  public ResultCode doToolProcessing()
783  {
784    runningThread = Thread.currentThread();
785
786    try
787    {
788      return doToolProcessingInternal();
789    }
790    finally
791    {
792      runningThread = null;
793    }
794  }
795
796
797
798  /**
799   * Performs the actual processing for this tool.  In this case, it gets a
800   * connection to the directory server and uses it to perform the requested
801   * searches.
802   *
803   * @return  The result code for the processing that was performed.
804   */
805  private ResultCode doToolProcessingInternal()
806  {
807    // If the sample rate file argument was specified, then generate the sample
808    // variable rate data file and return.
809    if (sampleRateFile.isPresent())
810    {
811      try
812      {
813        RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
814        return ResultCode.SUCCESS;
815      }
816      catch (final Exception e)
817      {
818        debugException(e);
819        err("An error occurred while trying to write sample variable data " +
820             "rate file '", sampleRateFile.getValue().getAbsolutePath(),
821             "':  ", getExceptionMessage(e));
822        return ResultCode.LOCAL_ERROR;
823      }
824    }
825
826
827    // Determine the random seed to use.
828    final Long seed;
829    if (randomSeed.isPresent())
830    {
831      seed = Long.valueOf(randomSeed.getValue());
832    }
833    else
834    {
835      seed = null;
836    }
837
838    // Create value patterns for the base DN, filter, and proxied authorization
839    // DN.
840    final ValuePattern dnPattern;
841    try
842    {
843      dnPattern = new ValuePattern(baseDN.getValue(), seed);
844    }
845    catch (final ParseException pe)
846    {
847      debugException(pe);
848      err("Unable to parse the base DN value pattern:  ", pe.getMessage());
849      return ResultCode.PARAM_ERROR;
850    }
851
852    final ValuePattern filterPattern;
853    try
854    {
855      filterPattern = new ValuePattern(filter.getValue(), seed);
856    }
857    catch (final ParseException pe)
858    {
859      debugException(pe);
860      err("Unable to parse the filter pattern:  ", pe.getMessage());
861      return ResultCode.PARAM_ERROR;
862    }
863
864    final ValuePattern authzIDPattern;
865    if (proxyAs.isPresent())
866    {
867      try
868      {
869        authzIDPattern = new ValuePattern(proxyAs.getValue(), seed);
870      }
871      catch (final ParseException pe)
872      {
873        debugException(pe);
874        err("Unable to parse the proxied authorization pattern:  ",
875            pe.getMessage());
876        return ResultCode.PARAM_ERROR;
877      }
878    }
879    else
880    {
881      authzIDPattern = null;
882    }
883
884    // Get the set of controls to include in search requests.
885    final ArrayList<Control> controlList = new ArrayList<Control>(5);
886    if (assertionFilter.isPresent())
887    {
888      controlList.add(new AssertionRequestControl(assertionFilter.getValue()));
889    }
890
891    if (sortOrder.isPresent())
892    {
893      final ArrayList<SortKey> sortKeys = new ArrayList<SortKey>(5);
894      final StringTokenizer tokenizer =
895           new StringTokenizer(sortOrder.getValue(), ",");
896      while (tokenizer.hasMoreTokens())
897      {
898        String token = tokenizer.nextToken().trim();
899
900        final boolean ascending;
901        if (token.startsWith("+"))
902        {
903          ascending = true;
904          token = token.substring(1);
905        }
906        else if (token.startsWith("-"))
907        {
908          ascending = false;
909          token = token.substring(1);
910        }
911        else
912        {
913          ascending = true;
914        }
915
916        final String attributeName;
917        final String matchingRuleID;
918        final int colonPos = token.indexOf(':');
919        if (colonPos < 0)
920        {
921          attributeName = token;
922          matchingRuleID = null;
923        }
924        else
925        {
926          attributeName = token.substring(0, colonPos);
927          matchingRuleID = token.substring(colonPos+1);
928        }
929
930        sortKeys.add(new SortKey(attributeName, matchingRuleID, (! ascending)));
931      }
932
933      controlList.add(new ServerSideSortRequestControl(sortKeys));
934    }
935
936    if (control.isPresent())
937    {
938      controlList.addAll(control.getValues());
939    }
940
941
942    // Get the attributes to return.
943    final String[] attrs;
944    if (attributes.isPresent())
945    {
946      final List<String> attrList = attributes.getValues();
947      attrs = new String[attrList.size()];
948      attrList.toArray(attrs);
949    }
950    else
951    {
952      attrs = NO_STRINGS;
953    }
954
955
956    // If the --ratePerSecond option was specified, then limit the rate
957    // accordingly.
958    FixedRateBarrier fixedRateBarrier = null;
959    if (ratePerSecond.isPresent() || variableRateData.isPresent())
960    {
961      // We might not have a rate per second if --variableRateData is specified.
962      // The rate typically doesn't matter except when we have warm-up
963      // intervals.  In this case, we'll run at the max rate.
964      final int intervalSeconds = collectionInterval.getValue();
965      final int ratePerInterval =
966           (ratePerSecond.getValue() == null)
967           ? Integer.MAX_VALUE
968           : ratePerSecond.getValue() * intervalSeconds;
969      fixedRateBarrier =
970           new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
971    }
972
973
974    // If --variableRateData was specified, then initialize a RateAdjustor.
975    RateAdjustor rateAdjustor = null;
976    if (variableRateData.isPresent())
977    {
978      try
979      {
980        rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
981             ratePerSecond.getValue(), variableRateData.getValue());
982      }
983      catch (final IOException e)
984      {
985        debugException(e);
986        err("Initializing the variable rates failed: " + e.getMessage());
987        return ResultCode.PARAM_ERROR;
988      }
989      catch (final IllegalArgumentException e)
990      {
991        debugException(e);
992        err("Initializing the variable rates failed: " + e.getMessage());
993        return ResultCode.PARAM_ERROR;
994      }
995    }
996
997
998    // If the --maxOutstandingRequests option was specified, then create the
999    // semaphore used to enforce that limit.
1000    final Semaphore asyncSemaphore;
1001    if (maxOutstandingRequests.isPresent())
1002    {
1003      asyncSemaphore = new Semaphore(maxOutstandingRequests.getValue());
1004    }
1005    else
1006    {
1007      asyncSemaphore = null;
1008    }
1009
1010
1011    // Determine whether to include timestamps in the output and if so what
1012    // format should be used for them.
1013    final boolean includeTimestamp;
1014    final String timeFormat;
1015    if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
1016    {
1017      includeTimestamp = true;
1018      timeFormat       = "dd/MM/yyyy HH:mm:ss";
1019    }
1020    else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
1021    {
1022      includeTimestamp = true;
1023      timeFormat       = "HH:mm:ss";
1024    }
1025    else
1026    {
1027      includeTimestamp = false;
1028      timeFormat       = null;
1029    }
1030
1031
1032    // Determine whether any warm-up intervals should be run.
1033    final long totalIntervals;
1034    final boolean warmUp;
1035    int remainingWarmUpIntervals = warmUpIntervals.getValue();
1036    if (remainingWarmUpIntervals > 0)
1037    {
1038      warmUp = true;
1039      totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
1040    }
1041    else
1042    {
1043      warmUp = true;
1044      totalIntervals = 0L + numIntervals.getValue();
1045    }
1046
1047
1048    // Create the table that will be used to format the output.
1049    final OutputFormat outputFormat;
1050    if (csvFormat.isPresent())
1051    {
1052      outputFormat = OutputFormat.CSV;
1053    }
1054    else
1055    {
1056      outputFormat = OutputFormat.COLUMNS;
1057    }
1058
1059    final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
1060         timeFormat, outputFormat, " ",
1061         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1062                  "Searches/Sec"),
1063         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1064                  "Avg Dur ms"),
1065         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1066                  "Entries/Srch"),
1067         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
1068                  "Errors/Sec"),
1069         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
1070                  "Searches/Sec"),
1071         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
1072                  "Avg Dur ms"));
1073
1074
1075    // Create values to use for statistics collection.
1076    final AtomicLong        searchCounter   = new AtomicLong(0L);
1077    final AtomicLong        entryCounter    = new AtomicLong(0L);
1078    final AtomicLong        errorCounter    = new AtomicLong(0L);
1079    final AtomicLong        searchDurations = new AtomicLong(0L);
1080    final ResultCodeCounter rcCounter       = new ResultCodeCounter();
1081
1082
1083    // Determine the length of each interval in milliseconds.
1084    final long intervalMillis = 1000L * collectionInterval.getValue();
1085
1086
1087    // Create the threads to use for the searches.
1088    final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
1089    final SearchRateThread[] threads =
1090         new SearchRateThread[numThreads.getValue()];
1091    for (int i=0; i < threads.length; i++)
1092    {
1093      final LDAPConnection connection;
1094      try
1095      {
1096        connection = getConnection();
1097      }
1098      catch (final LDAPException le)
1099      {
1100        debugException(le);
1101        err("Unable to connect to the directory server:  ",
1102            getExceptionMessage(le));
1103        return le.getResultCode();
1104      }
1105
1106      threads[i] = new SearchRateThread(this, i, connection,
1107           asynchronousMode.isPresent(), dnPattern, scopeArg.getValue(),
1108           filterPattern, attrs, authzIDPattern, simplePageSize.getValue(),
1109           controlList, iterationsBeforeReconnect.getValue(), barrier,
1110           searchCounter, entryCounter, searchDurations, errorCounter,
1111           rcCounter, fixedRateBarrier, asyncSemaphore);
1112      threads[i].start();
1113    }
1114
1115
1116    // Display the table header.
1117    for (final String headerLine : formatter.getHeaderLines(true))
1118    {
1119      out(headerLine);
1120    }
1121
1122
1123    // Start the RateAdjustor before the threads so that the initial value is
1124    // in place before any load is generated unless we're doing a warm-up in
1125    // which case, we'll start it after the warm-up is complete.
1126    if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
1127    {
1128      rateAdjustor.start();
1129    }
1130
1131
1132    // Indicate that the threads can start running.
1133    try
1134    {
1135      barrier.await();
1136    }
1137    catch (final Exception e)
1138    {
1139      debugException(e);
1140    }
1141
1142    long overallStartTime = System.nanoTime();
1143    long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
1144
1145
1146    boolean setOverallStartTime = false;
1147    long    lastDuration        = 0L;
1148    long    lastNumEntries      = 0L;
1149    long    lastNumErrors       = 0L;
1150    long    lastNumSearches     = 0L;
1151    long    lastEndTime         = System.nanoTime();
1152    for (long i=0; i < totalIntervals; i++)
1153    {
1154      if (rateAdjustor != null)
1155      {
1156        if (! rateAdjustor.isAlive())
1157        {
1158          out("All of the rates in " + variableRateData.getValue().getName() +
1159              " have been completed.");
1160          break;
1161        }
1162      }
1163
1164      final long startTimeMillis = System.currentTimeMillis();
1165      final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
1166      nextIntervalStartTime += intervalMillis;
1167      if (sleepTimeMillis > 0)
1168      {
1169        sleeper.sleep(sleepTimeMillis);
1170      }
1171
1172      if (stopRequested.get())
1173      {
1174        break;
1175      }
1176
1177      final long endTime          = System.nanoTime();
1178      final long intervalDuration = endTime - lastEndTime;
1179
1180      final long numSearches;
1181      final long numEntries;
1182      final long numErrors;
1183      final long totalDuration;
1184      if (warmUp && (remainingWarmUpIntervals > 0))
1185      {
1186        numSearches   = searchCounter.getAndSet(0L);
1187        numEntries    = entryCounter.getAndSet(0L);
1188        numErrors     = errorCounter.getAndSet(0L);
1189        totalDuration = searchDurations.getAndSet(0L);
1190      }
1191      else
1192      {
1193        numSearches   = searchCounter.get();
1194        numEntries    = entryCounter.get();
1195        numErrors     = errorCounter.get();
1196        totalDuration = searchDurations.get();
1197      }
1198
1199      final long recentNumSearches = numSearches - lastNumSearches;
1200      final long recentNumEntries = numEntries - lastNumEntries;
1201      final long recentNumErrors = numErrors - lastNumErrors;
1202      final long recentDuration = totalDuration - lastDuration;
1203
1204      final double numSeconds = intervalDuration / 1000000000.0d;
1205      final double recentSearchRate = recentNumSearches / numSeconds;
1206      final double recentErrorRate  = recentNumErrors / numSeconds;
1207
1208      final double recentAvgDuration;
1209      final double recentEntriesPerSearch;
1210      if (recentNumSearches > 0L)
1211      {
1212        recentEntriesPerSearch = 1.0d * recentNumEntries / recentNumSearches;
1213        recentAvgDuration = 1.0d * recentDuration / recentNumSearches / 1000000;
1214      }
1215      else
1216      {
1217        recentEntriesPerSearch = 0.0d;
1218        recentAvgDuration = 0.0d;
1219      }
1220
1221
1222      if (warmUp && (remainingWarmUpIntervals > 0))
1223      {
1224        out(formatter.formatRow(recentSearchRate, recentAvgDuration,
1225             recentEntriesPerSearch, recentErrorRate, "warming up",
1226             "warming up"));
1227
1228        remainingWarmUpIntervals--;
1229        if (remainingWarmUpIntervals == 0)
1230        {
1231          out("Warm-up completed.  Beginning overall statistics collection.");
1232          setOverallStartTime = true;
1233          if (rateAdjustor != null)
1234          {
1235            rateAdjustor.start();
1236          }
1237        }
1238      }
1239      else
1240      {
1241        if (setOverallStartTime)
1242        {
1243          overallStartTime    = lastEndTime;
1244          setOverallStartTime = false;
1245        }
1246
1247        final double numOverallSeconds =
1248             (endTime - overallStartTime) / 1000000000.0d;
1249        final double overallSearchRate = numSearches / numOverallSeconds;
1250
1251        final double overallAvgDuration;
1252        if (numSearches > 0L)
1253        {
1254          overallAvgDuration = 1.0d * totalDuration / numSearches / 1000000;
1255        }
1256        else
1257        {
1258          overallAvgDuration = 0.0d;
1259        }
1260
1261        out(formatter.formatRow(recentSearchRate, recentAvgDuration,
1262             recentEntriesPerSearch, recentErrorRate, overallSearchRate,
1263             overallAvgDuration));
1264
1265        lastNumSearches = numSearches;
1266        lastNumEntries  = numEntries;
1267        lastNumErrors   = numErrors;
1268        lastDuration    = totalDuration;
1269      }
1270
1271      final List<ObjectPair<ResultCode,Long>> rcCounts =
1272           rcCounter.getCounts(true);
1273      if ((! suppressErrors.isPresent()) && (! rcCounts.isEmpty()))
1274      {
1275        err("\tError Results:");
1276        for (final ObjectPair<ResultCode,Long> p : rcCounts)
1277        {
1278          err("\t", p.getFirst().getName(), ":  ", p.getSecond());
1279        }
1280      }
1281
1282      lastEndTime = endTime;
1283    }
1284
1285
1286    // Shut down the RateAdjustor if we have one.
1287    if (rateAdjustor != null)
1288    {
1289      rateAdjustor.shutDown();
1290    }
1291
1292
1293    // Stop all of the threads.
1294    ResultCode resultCode = ResultCode.SUCCESS;
1295    for (final SearchRateThread t : threads)
1296    {
1297      t.signalShutdown();
1298    }
1299    for (final SearchRateThread t : threads)
1300    {
1301      final ResultCode r = t.waitForShutdown();
1302      if (resultCode == ResultCode.SUCCESS)
1303      {
1304        resultCode = r;
1305      }
1306    }
1307
1308    return resultCode;
1309  }
1310
1311
1312
1313  /**
1314   * Requests that this tool stop running.  This method will attempt to wait
1315   * for all threads to complete before returning control to the caller.
1316   */
1317  public void stopRunning()
1318  {
1319    stopRequested.set(true);
1320    sleeper.wakeup();
1321
1322    final Thread t = runningThread;
1323    if (t != null)
1324    {
1325      try
1326      {
1327        t.join();
1328      }
1329      catch (final Exception e)
1330      {
1331        debugException(e);
1332
1333        if (e instanceof InterruptedException)
1334        {
1335          Thread.currentThread().interrupt();
1336        }
1337      }
1338    }
1339  }
1340
1341
1342
1343  /**
1344   * Retrieves the maximum number of outstanding requests that may be in
1345   * progress at any time, if appropriate.
1346   *
1347   * @return  The maximum number of outstanding requests that may be in progress
1348   *          at any time, or -1 if the tool was not configured to perform
1349   *          asynchronous searches with a maximum number of outstanding
1350   *          requests.
1351   */
1352  int getMaxOutstandingRequests()
1353  {
1354    if (maxOutstandingRequests.isPresent())
1355    {
1356      return maxOutstandingRequests.getValue();
1357    }
1358    else
1359    {
1360      return -1;
1361    }
1362  }
1363
1364
1365
1366  /**
1367   * {@inheritDoc}
1368   */
1369  @Override()
1370  public LinkedHashMap<String[],String> getExampleUsages()
1371  {
1372    final LinkedHashMap<String[],String> examples =
1373         new LinkedHashMap<String[],String>(2);
1374
1375    String[] args =
1376    {
1377      "--hostname", "server.example.com",
1378      "--port", "389",
1379      "--bindDN", "uid=admin,dc=example,dc=com",
1380      "--bindPassword", "password",
1381      "--baseDN", "dc=example,dc=com",
1382      "--scope", "sub",
1383      "--filter", "(uid=user.[1-1000000])",
1384      "--attribute", "givenName",
1385      "--attribute", "sn",
1386      "--attribute", "mail",
1387      "--numThreads", "10"
1388    };
1389    String description =
1390         "Test search performance by searching randomly across a set " +
1391         "of one million users located below 'dc=example,dc=com' with ten " +
1392         "concurrent threads.  The entries returned to the client will " +
1393         "include the givenName, sn, and mail attributes.";
1394    examples.put(args, description);
1395
1396    args = new String[]
1397    {
1398      "--generateSampleRateFile", "variable-rate-data.txt"
1399    };
1400    description =
1401         "Generate a sample variable rate definition file that may be used " +
1402         "in conjunction with the --variableRateData argument.  The sample " +
1403         "file will include comments that describe the format for data to be " +
1404         "included in this file.";
1405    examples.put(args, description);
1406
1407    return examples;
1408  }
1409}