opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/MultiColumnPrinter.java
@@ -22,441 +22,508 @@ * * * Copyright 2010 Sun Microsystems, Inc. * Portions copyright 2012-2015 ForgeRock AS. * Portions copyright 2012-2016 ForgeRock AS. */ package com.forgerock.opendj.cli; import java.util.Arrays; import static com.forgerock.opendj.cli.Utils.repeat; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Vector; import java.util.Locale; import org.forgerock.util.Reject; /** * Utility class for printing aligned columns of text. * <P> * This class allows you to specify: * <UL> * <LI>The number of columns in the output. This will determine the dimension of * the string arrays passed to add(String[]) or addTitle(String[]). * <LI>spacing/gap between columns * <LI>character to use for title border (null means no border) * <LI>column alignment. Only LEFT/CENTER is supported for now. * </UL> * <P> * Example usage: * Utility class for printing columns of data. * <p> * This printer can be used to print data in formatted table or in csv format. * <p> * Regarding the formatting table feature, this class allows you to specify for each {@link Column}s: * <ul> * <li>A unique identifier</li> * <li>The column title which will be printed by a call to {@link MultiColumnPrinter#printTitleLine()}</li> * <li>The size (if a cell's data is bigger than the predefined size, then the data will not be truncated, * i.e. it will overflow)</li> * <li>The number of digits to keep (for {@link Double} data)</li> * </ul> * <p> * Code to write data is independent of the {@link MultiColumnPrinter} configuration: * <pre> * void printData(final MultiColumnPrinter printer) { * String[][] myData = new String[][] { * new String[]{"U.S.A", "34.2", "40.8", ".us"}, * new String[]{"United Kingdom", "261.1", "31.6", ".uk"}, * new String[]{"France", "98.8", "30.1", ".fr"} * }; * * <PRE> * MyPrinter mp = new MyPrinter(3, 2, "-"); * String oneRow[] = new String[3]; * oneRow[0] = "User Name"; * oneRow[1] = "Email Address"; * oneRow[2] = "Phone Number"; * mp.addTitle(oneRow); * oneRow[0] = "Bob"; * oneRow[1] = "bob@foo.com"; * oneRow[2] = "123-4567"; * mp.add(oneRow); * oneRow[0] = "John"; * oneRow[1] = "john@foo.com"; * oneRow[2] = "456-7890"; * mp.add(oneRow); * mp.print(); * </PRE> * <P> * The above would print: * <P> * * <PRE> * -------------------------------------- * User Name Email Address Phone Number * -------------------------------------- * Bob bob@foo.com 123-4567 * John john@foo.com 456-7890 * </PRE> * <P> * This class also supports multi-row titles and having title strings spanning * multiple columns. Example usage: * * <PRE> * TestPrinter tp = new TestPrinter(4, 2, "-"); * String oneRow[] = new String[4]; * int[] span = new int[4]; * span[0] = 2; // spans 2 columns * span[1] = 0; // spans 0 columns * span[2] = 2; // spans 2 columns * span[3] = 0; // spans 0 columns * tp.setTitleAlign(CENTER); * oneRow[0] = "Name"; * oneRow[1] = ""; * oneRow[2] = "Contact"; * oneRow[3] = ""; * tp.addTitle(oneRow, span); * oneRow[0] = "First"; * oneRow[1] = "Last"; * oneRow[2] = "Email"; * oneRow[3] = "Phone"; * tp.addTitle(oneRow); * oneRow[0] = "Bob"; * oneRow[1] = "Jones"; * oneRow[2] = "bob@foo.com"; * oneRow[3] = "123-4567"; * tp.add(oneRow); * oneRow[0] = "John"; * oneRow[1] = "Doe"; * oneRow[2] = "john@foo.com"; * oneRow[3] = "456-7890"; * tp.add(oneRow); * tp.println(); * </PRE> * <P> * The above would print: * <P> * * <PRE> * ------------------------------------ * Name Contact * First Last Email Phone * ------------------------------------ * Bob Jones bob@foo.com 123-4567 * John Doe john@foo.com 456-7890 * </PRE> * int i; * for (String[] countryData : myData) { * i = 0; * for (final MultiColumnPrinter.Column column : printer.getColumns()) { * printer.printData(countryData[i++]); * } * } * } * </pre> * <p> * The following code sample presents how to create a {@link MultiColumnPrinter} to write CSV data: * <pre> * final List<MultiColumnPrinter.Column> columns = new ArrayList<>(); * columns.add(MultiColumnPrinter.column("CountryNameColumnId", "country_name", 0)); * columns.add(MultiColumnPrinter.column("populationDensityId", "population_density", 1)); * columns.add(MultiColumnPrinter.column("GiniId", "gini", 1)); * columns.add(MultiColumnPrinter.column("internetTLDId", "internet_tld", 0)); * MultiColumnPrinter myCsvPrinter = MultiColumnPrinter.builder(System.out, columns) * .columnSeparator(",") * .build(); * printData(myCsvPrinter); * </pre> * <p> * The code above would print: * <pre> * country_name,population_density,gini,internet_tld * U.S.A,34.2,40.8,.us * United Kingdom,261.1,31.6,.uk * France,98.8,30.1,.fr * </pre> * <p> * The following code sample presents how to configure a {@link MultiColumnPrinter} * to print the same data on console with some title headers. * <pre> * final List<MultiColumnPrinter.Column> columns = new ArrayList<>(); * columns.add(MultiColumnPrinter.separatorColumn()); * columns.add(MultiColumnPrinter.column("CountryNameColumnId", "Country Name", 15, 0)); * columns.add(MultiColumnPrinter.column("populationDensityId", "Density", 10, 1)); * columns.add(MultiColumnPrinter.separatorColumn()); * columns.add(MultiColumnPrinter.column("GiniID", "GINI", 5, 1)); * columns.add(MultiColumnPrinter.column("internetTLDID", "TLD", 5, 0)); * columns.add(MultiColumnPrinter.separatorColumn()); * MultiColumnPrinter myPrinter = MultiColumnPrinter.builder(System.out, columns) * .format(true) * .columnSeparator(" ") * .titleAlignment(MultiColumnPrinter.Alignment.CENTER) * .build(); * myPrinter.printDashedLine(); * myPrinter.printTitleSection("General Information", 2); * myPrinter.printTitleSection("Data", 2); * myPrinter.printTitleLine(); * myPrinter.printDashedLine(); * printData(myPrinter); * myPrinter.printDashedLine(); * </pre> * <p> * The code above would print: * <pre> * -------------------------------------------------- * | General Information | Data | * | Country Name Density | GINI TLD | * -------------------------------------------------- * | U.S.A 34.2 | 40.8 .us | * | United Kingdom 261.1 | 31.6 .uk | * | France 98.8 | 30.1 .fr | * -------------------------------------------------- * </pre> */ public final class MultiColumnPrinter { /** Left ID. */ public static final int LEFT = 0; /** Center ID. */ public static final int CENTER = 1; /** Right ID. */ public static final int RIGHT = 2; /** The data alignment. */ public enum Alignment { /** Data will be left-aligned. */ LEFT, /** Data will be centered. */ CENTER, /** Data will be right-aligned. */ RIGHT } private int numCol = 2; private int gap = 4; private int align = CENTER; private int titleAlign = CENTER; private String border; private final List<String[]> titleTable = new Vector<>(); private final List<int[]> titleSpanTable = new Vector<>(); private final int[] curLength; private final ConsoleApplication app; private static final String SEPARATOR_ID = "separator"; private static int separatorIdNumber; /** * Creates a sorted new MultiColumnPrinter class using LEFT alignment and * with no title border. * Returns a new separator {@link Column}. * <p> * This kind of {@link Column} can be used to separate data sections. * * @param numCol * number of columns * @param gap * gap between each column * @param app * the console application to use for outputting data * @return A new separator {@link Column}. */ public MultiColumnPrinter(final int numCol, final int gap, final ConsoleApplication app) { this(numCol, gap, null, LEFT, app); public static Column separatorColumn() { return new Column(SEPARATOR_ID + separatorIdNumber++, "", 1, 0); } /** * Creates a sorted new MultiColumnPrinter class using LEFT alignment. * Creates a new {@link Column} with the provided arguments. * * @param numCol * number of columns * @param gap * gap between each column * @param border * character used to frame the titles * @param app * the console application to use for outputting data * @param id * The column identifier. * @param title * The column title. * @param doublePrecision * The double precision used to print {@link Double} data for this column. * See {@link MultiColumnPrinter#printData(Double)}. * @return * A new Column with the provided arguments. */ public MultiColumnPrinter(final int numCol, final int gap, final String border, final ConsoleApplication app) { this(numCol, gap, border, LEFT, app); public static Column column(final String id, final String title, final int doublePrecision) { return new Column(id, title, 1, doublePrecision); } /** * Creates a new MultiColumnPrinter class. * Creates a new Column with the provided arguments. * * @param numCol * number of columns * @param gap * gap between each column * @param border * character used to frame the titles * @param align * type of alignment within columns * @param app * the console application to use for outputting data * @param id * The column identifier. * @param title * The column title. * @param width * The column width. * This information will only be used if the associated * {@link MultiColumnPrinter} is configured to apply formatting. * See {@link Builder#format(boolean)}. * @param doublePrecision * The double precision to use to print data for this column. * @return * A new Column with the provided arguments. */ public MultiColumnPrinter(final int numCol, final int gap, final String border, final int align, final ConsoleApplication app) { curLength = new int[numCol]; this.numCol = numCol; this.gap = gap; this.border = border; this.align = align; this.titleAlign = LEFT; this.app = app; public static Column column(final String id, final String title, final int width, final int doublePrecision) { return new Column(id, title, Math.max(width, title.length()), doublePrecision); } /** * Adds to the row of strings to be used as the title for the table. * * @param row * Array of strings to print in one row of title. * This class describes a Column of data used in the {@link MultiColumnPrinter}. * <p> * A column consists in the following fields: * <ul> * <li>An identifier for the associated data. * <li>A title which is printed when {@link MultiColumnPrinter#printTitleLine()} is called. * <li>A width which is the max width for this column's data. * This information will only be used if the associated {@link MultiColumnPrinter} * is configure to apply formatting.See {@link Builder#format(boolean)}. * <li>A double precision which is the number of decimal to print for numeric data. * See {@link MultiColumnPrinter#printData(Double)}. * </ul> */ public void addTitle(final String[] row) { if (row == null) { return; } public static final class Column { private final String id; private final String title; private final int width; private final int doublePrecision; final int[] span = new int[row.length]; for (int i = 0; i < row.length; i++) { span[i] = 1; } addTitle(row, span); private Column(final String id, final String title, final int width, final int doublePrecision) { this.id = id; this.title = title; this.width = Math.max(width, title.length()); this.doublePrecision = doublePrecision; } /** * Adds to the row of strings to be used as the title for the table. Also * allows for certain title strings to span multiple columns The span * parameter is an array of integers which indicate how many columns the * corresponding title string will occupy. For a row that is 4 columns * wide, it is possible to have some title strings in a row to 'span' * multiple columns: * <P> * Returns this {@link Column} identifier. * * <PRE> * ------------------------------------ * Name Contact * First Last Email Phone * ------------------------------------ * Bob Jones bob@foo.com 123-4567 * John Doe john@foo.com 456-7890 * </PRE> * * In the example above, the title row has a string 'Name' that spans 2 * columns. The string 'Contact' also spans 2 columns. The above is done * by passing in to addTitle() an array that contains: * * <PRE> * span[0] = 2; // spans 2 columns * span[1] = 0; // spans 0 columns, ignore * span[2] = 2; // spans 2 columns * span[3] = 0; // spans 0 columns, ignore * </PRE> * <P> * A span value of 1 is the default. The method addTitle(String[] row) * basically does: * * <PRE> * int[] span = new int[row.length]; * for (int i = 0; i < row.length; i++) { * span[i] = 1; * } * addTitle(row, span); * </PRE> * * @param row * Array of strings to print in one row of title. * @param span * Array of integers that reflect the number of columns the * corresponding title string will occupy. * @return This {@link Column} identifier. */ public void addTitle(final String[] row, final int[] span) { // Need to create a new instance of it, otherwise the new values // will always overwrite the old values. titleTable.add(Arrays.copyOf(row, row.length)); titleSpanTable.add(span); public String getId() { return id; } } /** * Clears title strings. * Creates a new {@link Builder} to build a {@link MultiColumnPrinter}. * * @param stream * The {@link PrintStream} to use to print data. * @param columns * The {@link List} of {@link Column} data to print. * @return * A new {@link Builder} to build a {@link MultiColumnPrinter}. */ public void clearTitle() { titleTable.clear(); titleSpanTable.clear(); public static Builder builder(final PrintStream stream, final List<Column> columns) { return new Builder(stream, columns); } /** A fluent API for incrementally constructing {@link MultiColumnPrinter}. */ public static final class Builder { private final PrintStream stream; private final List<Column> columns; private Alignment titleAlignment = Alignment.RIGHT; private String columnSeparator = " "; private boolean format; private Builder(final PrintStream stream, final List<Column> columns) { Reject.ifNull(stream); this.stream = stream; this.columns = columns; } /** * Adds one row of text to output. * Sets whether the {@link MultiColumnPrinter} needs to apply formatting. * <br> * Default value is {@code false}. * * @param row * Array of strings to print in one row. * @param format * {@code true} if the {@link MultiColumnPrinter} needs to apply formatting. * @return This builder. */ public void printRow(final String... row) { for (int i = 0; i < numCol; i++) { if (titleAlign == RIGHT) { final int spaceBefore = curLength[i] - row[i].length(); printSpaces(spaceBefore); app.getOutputStream().print(row[i]); if (i < numCol - 1) { printSpaces(gap); public Builder format(final boolean format) { this.format = format; return this; } } else if (align == CENTER) { int space1, space2; space1 = (curLength[i] - row[i].length()) / 2; space2 = curLength[i] - row[i].length() - space1; printSpaces(space1); app.getOutputStream().print(row[i]); printSpaces(space2); if (i < numCol - 1) { printSpaces(gap); /** * Sets the alignment for title elements which will be printed by the {@link MultiColumnPrinter}. * <p> * This is used only if the printer is configured to * apply formatting, see {@link Builder#format(boolean)}. * <br> * Default value is {@link Alignment#RIGHT}. * * @param titleAlignment * The title alignment. * @return This builder. */ public Builder titleAlignment(final Alignment titleAlignment) { this.titleAlignment = titleAlignment; return this; } /** * Sets the sequence to use to separate column. * <p> * Default value is {@code " "}. * * @param separator * The sequence {@link String}. * @return This builder. */ public Builder columnSeparator(final String separator) { this.columnSeparator = separator; return this; } /** * Creates a new {@link MultiColumnPrinter} as configured in this {@link Builder}. * * @return A new {@link MultiColumnPrinter} as configured in this {@link Builder}. */ public MultiColumnPrinter build() { return new MultiColumnPrinter(this); } } private final PrintStream stream; private final List<Column> columns; private final boolean format; private final Alignment titleAlignment; private final String columnSeparator; private List<Column> printableColumns; private final int lineLength; private Iterator<Column> columnIterator; private Column currentColumn; private MultiColumnPrinter(final Builder builder) { this.stream = builder.stream; this.columns = Collections.unmodifiableList(builder.columns); this.format = builder.format; this.columnSeparator = builder.columnSeparator; this.titleAlignment = builder.titleAlignment; this.lineLength = computeLineLength(); resetIterator(); computePrintableColumns(); } /** Prints a dashed line. */ public void printDashedLine() { startNewLineIfNeeded(); for (int i = 0; i < lineLength; i++) { stream.print('-'); } stream.println(); } /** * Formats and prints the provided text data. * Merge the provided text over the provided number of column. * <p> * Separator columns between merged columns will not be printed. * * @param data * The section title to print. * @param rowSpan * Specifies the number of rows a cell should span. */ public void printTitleSection(final String data, final int rowSpan) { consumeSeparatorColumn(); int lengthToPad = 0; int nbColumnMerged = 0; while (columnIterator.hasNext() && nbColumnMerged < rowSpan) { lengthToPad += currentColumn.width + columnSeparator.length(); if (!isSeparatorColumn(currentColumn)) { nbColumnMerged++; } currentColumn = columnIterator.next(); } stream.print(align(data, titleAlignment, lengthToPad)); consumeSeparatorColumn(); if (!columnIterator.hasNext()) { nextLine(); } } /** Prints a line with all column title and separator. */ public void printTitleLine() { startNewLineIfNeeded(); passFirstSeparatorColumn(); for (final Column column : this.printableColumns) { printCell(column.title, Alignment.RIGHT); } } /** * Prints the provided {@link Double} value on the current column. * <p> * If this {@link MultiColumnPrinter} is configured to apply formatting, * the provided value will be truncated according to the decimal * precision set in the corresponding {@link Column}. * <br> * See {@link MultiColumnPrinter#column(String, String, int, int)} for more details. * * @param value * The double value to print. */ public void printData(final Double value) { passFirstSeparatorColumn(); printData(value.isNaN() ? "-" : String.format(Locale.ENGLISH, "%." + currentColumn.doublePrecision + "f", value)); } /** * Prints the provided text data on the current column. * * @param data * The text data to print. */ public void printData(final String data) { passFirstSeparatorColumn(); printCell(data, Alignment.RIGHT); } /** * Returns the data {@link Column} list of this {@link MultiColumnPrinter}. * <p> * Separator columns are filtered out. * * @return The {@link Column} list of this {@link MultiColumnPrinter}. */ public List<Column> getColumns() { return printableColumns; } private void printCell(final String data, final Alignment alignment) { String toPrint = format ? align(data, alignment, currentColumn.width) : data; if (columnIterator.hasNext()) { toPrint += columnSeparator; } stream.print(toPrint); nextLineOnEOLOrNextColumn(); } /** Provided the provided string data according to the provided width and the provided alignment. */ private String align(final String data, final Alignment alignment, final int width) { final String rawData = data.trim(); final int padding = width - rawData.length(); if (padding <= 0) { return rawData; } switch (alignment) { case RIGHT: return pad(padding, rawData, 0); case LEFT: return pad(0, rawData, padding); case CENTER: final int paddingBefore = padding / 2; return pad(paddingBefore, rawData, padding - paddingBefore); default: return ""; } } private String pad(final int leftPad, final String s, final int rightPad) { return new StringBuilder().append(repeat(' ', leftPad)) .append(s) .append(repeat(' ', rightPad)) .toString(); } private void passFirstSeparatorColumn() { if (cursorOnLineStart()) { consumeSeparatorColumn(); } } private void consumeSeparatorColumn() { if (isSeparatorColumn(currentColumn)) { stream.print('|' + columnSeparator); nextLineOnEOLOrNextColumn(); } } private void startNewLineIfNeeded() { if (!cursorOnLineStart()) { nextLine(); } } private void nextLineOnEOLOrNextColumn() { if (columnIterator.hasNext()) { currentColumn = columnIterator.next(); consumeSeparatorColumn(); } else { app.getOutputStream().print(row[i]); if (i < numCol - 1) { printSpaces(curLength[i] - row[i].length() + gap); nextLine(); } } } app.getOutputStream().println(""); private void nextLine() { stream.println(); resetIterator(); } /** * Prints the table title. */ public void printTitle() { // Get the longest string for each column and store in curLength[] // Scan through title rows Iterator<int[]> spanEnum = titleSpanTable.iterator(); for (String[] row : titleTable) { final int[] curSpan = spanEnum.next(); for (int i = 0; i < numCol; i++) { // None of the fields should be null, but if it // happens to be so, replace it with "-". if (row[i] == null) { row[i] = "-"; private void resetIterator() { columnIterator = columns.iterator(); currentColumn = columnIterator.next(); } int len = row[i].length(); /* * If a title string spans multiple columns, then the space it * occupies in each column is at most len/span (since we have * gap to take into account as well). */ final int span = curSpan[i]; int rem = 0; if (span > 1) { rem = len % span; len = len / span; private boolean cursorOnLineStart() { return currentColumn == columns.get(0); } if (curLength[i] < len) { curLength[i] = len; if ((span > 1) && ((i + span) <= numCol)) { for (int j = i + 1; j < (i + span); ++j) { curLength[j] = len; private boolean isSeparatorColumn(final Column column) { return column.id.startsWith(SEPARATOR_ID); } /* * Add remainder to last column in span to avoid * round-off errors. */ curLength[(i + span) - 1] += rem; } private void computePrintableColumns() { printableColumns = new ArrayList<>(columns); final Iterator<Column> it = printableColumns.iterator(); while (it.hasNext()) { if (isSeparatorColumn(it.next())) { it.remove(); } } } printBorder(); spanEnum = titleSpanTable.iterator(); for (String[] row : titleTable) { final int[] curSpan = spanEnum.next(); for (int i = 0; i < numCol; i++) { int availableSpace = 0; final int span = curSpan[i]; if (span == 0) { continue; private int computeLineLength() { int lineLength = 0; final int separatorLength = this.columnSeparator.length(); for (final Column column : this.columns) { lineLength += column.width + separatorLength; } availableSpace = curLength[i]; if ((span > 1) && ((i + span) <= numCol)) { for (int j = i + 1; j < (i + span); ++j) { availableSpace += gap; availableSpace += curLength[j]; } } if (titleAlign == RIGHT) { final int spaceBefore = availableSpace - row[i].length(); printSpaces(spaceBefore); app.getOutputStream().print(row[i]); if (i < numCol - 1) { printSpaces(gap); } } else if (titleAlign == CENTER) { int spaceBefore, spaceAfter; spaceBefore = (availableSpace - row[i].length()) / 2; spaceAfter = availableSpace - row[i].length() - spaceBefore; printSpaces(spaceBefore); app.getOutputStream().print(row[i]); printSpaces(spaceAfter); if (i < numCol - 1) { printSpaces(gap); } } else { app.getOutputStream().print(row[i]); if (i < numCol - 1) { printSpaces(availableSpace - row[i].length() + gap); } } } app.getOutputStream().println(""); } printBorder(); } /** * Set alignment for title strings. * * @param titleAlign * The alignment which should be one of {@code LEFT}, * {@code RIGHT}, or {@code CENTER}. */ public void setTitleAlign(final int titleAlign) { this.titleAlign = titleAlign; } private void printBorder() { if (border == null) { return; } // For the value in each column for (int i = 0; i < numCol; i++) { for (int j = 0; j < curLength[i]; j++) { app.getOutputStream().print(border); } } // For the gap between each column for (int i = 0; i < numCol - 1; i++) { for (int j = 0; j < gap; j++) { app.getOutputStream().print(border); } } app.getOutputStream().println(""); } private void printSpaces(final int count) { for (int i = 0; i < count; ++i) { app.getOutputStream().print(" "); } return lineLength - separatorLength; } } opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
@@ -22,7 +22,7 @@ * * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions copyright 2014-2015 ForgeRock AS. * Portions copyright 2014-2016 ForgeRock AS. */ package com.forgerock.opendj.cli; @@ -40,6 +40,7 @@ import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.StringTokenizer; import java.util.TimeZone; @@ -619,4 +620,19 @@ public static void printWrappedText(final PrintStream stream, final LocalizableMessage message) { printWrappedText(stream, message != null ? message.toString() : null); } /** * Repeats the given {@link char} n times. * * @param charToRepeat * The {@link char} to repeat. * @param length * The repetition count. * @return The given {@link char} n times. */ public static String repeat(final char charToRepeat, final int length) { final char[] str = new char[length]; Arrays.fill(str, charToRepeat); return new String(str); } } opendj-sdk/opendj-ldap-toolkit/legal-notices/THIRDPARTYREADME.txt
@@ -177,6 +177,95 @@ *************************************************************************** Apache Software License, Version 2.0 *************************************************************************** Version: jsr305.jar (3.0.0) Copyright: Copyright (c) 2005 Brian Goetz Version: metrics-core.jar (3.1.2) Copyright: Copyright (c) 2010-2014 Coda Hale, Yammer.com Version: hdrhistogram-metrics-reservoir.jar (1.1.0) Copyright: Copyright (c) 2014-2015 Marshall Pierce ================== Full license text: ================== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. *************************************************************************** The MIT License (MIT) *************************************************************************** @@ -211,3 +300,39 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *************************************************************************** The BSD 2-Clause License *************************************************************************** Version: HdrHistogram.jar (2.1.4) Copyright (c) 2012, 2013, 2014 Gil Tene Copyright (c) 2014 Michael Barker Copyright (c) 2014 Matt Warren ================== Full license text: ================== Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. opendj-sdk/opendj-ldap-toolkit/pom.xml
@@ -42,6 +42,17 @@ <dependencies> <dependency> <groupId>io.dropwizard.metrics</groupId> <artifactId>metrics-core</artifactId> </dependency> <dependency> <groupId>org.mpierce.metrics.reservoir</groupId> <artifactId>hdrhistogram-metrics-reservoir</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>org.forgerock.opendj</groupId> <artifactId>opendj-core</artifactId> </dependency> opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java
@@ -25,12 +25,13 @@ */ package com.forgerock.opendj.ldap.tools; import static java.util.Locale.ENGLISH; import static com.forgerock.opendj.cli.MultiColumnPrinter.column; import static java.util.concurrent.TimeUnit.*; import static org.forgerock.opendj.ldap.LdapException.*; import static org.forgerock.opendj.ldap.ResultCode.*; import static org.forgerock.opendj.ldap.requests.Requests.*; import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest; import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest; import static org.forgerock.util.promise.Promises.*; import static com.forgerock.opendj.cli.ArgumentConstants.*; @@ -40,12 +41,16 @@ import java.io.IOException; import java.io.PrintStream; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import com.codahale.metrics.Counter; import com.codahale.metrics.RatioGauge; import com.forgerock.opendj.cli.MultiColumnPrinter; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.ldap.Connection; import org.forgerock.opendj.ldap.ConnectionFactory; @@ -91,9 +96,7 @@ } @Override public void handleResult(final Result result) { super.handleResult(result); void updateAdditionalStatsOnResult() { switch (delStrategy) { case RANDOM: long newKey; @@ -102,7 +105,7 @@ } while (dnEntriesAdded.putIfAbsent(newKey, entryDN) != null); break; case FIFO: long uniqueTime = currentTime; long uniqueTime = operationStartTimeNs; while (dnEntriesAdded.putIfAbsent(uniqueTime, entryDN) != null) { uniqueTime++; } @@ -111,9 +114,8 @@ break; } recentAdds.getAndIncrement(); totalAdds.getAndIncrement(); entryCount.getAndIncrement(); addCounter.inc(); entryCount.inc(); } } @@ -123,36 +125,37 @@ } @Override public void handleResult(final Result result) { super.handleResult(result); recentDeletes.getAndIncrement(); entryCount.getAndDecrement(); void updateAdditionalStatsOnResult() { deleteCounter.inc(); entryCount.dec(); } } private final class AddRateStatsThread extends StatsThread { private final String[] extraColumn = new String[1]; private static final int PERCENTAGE_ADD_COLUMN_WIDTH = 6; private static final String PERCENTAGE_ADD = STAT_ID_PREFIX + "add_percentage"; private AddRateStatsThread() { super("Add%"); private AddRateStatsThread(final PerformanceRunner perfRunner, final ConsoleApplication app) { super(perfRunner, app); } @Override void resetStats() { super.resetStats(); recentAdds.set(0); recentDeletes.set(0); void resetAdditionalStats() { addCounter = newIntervalCounter(); deleteCounter = newIntervalCounter(); } @Override String[] getAdditionalColumns() { final int adds = recentAdds.getAndSet(0); final int deleteStat = recentDeletes.getAndSet(0); final int total = adds + deleteStat; extraColumn[0] = String.format(ENGLISH, "%.2f", total > 0 ? ((double) adds / total) * 100 : 0.0); return extraColumn; List<MultiColumnPrinter.Column> registerAdditionalColumns() { registry.register(PERCENTAGE_ADD, new RatioGauge() { @Override protected Ratio getRatio() { final long addIntervalCount = addCounter.refreshIntervalCount(); final long deleteIntervalCount = deleteCounter.refreshIntervalCount(); return Ratio.of(addIntervalCount * 100, addIntervalCount + deleteIntervalCount); } }); return Collections.singletonList(column(PERCENTAGE_ADD, "Add%", PERCENTAGE_ADD_COLUMN_WIDTH, 2)); } } @@ -163,16 +166,16 @@ @Override public Promise<?, LdapException> performOperation( final Connection connection, final DataSource[] dataSources, final long currentTime) { final Connection connection, final DataSource[] dataSources, final long currentTimeNs) { startPurgeIfMaxNumberAddReached(); startToggleDeleteIfAgeThresholdReached(currentTime); startToggleDeleteIfAgeThresholdReached(currentTimeNs); try { String entryToRemove = getEntryToRemove(); if (entryToRemove != null) { return doDelete(connection, currentTime, entryToRemove); return doDelete(connection, currentTimeNs, entryToRemove); } return doAdd(connection, currentTime); return doAdd(connection, currentTimeNs); } catch (final AddRateExecutionEndedException a) { return newResultPromise(OTHER); } catch (final IOException e) { @@ -185,14 +188,14 @@ && delThreshold == DeleteThreshold.AGE_THRESHOLD && !dnEntriesAdded.isEmpty() && dnEntriesAdded.firstKey() + timeToWait < currentTime) { setSizeThreshold(entryCount.get()); setSizeThreshold(entryCount.getCount()); } } private void startPurgeIfMaxNumberAddReached() { AtomicBoolean purgeLatch = new AtomicBoolean(); if (!isPurgeBranchRunning.get() && 0 < maxNbAddIterations && maxNbAddIterations < totalAdds.get() && 0 < maxNbAddIterations && maxNbAddIterations < addCounter.getCount() && purgeLatch.compareAndSet(false, true)) { newPurgerThread().start(); } @@ -204,7 +207,7 @@ private String getEntryToRemove() throws AddRateExecutionEndedException { if (isPurgeBranchRunning.get()) { return purgeEntry(); } else if (toggleDelete && entryCount.get() > sizeThreshold) { } else if (toggleDelete && entryCount.getCount() > sizeThreshold) { return removeFirstAddedEntry(); } return null; @@ -226,7 +229,7 @@ private Promise<Result, LdapException> doAdd( final Connection connection, final long currentTime) throws IOException { Entry entry; final Entry entry; synchronized (generator) { entry = generator.readEntry(); } @@ -234,16 +237,14 @@ final LdapResultHandler<Result> addHandler = new AddStatsHandler( currentTime, entry.getName().toString()); return connection.addAsync(newAddRequest(entry)) .thenOnResult(addHandler) .thenOnException(addHandler); .thenOnResultOrException(addHandler, addHandler); } private Promise<?, LdapException> doDelete( final Connection connection, final long currentTime, final String entryToRemove) { final LdapResultHandler<Result> deleteHandler = new DeleteStatsHandler(currentTime); return connection.deleteAsync(newDeleteRequest(entryToRemove)) .thenOnResult(deleteHandler) .thenOnException(deleteHandler); .thenOnResultOrException(deleteHandler, deleteHandler); } } @@ -280,15 +281,14 @@ private EntryGenerator generator; private DeleteStrategy delStrategy; private DeleteThreshold delThreshold; private int sizeThreshold; private long sizeThreshold; private volatile boolean toggleDelete; private long timeToWait; private int maxNbAddIterations; private boolean purgeEnabled; private final AtomicInteger recentAdds = new AtomicInteger(); private final AtomicInteger recentDeletes = new AtomicInteger(); private final AtomicInteger totalAdds = new AtomicInteger(); private final AtomicInteger entryCount = new AtomicInteger(); private StatsThread.IntervalCounter addCounter = StatsThread.newIntervalCounter(); private StatsThread.IntervalCounter deleteCounter = StatsThread.newIntervalCounter(); private final Counter entryCount = new Counter(); private final AtomicBoolean isPurgeBranchRunning = new AtomicBoolean(); private AddPerformanceRunner(final PerformanceRunnerOptions options) throws ArgumentException { @@ -302,13 +302,13 @@ } @Override StatsThread newStatsThread() { return new AddRateStatsThread(); StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app) { return new AddRateStatsThread(performanceRunner, app); } @Override TimerThread newEndTimerThread(final long timeTowait) { return new AddRateTimerThread(timeTowait); TimerThread newEndTimerThread(final long timeToWait) { return new AddRateTimerThread(timeToWait); } TimerThread newPurgerThread() { @@ -347,7 +347,7 @@ } } private void setSizeThreshold(int entriesSizeThreshold) { private void setSizeThreshold(final long entriesSizeThreshold) { sizeThreshold = entriesSizeThreshold; toggleDelete = true; } opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
@@ -27,17 +27,20 @@ package com.forgerock.opendj.ldap.tools; import static com.forgerock.opendj.cli.ArgumentConstants.*; import static com.forgerock.opendj.cli.MultiColumnPrinter.column; import static com.forgerock.opendj.cli.Utils.*; import static com.forgerock.opendj.ldap.tools.ToolsMessages.*; import static com.forgerock.opendj.ldap.tools.Utils.*; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.concurrent.atomic.AtomicLong; import com.codahale.metrics.RatioGauge; import com.forgerock.opendj.cli.MultiColumnPrinter; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.ldap.Connection; import org.forgerock.opendj.ldap.ConnectionFactory; @@ -74,23 +77,37 @@ * requests using one or more LDAP connections. */ public final class AuthRate extends ConsoleApplication { private final class BindPerformanceRunner extends PerformanceRunner { private final class BindStatsThread extends StatsThread { private final String[] extraColumn; private static final int BIND_TIME_PERCENTAGE_COLUMN_WIDTH = 5; private static final String BIND_TIME_PERCENTAGE = STAT_ID_PREFIX + "bind_time_percentage"; private BindStatsThread(final boolean extraFieldRequired) { super(extraFieldRequired ? new String[] { "bind time %" } : new String[0]); extraColumn = new String[extraFieldRequired ? 1 : 0]; private final boolean computeBindTime; private BindStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app, final boolean computeBindTime) { super(performanceRunner, app); this.computeBindTime = computeBindTime; } @Override String[] getAdditionalColumns() { if (extraColumn.length != 0) { final long searchWaitTimeNs = searchWaitRecentTimeNs.getAndSet(0); extraColumn[0] = getDivisionResult( 100 * (intervalWaitTimeNs - searchWaitTimeNs), intervalWaitTimeNs, 1, "-"); List<MultiColumnPrinter.Column> registerAdditionalColumns() { if (!computeBindTime) { return Collections.emptyList(); } return extraColumn; registry.register(BIND_TIME_PERCENTAGE, new RatioGauge() { @Override protected Ratio getRatio() { final long searchWaitTimeIntervalNs = searchWaitRecentTimeNs.getLastIntervalCount(); final long waitTimeIntervalNs = waitDurationNsCount.getLastIntervalCount(); return Ratio.of(100 * (waitTimeIntervalNs - searchWaitTimeIntervalNs), waitTimeIntervalNs); } }); return Collections.singletonList( column(BIND_TIME_PERCENTAGE, "bind time %", BIND_TIME_PERCENTAGE_COLUMN_WIDTH, 1)); } } @@ -113,7 +130,7 @@ @Override public Promise<?, LdapException> performOperation(final Connection connection, final DataSource[] dataSources, final long startTime) { final DataSource[] dataSources, final long currentTimeNs) { if (dataSources != null) { data = DataSource.generateData(dataSources, data); if (data.length == dataSources.length) { @@ -144,7 +161,7 @@ @Override public Promise<BindResult, LdapException> apply(SearchResultEntry result) throws LdapException { searchWaitRecentTimeNs.getAndAdd(System.nanoTime() - startTime); searchWaitRecentTimeNs.inc(System.nanoTime() - currentTimeNs); if (data == null) { data = new Object[1]; } @@ -158,8 +175,8 @@ } incrementIterationCount(); return returnedPromise.thenOnResult(new UpdateStatsResultHandler<BindResult>(startTime)) .thenOnException(new UpdateStatsResultHandler<BindResult>(startTime)); return returnedPromise.thenOnResult(new UpdateStatsResultHandler<BindResult>(currentTimeNs)) .thenOnException(new UpdateStatsResultHandler<BindResult>(currentTimeNs)); } private Promise<BindResult, LdapException> performBind(final Connection connection, @@ -288,7 +305,7 @@ } } private final AtomicLong searchWaitRecentTimeNs = new AtomicLong(); private final StatsThread.IntervalCounter searchWaitRecentTimeNs = StatsThread.newIntervalCounter(); private String filter; private String baseDN; private SearchScope scope; @@ -314,8 +331,8 @@ } @Override StatsThread newStatsThread() { return new BindStatsThread(filter != null && baseDN != null); StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app) { return new BindStatsThread(performanceRunner, app, filter != null && baseDN != null); } } opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
@@ -22,7 +22,7 @@ * * * Copyright 2010 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS. * Portions Copyright 2011-2016 ForgeRock AS. */ package com.forgerock.opendj.ldap.tools; @@ -67,12 +67,12 @@ @Override public Promise<?, LdapException> performOperation(final Connection connection, final DataSource[] dataSources, final long startTime) { final DataSource[] dataSources, final long currentTimeNs) { if (dataSources != null) { data = DataSource.generateData(dataSources, data); } mr = newModifyRequest(data); LdapResultHandler<Result> modRes = new UpdateStatsResultHandler<>(startTime); LdapResultHandler<Result> modRes = new UpdateStatsResultHandler<>(currentTimeNs); incrementIterationCount(); return connection.modifyAsync(mr).thenOnResult(modRes).thenOnException(modRes); @@ -118,8 +118,8 @@ } @Override StatsThread newStatsThread() { return new StatsThread(new String[0]); StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app) { return new StatsThread(performanceRunner, app); } } opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
@@ -27,7 +27,6 @@ package com.forgerock.opendj.ldap.tools; import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage; import static java.util.Locale.ENGLISH; import static java.util.concurrent.TimeUnit.*; import static org.forgerock.util.Utils.*; @@ -35,22 +34,9 @@ import static com.forgerock.opendj.ldap.tools.ToolsMessages.*; import java.io.IOException; import java.io.PrintStream; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; import java.util.Queue; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.ldap.Connection; @@ -68,424 +54,12 @@ import com.forgerock.opendj.cli.BooleanArgument; import com.forgerock.opendj.cli.ConsoleApplication; import com.forgerock.opendj.cli.IntegerArgument; import com.forgerock.opendj.cli.MultiColumnPrinter; import com.forgerock.opendj.cli.StringArgument; import com.forgerock.opendj.util.StaticUtils; /** Benchmark application framework. */ abstract class PerformanceRunner implements ConnectionEventListener { static final class ResponseTimeBuckets { private static final long NS_1_US = NANOSECONDS.convert(1, MICROSECONDS); private static final long NS_100_US = NANOSECONDS.convert(100, MICROSECONDS); private static final long NS_1_MS = NANOSECONDS.convert(1, MILLISECONDS); private static final long NS_10_MS = NANOSECONDS.convert(10, MILLISECONDS); private static final long NS_100_MS = NANOSECONDS.convert(100, MILLISECONDS); private static final long NS_1_S = NANOSECONDS.convert(1, SECONDS); private static final long NS_5_S = NANOSECONDS.convert(5, SECONDS); private static final int NB_INDEX = 2120; private static final int RANGE_100_MICROSECONDS_START_INDEX = 1000; private static final int RANGE_1_MILLISECOND_START_INDEX = 1090; private static final int RANGE_100_MILLISECONDS_START_INDEX = 2080; /** * Array of response time buckets. * * <pre> * index 0 -> 999: 1000 buckets for 0ms - 1ms interval with 1 µs increments * index 1000 -> 1089: 90 buckets for 1ms - 10ms interval with 100 µs increments * index 1090 -> 2079: 990 buckets for 10ms - 1000ms interval with 1000 µs increments * index 2080 -> 2119: 40 buckets for 1000ms - 5000ms interval with 100 000 µs increments * </pre> */ private final AtomicLong[] index2Frequency = new AtomicLong[NB_INDEX]; /** * Store the lower bounds (in microseconds) of the eTime buckets. */ private final long[] index2Etime = new long[NB_INDEX]; /** * Sorted map used for storing response times from 5s+ with 500 millisecond increments. * * Keys (Long in microseconds) of this map must respect this pattern: n * 500 000 + 5 000 000, * where n is a natural integer. */ private final ConcurrentSkipListMap<Long, AtomicLong> bigEtimes = new ConcurrentSkipListMap<>(); /** * Initialize both index2Frequency and index2Etime arrays. */ private ResponseTimeBuckets() { // Helpful variables to compute index2Etime values. long rangeWidthMicroSecs; long rangeStart = 0; long initialTimeMicroSecs = 0; for (int i = 0; i < NB_INDEX; i++) { index2Frequency[i] = new AtomicLong(); if (i < RANGE_100_MICROSECONDS_START_INDEX) { // 0ms-1ms in 1 us increments rangeWidthMicroSecs = 1; } else if (i < RANGE_1_MILLISECOND_START_INDEX) { // 1ms-10ms in 100 us increments rangeWidthMicroSecs = 100; rangeStart = RANGE_100_MICROSECONDS_START_INDEX; initialTimeMicroSecs = MICROSECONDS.convert(1, MILLISECONDS); } else if (i < RANGE_100_MILLISECONDS_START_INDEX) { // 10ms-1000ms in 1000 us increments rangeWidthMicroSecs = MICROSECONDS.convert(1, MILLISECONDS); rangeStart = RANGE_1_MILLISECOND_START_INDEX; initialTimeMicroSecs = MICROSECONDS.convert(10, MILLISECONDS); } else { // 1000ms-5000ms with 100 000 us increments rangeWidthMicroSecs = MICROSECONDS.convert(100, MILLISECONDS); rangeStart = RANGE_100_MILLISECONDS_START_INDEX; initialTimeMicroSecs = MICROSECONDS.convert(1, SECONDS); } index2Etime[i] = (i - rangeStart) * rangeWidthMicroSecs + initialTimeMicroSecs; } } /** * Compute the closest response time values for each percentile given in * parameter. Percentiles array has to be sorted from lower to higher * percentiles. * * @param percentiles * array of {@code double} * * @param nbData * number of response times recorded. * * @return array of response times in microseconds corresponding to * percentiles. */ List<Long> getPercentile(double[] percentiles, long nbData) { List<Long> responseTimes = new ArrayList<>(); Queue<Long> nbDataThresholds = new LinkedList<>(); long nbDataSum = nbData; for (int i = percentiles.length - 1; i >= 0; i--) { nbDataThresholds.add((long) (percentiles[i] * nbData) / 100); } Iterator<Entry<Long, AtomicLong>> iter = bigEtimes.descendingMap().entrySet().iterator(); while (iter.hasNext() && !nbDataThresholds.isEmpty()) { Entry<Long, AtomicLong> currentETime = iter.next(); nbDataSum -= currentETime.getValue().get(); computePercentiles(nbDataThresholds, responseTimes, nbDataSum, currentETime.getKey()); } int stdTimeIndex = NB_INDEX - 1; while (stdTimeIndex >= 0 && !nbDataThresholds.isEmpty()) { long currentETime = index2Etime[stdTimeIndex]; nbDataSum -= index2Frequency[stdTimeIndex].get(); computePercentiles(nbDataThresholds, responseTimes, nbDataSum, currentETime); stdTimeIndex--; } return responseTimes; } private void computePercentiles(Queue<Long> currentDataThreshold, List<Long> responseTimes, long currentSum, long currentETime) { while (currentDataThreshold.peek() != null && currentDataThreshold.peek() >= currentSum) { responseTimes.add(currentETime); currentDataThreshold.poll(); } } void addTimeToInterval(long responseTimeNanoSecs) { if (responseTimeNanoSecs >= NS_5_S) { long matchingKey = responseTimeNanoSecs / NS_100_MS; matchingKey -= matchingKey % 5; matchingKey = matchingKey * MICROSECONDS.convert(100, MILLISECONDS); // We now have a key corresponding to pattern 5 000 000 + n * 500 000 µs AtomicLong existingKey = bigEtimes.putIfAbsent(matchingKey, new AtomicLong(1)); if (existingKey != null) { existingKey.getAndIncrement(); } return; } final int startRangeIndex; final long rangeWidthNanoSecs; if (responseTimeNanoSecs < NS_1_MS) { rangeWidthNanoSecs = NS_1_US; startRangeIndex = 0; } else if (responseTimeNanoSecs < NS_10_MS) { rangeWidthNanoSecs = NS_100_US; startRangeIndex = RANGE_100_MICROSECONDS_START_INDEX - 10; } else if (responseTimeNanoSecs < NS_1_S) { rangeWidthNanoSecs = NS_1_MS; startRangeIndex = RANGE_1_MILLISECOND_START_INDEX - 10; } else { rangeWidthNanoSecs = NS_100_MS; startRangeIndex = RANGE_100_MILLISECONDS_START_INDEX - 10; } final int intervalIndex = ((int) (responseTimeNanoSecs / rangeWidthNanoSecs)) + startRangeIndex; index2Frequency[intervalIndex].getAndIncrement(); } } /** To allow tests. */ static ResponseTimeBuckets getResponseTimeBuckets() { return new ResponseTimeBuckets(); } /** Statistics thread base implementation. */ class StatsThread extends Thread { protected long totalResultCount; protected long totalOperationCount; protected double totalDurationSec; protected long totalWaitTimeNs; protected int intervalSuccessCount; protected int intervalOperationCount; protected int intervalFailedCount; protected double intervalDurationSec; protected long intervalWaitTimeNs; protected long lastStatTimeMs; protected long lastGCDurationMs; private final int numColumns; private final String[] additionalColumns; private final double[] percentiles; private final List<GarbageCollectorMXBean> gcBeans; private final boolean isScriptFriendly = app.isScriptFriendly(); private MultiColumnPrinter printer; private long totalStatTimeMs; private long gcDurationMs; public StatsThread(final String... additionalColumns) { super("Stats Thread"); this.additionalColumns = additionalColumns; if (!percentilesArgument.isPresent()) { this.percentiles = new double[] { 99.9, 99.99, 99.999 }; } else { this.percentiles = new double[percentilesArgument.getValues().size()]; int index = 0; for (final String percentile : percentilesArgument.getValues()) { percentiles[index++] = Double.parseDouble(percentile); } Arrays.sort(percentiles); } this.numColumns = 5 + this.percentiles.length + additionalColumns.length + (isAsync ? 1 : 0); this.gcBeans = ManagementFactory.getGarbageCollectorMXBeans(); } private void printResultsTitle() { if (isScriptFriendly) { printResultsTitleScriptFriendly(); return; } printer = new MultiColumnPrinter(numColumns, 2, "-", MultiColumnPrinter.RIGHT, app); printer.setTitleAlign(MultiColumnPrinter.RIGHT); printResultTitleHeaders(); printResultTitleDetails(); } private void printResultTitleHeaders() { final String[][] titleHeaders = new String[2][numColumns]; for (final String[] titleLine : titleHeaders) { Arrays.fill(titleLine, ""); } titleHeaders[0][0] = "Throughput"; titleHeaders[0][2] = "Response Time"; titleHeaders[1][0] = "(ops/second)"; titleHeaders[1][2] = "(milliseconds)"; final int[] span = new int[numColumns]; span[0] = 2; span[1] = 0; span[2] = 2 + this.percentiles.length; Arrays.fill(span, 3, 4 + this.percentiles.length, 0); Arrays.fill(span, 4 + this.percentiles.length, span.length, 1); for (final String[] titleLine : titleHeaders) { printer.addTitle(titleLine, span); } } private void printResultTitleDetails() { final String[] titleDetails = new String[numColumns]; titleDetails[0] = "recent"; titleDetails[1] = "average"; titleDetails[2] = "recent"; titleDetails[3] = "average"; int i = 4; for (double percentile :percentiles) { titleDetails[i++] = percentile + "%"; } titleDetails[i++] = "err/sec"; if (isAsync) { titleDetails[i++] = "req/res"; } for (final String column : additionalColumns) { titleDetails[i++] = column; } final int[] span = new int[numColumns]; Arrays.fill(span, 1); printer.addTitle(titleDetails, span); printer.printTitle(); } private void printResultsTitleScriptFriendly() { final PrintStream out = app.getOutputStream(); out.print("Time (seconds)"); out.print(","); out.print("Recent throughput (ops/second)"); out.print(","); out.print("Average throughput (ops/second)"); out.print(","); out.print("Recent response time (milliseconds)"); out.print(","); out.print("Average response time (milliseconds)"); for (final double percentile : this.percentiles) { out.print(","); out.print(percentile); out.print("% response time (milliseconds)"); } out.print(","); out.print("Errors/second"); if (isAsync) { out.print(","); out.print("Requests/response"); } for (final String column : additionalColumns) { out.print(","); out.print(column); } out.println(); } private void printTitle() { printResultsTitle(); } private void initStats() { totalStatTimeMs = System.currentTimeMillis(); gcDurationMs = getGCDuration(); } @Override public void run() { lastStatTimeMs = totalStatTimeMs; totalStatTimeMs = System.currentTimeMillis(); lastGCDurationMs = gcDurationMs; gcDurationMs = getGCDuration(); final long gcIntervalDurationMs = gcDurationMs - lastGCDurationMs; computeStatsForInterval(totalStatTimeMs, gcIntervalDurationMs); final long intervalResultCount = intervalSuccessCount + intervalFailedCount; final String[] printableStats = new String[numColumns]; Arrays.fill(printableStats, "-"); printableStats[0] = getDivisionResult(intervalResultCount, intervalDurationSec, 1); printableStats[1] = getDivisionResult(totalResultCount, totalDurationSec, 1); final long intervalWaitTimeMs = NANOSECONDS.toMillis(intervalWaitTimeNs) - gcIntervalDurationMs; printableStats[2] = getDivisionResult(intervalWaitTimeMs, intervalResultCount, 3); final long totalWaitTimeMs = NANOSECONDS.toMillis(totalWaitTimeNs) - gcDurationMs; printableStats[3] = getDivisionResult(totalWaitTimeMs, totalResultCount, 3); int i = 4; final List<Long> computedPercentiles = eTimesBuckets.getPercentile(percentiles, totalOperationCount); for (int j = computedPercentiles.size() - 1; j >= 0; j--) { printableStats[i++] = getDivisionResult(computedPercentiles.get(j) , 1000.0, 2); } i = 4 + percentiles.length; printableStats[i++] = intervalFailedCount == 0 ? "0.0" : getDivisionResult(intervalFailedCount, intervalDurationSec, 1); if (isAsync) { printableStats[i++] = getDivisionResult(intervalOperationCount, intervalResultCount, 1); } for (final String column : getAdditionalColumns()) { printableStats[i++] = column; } if (isScriptFriendly) { printScriptFriendlyStats(printableStats); } else { printer.printRow(printableStats); } } private void computeStatsForInterval(final long statTime, final long gcIntervalDurationMs) { intervalOperationCount = operationRecentCount.getAndSet(0); intervalSuccessCount = successRecentCount.getAndSet(0); intervalFailedCount = failedRecentCount.getAndSet(0); intervalWaitTimeNs = waitRecentTimeNs.getAndSet(0); totalOperationCount += intervalOperationCount; totalResultCount += intervalSuccessCount + intervalFailedCount; totalWaitTimeNs += intervalWaitTimeNs; final long intervalDurationMs = statTime - lastStatTimeMs; intervalDurationSec = (intervalDurationMs - gcIntervalDurationMs) / 1000.0; totalDurationSec += intervalDurationSec; } private long getGCDuration() { long gcDuration = 0; for (final GarbageCollectorMXBean bean : gcBeans) { gcDuration += bean.getCollectionTime(); } return gcDuration; } private String getDivisionResult(final long numerator, final double denominator, final int precision) { return getDivisionResult(numerator, denominator, precision, "-"); } protected String getDivisionResult( final long numerator, final double denominator, final int precision, final String fallBack) { return denominator > 0 ? String.format(ENGLISH, "%." + precision + "f", numerator / denominator) : fallBack; } private void printScriptFriendlyStats(String[] printableStats) { final PrintStream out = app.getOutputStream(); out.print(String.format(ENGLISH, "%.3f", totalDurationSec)); for (final String s : printableStats) { out.print(","); out.print(s); } out.println(); } String[] getAdditionalColumns() { return EMPTY_STRINGS; } /** Resets both general and recent statistic indicators. */ void resetStats() { intervalFailedCount = 0; intervalOperationCount = 0; intervalSuccessCount = 0; operationRecentCount.set(0); successRecentCount.set(0); failedRecentCount.set(0); waitRecentTimeNs.set(0); } } private static final double[] DEFAULT_PERCENTILES = new double[] { 99.9, 99.99, 99.999 }; class TimerThread extends Thread { private final long timeToWait; @@ -517,31 +91,31 @@ * The type of expected result. */ class UpdateStatsResultHandler<S extends Result> implements LdapResultHandler<S> { protected final long currentTime; protected final long operationStartTimeNs; UpdateStatsResultHandler(final long currentTime) { this.currentTime = currentTime; UpdateStatsResultHandler(final long currentTimeNs) { this.operationStartTimeNs = currentTimeNs; } @Override public void handleException(final LdapException exception) { failedRecentCount.getAndIncrement(); updateStats(); public final void handleException(final LdapException exception) { statsThread.incrementFailedCount(); updateResponseTime(); app.errPrintVerboseMessage(LocalizableMessage.raw(exception.getResult().toString())); } @Override public void handleResult(final S result) { successRecentCount.getAndIncrement(); updateStats(); public final void handleResult(final S result) { statsThread.incrementSuccessCount(); updateResponseTime(); updateAdditionalStatsOnResult(); } private void updateStats() { if (!isWarmingUp) { final long eTime = System.nanoTime() - currentTime; waitRecentTimeNs.getAndAdd(eTime); eTimesBuckets.addTimeToInterval(eTime); } /** Do nothing by default, child classes which manage additional stats need to override this method. */ void updateAdditionalStatsOnResult() { } private void updateResponseTime() { statsThread.addResponseTime(System.nanoTime() - operationStartTimeNs); } } @@ -559,7 +133,7 @@ } public abstract Promise<?, LdapException> performOperation( Connection connection, DataSource[] dataSources, long startTime); Connection connection, DataSource[] dataSources, long currentTimeNs); @Override public void run() { @@ -582,7 +156,7 @@ long startTimeNs = System.nanoTime(); promise = performOperation(connection, dataSources.get(), startTimeNs); operationRecentCount.getAndIncrement(); statsThread.incrementOperationCount(); if (!isAsync) { try { promise.getOrThrow(); @@ -592,7 +166,7 @@ } catch (final LdapException e) { if (!stopRequested && e.getCause() instanceof IOException) { e.getCause().printStackTrace(app.getErrorStream()); stopTool(e); stopTool(true); break; } // Ignore. Handled by result handler @@ -638,14 +212,6 @@ } } private static final String[] EMPTY_STRINGS = new String[0]; private final AtomicInteger operationRecentCount = new AtomicInteger(); protected final AtomicInteger successRecentCount = new AtomicInteger(); protected final AtomicInteger failedRecentCount = new AtomicInteger(); private final AtomicLong waitRecentTimeNs = new AtomicLong(); private final ResponseTimeBuckets eTimesBuckets = new ResponseTimeBuckets(); private final ConsoleApplication app; private DataSource[] dataSourcePrototypes; @@ -669,13 +235,12 @@ int numConnections; private boolean stopRequested; private volatile boolean isWarmingUp; private int targetThroughput; private int maxIterations; /** Warm-up duration time in ms. **/ private long warmUpDuration; private long warmUpDurationMs; /** Max duration time in ms, 0 for unlimited. **/ private long maxDurationTime; private long maxDurationTimeMs; private boolean isAsync; private boolean noRebind; private BindRequest bindRequest; @@ -694,8 +259,7 @@ protected final IntegerArgument warmUpArgument; private final List<Thread> workerThreads = new ArrayList<>(); private final ScheduledExecutorService statThreadScheduler = Executors.newSingleThreadScheduledExecutor(); private StatsThread statsThread; StatsThread statsThread; PerformanceRunner(final PerformanceRunnerOptions options) throws ArgumentException { ArgumentParser argParser = options.getArgumentParser(); @@ -816,11 +380,11 @@ @Override public synchronized void handleConnectionError(final boolean isDisconnectNotification, final LdapException error) { if (!stopRequested) { app.errPrintln(ERROR_RATE_TOOLS_CAN_NOT_GET_CONNECTION.get(error.getMessage())); app.errPrintln(ERROR_RATE_TOOLS_CANNOT_GET_CONNECTION.get(error.getMessage())); if (error.getCause() != null && app.isVerbose()) { error.getCause().printStackTrace(app.getErrorStream()); } stopTool(error); stopTool(true); } } @@ -832,9 +396,9 @@ public final void validate() throws ArgumentException { numConnections = numConnectionsArgument.getIntValue(); numThreads = numThreadsArgument.getIntValue(); warmUpDuration = warmUpArgument.getIntValue() * 1000L; warmUpDurationMs = warmUpArgument.getIntValue() * 1000L; maxIterations = maxIterationsArgument.getIntValue() / numConnections / numThreads; maxDurationTime = maxDurationArgument.getIntValue() * 1000L; maxDurationTimeMs = maxDurationArgument.getIntValue() * 1000L; statsIntervalMs = statsIntervalArgument.getIntValue() * 1000; targetThroughput = targetThroughputArgument.getIntValue(); @@ -868,19 +432,18 @@ } abstract WorkerThread newWorkerThread(final Connection connection, final ConnectionFactory connectionFactory); abstract StatsThread newStatsThread(); abstract StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app); TimerThread newEndTimerThread(final long timeTowait) { return new TimerThread(timeTowait); TimerThread newEndTimerThread(final long timeToWait) { return new TimerThread(timeToWait); } final int run(final ConnectionFactory connectionFactory) { final List<Connection> connections = new ArrayList<>(); statsThread = newStatsThread(this, app); try { validateCanConnectToServer(connectionFactory); isWarmingUp = warmUpDuration > 0; for (int i = 0; i < numConnections; i++) { Connection connection = null; if (keepConnectionsOpen.isPresent() || noRebindArgument.isPresent()) { @@ -895,30 +458,17 @@ } } if (maxDurationTime > 0) { newEndTimerThread(maxDurationTime).start(); if (maxDurationTimeMs > 0) { newEndTimerThread(maxDurationTimeMs).start(); } statsThread = newStatsThread(); if (isWarmingUp) { if (!app.isScriptFriendly()) { app.println(INFO_TOOL_WARMING_UP.get(warmUpDuration / 1000)); } Thread.sleep(warmUpDuration); statsThread.resetStats(); isWarmingUp = false; } statsThread.printTitle(); statsThread.initStats(); statThreadScheduler.scheduleAtFixedRate( statsThread, statsIntervalMs, statsIntervalMs, TimeUnit.MILLISECONDS); statsThread.startReporting(); joinAllWorkerThreads(); stopTool(); } catch (final InterruptedException e) { stopTool(e); stopTool(true); } catch (final LdapException e) { stopTool(e); stopTool(true); printErrorMessage(app, e); return e.getResult().getResultCode().intValue(); } finally { @@ -935,22 +485,13 @@ } synchronized void stopTool() { stopTool(null); stopTool(false); } synchronized void stopTool(final Exception e) { synchronized void stopTool(final boolean stoppedByError) { if (!stopRequested) { stopRequested = true; statThreadScheduler.shutdown(); if (e == null) { // If stats thread is printing stats, wait for it to finish. try { statThreadScheduler.awaitTermination(50, TimeUnit.MILLISECONDS); } catch (InterruptedException ignored) { // Do nothing. } statsThread.run(); } statsThread.stopRecording(stoppedByError); } } @@ -958,13 +499,35 @@ this.bindRequest = request; } BindRequest getBindRequest() { return bindRequest; } protected void joinAllWorkerThreads() throws InterruptedException { for (final Thread t : workerThreads) { t.join(); } } boolean isAsync() { return isAsync; } double[] getPercentiles() { if (percentilesArgument.isPresent()) { double[] percentiles = new double[percentilesArgument.getValues().size()]; int index = 0; for (final String percentile : percentilesArgument.getValues()) { percentiles[index++] = Double.parseDouble(percentile); } Arrays.sort(percentiles); return percentiles; } return DEFAULT_PERCENTILES; } long getWarmUpDurationMs() { return warmUpDurationMs; } long getStatsInterval() { return statsIntervalMs; } } opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
@@ -22,19 +22,22 @@ * * * Copyright 2010 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS. * Portions Copyright 2011-2016 ForgeRock AS. */ package com.forgerock.opendj.ldap.tools; import static com.forgerock.opendj.cli.ArgumentConstants.*; import static com.forgerock.opendj.cli.MultiColumnPrinter.column; import static com.forgerock.opendj.cli.Utils.*; import static com.forgerock.opendj.ldap.tools.ToolsMessages.*; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import com.codahale.metrics.RatioGauge; import com.forgerock.opendj.cli.MultiColumnPrinter; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.ldap.Connection; import org.forgerock.opendj.ldap.ConnectionFactory; @@ -65,15 +68,14 @@ */ public final class SearchRate extends ConsoleApplication { private final class SearchPerformanceRunner extends PerformanceRunner { private final class SearchStatsHandler extends UpdateStatsResultHandler<Result> implements SearchResultHandler { private final class SearchStatsHandler extends UpdateStatsResultHandler<Result> implements SearchResultHandler { private SearchStatsHandler(final long startTime) { super(startTime); } @Override public boolean handleEntry(final SearchResultEntry entry) { entryRecentCount.getAndIncrement(); entryCount.inc(); return true; } @@ -84,23 +86,28 @@ } private final class SearchStatsThread extends StatsThread { private final String[] extraColumn = new String[1]; private static final int ENTRIES_PER_SEARCH_COLUMN_WIDTH = 5; private static final String ENTRIES_PER_SEARCH = STAT_ID_PREFIX + "entries_per_search"; private SearchStatsThread() { super("Entries/Srch"); private SearchStatsThread(final PerformanceRunner perfRunner, final ConsoleApplication app) { super(perfRunner, app); } @Override void resetStats() { super.resetStats(); entryRecentCount.set(0); void resetAdditionalStats() { entryCount = newIntervalCounter(); } @Override String[] getAdditionalColumns() { final int entryCount = entryRecentCount.getAndSet(0); extraColumn[0] = getDivisionResult(entryCount, intervalSuccessCount, 1, "0.0"); return extraColumn; List<MultiColumnPrinter.Column> registerAdditionalColumns() { registry.register(ENTRIES_PER_SEARCH, new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(entryCount.refreshIntervalCount(), successCount.getLastIntervalCount()); } }); return Collections.singletonList( column(ENTRIES_PER_SEARCH, "Entries/Srch", ENTRIES_PER_SEARCH_COLUMN_WIDTH, 1)); } } @@ -115,7 +122,7 @@ @Override public Promise<?, LdapException> performOperation(final Connection connection, final DataSource[] dataSources, final long startTime) { final DataSource[] dataSources, final long currentTimeNs) { if (sr == null) { if (dataSources == null) { sr = Requests.newSearchRequest(baseDN, scope, filter, attributes); @@ -132,7 +139,7 @@ sr.setName(String.format(baseDN, data)); } final SearchStatsHandler handler = new SearchStatsHandler(startTime); final SearchStatsHandler handler = new SearchStatsHandler(currentTimeNs); incrementIterationCount(); return connection.searchAsync(sr, handler).thenOnResult(handler).thenOnException(handler); } @@ -156,8 +163,8 @@ } @Override StatsThread newStatsThread() { return new SearchStatsThread(); StatsThread newStatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication app) { return new SearchStatsThread(performanceRunner, app); } } @@ -167,17 +174,14 @@ * @param args * The command-line arguments provided to this program. */ public static void main(final String[] args) { final int retCode = new SearchRate().run(args); System.exit(filterExitCode(retCode)); } private BooleanArgument verbose; private BooleanArgument scriptFriendly; private final AtomicInteger entryRecentCount = new AtomicInteger(); private StatsThread.IntervalCounter entryCount = StatsThread.newIntervalCounter(); private SearchRate() { // Nothing to do. opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/StatsThread.java
New file @@ -0,0 +1,446 @@ /* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2016 ForgeRock AS. */ package com.forgerock.opendj.ldap.tools; import static com.forgerock.opendj.cli.MultiColumnPrinter.column; import static com.forgerock.opendj.cli.MultiColumnPrinter.separatorColumn; import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_TOOL_WARMING_UP; import static java.util.concurrent.TimeUnit.MILLISECONDS; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.SortedMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricFilter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.RatioGauge; import com.codahale.metrics.ScheduledReporter; import com.codahale.metrics.Timer; import com.forgerock.opendj.cli.ConsoleApplication; import com.forgerock.opendj.cli.MultiColumnPrinter; import org.mpierce.metrics.reservoir.hdrhistogram.HdrHistogramReservoir; /** * Statistics thread base implementation. * <p> * The goal of this class is to compute and print rate tool general statistics. */ class StatsThread extends Thread { static final String STAT_ID_PREFIX = "org.forgerock.opendj."; private static final String TIME_NOW = STAT_ID_PREFIX + "current_time"; private static final String RECENT_THROUGHPUT = STAT_ID_PREFIX + "recent_throughput"; private static final String AVERAGE_THROUGHPUT = STAT_ID_PREFIX + "average_throughput"; private static final String RECENT_RESPONSE_TIME_MS = STAT_ID_PREFIX + "recent_response_time"; private static final String AVERAGE_RESPONSE_TIME_MS = STAT_ID_PREFIX + "average_response_time"; private static final String PERCENTILES = STAT_ID_PREFIX + "percentiles"; private static final String ERROR_PER_SECOND = STAT_ID_PREFIX + "error_per_second"; private static final String REQUEST_PER_RESPONSE = STAT_ID_PREFIX + "request_per_response"; public static final double MS_IN_S = TimeUnit.SECONDS.toMillis(1); public static final double NS_IN_MS = TimeUnit.MILLISECONDS.toNanos(1); private abstract class RateReporter extends ScheduledReporter { final MultiColumnPrinter printer; private RateReporter() { super(registry, "", MetricFilter.ALL, TimeUnit.SECONDS, TimeUnit.MILLISECONDS); printer = createPrinter(); } abstract MultiColumnPrinter createPrinter(); abstract void printTitle(); @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public void report(final SortedMap<String, Gauge> gauges, final SortedMap<String, Counter> counters, final SortedMap<String, Histogram> histograms, final SortedMap<String, Meter> meters, final SortedMap<String, Timer> timers) { int percentileIndex = 0; for (final MultiColumnPrinter.Column column : printer.getColumns()) { final String statKey = column.getId(); if (gauges.containsKey(statKey)) { printer.printData(((Gauge<Double>) gauges.get(statKey)).getValue()); } else if (statKey.startsWith(PERCENTILES)) { final double quantile = percentiles[percentileIndex++] / 100.0; printer.printData( histograms.get(PERCENTILES).getSnapshot().getValue(quantile) / MILLISECONDS.toNanos(1)); } else { printer.printData("-"); } } } } private final class ConsoleRateReporter extends RateReporter { private static final int STANDARD_WIDTH = 8; private List<MultiColumnPrinter.Column> additionalColumns; @Override void printTitle() { final int throughputRawSpan = 2; final int responseTimeRawSpan = 2 + percentiles.length; final int additionalStatsRawSpan = 1 + (performanceRunner.isAsync() ? 1 : 0) + additionalColumns.size(); printer.printDashedLine(); printer.printTitleSection("Throughput", throughputRawSpan); printer.printTitleSection("Response Time", responseTimeRawSpan); printer.printTitleSection(additionalStatsRawSpan > 1 ? "Additional" : "", additionalStatsRawSpan); printer.printTitleSection("(ops/second)", throughputRawSpan); printer.printTitleSection("(milliseconds)", responseTimeRawSpan); printer.printTitleSection(additionalStatsRawSpan > 1 ? "Statistics" : "", additionalStatsRawSpan); printer.printTitleLine(); printer.printDashedLine(); } @Override MultiColumnPrinter createPrinter() { final List<MultiColumnPrinter.Column> columns = new ArrayList<>(); // Throughput (ops/sec) columns.add(separatorColumn()); columns.add(column(RECENT_THROUGHPUT, "recent", STANDARD_WIDTH, 1)); columns.add(column(AVERAGE_THROUGHPUT, "average", STANDARD_WIDTH, 1)); // Response Time (ms) columns.add(separatorColumn()); columns.add(column(RECENT_RESPONSE_TIME_MS, "recent", STANDARD_WIDTH, 3)); columns.add(column(AVERAGE_RESPONSE_TIME_MS, "average", STANDARD_WIDTH, 3)); for (double percentile : percentiles) { columns.add(column(PERCENTILES + percentile, percentile + "%", STANDARD_WIDTH, 2)); } // Additional stats columns.add(separatorColumn()); columns.add(column(ERROR_PER_SECOND, "err/sec", STANDARD_WIDTH, 1)); if (performanceRunner.isAsync()) { columns.add(separatorColumn()); columns.add(column(REQUEST_PER_RESPONSE, "req/res", 4, 1)); } additionalColumns = registerAdditionalColumns(); if (!additionalColumns.isEmpty()) { columns.addAll(additionalColumns); } columns.add(separatorColumn()); return MultiColumnPrinter.builder(app.getOutputStream(), columns) .format(true) .titleAlignment(MultiColumnPrinter.Alignment.CENTER) .build(); } } private final class CsvRateReporter extends RateReporter { @Override void printTitle() { printer.printTitleLine(); } @Override MultiColumnPrinter createPrinter() { final List<MultiColumnPrinter.Column> columns = new ArrayList<>(); columns.add(column(TIME_NOW, "time", 3)); columns.add(column(RECENT_THROUGHPUT, "recent throughput", 1)); columns.add(column(AVERAGE_THROUGHPUT, "average throughput", 1)); columns.add(column(RECENT_RESPONSE_TIME_MS, "recent response time", 3)); columns.add(column(AVERAGE_RESPONSE_TIME_MS, "average response time", 3)); for (double percentile : percentiles) { columns.add(column( PERCENTILES + percentile, percentile + "% response time", 2)); } columns.add(column(ERROR_PER_SECOND, "errors/second", 1)); if (performanceRunner.isAsync()) { columns.add(column(REQUEST_PER_RESPONSE, "Requests/response", 1)); } columns.addAll(registerAdditionalColumns()); return MultiColumnPrinter.builder(app.getOutputStream(), columns) .columnSeparator(",") .build(); } } /** A timer to prevent adding temporary variables in {@link StatsThread#run()}. **/ private static abstract class StatsTimer { private long startTimeMeasure; private long elapsed; abstract long getInstantTime(); private void start() { startTimeMeasure = getInstantTime(); } private long reset() { final long time = getInstantTime(); elapsed = time - this.startTimeMeasure; this.startTimeMeasure = time; return elapsed; } private long elapsed() { return elapsed; } } /** A counter to prevent adding temporary variables in {@link StatsThread#run()}. **/ static final class IntervalCounter extends Counter { private long lastIntervalCount; private long lastTotalCount; long refreshIntervalCount() { final long totalCount = getCount(); lastIntervalCount = totalCount - lastTotalCount; lastTotalCount = totalCount; return lastIntervalCount; } long getLastIntervalCount() { return lastIntervalCount; } long getLastTotalCount() { return lastTotalCount; } } static final IntervalCounter newIntervalCounter() { return new IntervalCounter(); } final MetricRegistry registry = new MetricRegistry(); private final Histogram responseTimes = new Histogram(new HdrHistogramReservoir()); private final StatsTimer gcTimerMs = new StatsTimer() { private final List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans(); @Override long getInstantTime() { long gcDurationMs = 0; for (final GarbageCollectorMXBean bean : gcBeans) { gcDurationMs += bean.getCollectionTime(); } return gcDurationMs; } }; private final StatsTimer timerMs = new StatsTimer() { @Override long getInstantTime() { return System.currentTimeMillis(); } }; IntervalCounter waitDurationNsCount; IntervalCounter successCount; private IntervalCounter operationCount; private IntervalCounter errorCount; private IntervalCounter durationMsCount; private final ConsoleApplication app; private final double[] percentiles; private final PerformanceRunner performanceRunner; private final RateReporter reporter; private long startTimeMs; private volatile boolean warmingUp; private final ScheduledExecutorService statThreadScheduler = Executors.newSingleThreadScheduledExecutor(); StatsThread(final PerformanceRunner performanceRunner, final ConsoleApplication application) { super("Stats Thread"); resetStats(); this.performanceRunner = performanceRunner; this.app = application; this.percentiles = performanceRunner.getPercentiles(); this.reporter = app.isScriptFriendly() ? new CsvRateReporter() : new ConsoleRateReporter(); registerStats(); } /** Resets both general and recent statistic indicators. */ final void resetStats() { errorCount = newIntervalCounter(); operationCount = newIntervalCounter(); successCount = newIntervalCounter(); waitDurationNsCount = newIntervalCounter(); durationMsCount = newIntervalCounter(); resetAdditionalStats(); } private void registerStats() { if (app.isScriptFriendly()) { registry.register(TIME_NOW, new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(System.currentTimeMillis() - startTimeMs, MS_IN_S); } }); } registry.register(RECENT_THROUGHPUT, new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(successCount.getLastIntervalCount() + errorCount.getLastIntervalCount(), durationMsCount.getLastIntervalCount() / MS_IN_S); } }); registry.register(AVERAGE_THROUGHPUT, new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(successCount.getLastTotalCount() + errorCount.getLastTotalCount(), durationMsCount.getLastTotalCount() / MS_IN_S); } }); registry.register(RECENT_RESPONSE_TIME_MS, new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of((waitDurationNsCount.getLastIntervalCount() / NS_IN_MS) - gcTimerMs.elapsed(), successCount.getLastIntervalCount() + errorCount.getLastIntervalCount()); } }); registry.register(AVERAGE_RESPONSE_TIME_MS, new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of((waitDurationNsCount.getLastTotalCount() / NS_IN_MS) - gcTimerMs.getInstantTime(), successCount.getLastTotalCount() + errorCount.getLastTotalCount()); } }); registry.register(ERROR_PER_SECOND, new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(errorCount.getLastIntervalCount(), durationMsCount.getLastIntervalCount() / MS_IN_S); } }); if (performanceRunner.isAsync()) { registry.register(REQUEST_PER_RESPONSE, new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(operationCount.getLastIntervalCount(), successCount.getLastIntervalCount() + errorCount.getLastIntervalCount()); } }); } registry.register(PERCENTILES, responseTimes); } void startReporting() throws InterruptedException { warmUp(); init(); final long statsIntervalMs = performanceRunner.getStatsInterval(); statThreadScheduler.scheduleAtFixedRate(this, statsIntervalMs, statsIntervalMs, TimeUnit.MILLISECONDS); } private void warmUp() throws InterruptedException { final long warmUpDurationMs = performanceRunner.getWarmUpDurationMs(); if (warmUpDurationMs > 0) { if (!app.isScriptFriendly()) { app.println(INFO_TOOL_WARMING_UP.get(warmUpDurationMs / TimeUnit.SECONDS.toMillis(1))); } Thread.sleep(warmUpDurationMs); resetStats(); } warmingUp = false; } private void init() { reporter.printTitle(); timerMs.start(); gcTimerMs.start(); startTimeMs = System.currentTimeMillis(); } public void stopRecording(final boolean stoppedByError) { statThreadScheduler.shutdown(); if (!stoppedByError) { // If stats thread is printing stats, wait for it to finish and print a last line of stats. try { statThreadScheduler.awaitTermination(50, TimeUnit.MILLISECONDS); } catch (InterruptedException ignored) { // Do nothing. } run(); } } /** Performs stat snapshots and reports results to application. */ @Override public void run() { durationMsCount.inc(timerMs.reset() - gcTimerMs.reset()); durationMsCount.refreshIntervalCount(); operationCount.refreshIntervalCount(); successCount.refreshIntervalCount(); errorCount.refreshIntervalCount(); waitDurationNsCount.refreshIntervalCount(); reporter.report(); } void addResponseTime(final long responseTimeNs) { if (!warmingUp) { waitDurationNsCount.inc(responseTimeNs); responseTimes.update(responseTimeNs); } } void incrementFailedCount() { errorCount.inc(); } void incrementSuccessCount() { successCount.inc(); } void incrementOperationCount() { operationCount.inc(); } /** Child classes which manage additional stats need to override this method. */ List<MultiColumnPrinter.Column> registerAdditionalColumns() { return Collections.emptyList(); } /** Do nothing by default, child classes which manage additional stats need to override this method. */ void resetAdditionalStats() { } } opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties
@@ -138,7 +138,7 @@ INFO_ARGPARSER_USAGE_JAVA_SCRIPTNAME=Usage: %s {options} INFO_ARGPARSER_USAGE_TRAILINGARGS={trailing-arguments} INFO_ARGPARSER_USAGE_DEFAULT_VALUE=Default value: %s ERROR_RATE_TOOLS_CAN_NOT_GET_CONNECTION=%s\nThe tool is going to stop. ERROR_RATE_TOOLS_CANNOT_GET_CONNECTION=%s\nStopping... # # Extension messages # opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AuthRateITCase.java
@@ -40,7 +40,7 @@ @SuppressWarnings("javadoc") public class AuthRateITCase extends ToolsITCase { private static final String THROUGHPUT_TEXT = "Recent throughput (ops/second)"; private static final String THROUGHPUT_TEXT = "recent throughput"; @DataProvider public Object[][] authRateArgs() throws Exception { opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerStatsTestCase.java
File was deleted opendj-sdk/opendj-sdk-parent/pom.xml
@@ -59,6 +59,7 @@ <forgerock-build-tools.version>1.0.2</forgerock-build-tools.version> <forgerock-doc-plugin.version>3.1.0</forgerock-doc-plugin.version> <grizzly-framework.version>2.3.23</grizzly-framework.version> <metrics-core.version>3.1.2</metrics-core.version> <!-- OSGi bundles properties --> <opendj.osgi.import.additional /> @@ -76,6 +77,13 @@ <dependencyManagement> <dependencies> <!-- Dropwizard metrics-core --> <dependency> <groupId>io.dropwizard.metrics</groupId> <artifactId>metrics-core</artifactId> <version>${metrics-core.version}</version> </dependency> <!-- Commons --> <dependency> <groupId>org.forgerock</groupId>