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