IGNITE-13888 Provide the utility to output performance statistics operations to the console (#40)

diff --git a/modules/performance-statistics-ext/bin/print-statistics.sh b/modules/performance-statistics-ext/bin/print-statistics.sh
new file mode 100644
index 0000000..4d250a1
--- /dev/null
+++ b/modules/performance-statistics-ext/bin/print-statistics.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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 script is used to read performance statistics files to the console or file.
+#
+
+SCRIPT_DIR=$(cd $(dirname "$0"); pwd)
+
+#
+# JVM options. See http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp for more details.
+#
+# ADD YOUR/CHANGE ADDITIONAL OPTIONS HERE
+#
+JVM_OPTS="-Xms32m -Xmx512m"
+
+#
+# Define classpath
+#
+CP="${SCRIPT_DIR}/libs/*"
+
+#
+# Set main class to run the tool.
+#
+MAIN_CLASS=org.apache.ignite.internal.performancestatistics.PerformanceStatisticsPrinter
+
+#
+# Garbage Collection options.
+#
+JVM_OPTS="\
+    -XX:+UseG1GC \
+     ${JVM_OPTS}"
+
+#
+# Run tool.
+#
+java ${JVM_OPTS} -cp "${CP}" ${MAIN_CLASS} $@
diff --git a/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/PerformanceStatisticsPrinter.java b/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/PerformanceStatisticsPrinter.java
new file mode 100644
index 0000000..789bf5b
--- /dev/null
+++ b/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/PerformanceStatisticsPrinter.java
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.ignite.internal.performancestatistics;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.performancestatistics.handlers.PrintHandler;
+import org.apache.ignite.internal.processors.performancestatistics.FilePerformanceStatisticsReader;
+import org.apache.ignite.internal.processors.performancestatistics.OperationType;
+import org.apache.ignite.internal.util.typedef.internal.A;
+import org.apache.ignite.internal.util.typedef.internal.CU;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.jetbrains.annotations.Nullable;
+
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.joining;
+
+/**
+ * Performance statistics printer.
+ */
+public class PerformanceStatisticsPrinter {
+    /**
+     * @param args Program arguments or '-h' to get usage help.
+     */
+    public static void main(String... args) throws Exception {
+        Parameters params = parseArguments(args);
+
+        validateParameters(params);
+
+        PrintStream ps = printStream(params.outFile);
+
+        try {
+            new FilePerformanceStatisticsReader(
+                new PrintHandler(ps, params.ops, params.from, params.to,params.cacheIds))
+                .read(singletonList(new File(params.statFileOrDir)));
+        }
+        finally {
+            if (params.outFile != null)
+                ps.close();
+        }
+    }
+
+    /**
+     * Parses arguments or print help.
+     *
+     * @param args Arguments to parse.
+     * @return Program arguments.
+     */
+    private static Parameters parseArguments(String[] args) {
+        if (args == null || args.length == 0 || "--help".equalsIgnoreCase(args[0]) || "-h".equalsIgnoreCase(args[0]))
+            printHelp();
+
+        Parameters params = new Parameters();
+
+        Iterator<String> iter = Arrays.asList(args).iterator();
+
+        params.statFileOrDir = iter.next();
+
+        while (iter.hasNext()) {
+            String arg = iter.next();
+
+            if ("--out".equalsIgnoreCase(arg)) {
+                A.ensure(iter.hasNext(), "Expected output file name");
+
+                params.outFile = iter.next();
+            }
+            else if ("--ops".equalsIgnoreCase(arg)) {
+                A.ensure(iter.hasNext(), "Expected operation types");
+
+                String[] ops = iter.next().split(",");
+
+                A.ensure(ops.length > 0, "Expected at least one operation");
+
+                params.ops = new BitSet();
+
+                for (String op : ops) {
+                    OperationType opType = enumIgnoreCase(op, OperationType.class);
+
+                    A.ensure(opType != null, "Unknown operation type [op=" + op + ']');
+
+                    params.ops.set(opType.id());
+                }
+            }
+            else if ("--from".equalsIgnoreCase(arg)) {
+                A.ensure(iter.hasNext(), "Expected time from");
+
+                params.from = Long.parseLong(iter.next());
+            }
+            else if ("--to".equalsIgnoreCase(arg)) {
+                A.ensure(iter.hasNext(), "Expected time to");
+
+                params.to = Long.parseLong(iter.next());
+            }
+            else if ("--caches".equalsIgnoreCase(arg)) {
+                A.ensure(iter.hasNext(), "Expected cache names");
+
+                String[] caches = iter.next().split(",");
+
+                A.ensure(caches.length > 0, "Expected at least one cache name");
+
+                params.cacheIds = Arrays.stream(caches).map(CU::cacheId).collect(Collectors.toSet());
+            }
+            else
+                throw new IllegalArgumentException("Unknown command: " + arg);
+        }
+
+        return params;
+    }
+
+    /** Prints help. */
+    private static void printHelp() {
+        String ops = Arrays.stream(OperationType.values()).map(Enum::toString).collect(joining(", "));
+
+        System.out.println("The script is used to print performance statistics files to the console or file." +
+            U.nl() + U.nl() +
+            "Usage: print-statistics.sh path_to_files [--out out_file] [--ops op_types] " +
+            "[--from startTimeFrom] [--to startTimeTo] [--caches cache_names]" + U.nl() +
+            U.nl() +
+            "  path_to_files - Performance statistics file or files directory." + U.nl() +
+            "  out_file      - Output file." + U.nl() +
+            "  op_types      - Comma separated list of operation types to filter the output." + U.nl() +
+            "  from          - The minimum operation start time to filter the output." + U.nl() +
+            "  to            - The maximum operation start time to filter the output." + U.nl() +
+            "  cache_names   - Comma separated list of cache names to filter the output." + U.nl() +
+            U.nl() +
+            "Times must be specified in the Unix time format in milliseconds." + U.nl() +
+            "List of operation types: " + ops + '.');
+
+        System.exit(0);
+    }
+
+    /** @param params Validates parameters. */
+    private static void validateParameters(Parameters params) {
+        File statFileOrDir = new File(params.statFileOrDir);
+
+        A.ensure(statFileOrDir.exists(), "Performance statistics file or files directory does not exists");
+    }
+
+    /** @return Print stream to the console or file. */
+    private static PrintStream printStream(@Nullable String outFile) {
+        PrintStream ps;
+
+        if (outFile != null) {
+            try {
+                ps = new PrintStream(new BufferedOutputStream(new FileOutputStream(new File(outFile), true)));
+            }
+            catch (IOException e) {
+                throw new IllegalArgumentException("Cannot write to output file", e);
+            }
+        }
+        else
+            ps = System.out;
+
+        return ps;
+    }
+
+    /**
+     * Gets the enum for the given name ignore case.
+     *
+     * @param name Enum name.
+     * @param cls Enum class.
+     * @return The enum or {@code null} if not found.
+     */
+    private static <E extends Enum<E>> @Nullable E enumIgnoreCase(String name, Class<E> cls) {
+        for (E e : cls.getEnumConstants()) {
+            if (e.name().equalsIgnoreCase(name))
+                return e;
+        }
+
+        return null;
+    }
+
+    /** Printer parameters. */
+    private static class Parameters {
+        /** Performance statistics file or files directory. */
+        private String statFileOrDir;
+
+        /** Output file. */
+        @Nullable private String outFile;
+
+        /** Operation types to print. */
+        @Nullable private BitSet ops;
+
+        /** The minimum operation start time to filter the output. */
+        private long from = Long.MIN_VALUE;
+
+        /** The maximum operation start time to filter the output. */
+        private long to = Long.MAX_VALUE;
+
+        /** Cache identifiers to filter the output. */
+        @Nullable private Set<Integer> cacheIds;
+    }
+}
diff --git a/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/handlers/PrintHandler.java b/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/handlers/PrintHandler.java
new file mode 100644
index 0000000..df27ae8
--- /dev/null
+++ b/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/handlers/PrintHandler.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.ignite.internal.performancestatistics.handlers;
+
+import java.io.PrintStream;
+import java.util.BitSet;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.ignite.internal.processors.cache.query.GridCacheQueryType;
+import org.apache.ignite.internal.processors.performancestatistics.OperationType;
+import org.apache.ignite.internal.processors.performancestatistics.PerformanceStatisticsHandler;
+import org.apache.ignite.internal.util.GridIntIterator;
+import org.apache.ignite.internal.util.GridIntList;
+import org.apache.ignite.lang.IgniteUuid;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.ignite.internal.performancestatistics.util.Utils.printEscaped;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.CACHE_START;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.JOB;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.QUERY;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.QUERY_READS;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.TASK;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.TX_COMMIT;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.TX_ROLLBACK;
+
+/**
+ * Handler to print performance statistics operations.
+ */
+public class PrintHandler implements PerformanceStatisticsHandler {
+    /** Print stream. */
+    private final PrintStream ps;
+
+    /** Operation types. */
+    @Nullable private final BitSet ops;
+
+    /** Start time from. */
+    private final long from;
+
+    /** Start time to. */
+    private final long to;
+
+    /** Cache identifiers to filter the output. */
+    @Nullable private final Set<Integer> cacheIds;
+
+    /**
+     * @param ps Print stream.
+     * @param ops Set of operations to print.
+     * @param from The minimum operation start time to filter the output.
+     * @param to The maximum operation start time to filter the output.
+     * @param cacheIds Cache identifiers to filter the output.
+     */
+    public PrintHandler(PrintStream ps, @Nullable BitSet ops, long from, long to, @Nullable Set<Integer> cacheIds) {
+        this.ps = ps;
+        this.ops = ops;
+        this.from = from;
+        this.to = to;
+        this.cacheIds = cacheIds;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void cacheStart(UUID nodeId, int cacheId, String name) {
+        if (skip(CACHE_START, cacheId))
+            return;
+
+        ps.print("{\"op\":\"" + CACHE_START);
+        ps.print("\",\"nodeId\":\"");
+        ps.print(nodeId);
+        ps.print("\",\"cacheId\":");
+        ps.print(cacheId);
+        ps.print(",\"name\":\"");
+        printEscaped(ps, name);
+        ps.println("\"}");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void cacheOperation(UUID nodeId, OperationType type, int cacheId, long startTime, long duration) {
+        if (skip(type, startTime, cacheId))
+            return;
+
+        ps.print("{\"op\":\"");
+        ps.print(type);
+        ps.print("\",\"nodeId\":\"");
+        ps.print(nodeId);
+        ps.print("\",\"cacheId\":");
+        ps.print(cacheId);
+        ps.print(",\"startTime\":");
+        ps.print(startTime);
+        ps.print(",\"duration\":");
+        ps.print(duration);
+        ps.println("}");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void transaction(UUID nodeId, GridIntList cacheIds, long startTime, long duration,
+        boolean commited) {
+        OperationType op = commited ? TX_COMMIT : TX_ROLLBACK;
+
+        if (skip(op, startTime, cacheIds))
+            return;
+
+        ps.print("{\"op\":\"");
+        ps.print(op);
+        ps.print("\",\"nodeId\":\"");
+        ps.print(nodeId);
+        ps.print("\",\"cacheIds\":");
+        ps.print(cacheIds);
+        ps.print(",\"startTime\":");
+        ps.print(startTime);
+        ps.print(",\"duration\":");
+        ps.print(duration);
+        ps.println("}");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void query(UUID nodeId, GridCacheQueryType type, String text, long id, long startTime,
+        long duration, boolean success) {
+        if (skip(QUERY, startTime))
+            return;
+
+        ps.print("{\"op\":\"" + QUERY);
+        ps.print("\",\"nodeId\":\"");
+        ps.print(nodeId);
+        ps.print("\",\"type\":\"");
+        ps.print(type);
+        ps.print("\",\"text\":\"");
+        printEscaped(ps, text);
+        ps.print("\",\"id\":");
+        ps.print(id);
+        ps.print(",\"startTime\":");
+        ps.print(startTime);
+        ps.print(",\"duration\":");
+        ps.print(duration);
+        ps.print(",\"success\":");
+        ps.print(success);
+        ps.println("}");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void queryReads(UUID nodeId, GridCacheQueryType type, UUID queryNodeId, long id,
+        long logicalReads, long physicalReads) {
+        if (skip(QUERY_READS))
+            return;
+
+        ps.print("{\"op\":\"" + QUERY_READS);
+        ps.print("\",\"nodeId\":\"");
+        ps.print(nodeId);
+        ps.print("\",\"type\":\"");
+        ps.print(type);
+        ps.print("\",\"queryNodeId\":\"");
+        ps.print(queryNodeId);
+        ps.print("\",\"id\":");
+        ps.print(id);
+        ps.print(",\"logicalReads\":");
+        ps.print(logicalReads);
+        ps.print(",\"physicalReads\":");
+        ps.print(physicalReads);
+        ps.println("}");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void task(UUID nodeId, IgniteUuid sesId, String taskName, long startTime, long duration,
+        int affPartId) {
+        if (skip(TASK, startTime))
+            return;
+
+        ps.print("{\"op\":\"" + TASK);
+        ps.print("\",\"nodeId\":\"");
+        ps.print(nodeId);
+        ps.print("\",\"sesId\":\"");
+        ps.print(sesId);
+        ps.print("\",\"taskName\":\"");
+        printEscaped(ps, taskName);
+        ps.print("\",\"startTime\":");
+        ps.print(startTime);
+        ps.print(",\"duration\":");
+        ps.print(duration);
+        ps.print(",\"affPartId\":");
+        ps.print(affPartId);
+        ps.println("}");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void job(UUID nodeId, IgniteUuid sesId, long queuedTime, long startTime, long duration,
+        boolean timedOut) {
+        if (skip(JOB, startTime))
+            return;
+
+        ps.print("{\"op\":\"" + JOB);
+        ps.print("\",\"nodeId\":\"");
+        ps.print(nodeId);
+        ps.print("\",\"sesId\":\"");
+        ps.print(sesId);
+        ps.print("\",\"queuedTime\":");
+        ps.print(queuedTime);
+        ps.print(",\"startTime\":");
+        ps.print(startTime);
+        ps.print(",\"duration\":");
+        ps.print(duration);
+        ps.print(",\"timedOut\":");
+        ps.print(timedOut);
+        ps.println("}");
+    }
+
+    /** @return {@code True} if the operation should be skipped. */
+    private boolean skip(OperationType op) {
+        return !(ops == null || ops.get(op.id()));
+    }
+
+    /** @return {@code True} if the operation should be skipped. */
+    private boolean skip(OperationType op, long startTime) {
+        return skip(op) || startTime < from || startTime > to;
+    }
+
+    /** @return {@code True} if the operation should be skipped. */
+    private boolean skip(OperationType op, int cacheId) {
+        return skip(op) || !(cacheIds == null || cacheIds.contains(cacheId));
+    }
+
+    /** @return {@code True} if the operation should be skipped. */
+    private boolean skip(OperationType op, long startTime, int cacheId) {
+        return skip(op, startTime) || !(cacheIds == null || cacheIds.contains(cacheId));
+    }
+
+    /** @return {@code True} if the operation should be skipped. */
+    private boolean skip(OperationType op, long startTime, GridIntList cacheIds) {
+        if (skip(op, startTime))
+            return true;
+
+        if (this.cacheIds == null)
+            return false;
+
+        GridIntIterator iter = cacheIds.iterator();
+
+        while (iter.hasNext()) {
+            if (this.cacheIds.contains(iter.next()))
+                return false;
+        }
+
+        return true;
+    }
+}
diff --git a/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/util/Utils.java b/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/util/Utils.java
index 1e7249e..01a0312 100644
--- a/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/util/Utils.java
+++ b/modules/performance-statistics-ext/src/main/java/org/apache/ignite/internal/performancestatistics/util/Utils.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.performancestatistics.util;
 
+import java.io.PrintStream;
+import com.fasterxml.jackson.core.io.CharTypes;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -26,6 +28,9 @@
     /** Json mapper. */
     public static final ObjectMapper MAPPER = new ObjectMapper();
 
+    /** */
+    private final static char[] HC = "0123456789ABCDEF".toCharArray();
+
     /** Creates empty object for given value if absent. */
     public static ObjectNode createObjectIfAbsent(String val, ObjectNode json) {
         ObjectNode node = (ObjectNode)json.get(val);
@@ -51,4 +56,44 @@
 
         return node;
     }
+
+    /**
+     * Prints JSON-escaped string to the stream.
+     *
+     * @param ps Print stream to write to.
+     * @param str String to print.
+     * @see CharTypes#appendQuoted(StringBuilder, String)
+     */
+    public static void printEscaped(PrintStream ps, String str) {
+        int[] escCodes = CharTypes.get7BitOutputEscapes();
+
+        int escLen = escCodes.length;
+
+        for (int i = 0, len = str.length(); i < len; ++i) {
+            char c = str.charAt(i);
+
+            if (c >= escLen || escCodes[c] == 0) {
+                ps.print(c);
+
+                continue;
+            }
+
+            ps.print('\\');
+
+            int escCode = escCodes[c];
+
+            if (escCode < 0) {
+                ps.print('u');
+                ps.print('0');
+                ps.print('0');
+
+                int val = c;
+
+                ps.print(HC[val >> 4]);
+                ps.print(HC[val & 0xF]);
+            }
+            else
+                ps.print((char)escCode);
+        }
+    }
 }
diff --git a/modules/performance-statistics-ext/src/test/java/org/apache/ignite/internal/performancestatistics/IgnitePerformanceStatisticsReportTestSuite.java b/modules/performance-statistics-ext/src/test/java/org/apache/ignite/internal/performancestatistics/IgnitePerformanceStatisticsReportTestSuite.java
index 35265e7..c21ddb2 100644
--- a/modules/performance-statistics-ext/src/test/java/org/apache/ignite/internal/performancestatistics/IgnitePerformanceStatisticsReportTestSuite.java
+++ b/modules/performance-statistics-ext/src/test/java/org/apache/ignite/internal/performancestatistics/IgnitePerformanceStatisticsReportTestSuite.java
@@ -25,7 +25,8 @@
  */
 @RunWith(Suite.class)
 @Suite.SuiteClasses({
-    PerformanceStatisticsReportSelfTest.class
+    PerformanceStatisticsReportSelfTest.class,
+    PerformanceStatisticsPrinterTest.class
 })
 public class IgnitePerformanceStatisticsReportTestSuite {
 }
diff --git a/modules/performance-statistics-ext/src/test/java/org/apache/ignite/internal/performancestatistics/PerformanceStatisticsPrinterTest.java b/modules/performance-statistics-ext/src/test/java/org/apache/ignite/internal/performancestatistics/PerformanceStatisticsPrinterTest.java
new file mode 100644
index 0000000..4cd267f
--- /dev/null
+++ b/modules/performance-statistics-ext/src/test/java/org/apache/ignite/internal/performancestatistics/PerformanceStatisticsPrinterTest.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.ignite.internal.performancestatistics;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Consumer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ignite.internal.processors.cache.query.GridCacheQueryType;
+import org.apache.ignite.internal.processors.performancestatistics.FilePerformanceStatisticsWriter;
+import org.apache.ignite.internal.processors.performancestatistics.OperationType;
+import org.apache.ignite.internal.util.GridIntList;
+import org.apache.ignite.internal.util.typedef.F;
+import org.apache.ignite.internal.util.typedef.internal.CU;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.lang.IgniteUuid;
+import org.apache.ignite.logger.java.JavaLogger;
+import org.apache.ignite.testframework.junits.GridTestKernalContext;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static java.util.stream.Collectors.joining;
+import static org.apache.ignite.internal.processors.performancestatistics.FilePerformanceStatisticsWriter.PERF_STAT_DIR;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.CACHE_GET;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.CACHE_PUT;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.CACHE_START;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.JOB;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.QUERY;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.QUERY_READS;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.TASK;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.TX_COMMIT;
+import static org.apache.ignite.internal.processors.performancestatistics.OperationType.TX_ROLLBACK;
+import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests the performance statistics printer.
+ */
+public class PerformanceStatisticsPrinterTest {
+    /** Test node ID. */
+    private final static UUID NODE_ID = UUID.randomUUID();
+
+    /** */
+    @Before
+    public void beforeTest() throws Exception {
+        U.delete(new File(U.defaultWorkDirectory()));
+    }
+
+    /** */
+    @After
+    public void afterTest() throws Exception {
+        U.delete(new File(U.defaultWorkDirectory()));
+    }
+
+    /** @throws Exception If failed. */
+    @Test
+    public void testOperationsFilter() throws Exception {
+        createStatistics(writer -> {
+            writer.cacheStart(0, "cache");
+            writer.cacheOperation(CACHE_GET, 0, 0, 0);
+            writer.transaction(GridIntList.asList(0), 0, 0, true);
+            writer.transaction(GridIntList.asList(0), 0, 0, false);
+            writer.query(GridCacheQueryType.SQL_FIELDS, "query", 0, 0, 0, true);
+            writer.queryReads(GridCacheQueryType.SQL_FIELDS, NODE_ID, 0, 0, 0);
+            writer.task(new IgniteUuid(NODE_ID, 0), "task", 0, 0, 0);
+            writer.job(new IgniteUuid(NODE_ID, 0), 0, 0, 0, true);
+        });
+
+        List<OperationType> expOps = F.asList(CACHE_START, CACHE_GET, TX_COMMIT, TX_ROLLBACK, QUERY, QUERY_READS,
+            TASK, JOB);
+
+        checkOperationFilter(null, expOps);
+        checkOperationFilter(F.asList(CACHE_START), F.asList(CACHE_START));
+        checkOperationFilter(F.asList(TASK, JOB), F.asList(TASK, JOB));
+        checkOperationFilter(F.asList(CACHE_PUT), Collections.emptyList());
+    }
+
+    /** */
+    private void checkOperationFilter(List<OperationType> opsArg, List<OperationType> expOps) throws Exception {
+        List<String> args = new LinkedList<>();
+
+        if (opsArg != null) {
+            args.add("--ops");
+
+            args.add(opsArg.stream().map(Enum::toString).collect(joining(",")));
+        }
+
+        List<OperationType> ops = new LinkedList<>(expOps);
+
+        readStatistics(args, json -> {
+            OperationType op = OperationType.valueOf(json.get("op").asText());
+
+            assertTrue("Unexpected operation: " + op, ops.remove(op));
+
+            UUID nodeId = UUID.fromString(json.get("nodeId").asText());
+
+            assertEquals(NODE_ID, nodeId);
+        });
+
+        assertTrue("Expected operations:" + ops, ops.isEmpty());
+    }
+
+    /** @throws Exception If failed. */
+    @Test
+    public void testStartTimeFilter() throws Exception {
+        long startTime1 = 10;
+        long startTime2 = 20;
+
+        createStatistics(writer -> {
+            for (long startTime : new long[] {startTime1, startTime2}) {
+                writer.cacheOperation(CACHE_GET, 0, startTime, 0);
+                writer.transaction(GridIntList.asList(0), startTime, 0, true);
+                writer.transaction(GridIntList.asList(0), startTime, 0, false);
+                writer.query(GridCacheQueryType.SQL_FIELDS, "query", 0, startTime, 0, true);
+                writer.task(new IgniteUuid(NODE_ID, 0), "", startTime, 0, 0);
+                writer.job(new IgniteUuid(NODE_ID, 0), 0, startTime, 0, true);
+            }
+        });
+
+        checkStartTimeFilter(null, null, F.asList(startTime1, startTime2));
+        checkStartTimeFilter(null, startTime1, F.asList(startTime1));
+        checkStartTimeFilter(startTime2, null, F.asList(startTime2));
+        checkStartTimeFilter(startTime1, startTime2, F.asList(startTime1, startTime2));
+        checkStartTimeFilter(startTime2 + 1, null, Collections.emptyList());
+        checkStartTimeFilter(null, startTime1 - 1, Collections.emptyList());
+    }
+
+    /** */
+    private void checkStartTimeFilter(Long fromArg, Long toArg, List<Long> expTimes) throws Exception {
+        List<OperationType> opsWithStartTime = F.asList(CACHE_GET, TX_COMMIT, TX_ROLLBACK, QUERY, TASK, JOB);
+
+        List<String> args = new LinkedList<>();
+
+        if (fromArg != null) {
+            args.add("--from");
+
+            args.add(fromArg.toString());
+        }
+
+        if (toArg != null) {
+            args.add("--to");
+
+            args.add(toArg.toString());
+        }
+
+        Map<Long, List<OperationType>> opsByTime = new HashMap<>();
+
+        for (Long time : expTimes)
+            opsByTime.put(time, new LinkedList<>(opsWithStartTime));
+
+        readStatistics(args, json -> {
+            OperationType op = OperationType.valueOf(json.get("op").asText());
+
+            if (opsWithStartTime.contains(op)) {
+                long startTime = json.get("startTime").asLong();
+
+                assertTrue("Unexpected startTime: " + startTime, opsByTime.containsKey(startTime));
+                assertTrue("Unexpected operation: " + op, opsByTime.get(startTime).remove(op));
+            }
+        });
+
+        assertTrue("Expected operations: " + opsByTime, opsByTime.values().stream().allMatch(List::isEmpty));
+    }
+
+    /** @throws Exception If failed. */
+    @Test
+    public void testCachesFilter() throws Exception {
+        String cache1 = "cache1";
+        String cache2 = "cache2";
+
+        createStatistics(writer -> {
+            for (String cache : new String[] {cache1, cache2}) {
+                writer.cacheStart(CU.cacheId(cache), cache);
+                writer.cacheOperation(CACHE_GET, CU.cacheId(cache), 0, 0);
+                writer.transaction(GridIntList.asList(CU.cacheId(cache)), 0, 0, true);
+                writer.transaction(GridIntList.asList(CU.cacheId(cache)), 0, 0, false);
+            }
+        });
+
+        checkCachesFilter(null, new String[] {cache1, cache2});
+        checkCachesFilter(new String[] {cache1}, new String[] {cache1});
+        checkCachesFilter(new String[] {cache2}, new String[] {cache2});
+        checkCachesFilter(new String[] {cache1, cache2}, new String[] {cache1, cache2});
+        checkCachesFilter(new String[] {"unknown_cache"}, new String[0]);
+    }
+
+    /** */
+    private void checkCachesFilter(String[] cachesArg, String[] expCaches) throws Exception {
+        List<OperationType> cacheIdOps = F.asList(CACHE_START, CACHE_GET, TX_COMMIT, TX_ROLLBACK);
+
+        List<String> args = new LinkedList<>();
+
+        if (cachesArg != null) {
+            args.add("--caches");
+
+            args.add(String.join(",", cachesArg));
+        }
+
+        Map<Integer, List<OperationType>> opsById = new HashMap<>();
+
+        for (String cache : expCaches)
+            opsById.put(CU.cacheId(cache), new LinkedList<>(cacheIdOps));
+
+        readStatistics(args, json -> {
+            OperationType op = OperationType.valueOf(json.get("op").asText());
+
+            Integer id = null;
+
+            if (OperationType.cacheOperation(op) || op == CACHE_START)
+                id = json.get("cacheId").asInt();
+            else if (OperationType.transactionOperation(op)) {
+                assertTrue(json.get("cacheIds").isArray());
+                assertEquals(1, json.get("cacheIds").size());
+
+                id = json.get("cacheIds").get(0).asInt();
+            }
+            else
+                fail("Unexpected operation: " + op);
+
+            assertTrue("Unexpected cache id: " + id, opsById.containsKey(id));
+            assertTrue("Unexpected operation: " + op, opsById.get(id).remove(op));
+        });
+
+        assertTrue("Expected operations: " + opsById, opsById.values().stream().allMatch(List::isEmpty));
+    }
+
+    /** Writes statistics through passed writer. */
+    private void createStatistics(Consumer<FilePerformanceStatisticsWriter> c) throws Exception {
+        FilePerformanceStatisticsWriter writer = new FilePerformanceStatisticsWriter(new TestKernalContext(NODE_ID));
+
+        writer.start();
+
+        waitForCondition(() -> U.field((Object)U.field(writer, "fileWriter"), "runner") != null, 30_000);
+
+        c.accept(writer);
+
+        writer.stop();
+    }
+
+    /**
+     * @param args Additional program arguments.
+     * @param c Consumer to handle operations.
+     * @throws Exception If failed.
+     */
+    private void readStatistics(List<String> args, Consumer<JsonNode> c) throws Exception {
+        File perfStatDir = new File(U.defaultWorkDirectory(), PERF_STAT_DIR);
+
+        assertTrue(perfStatDir.exists());
+
+        File out = new File(U.defaultWorkDirectory(), "report.txt");
+
+        U.delete(out);
+
+        List<String> pArgs = new LinkedList<>();
+
+        pArgs.add(perfStatDir.getAbsolutePath());
+        pArgs.add("--out");
+        pArgs.add(out.getAbsolutePath());
+
+        pArgs.addAll(args);
+
+        PerformanceStatisticsPrinter.main(pArgs.toArray(new String[0]));
+
+        assertTrue(out.exists());
+
+        ObjectMapper mapper = new ObjectMapper();
+
+        try (BufferedReader reader = new BufferedReader(new FileReader(out))) {
+            String line;
+
+            while ((line = reader.readLine()) != null) {
+                JsonNode json = mapper.readTree(line);
+
+                assertTrue(json.isObject());
+
+                UUID nodeId = UUID.fromString(json.get("nodeId").asText());
+
+                assertEquals(NODE_ID, nodeId);
+
+                c.accept(json);
+            }
+        }
+    }
+
+    /** Test kernal context. */
+    private static class TestKernalContext extends GridTestKernalContext {
+        /** Node ID. */
+        private final UUID nodeId;
+
+        /** @param nodeId Node ID. */
+        public TestKernalContext(UUID nodeId) {
+            super(new JavaLogger());
+
+            this.nodeId = nodeId;
+        }
+
+        /** {@inheritDoc} */
+        @Override public UUID localNodeId() {
+            return nodeId;
+        }
+    }
+}