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