/******************************************************************************* * ALMA - Atacama Large Millimeter Array * Copyright (c) ESO - European Southern Observatory, 2011 * (in the framework of the ALMA collaboration). * All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *******************************************************************************/ package alma.acs.profiling.orb; import java.util.HashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import org.jacorb.orb.acs.AcsORBProfiler; import alma.acs.logging.AcsLogger; import alma.acs.logging.RepeatGuard; import alma.acs.util.IsoDateFormat; /** * Profiler implementation that can be used as a base class (or directly) for ORB profiling * of containers, manager, OMC etc. * <p> * If registered with the ORB, this class will collect ORB callbacks and log statistics every 10 seconds, * see {@link #checkAndLogStatus()}. * <p> * To get logs for every ORB callback (only available for stdout), use {@link #DEBUG_CONFIG_PROPERTYNAME}. * * @author msekoran (first impl as cdb_rdb :: ORBRequestTimer), hsommer */ public class AcsORBProfilerImplBase implements AcsORBProfiler { /** * This property can optionally be set to one or more (comma and/or space separated) names of * callback methods as defined in {@link AcsORBProfiler}, whose invocations will then be printed to stdout * in addition to the normal profiling summary statistics that gets logged. * <p> * Example: <code>-Dalma.acs.profiling.orb.debugLogs='requestFinished,undeliveredRequest requestQueueSizeChanged'"</code> */ public static final String DEBUG_CONFIG_PROPERTYNAME = "alma.acs.profiling.orb.debugLogs"; /** * Logger passed in the c'tor. */ protected final AcsLogger logger; /** * Repeat guard wrapped around {@link #logger}, to control the number of ORB status messages logged. * @see #checkAndLogStatus() */ private final RepeatGuard orbStatusLogRepeatGuard; /** * Percentage of busy threads in the connection thread pool. */ protected volatile int connectionPoolUsePercent; /** * Number of undelivered requests since the last ORB profiler status log. * @see #undeliveredRequest(int, String, String, boolean) */ private final AtomicInteger undeliveredRequests = new AtomicInteger(0); /** * Maximum request queue usage in percent, for the queue owned by the POA given in {@link #requestQueueMaxUsePOA}. * <p> * Note about usage of AtomicInteger: What we really would like to use is an AtomicReference class * whose value is a custom class that bundles together an "int requestQueueMaxUsePercent" * and "String requestQueueMaxUsePOA" while offering a method "compareGreaterEqualsAndSet" * that can swap a new "usepercent / poa" pair if the new integer is greater or equal than the old one. * In this way we could get rid of the synchronized blocks in methods requestQueueSizeChanged and logStatus. * Since such an "enhanced AtomicReference" is not available and I don't dare to write one, we do use * synchronized blocks, and "abuse" this AtomicInteger as a monitor which otherwise would have to be a separate field. */ private final AtomicInteger requestQueueMaxUsePercent = new AtomicInteger(0); /** * Name of the POA who got the longest request queue (see {@link #requestQueueMaxUsePercent}) * since the last ORB profiler status log. */ private volatile String requestQueueMaxUsePOA = "---"; /** * Used to trace a request from {@link #requestStarted(int, String, String)} * to {@link #requestFinished(int, String, String)}. */ private final HashMap<ThreadRequestId, Long> requestTimeMap = new HashMap<ThreadRequestId,Long>(); /** * Can be set via {@link #DEBUG_CONFIG_PROPERTYNAME}. * We convert to explicit booleans instead of string-based lookup to improve performance. */ protected boolean debugConnectionThreadPoolSizeChanged = false; protected boolean debugUndeliveredRequest = false; protected boolean debugRequestQueueSizeChanged = false; protected boolean debugThreadPoolSizeChanged = false; protected boolean debugRequestStarted = false; protected boolean debugRequestFinished = false; public AcsORBProfilerImplBase(AcsLogger logger) { this.logger = logger; orbStatusLogRepeatGuard = new RepeatGuard(30, TimeUnit.SECONDS, -1); String debugConfig = System.getProperty(DEBUG_CONFIG_PROPERTYNAME); if (debugConfig != null) { String[] debugMethodNames = debugConfig.split("[ ,]+"); for (int i = 0; i < debugMethodNames.length; i++) { if (debugMethodNames[i].equals("connectionThreadPoolSizeChanged")) { debugConnectionThreadPoolSizeChanged = true; } else if (debugMethodNames[i].equals("undeliveredRequest")) { debugUndeliveredRequest = true; } else if (debugMethodNames[i].equals("requestQueueSizeChanged")) { debugRequestQueueSizeChanged = true; } else if (debugMethodNames[i].equals("threadPoolSizeChanged")) { debugThreadPoolSizeChanged = true; } else if (debugMethodNames[i].equals("requestStarted")) { debugRequestStarted = true; } else if (debugMethodNames[i].equals("requestFinished")) { debugRequestFinished = true; } } } } @Override public void connectionThreadPoolSizeChanged(int idleThreads, int totalThreads, int maxThreads) { if (debugConnectionThreadPoolSizeChanged) { System.out.println(IsoDateFormat.formatCurrentDate() + " connectionThreadPoolSizeChanged: idleThreads=" + idleThreads + ", totalThreads=" + totalThreads + ", maxThreads=" + maxThreads); } connectionPoolUsePercent = (int)(((totalThreads-idleThreads)/(double)maxThreads)*100); checkAndLogStatus(); } @Override public void undeliveredRequest(int messageSize, String poaName, String operation, boolean causedByQueueFull) { if (debugUndeliveredRequest) { System.out.println(IsoDateFormat.formatCurrentDate() + " undeliveredRequest: messageSize=" + messageSize + ", poaName=" + poaName + ", operation=" + operation); } undeliveredRequests.incrementAndGet(); checkAndLogStatus(); } /** * Only records the maximum queue length and the POA name owning that queue. * For more information, we could record the queue lengths for all POAs, but then have to watch out that * the backing map etc structure does not overflow when different POAs come and go. * @see org.jacorb.orb.acs.AcsORBProfiler#requestQueueSizeChanged(int, java.lang.String, int, int) */ @Override public void requestQueueSizeChanged(int requestId, String poaName, int queueSize, int maxQueueLength) { if (debugRequestQueueSizeChanged) { System.out.println(IsoDateFormat.formatCurrentDate() + " requestQueueSizeChanged: requestId=" + requestId + ", poaName=" + poaName + ", queueSize=" + queueSize + ", maxQueueLength=" + maxQueueLength); } int requestQueueUsePercent = (int)((queueSize/(double)maxQueueLength)*100); synchronized (requestQueueMaxUsePercent) { // cannot compare >= and include requestQueueMaxUsePOA with just AtomicInteger methods. if (requestQueueUsePercent >= requestQueueMaxUsePercent.get()) { requestQueueMaxUsePercent.set(requestQueueUsePercent); requestQueueMaxUsePOA = poaName; } } checkAndLogStatus(); } @Override public void threadPoolSizeChanged(String poaName, int idleThreads, int totalThreads, int maxThreads) { if (debugThreadPoolSizeChanged) { System.out.println(IsoDateFormat.formatCurrentDate() + " threadPoolSizeChanged: poaName=" + poaName + ", idleThreads=" + idleThreads + ", totalThreads=" + totalThreads + ", maxThreads=" + maxThreads); } checkAndLogStatus(); } /** * Uses requestId and threadId to trace a call from requestStarted to requestFinished. * <p> * Note that multiple clients may generate the same requestId concurrently, * and that at least for JacORB a single client may generate the same requestId concurrently for different ClientConnections. * <p> * TODO: Wouldn't using only the thread ID be good enough? */ static final class ThreadRequestId { private long threadId; private int requestId; public ThreadRequestId(long threadId, int requestId) { this.threadId = threadId; this.requestId = requestId; } @Override public int hashCode() { return (int)threadId * 911 + requestId; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ThreadRequestId other = (ThreadRequestId) obj; if (requestId != other.requestId) return false; if (threadId != other.threadId) return false; return true; } } @Override public void requestStarted(int requestId, String poaName, String operation) { long threadId = Thread.currentThread().getId(); if (debugRequestStarted) { System.out.println(IsoDateFormat.formatCurrentDate() + " requestStarted(" + requestId + ", " + poaName + ", " + operation + ", " + threadId + ")"); } synchronized (requestTimeMap) { requestTimeMap.put(new ThreadRequestId(threadId, requestId), System.currentTimeMillis()); } } @Override public void requestFinished(int requestId, String poaName, String operation) { synchronized (requestTimeMap) { long threadId = Thread.currentThread().getId(); Long startTime = requestTimeMap.remove(new ThreadRequestId(threadId, requestId)); if (startTime != null) { long timeSpent = System.currentTimeMillis() - startTime.longValue(); if (debugRequestFinished) { System.out.println(IsoDateFormat.formatCurrentDate() + " requestFinished(" + requestId + ", " + poaName + ", " + operation + ", " + threadId + ") in " + timeSpent + " ms"); } } else { if (debugRequestFinished) { System.out.println(IsoDateFormat.formatCurrentDate() + " requestFinished(" + requestId + ", " + poaName + ", " + operation + ", " + threadId + ")"); } } } } /** * Logs the ORB status and resets {@link #undeliveredRequests}, {@link #requestQueueMaxUsePercent}. * We use repeat guard {@link #orbStatusLogRepeatGuard} so that at most one status message * gets logged per configured time interval, while nothing is logged if the ORB does not get called. * <p> * Subclasses that want to modify the log message should override {@link #logStatus(String)}. */ private void checkAndLogStatus() { if (orbStatusLogRepeatGuard.checkAndIncrement()) { String msg = null; int snapshotConnectionPoolUsePercent = -1; int snapshotUndeliveredRequests = -1; int snapshotRequestQueueMaxUsePercent = -1; String snapshotRequestQueueMaxUsePOA = null; synchronized (requestQueueMaxUsePercent) { snapshotConnectionPoolUsePercent = connectionPoolUsePercent; snapshotUndeliveredRequests = undeliveredRequests.getAndSet(0); snapshotRequestQueueMaxUsePercent = requestQueueMaxUsePercent.getAndSet(0); snapshotRequestQueueMaxUsePOA = requestQueueMaxUsePOA; requestQueueMaxUsePOA = "---"; } msg = "ORB status: connectionThreadsUsed=" + snapshotConnectionPoolUsePercent + "%, lost calls=" + snapshotUndeliveredRequests + ", requestQueueMaxUsePercent=" + snapshotRequestQueueMaxUsePercent + "% (in POA '" + snapshotRequestQueueMaxUsePOA + "')."; logStatus(msg, Level.FINE, snapshotConnectionPoolUsePercent, snapshotUndeliveredRequests, snapshotRequestQueueMaxUsePercent, snapshotRequestQueueMaxUsePOA); } } /** * This method is broken out from {@link #checkAndLogStatus()} so that subclasses can change or suppress the log message, * without having to worry about log repeat guard or synchronization. * * @param defaultLogMessage A default log message, that can be used or replaced by another message. * @param defaultLogLevel A suggested log level. * @param connectionPoolUsePercent See {@link #connectionPoolUsePercent}. Can be used to build a custom message. * @param undeliveredRequests See {@link #undeliveredRequests}. Can be used to build a custom message. * @param requestQueueMaxUsePercent See {@link #requestQueueMaxUsePercent}. Can be used to build a custom message. * @param requestQueueMaxUsePOA See {@link #requestQueueMaxUsePOA}. Can be used to build a custom message. */ protected void logStatus(String defaultLogMessage, Level defaultLogLevel, int connectionPoolUsePercent, int undeliveredRequests, int requestQueueMaxUsePercent, String requestQueueMaxUsePOA) { logger.log(defaultLogLevel, defaultLogMessage); } }