001    /*
002     * Copyright 2008-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2013 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.ldap.sdk.examples;
022    
023    
024    
025    import java.io.OutputStream;
026    import java.io.Serializable;
027    import java.text.ParseException;
028    import java.util.LinkedHashMap;
029    import java.util.LinkedHashSet;
030    import java.util.List;
031    import java.util.Random;
032    import java.util.concurrent.CyclicBarrier;
033    import java.util.concurrent.atomic.AtomicLong;
034    
035    import com.unboundid.ldap.sdk.LDAPConnection;
036    import com.unboundid.ldap.sdk.LDAPConnectionOptions;
037    import com.unboundid.ldap.sdk.LDAPException;
038    import com.unboundid.ldap.sdk.ResultCode;
039    import com.unboundid.ldap.sdk.Version;
040    import com.unboundid.util.ColumnFormatter;
041    import com.unboundid.util.FixedRateBarrier;
042    import com.unboundid.util.FormattableColumn;
043    import com.unboundid.util.HorizontalAlignment;
044    import com.unboundid.util.LDAPCommandLineTool;
045    import com.unboundid.util.ObjectPair;
046    import com.unboundid.util.OutputFormat;
047    import com.unboundid.util.ResultCodeCounter;
048    import com.unboundid.util.ThreadSafety;
049    import com.unboundid.util.ThreadSafetyLevel;
050    import com.unboundid.util.ValuePattern;
051    import com.unboundid.util.args.ArgumentException;
052    import com.unboundid.util.args.ArgumentParser;
053    import com.unboundid.util.args.BooleanArgument;
054    import com.unboundid.util.args.IntegerArgument;
055    import com.unboundid.util.args.StringArgument;
056    
057    import static com.unboundid.util.StaticUtils.*;
058    
059    
060    
061    /**
062     * This class provides a tool that can be used to perform repeated modifications
063     * in an LDAP directory server using multiple threads.  It can help provide an
064     * estimate of the modify performance that a directory server is able to
065     * achieve.  The target entry DN may be a value pattern as described in the
066     * {@link ValuePattern} class.  This makes it possible to modify a range of
067     * entries rather than repeatedly updating the same entry.
068     * <BR><BR>
069     * Some of the APIs demonstrated by this example include:
070     * <UL>
071     *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
072     *       package)</LI>
073     *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
074     *       package)</LI>
075     *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
076     *       package)</LI>
077     *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
078     * </UL>
079     * <BR><BR>
080     * All of the necessary information is provided using command line arguments.
081     * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
082     * class, as well as the following additional arguments:
083     * <UL>
084     *   <LI>"-b {entryDN}" or "--targetDN {baseDN}" -- specifies the DN of the
085     *       entry to be modified.  This must be provided.  It may be a simple DN,
086     *       or it may be a value pattern to express a range of entry DNs.</LI>
087     *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of the
088     *       attribute to modify.  Multiple attributes may be modified by providing
089     *       multiple instances of this argument.  At least one attribute must be
090     *       provided.</LI>
091     *   <LI>"-l {num}" or "--valueLength {num}" -- specifies the length in bytes to
092     *       use for the values of the target attributes.  If this is not provided,
093     *       then a default length of 10 bytes will be used.</LI>
094     *   <LI>"-C {chars}" or "--characterSet {chars}" -- specifies the set of
095     *       characters that will be used to generate the values to use for the
096     *       target attributes.  It should only include ASCII characters.  Values
097     *       will be generated from randomly-selected characters from this set.  If
098     *       this is not provided, then a default set of lowercase alphabetic
099     *       characters will be used.</LI>
100     *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
101     *       concurrent threads to use when performing the modifications.  If this
102     *       is not provided, then a default of one thread will be used.</LI>
103     *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
104     *       time in seconds between lines out output.  If this is not provided,
105     *       then a default interval duration of five seconds will be used.</LI>
106     *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
107     *       intervals for which to run.  If this is not provided, then it will
108     *       run forever.</LI>
109     *   <LI>"--iterationsBeforeReconnect {num}" -- specifies the number of modify
110     *       iterations that should be performed on a connection before that
111     *       connection is closed and replaced with a newly-established (and
112     *       authenticated, if appropriate) connection.</LI>
113     *   <LI>"-r {modifies-per-second}" or "--ratePerSecond {modifies-per-second}"
114     *       -- specifies the target number of modifies to perform per second.  It
115     *       is still necessary to specify a sufficient number of threads for
116     *       achieving this rate.  If this option is not provided, then the tool
117     *       will run at the maximum rate for the specified number of threads.</LI>
118     *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
119     *       complete before beginning overall statistics collection.</LI>
120     *   <LI>"--timestampFormat {format}" -- specifies the format to use for
121     *       timestamps included before each output line.  The format may be one of
122     *       "none" (for no timestamps), "with-date" (to include both the date and
123     *       the time), or "without-date" (to include only time time).</LI>
124     *   <LI>"-Y {authzID}" or "--proxyAs {authzID}" -- Use the proxied
125     *       authorization v2 control to request that the operation be processed
126     *       using an alternate authorization identity.  In this case, the bind DN
127     *       should be that of a user that has permission to use this control.  The
128     *       authorization identity may be a value pattern.</LI>
129     *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
130     *       result codes for failed operations should not be displayed.</LI>
131     *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
132     *       display-friendly format.</LI>
133     * </UL>
134     */
135    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
136    public final class ModRate
137           extends LDAPCommandLineTool
138           implements Serializable
139    {
140      /**
141       * The serial version UID for this serializable class.
142       */
143      private static final long serialVersionUID = 2709717414202815822L;
144    
145    
146    
147      // The argument used to indicate whether to generate output in CSV format.
148      private BooleanArgument csvFormat;
149    
150      // The argument used to indicate whether to suppress information about error
151      // result codes.
152      private BooleanArgument suppressErrorsArgument;
153    
154      // The argument used to specify the collection interval.
155      private IntegerArgument collectionInterval;
156    
157      // The argument used to specify the number of modify iterations on a
158      // connection before it is closed and re-established.
159      private IntegerArgument iterationsBeforeReconnect;
160    
161      // The argument used to specify the number of intervals.
162      private IntegerArgument numIntervals;
163    
164      // The argument used to specify the number of threads.
165      private IntegerArgument numThreads;
166    
167      // The argument used to specify the seed to use for the random number
168      // generator.
169      private IntegerArgument randomSeed;
170    
171      // The target rate of modifies per second.
172      private IntegerArgument ratePerSecond;
173    
174      // The argument used to specify the length of the values to generate.
175      private IntegerArgument valueLength;
176    
177      // The number of warm-up intervals to perform.
178      private IntegerArgument warmUpIntervals;
179    
180      // The argument used to specify the name of the attribute to modify.
181      private StringArgument attribute;
182    
183      // The argument used to specify the set of characters to use when generating
184      // values.
185      private StringArgument characterSet;
186    
187      // The argument used to specify the DNs of the entries to modify.
188      private StringArgument entryDN;
189    
190      // The argument used to specify the proxied authorization identity.
191      private StringArgument proxyAs;
192    
193      // The argument used to specify the timestamp format.
194      private StringArgument timestampFormat;
195    
196    
197    
198      /**
199       * Parse the provided command line arguments and make the appropriate set of
200       * changes.
201       *
202       * @param  args  The command line arguments provided to this program.
203       */
204      public static void main(final String[] args)
205      {
206        final ResultCode resultCode = main(args, System.out, System.err);
207        if (resultCode != ResultCode.SUCCESS)
208        {
209          System.exit(resultCode.intValue());
210        }
211      }
212    
213    
214    
215      /**
216       * Parse the provided command line arguments and make the appropriate set of
217       * changes.
218       *
219       * @param  args       The command line arguments provided to this program.
220       * @param  outStream  The output stream to which standard out should be
221       *                    written.  It may be {@code null} if output should be
222       *                    suppressed.
223       * @param  errStream  The output stream to which standard error should be
224       *                    written.  It may be {@code null} if error messages
225       *                    should be suppressed.
226       *
227       * @return  A result code indicating whether the processing was successful.
228       */
229      public static ResultCode main(final String[] args,
230                                    final OutputStream outStream,
231                                    final OutputStream errStream)
232      {
233        final ModRate modRate = new ModRate(outStream, errStream);
234        return modRate.runTool(args);
235      }
236    
237    
238    
239      /**
240       * Creates a new instance of this tool.
241       *
242       * @param  outStream  The output stream to which standard out should be
243       *                    written.  It may be {@code null} if output should be
244       *                    suppressed.
245       * @param  errStream  The output stream to which standard error should be
246       *                    written.  It may be {@code null} if error messages
247       *                    should be suppressed.
248       */
249      public ModRate(final OutputStream outStream, final OutputStream errStream)
250      {
251        super(outStream, errStream);
252      }
253    
254    
255    
256      /**
257       * Retrieves the name for this tool.
258       *
259       * @return  The name for this tool.
260       */
261      @Override()
262      public String getToolName()
263      {
264        return "modrate";
265      }
266    
267    
268    
269      /**
270       * Retrieves the description for this tool.
271       *
272       * @return  The description for this tool.
273       */
274      @Override()
275      public String getToolDescription()
276      {
277        return "Perform repeated modifications against " +
278               "an LDAP directory server.";
279      }
280    
281    
282    
283      /**
284       * Retrieves the version string for this tool.
285       *
286       * @return  The version string for this tool.
287       */
288      @Override()
289      public String getToolVersion()
290      {
291        return Version.NUMERIC_VERSION_STRING;
292      }
293    
294    
295    
296      /**
297       * Adds the arguments used by this program that aren't already provided by the
298       * generic {@code LDAPCommandLineTool} framework.
299       *
300       * @param  parser  The argument parser to which the arguments should be added.
301       *
302       * @throws  ArgumentException  If a problem occurs while adding the arguments.
303       */
304      @Override()
305      public void addNonLDAPArguments(final ArgumentParser parser)
306             throws ArgumentException
307      {
308        String description = "The DN of the entry to modify.  It may be a simple " +
309             "DN or a value pattern to specify a range of DN (e.g., " +
310             "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  This must be " +
311             "provided.";
312        entryDN = new StringArgument('b', "entryDN", true, 1, "{dn}", description);
313        parser.addArgument(entryDN);
314    
315    
316        description = "The name of the attribute to modify.  Multiple attributes " +
317                      "may be specified by providing this argument multiple " +
318                      "times.  At least one attribute must be specified.";
319        attribute = new StringArgument('A', "attribute", true, 0, "{name}",
320                                       description);
321        parser.addArgument(attribute);
322    
323    
324        description = "The length in bytes to use when generating values for the " +
325                      "modifications.  If this is not provided, then a default " +
326                      "length of ten bytes will be used.";
327        valueLength = new IntegerArgument('l', "valueLength", true, 1, "{num}",
328                                          description, 1, Integer.MAX_VALUE, 10);
329        parser.addArgument(valueLength);
330    
331    
332        description = "The set of characters to use to generate the values for " +
333                      "the modifications.  It should only include ASCII " +
334                      "characters.  If this is not provided, then a default set " +
335                      "of lowercase alphabetic characters will be used.";
336        characterSet = new StringArgument('C', "characterSet", true, 1, "{chars}",
337                                          description,
338                                          "abcdefghijklmnopqrstuvwxyz");
339        parser.addArgument(characterSet);
340    
341    
342        description = "The number of threads to use to perform the " +
343                      "modifications.  If this is not provided, a single thread " +
344                      "will be used.";
345        numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
346                                         description, 1, Integer.MAX_VALUE, 1);
347        parser.addArgument(numThreads);
348    
349    
350        description = "The length of time in seconds between output lines.  If " +
351                      "this is not provided, then a default interval of five " +
352                      "seconds will be used.";
353        collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
354                                                 "{num}", description, 1,
355                                                 Integer.MAX_VALUE, 5);
356        parser.addArgument(collectionInterval);
357    
358    
359        description = "The maximum number of intervals for which to run.  If " +
360                      "this is not provided, then the tool will run until it is " +
361                      "interrupted.";
362        numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
363                                           description, 1, Integer.MAX_VALUE,
364                                           Integer.MAX_VALUE);
365        parser.addArgument(numIntervals);
366    
367        description = "The number of modify iterations that should be processed " +
368                      "on a connection before that connection is closed and " +
369                      "replaced with a newly-established (and authenticated, if " +
370                      "appropriate) connection.  If this is not provided, then " +
371                      "connections will not be periodically closed and " +
372                      "re-established.";
373        iterationsBeforeReconnect = new IntegerArgument(null,
374             "iterationsBeforeReconnect", false, 1, "{num}", description, 0);
375        parser.addArgument(iterationsBeforeReconnect);
376    
377        description = "The target number of modifies to perform per second.  It " +
378                      "is still necessary to specify a sufficient number of " +
379                      "threads for achieving this rate.  If this option is not " +
380                      "provided, then the tool will run at the maximum rate for " +
381                      "the specified number of threads.";
382        ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
383                                            "{modifies-per-second}", description,
384                                            1, Integer.MAX_VALUE);
385        parser.addArgument(ratePerSecond);
386    
387        description = "The number of intervals to complete before beginning " +
388                      "overall statistics collection.  Specifying a nonzero " +
389                      "number of warm-up intervals gives the client and server " +
390                      "a chance to warm up without skewing performance results.";
391        warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
392             "{num}", description, 0, Integer.MAX_VALUE, 0);
393        parser.addArgument(warmUpIntervals);
394    
395        description = "Indicates the format to use for timestamps included in " +
396                      "the output.  A value of 'none' indicates that no " +
397                      "timestamps should be included.  A value of 'with-date' " +
398                      "indicates that both the date and the time should be " +
399                      "included.  A value of 'without-date' indicates that only " +
400                      "the time should be included.";
401        final LinkedHashSet<String> allowedFormats = new LinkedHashSet<String>(3);
402        allowedFormats.add("none");
403        allowedFormats.add("with-date");
404        allowedFormats.add("without-date");
405        timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
406             "{format}", description, allowedFormats, "none");
407        parser.addArgument(timestampFormat);
408    
409        description = "Indicates that the proxied authorization control (as " +
410                      "defined in RFC 4370) should be used to request that " +
411                      "operations be processed using an alternate authorization " +
412                      "identity.";
413        proxyAs = new StringArgument('Y', "proxyAs", false, 1, "{authzID}",
414                                     description);
415        parser.addArgument(proxyAs);
416    
417        description = "Indicates that information about the result codes for " +
418                      "failed operations should not be displayed.";
419        suppressErrorsArgument = new BooleanArgument(null,
420             "suppressErrorResultCodes", 1, description);
421        parser.addArgument(suppressErrorsArgument);
422    
423        description = "Generate output in CSV format rather than a " +
424                      "display-friendly format";
425        csvFormat = new BooleanArgument('c', "csv", 1, description);
426        parser.addArgument(csvFormat);
427    
428        description = "Specifies the seed to use for the random number generator.";
429        randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
430             description);
431        parser.addArgument(randomSeed);
432      }
433    
434    
435    
436      /**
437       * Indicates whether this tool supports creating connections to multiple
438       * servers.  If it is to support multiple servers, then the "--hostname" and
439       * "--port" arguments will be allowed to be provided multiple times, and
440       * will be required to be provided the same number of times.  The same type of
441       * communication security and bind credentials will be used for all servers.
442       *
443       * @return  {@code true} if this tool supports creating connections to
444       *          multiple servers, or {@code false} if not.
445       */
446      @Override()
447      protected boolean supportsMultipleServers()
448      {
449        return true;
450      }
451    
452    
453    
454      /**
455       * Retrieves the connection options that should be used for connections
456       * created for use with this tool.
457       *
458       * @return  The connection options that should be used for connections created
459       *          for use with this tool.
460       */
461      @Override()
462      public LDAPConnectionOptions getConnectionOptions()
463      {
464        final LDAPConnectionOptions options = new LDAPConnectionOptions();
465        options.setAutoReconnect(true);
466        options.setUseSynchronousMode(true);
467        return options;
468      }
469    
470    
471    
472      /**
473       * Performs the actual processing for this tool.  In this case, it gets a
474       * connection to the directory server and uses it to perform the requested
475       * modifications.
476       *
477       * @return  The result code for the processing that was performed.
478       */
479      @Override()
480      public ResultCode doToolProcessing()
481      {
482        // Determine the random seed to use.
483        final Long seed;
484        if (randomSeed.isPresent())
485        {
486          seed = Long.valueOf(randomSeed.getValue());
487        }
488        else
489        {
490          seed = null;
491        }
492    
493        // Create the value patterns for the target entry DN and proxied
494        // authorization identities.
495        final ValuePattern dnPattern;
496        try
497        {
498          dnPattern = new ValuePattern(entryDN.getValue(), seed);
499        }
500        catch (ParseException pe)
501        {
502          err("Unable to parse the entry DN value pattern:  ", pe.getMessage());
503          return ResultCode.PARAM_ERROR;
504        }
505    
506        final ValuePattern authzIDPattern;
507        if (proxyAs.isPresent())
508        {
509          try
510          {
511            authzIDPattern = new ValuePattern(proxyAs.getValue(), seed);
512          }
513          catch (ParseException pe)
514          {
515            err("Unable to parse the proxied authorization pattern:  ",
516                pe.getMessage());
517            return ResultCode.PARAM_ERROR;
518          }
519        }
520        else
521        {
522          authzIDPattern = null;
523        }
524    
525    
526        // Get the names of the attributes to modify.
527        final String[] attrs = new String[attribute.getValues().size()];
528        attribute.getValues().toArray(attrs);
529    
530    
531        // Get the character set as a byte array.
532        final byte[] charSet = getBytes(characterSet.getValue());
533    
534    
535        // If the --ratePerSecond option was specified, then limit the rate
536        // accordingly.
537        FixedRateBarrier fixedRateBarrier = null;
538        if (ratePerSecond.isPresent())
539        {
540          final int intervalSeconds = collectionInterval.getValue();
541          final int ratePerInterval = ratePerSecond.getValue() * intervalSeconds;
542    
543          fixedRateBarrier =
544               new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
545        }
546    
547    
548        // Determine whether to include timestamps in the output and if so what
549        // format should be used for them.
550        final boolean includeTimestamp;
551        final String timeFormat;
552        if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
553        {
554          includeTimestamp = true;
555          timeFormat       = "dd/MM/yyyy HH:mm:ss";
556        }
557        else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
558        {
559          includeTimestamp = true;
560          timeFormat       = "HH:mm:ss";
561        }
562        else
563        {
564          includeTimestamp = false;
565          timeFormat       = null;
566        }
567    
568    
569        // Determine whether any warm-up intervals should be run.
570        final long totalIntervals;
571        final boolean warmUp;
572        int remainingWarmUpIntervals = warmUpIntervals.getValue();
573        if (remainingWarmUpIntervals > 0)
574        {
575          warmUp = true;
576          totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
577        }
578        else
579        {
580          warmUp = true;
581          totalIntervals = 0L + numIntervals.getValue();
582        }
583    
584    
585        // Create the table that will be used to format the output.
586        final OutputFormat outputFormat;
587        if (csvFormat.isPresent())
588        {
589          outputFormat = OutputFormat.CSV;
590        }
591        else
592        {
593          outputFormat = OutputFormat.COLUMNS;
594        }
595    
596        final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
597             timeFormat, outputFormat, " ",
598             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
599                      "Mods/Sec"),
600             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
601                      "Avg Dur ms"),
602             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
603                      "Errors/Sec"),
604             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
605                      "Mods/Sec"),
606             new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
607                      "Avg Dur ms"));
608    
609    
610        // Create values to use for statistics collection.
611        final AtomicLong        modCounter   = new AtomicLong(0L);
612        final AtomicLong        errorCounter = new AtomicLong(0L);
613        final AtomicLong        modDurations = new AtomicLong(0L);
614        final ResultCodeCounter rcCounter    = new ResultCodeCounter();
615    
616    
617        // Determine the length of each interval in milliseconds.
618        final long intervalMillis = 1000L * collectionInterval.getValue();
619    
620    
621        // Create a random number generator to use for seeding the per-thread
622        // generators.
623        final Random random = new Random();
624    
625    
626        // Create the threads to use for the modifications.
627        final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
628        final ModRateThread[] threads = new ModRateThread[numThreads.getValue()];
629        for (int i=0; i < threads.length; i++)
630        {
631          final LDAPConnection connection;
632          try
633          {
634            connection = getConnection();
635          }
636          catch (LDAPException le)
637          {
638            err("Unable to connect to the directory server:  ",
639                getExceptionMessage(le));
640            return le.getResultCode();
641          }
642    
643          threads[i] = new ModRateThread(this, i, connection, dnPattern, attrs,
644               charSet, valueLength.getValue(), authzIDPattern, random.nextLong(),
645               iterationsBeforeReconnect.getValue(), barrier, modCounter,
646               modDurations, errorCounter, rcCounter, fixedRateBarrier);
647          threads[i].start();
648        }
649    
650    
651        // Display the table header.
652        for (final String headerLine : formatter.getHeaderLines(true))
653        {
654          out(headerLine);
655        }
656    
657    
658        // Indicate that the threads can start running.
659        try
660        {
661          barrier.await();
662        } catch (Exception e) {}
663        long overallStartTime = System.nanoTime();
664        long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
665    
666    
667        boolean setOverallStartTime = false;
668        long    lastDuration        = 0L;
669        long    lastNumErrors       = 0L;
670        long    lastNumMods         = 0L;
671        long    lastEndTime         = System.nanoTime();
672        for (long i=0; i < totalIntervals; i++)
673        {
674          final long startTimeMillis = System.currentTimeMillis();
675          final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
676          nextIntervalStartTime += intervalMillis;
677          try
678          {
679            if (sleepTimeMillis > 0)
680            {
681              Thread.sleep(sleepTimeMillis);
682            }
683          } catch (Exception e) {}
684    
685          final long endTime          = System.nanoTime();
686          final long intervalDuration = endTime - lastEndTime;
687    
688          final long numMods;
689          final long numErrors;
690          final long totalDuration;
691          if (warmUp && (remainingWarmUpIntervals > 0))
692          {
693            numMods       = modCounter.getAndSet(0L);
694            numErrors     = errorCounter.getAndSet(0L);
695            totalDuration = modDurations.getAndSet(0L);
696          }
697          else
698          {
699            numMods       = modCounter.get();
700            numErrors     = errorCounter.get();
701            totalDuration = modDurations.get();
702          }
703    
704          final long recentNumMods = numMods - lastNumMods;
705          final long recentNumErrors = numErrors - lastNumErrors;
706          final long recentDuration = totalDuration - lastDuration;
707    
708          final double numSeconds = intervalDuration / 1000000000.0d;
709          final double recentModRate = recentNumMods / numSeconds;
710          final double recentErrorRate  = recentNumErrors / numSeconds;
711    
712          final double recentAvgDuration;
713          if (recentNumMods > 0L)
714          {
715            recentAvgDuration = 1.0d * recentDuration / recentNumMods / 1000000;
716          }
717          else
718          {
719            recentAvgDuration = 0.0d;
720          }
721    
722          if (warmUp && (remainingWarmUpIntervals > 0))
723          {
724            out(formatter.formatRow(recentModRate, recentAvgDuration,
725                 recentErrorRate, "warming up", "warming up"));
726    
727            remainingWarmUpIntervals--;
728            if (remainingWarmUpIntervals == 0)
729            {
730              out("Warm-up completed.  Beginning overall statistics collection.");
731              setOverallStartTime = true;
732            }
733          }
734          else
735          {
736            if (setOverallStartTime)
737            {
738              overallStartTime    = lastEndTime;
739              setOverallStartTime = false;
740            }
741    
742            final double numOverallSeconds =
743                 (endTime - overallStartTime) / 1000000000.0d;
744            final double overallAuthRate = numMods / numOverallSeconds;
745    
746            final double overallAvgDuration;
747            if (numMods > 0L)
748            {
749              overallAvgDuration = 1.0d * totalDuration / numMods / 1000000;
750            }
751            else
752            {
753              overallAvgDuration = 0.0d;
754            }
755    
756            out(formatter.formatRow(recentModRate, recentAvgDuration,
757                 recentErrorRate, overallAuthRate, overallAvgDuration));
758    
759            lastNumMods     = numMods;
760            lastNumErrors   = numErrors;
761            lastDuration    = totalDuration;
762          }
763    
764          final List<ObjectPair<ResultCode,Long>> rcCounts =
765               rcCounter.getCounts(true);
766          if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty()))
767          {
768            err("\tError Results:");
769            for (final ObjectPair<ResultCode,Long> p : rcCounts)
770            {
771              err("\t", p.getFirst().getName(), ":  ", p.getSecond());
772            }
773          }
774    
775          lastEndTime = endTime;
776        }
777    
778    
779        // Stop all of the threads.
780        ResultCode resultCode = ResultCode.SUCCESS;
781        for (final ModRateThread t : threads)
782        {
783          final ResultCode r = t.stopRunning();
784          if (resultCode == ResultCode.SUCCESS)
785          {
786            resultCode = r;
787          }
788        }
789    
790        return resultCode;
791      }
792    
793    
794    
795      /**
796       * {@inheritDoc}
797       */
798      @Override()
799      public LinkedHashMap<String[],String> getExampleUsages()
800      {
801        final LinkedHashMap<String[],String> examples =
802             new LinkedHashMap<String[],String>(1);
803    
804        final String[] args =
805        {
806          "--hostname", "server.example.com",
807          "--port", "389",
808          "--bindDN", "uid=admin,dc=example,dc=com",
809          "--bindPassword", "password",
810          "--entryDN", "uid=user.[1-1000000],ou=People,dc=example,dc=com",
811          "--attribute", "description",
812          "--valueLength", "12",
813          "--numThreads", "10"
814        };
815        final String description =
816             "Test modify performance by randomly selecting entries across a set " +
817             "of one million users located below 'ou=People,dc=example,dc=com' " +
818             "with ten concurrent threads and replacing the values for the " +
819             "description attribute with a string of 12 randomly-selected " +
820             "lowercase alphabetic characters.";
821        examples.put(args, description);
822    
823        return examples;
824      }
825    }