/* * -----------------------------------------------------------------------\ * PerfCake *   * Copyright (C) 2010 - 2016 the original author or authors. *   * 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. * -----------------------------------------------------------------------/ */ package org.perfcake.reporting.reporter; import org.perfcake.PerfCakeConst; import org.perfcake.agent.AgentCommand; import org.perfcake.common.PeriodType; import org.perfcake.common.TimestampedRecord; import org.perfcake.reporting.Measurement; import org.perfcake.reporting.MeasurementUnit; import org.perfcake.reporting.Quantity; import org.perfcake.reporting.ReportingException; import org.perfcake.reporting.destination.Destination; import org.perfcake.util.Utils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.InetAddress; import java.net.Socket; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; /** * Reports memory usage information from a remote JVM, * where PerfCake agent is deployed. It communicates with the PerfCake agent to * get the information via TCP sockets. * * @author <a href="mailto:pavel.macik@gmail.com">Pavel Macík</a> */ public class MemoryUsageReporter extends AbstractReporter { /** * MiB to bytes conversion factor. */ private static final long BYTES_IN_MIB = 1_048_576L; /** * The reporter's logger. */ private static final Logger log = LogManager.getLogger(MemoryUsageReporter.class); /** * Hostname where PerfCake agent is listening on. */ private String agentHostname = "localhost"; /** * Port number where PerfCake agent is listening on. */ private String agentPort = "8850"; /** * IP address of the PerfCake agent. */ private InetAddress host; /** * Socket used to communicate with the PerfCake agent. */ private Socket socket; /** * Reader to read response from PerfCake agent. */ private BufferedReader responseReader; /** * Writer to send requests to PerfCake agent. */ private PrintWriter requestWriter; /** * Used memory time window (number of latest records) for possible memory leak detection. */ private Queue<TimestampedRecord<Number>> usedMemoryTimeWindow; /** * Size of the memory time window (number of latest records) for possible memory leak detection. */ private int usedMemoryTimeWindowSize = 100; /** * Possible memory leak detection threshold. Possible memory leak is found, when the actual slope of the linear regression * line computed from the time window data set is greater than the threshold. */ private double memoryLeakSlopeThreshold = 1024; // default 1 KiB/s /** * Switch to enabling (disabling) the possible memory leak detection. */ private boolean memoryLeakDetectionEnabled = false; /** * Determines the period in which a memory usage is gathered from the PerfCake agent. */ private long memoryLeakDetectionMonitoringPeriod = 500L; /** * Allows enabling/disabling of garbage collection performed each time the memory usage of the * tested system is measured and published. * Since the garbage collection is CPU intensive operation be careful to enable it and to how often * the memory usage is measured because it will have a significant impact on the measured system and naturally the * measured results too. It is disabled (set to <code>false</code>) by default. */ private boolean performGcOnMemoryUsage = false; /** * A flag that indicates that a possible memory leak has been detected. */ private boolean memoryLeakDetected = false; /** * A flag that indicates that a heap dump was saved after a possible memory leak has been detected. */ private boolean heapDumpSaved = false; /** * Tha latest computed used memory trend slope value. */ private float memoryTrendSlope = 0; /** * The property to make a memory dump, when possible memory leak is detected. The {@link org.perfcake.reporting.reporter.MemoryUsageReporter} * will send a command to PerfCake agent that will create a heap dump. */ private boolean memoryDumpOnLeak = false; /** * The name of the memory dump file created by PerfCake agent. */ private String memoryDumpFile = null; /** * Gathers memory data for memory leak detection analysis. */ private MemoryDataGatheringTask memoryDataGatheringTask = null; @Override protected void doReset() { if (usedMemoryTimeWindow != null) { usedMemoryTimeWindow.clear(); } heapDumpSaved = false; } @Override protected void doReport(final MeasurementUnit measurementUnit) throws ReportingException { // nop } @Override public void start() { super.start(); try { host = InetAddress.getByName(agentHostname); if (log.isDebugEnabled()) { log.debug("Creating socket " + host + ":" + agentPort + "..."); } socket = new Socket(host, Integer.parseInt(agentPort)); requestWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), Utils.getDefaultEncoding()), true); responseReader = new BufferedReader(new InputStreamReader(socket.getInputStream(), Utils.getDefaultEncoding())); usedMemoryTimeWindow = new LinkedBlockingQueue<>(usedMemoryTimeWindowSize); if (memoryLeakDetectionEnabled) { // start the thread only if memory leak detection is enabled memoryDataGatheringTask = new MemoryDataGatheringTask(); final Thread memoryDataGatheringThread = new Thread(memoryDataGatheringTask); memoryDataGatheringThread.setName("PerfCake-memory-data-gathering-thread"); memoryDataGatheringThread.setDaemon(true); memoryDataGatheringThread.start(); } } catch (final IOException ioe) { ioe.printStackTrace(); } } @Override public void stop() { super.stop(); try { socket.close(); requestWriter.close(); responseReader.close(); if (memoryLeakDetectionEnabled) { memoryDataGatheringTask.stop(); } } catch (final IOException ioe) { ioe.printStackTrace(); } } @Override public void publishResult(final PeriodType periodType, final Destination destination) throws ReportingException { try { final Measurement m = newMeasurement(); if (performGcOnMemoryUsage) { sendAgentCommand(AgentCommand.GC.name()); } final long used = sendAgentCommand(AgentCommand.USED.name()); m.set("Used", (new Quantity<Number>((double) used / BYTES_IN_MIB, "MiB"))); m.set("Total", (new Quantity<Number>((double) sendAgentCommand(AgentCommand.TOTAL.name()) / BYTES_IN_MIB, "MiB"))); m.set("Max", (new Quantity<Number>((double) sendAgentCommand(AgentCommand.MAX.name()) / BYTES_IN_MIB, "MiB"))); if (memoryLeakDetectionEnabled) { if (usedMemoryTimeWindow.size() == usedMemoryTimeWindowSize) { m.set("UsedTrend", new Quantity<Number>(memoryTrendSlope, "B/s")); m.set("MemoryLeak", memoryLeakDetected); } else { m.set("UsedTrend", null); m.set("MemoryLeak", null); } } destination.report(m); if (log.isDebugEnabled()) { log.debug("Reporting: [" + m.toString() + "]"); } } catch (final IOException ioe) { throw new ReportingException("Could not publish result", ioe); } } /** * Gathers memory data internally for memory leak detection analysis. */ private class MemoryDataGatheringTask implements Runnable { private boolean running; @Override public void run() { this.running = true; long used = -1L; while (running) { try { used = sendAgentCommand(AgentCommand.USED.name()); } catch (final IOException e) { e.printStackTrace(); } if (usedMemoryTimeWindow.size() == usedMemoryTimeWindowSize) { usedMemoryTimeWindow.remove(); } usedMemoryTimeWindow.offer(new TimestampedRecord<Number>(runInfo.getRunTime(), used)); memoryTrendSlope = (float) Utils.computeRegressionTrend(usedMemoryTimeWindow); if (usedMemoryTimeWindow.size() == usedMemoryTimeWindowSize && memoryTrendSlope > memoryLeakSlopeThreshold) { memoryLeakDetected = true; if (memoryDumpOnLeak && !heapDumpSaved) { try { final StringBuffer cmd = new StringBuffer(); cmd.append(AgentCommand.DUMP.name()); cmd.append(":"); if (memoryDumpFile != null) { cmd.append(memoryDumpFile); } else { cmd.append("dump-" + System.getProperty(PerfCakeConst.TIMESTAMP_PROPERTY) + ".bin"); } if (sendAgentCommand(cmd.toString()) < 0) { throw new RuntimeException("An exception occured at PerfCake agent side."); } heapDumpSaved = true; } catch (final IOException e) { e.printStackTrace(); } } } try { Thread.sleep(memoryLeakDetectionMonitoringPeriod); } catch (final InterruptedException e) { e.printStackTrace(); } } } /** * Stops the task from running. */ public void stop() { this.running = false; } } /** * Sends a command to the PerfCake agent the reporter is connected to. * * @param command * PerfCake agent command. * @return Command response code. * @throws IOException * In the case of communication error. */ private long sendAgentCommand(final String command) throws IOException { if (log.isTraceEnabled()) { log.trace("sending " + command); } requestWriter.println(command); final long retVal = Long.parseLong(responseReader.readLine()); if (log.isTraceEnabled()) { log.trace("received " + retVal); } return retVal; } /** * Gets the agent hostname. * * @return The agent hostname. */ public String getAgentHostname() { return agentHostname; } /** * Sets the agent hostname. * * @param agentHostname * The agent hostname. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setAgentHostname(final String agentHostname) { this.agentHostname = agentHostname; return this; } /** * Gets the agent port. * * @return The agent port. */ public String getAgentPort() { return agentPort; } /** * Sets the agent port. * * @param agentPort * The agent port. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setAgentPort(final String agentPort) { this.agentPort = agentPort; return this; } /** * Gets the size of the memory time window (number of latest records) for possible memory leak detection. * * @return The used memory time window size. */ public int getUsedMemoryTimeWindowSize() { return usedMemoryTimeWindowSize; } /** * Sets the size of the memory time window (number of latest records) for possible memory leak detection. * * @param timeWindowSize * The used memory time window size. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setUsedMemoryTimeWindowSize(final int timeWindowSize) { this.usedMemoryTimeWindowSize = timeWindowSize; return this; } /** * Gets the possible memory leak detection threshold. Possible memory leak is found, * when the actual slope of the linear regression line computed from the time window data set is greater than the threshold. * * @return The memory leak slope threshold. */ public double getMemoryLeakSlopeThreshold() { return memoryLeakSlopeThreshold; } /** * Sets the possible memory leak detection threshold. Possible memory leak is found, * when the actual slope of the linear regression line computed from the time window data set is greater than the threshold. * * @param memoryLeakSlopeThreshold * The memory leak slope threshold. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setMemoryLeakSlopeThreshold(final double memoryLeakSlopeThreshold) { this.memoryLeakSlopeThreshold = memoryLeakSlopeThreshold; return this; } /** * Is the possible memory leak detection enabled? * * @return The memory leak detection status. */ public boolean isMemoryLeakDetectionEnabled() { return memoryLeakDetectionEnabled; } /** * Enables/disables the possible memory leak detection mechanism. * * @param memoryLeakDetectionEnabled * <code>true</code> to enable memory leak detection mechanism. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setMemoryLeakDetectionEnabled(final boolean memoryLeakDetectionEnabled) { this.memoryLeakDetectionEnabled = memoryLeakDetectionEnabled; return this; } /** * Gets the period in which a memory usage is gathered from the PerfCake agent. * * @return The memory leak detection monitoring period. */ public long getMemoryLeakDetectionMonitoringPeriod() { return memoryLeakDetectionMonitoringPeriod; } /** * Sets the period in which a memory usage is gathered from the PerfCake agent. * * @param memoryLeakDetectionMonitoringPeriod * The memory leak detection monitoring period. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setMemoryLeakDetectionMonitoringPeriod(final long memoryLeakDetectionMonitoringPeriod) { this.memoryLeakDetectionMonitoringPeriod = memoryLeakDetectionMonitoringPeriod; return this; } /** * Gets the name of the memory dump file created by PerfCake agent. * * @return Name of the memory dump file. */ public String getMemoryDumpFile() { return memoryDumpFile; } /** * Sets a the name of the memory dump file created by PerfCake agent. * * @param memoryDumpFile * Memory dump file name. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setMemoryDumpFile(final String memoryDumpFile) { this.memoryDumpFile = memoryDumpFile; return this; } /** * Returns a value of the property to make a memory dump, when possible memory leak is detected. The {@link org.perfcake.reporting.reporter.MemoryUsageReporter} * will send a command to PerfCake agent that will create a heap dump. * * @return <code>true</code> if the memory dump on leak is enabled. <code>false</code> otherwise. */ public boolean isMemoryDumpOnLeak() { return memoryDumpOnLeak; } /** * Sets the value of the property to make a memory dump, when possible memory leak is detected. The {@link org.perfcake.reporting.reporter.MemoryUsageReporter} * will send a command to PerfCake agent that will create a heap dump. * * @param memoryDumpOnLeak * Enables or disables the memory dump on leak. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setMemoryDumpOnLeak(final boolean memoryDumpOnLeak) { this.memoryDumpOnLeak = memoryDumpOnLeak; return this; } /** * Returns the value of the property that indicate if performing garbage collection (each time the memory usage of the * tested system is measured and published) is enabled or disabled. * * @return <code>true</code> if the garbage collection feature is enabled, <code>false</code> otherwise. */ public boolean isPerformGcOnMemoryUsage() { return performGcOnMemoryUsage; } /** * <p>Enables/disables garbage collection to be performed each time the memory usage of the * tested system is measured and published. * Since the garbage collection is CPU intensive operation be careful to enable it and to how often * the memory usage is measured because it will have a significant impact on the measured system and naturally the * measured results too.</p> * * <p>It is disabled by default.</p> * * @param performGcOnMemoryUsage * <code>true</code> to enable the feature. The <code>false</code> otherwise. * @return Instance of this to support fluent API. */ public MemoryUsageReporter setPerformGcOnMemoryUsage(final boolean performGcOnMemoryUsage) { this.performGcOnMemoryUsage = performGcOnMemoryUsage; return this; } }