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