/* * Copyright 2007-2010 Sun Microsystems, Inc. * * This file is part of Project Darkstar Server. * * Project Darkstar Server is free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation and * distributed hereunder to you. * * Project Darkstar Server 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * -- */ package com.sun.sgs.impl.profile; import com.sun.sgs.auth.Identity; import com.sun.sgs.impl.auth.IdentityImpl; import com.sun.sgs.impl.sharedutil.LoggerWrapper; import com.sun.sgs.impl.sharedutil.PropertiesWrapper; import com.sun.sgs.kernel.ComponentRegistry; import com.sun.sgs.kernel.KernelRunnable; import com.sun.sgs.management.ProfileControllerMXBean; import com.sun.sgs.management.TaskAggregateMXBean; import com.sun.sgs.profile.AccessedObjectsDetail; import com.sun.sgs.profile.ProfileCollector; import com.sun.sgs.profile.ProfileConsumer; import com.sun.sgs.profile.ProfileListener; import com.sun.sgs.profile.ProfileOperation; import com.sun.sgs.profile.ProfileParticipantDetail; import com.sun.sgs.profile.TransactionListenerDetail; import java.beans.PropertyChangeEvent; import java.lang.management.ManagementFactory; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Collections; import java.util.EmptyStackException; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.Stack; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; import javax.management.InstanceNotFoundException; import javax.management.JMException; import javax.management.MBeanRegistrationException; import javax.management.MBeanServer; import javax.management.MBeanServerFactory; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; /** * This is the implementation of {@code ProfileCollector} used by the * kernel to collect and report profiling data. It uses a single thread to * consume and report profiling data to {@code ProfileListeners}. * <p> * The {@link #ProfileCollectorImpl constructor} supports the following * properties: <p> * * <dl style="margin-left: 1em"> * * <dt> <i>Property:</i> <code><b> * com.sun.sgs.impl.profile.create.mbeanserver * </b></code><br> * <i>Default:</i> {@code false} <br> * Specifies whether a new {@code MBeanServer} should be created, rather * than using the existing platform {@code MBeanServer}. <p> * * </dl> <p> */ public final class ProfileCollectorImpl implements ProfileCollector { /** * The standard prefix for consumer names created by core packages. */ public static final String CORE_CONSUMER_PREFIX = "com.sun.sgs."; // the logger for this class private static final LoggerWrapper logger = new LoggerWrapper(Logger.getLogger(ProfileCollectorImpl. class.getName())); /** * The property telling us whether we should use the default platform * MBeanServer or create a new one. */ public static final String CREATE_MBEAN_SERVER_PROPERTY = "com.sun.sgs.impl.profile.create.mbeanserver"; // A map from profile consumer name to profile consumer object private final ConcurrentHashMap<String, ProfileConsumerImpl> consumers; // the number of threads currently in the scheduler private volatile int schedulerThreadCount; // the identifier for the local node private volatile long localNodeId = -1; // A map from registered listener to whether it can be removed or shutdown private final ConcurrentHashMap<ProfileListener, Boolean> listeners; // thread-local detail about the current task, used to let us // aggregate data across all participants in a given task private ThreadLocal<Stack<ProfileReportImpl>> profileReports = new ThreadLocal<Stack<ProfileReportImpl>>() { protected Stack<ProfileReportImpl> initialValue() { return new Stack<ProfileReportImpl>(); } }; // the incoming report queue private LinkedBlockingQueue<ProfileReportImpl> queue; // long-running thread to report data private final Thread reporterThread; // The default profiling level. This is initially set from // properties at startup. private ProfileLevel defaultProfileLevel; // The application properties, used to instantiate {@code ProfileListener}s private final Properties appProperties; // The system registry, used to instantiate {@code ProfileListener}s private final ComponentRegistry systemRegistry; // The MBeans registered for this node. private final ConcurrentMap<String, Object> registeredMBeans; // Our MBeanServer private final MBeanServer mbeanServer; // The statistics MBean for tasks private final TaskAggregateStats taskStats; /** * Creates an instance of {@code ProfileCollectorImpl}. * @param level the default system profiling level * @param appProperties the application properties, used for instantiating * {@code ProfileListener}s * @param systemRegistry the system registry, used for instantiating * {@code ProfileListener}s */ public ProfileCollectorImpl(ProfileLevel level, Properties appProperties, ComponentRegistry systemRegistry) { this.appProperties = appProperties; this.systemRegistry = systemRegistry; schedulerThreadCount = 0; listeners = new ConcurrentHashMap<ProfileListener, Boolean>(); queue = new LinkedBlockingQueue<ProfileReportImpl>(); consumers = new ConcurrentHashMap<String, ProfileConsumerImpl>(); defaultProfileLevel = level; registeredMBeans = new ConcurrentHashMap<String, Object>(); PropertiesWrapper wrappedProps = new PropertiesWrapper(appProperties); boolean createServer = wrappedProps.getBooleanProperty(CREATE_MBEAN_SERVER_PROPERTY, false); if (createServer) { mbeanServer = MBeanServerFactory.createMBeanServer(); } else { mbeanServer = ManagementFactory.getPlatformMBeanServer(); } // Create the task aggregator MBean and register it, as well as // the profile controller MBean. taskStats = new TaskAggregateStats(this, CORE_CONSUMER_PREFIX + "TaskAggregateStats"); try { registerMBean(taskStats, TaskAggregateMXBean.MXBEAN_NAME); registerMBean(new ProfileController(this), ProfileControllerMXBean.MXBEAN_NAME); } catch (JMException e) { // Continue on if we couldn't register this bean, although // it's probably a very bad sign logger.logThrow(Level.CONFIG, e, "Could not register MBean"); } // start a long-lived task to consume the other end of the queue reporterThread = new CollectorThread(); reporterThread.start(); } /** {@inheritDoc} */ public void shutdown() { // Shut down the reporterThread, waiting for it to finish what // its doing reporterThread.interrupt(); try { reporterThread.join(); } catch (InterruptedException e) { // do nothing } // Shut down each of the listeners if it was added with remove and // shutdown enabled. for (Map.Entry<ProfileListener, Boolean> entry : listeners.entrySet()) { if (entry.getValue().equals(Boolean.TRUE)) { entry.getKey().shutdown(); } } // attempt to unregister all our registered MBeans Set<String> keys = registeredMBeans.keySet(); for (String name : keys) { try { mbeanServer.unregisterMBean(new ObjectName(name)); } catch (MalformedObjectNameException ex) { logger.logThrow(Level.WARNING, ex, "Could not unregister MBean {0}", name); } catch (InstanceNotFoundException ex) { logger.logThrow(Level.WARNING, ex, "Could not unregister MBean {0}", name); } catch (MBeanRegistrationException ex) { logger.logThrow(Level.WARNING, ex, "Could not unregister MBean {0}", name); } finally { registeredMBeans.remove(name); } } } /** {@inheritDoc} */ public ProfileLevel getDefaultProfileLevel() { return defaultProfileLevel; } /** {@inheritDoc} */ public void setDefaultProfileLevel(ProfileLevel level) { if (level == null) { throw new NullPointerException("Profile level cannot be null"); } defaultProfileLevel = level; } /** {@inheritDoc} */ public ProfileConsumer getConsumer(String name) { if (name == null) { throw new NullPointerException("Name cannot be null"); } ProfileConsumerImpl pc = new ProfileConsumerImpl(this, name); ProfileConsumerImpl oldpc = consumers.putIfAbsent(name, pc); if (oldpc != null) { logger.log(Level.FINE, "Found consumer {0} already created", name); return oldpc; } else { logger.log(Level.FINE, "Created consumer named {0}", name); return pc; } } /** {@inheritDoc} */ public Map<String, ProfileConsumer> getConsumers() { // Create a copy of the map to get the type correct. ConcurrentHashMap<String, ProfileConsumer> retMap = new ConcurrentHashMap<String, ProfileConsumer>(consumers); return Collections.unmodifiableMap(retMap); } /** {@inheritDoc} */ public void addListener(ProfileListener listener, boolean canRemove) { if (listener == null) { throw new NullPointerException("Cannot add a null listener"); } listeners.put(listener, canRemove); PropertyChangeEvent event = new PropertyChangeEvent(this, "com.sun.sgs.profile.threadcount", null, schedulerThreadCount); listener.propertyChange(event); for (ProfileConsumerImpl pc : consumers.values()) { for (ProfileOperation p : pc.getOperations()) { event = new PropertyChangeEvent(this, "com.sun.sgs.profile.newop", null, p); listener.propertyChange(event); } } if (localNodeId > -1) { event = new PropertyChangeEvent(this, "com.sun.sgs.profile.nodeid", null, localNodeId); listener.propertyChange(event); } } /** {@inheritDoc} */ public void addListener(String listenerClassName) throws Exception { if (listenerClassName == null) { throw new NullPointerException("Class name cannot not be null"); } // make sure we can resolve the listener Class<?> listenerClass = Class.forName(listenerClassName); Constructor<?> listenerConstructor = listenerClass.getConstructor(Properties.class, Identity.class, ComponentRegistry.class); // create a new identity for the listener IdentityImpl owner = new IdentityImpl(listenerClassName); // try to create and register the listener Object obj = listenerConstructor.newInstance(appProperties, owner, systemRegistry); addListener((ProfileListener) obj, true); } /** {@inheritDoc} */ public List<ProfileListener> getListeners() { // Create a list from the keys ArrayList<ProfileListener> list = new ArrayList<ProfileListener>(listeners.keySet()); // and return a read-only copy. return Collections.unmodifiableList(list); } /** {@inheritDoc} */ public void removeListener(ProfileListener listener) { if (listener == null) { throw new NullPointerException("Listener cannot be null"); } // Check to see if we're allowed to remove this listener Boolean canRemove = listeners.get(listener); if (canRemove != null && canRemove.equals(Boolean.TRUE)) { listeners.remove(listener); listener.shutdown(); } } /** {@inheritDoc} */ public void registerMBean(Object mBean, String mBeanName) throws JMException { try { ObjectName name = new ObjectName(mBeanName); mbeanServer.registerMBean(mBean, name); registeredMBeans.putIfAbsent(mBeanName, mBean); logger.log(Level.CONFIG, "Registered MBean {0}", name); } catch (JMException ex) { logger.logThrow(Level.CONFIG, ex, "Could not register MBean {0}", mBeanName); throw ex; } } /** {@inheritDoc} */ public Object getRegisteredMBean(String mBeanName) { return registeredMBeans.get(mBeanName); } /* -- Methods to support ProfileCollectorHandle -- */ /** * Notifies the collector that a thread has been added to the scheduler. */ void notifyThreadAdded() { schedulerThreadCount++; PropertyChangeEvent event = new PropertyChangeEvent(this, "com.sun.sgs.profile.threadcount", schedulerThreadCount - 1, schedulerThreadCount); for (ProfileListener listener : listeners.keySet()) { listener.propertyChange(event); } } /** * Notifies the collector that a thread has been removed from the * scheduler. */ void notifyThreadRemoved() { schedulerThreadCount--; PropertyChangeEvent event = new PropertyChangeEvent(this, "com.sun.sgs.profile.threadcount", schedulerThreadCount + 1, schedulerThreadCount); for (ProfileListener listener : listeners.keySet()) { listener.propertyChange(event); } } /** * Notifies the collector that the node has been assigned its identifier. */ void notifyNodeIdAssigned(long nodeId) { localNodeId = nodeId; PropertyChangeEvent event = new PropertyChangeEvent(this, "com.sun.sgs.profile.nodeid", null, nodeId); for (ProfileListener listener : listeners.keySet()) { listener.propertyChange(event); } } /** * Tells the collector that a new task is starting in the context of * the calling thread. * * @param task the <code>KernelRunnable</code> that is starting * @param owner the <code>Identity</code> of the task owner * @param scheduledStartTime the requested starting time for the task * @param readyCount the number of ready tasks at the scheduler */ void startTask(KernelRunnable task, Identity owner, long scheduledStartTime, int readyCount) { if (task == null) { throw new NullPointerException("Task cannot be null"); } if (owner == null) { throw new NullPointerException("Owner cannot be null"); } profileReports.get().push(new ProfileReportImpl(task, owner, scheduledStartTime, readyCount)); } /** * Tells the collector that the current task associated with the calling * thread (as associated by a call to {@code startTask}) is * transactional. * * @param txnId the identifier for the transaction */ void noteTransactional(byte [] txnId) { if (txnId == null) { throw new NullPointerException("Transaction id cannot be null"); } ProfileReportImpl profileReport = null; try { profileReport = profileReports.get().peek(); } catch (EmptyStackException ese) { throw new IllegalStateException("No task is being profiled in " + "this thread"); } profileReport.transactionId = txnId; } /** * Tells the collector about a participant of a transaction when that * participant has finished participating (i.e., has committed, has * prepared read-only, or has aborted). * * @param participantDetail the detail associated with the participant */ void addParticipant(ProfileParticipantDetail participantDetail) { if (participantDetail == null) { throw new NullPointerException("Participant detail cannot be null"); } ProfileReportImpl profileReport = null; try { profileReport = profileReports.get().peek(); } catch (EmptyStackException ese) { throw new IllegalStateException("No task is being profiled in " + "this thread"); } if (!profileReport.wasTaskTransactional()) { throw new IllegalStateException("Participants cannot be added " + "to a non-transactional task"); } profileReport.addParticipant(participantDetail); } /** * Tells the collector about a listener of a transaction when that * listener is finished with its work (i.e., after its afterCompletion * method has been called). * * @param listenerDetail the detail associated with the listener */ void addTransactionListener(TransactionListenerDetail listenerDetail) { if (listenerDetail == null) { throw new NullPointerException("Listener detail cannot be null"); } ProfileReportImpl profileReport = null; try { profileReport = profileReports.get().peek(); } catch (EmptyStackException ese) { throw new IllegalStateException("No task is being profiled in " + "this thread"); } if (!profileReport.wasTaskTransactional()) { throw new IllegalStateException("Listeners cannot be added " + "to a non-transactional task"); } profileReport.addListener(listenerDetail); } /** * Sets the detail for all objects accessed during the task as * reported to the {@code AccessCoordinator}. * * @param detail all detail of the accessed objects */ void setAccessedObjectsDetail(AccessedObjectsDetail detail) { if (detail == null) { throw new NullPointerException("Access detail cannot be null"); } ProfileReportImpl profileReport = null; try { profileReport = profileReports.get().peek(); } catch (EmptyStackException ese) { throw new IllegalStateException("No task is being profiled in " + "this thread"); } if (!profileReport.wasTaskTransactional()) { throw new IllegalStateException("Object access cannot be added " + "to a non-transactional task"); } profileReport.setAccessedObjectsDetail(detail); } /** * Tells the collector that the current task associated with the * calling thread (as associated by a call to * {@code startTask}) has now successfully finished. * * @param tryCount the number of times that the task has tried to run */ void finishTask(int tryCount) { finishTask(tryCount, null); } /** * Tells the collector that the current task associated with the * calling thread (as associated by a call to * {@code startTask}) has now finished. * * @param tryCount the number of times that the task has tried to run * @param t the {@code Throwable} thrown during task execution */ void finishTask(int tryCount, Throwable t) { long stopTime = System.currentTimeMillis(); ProfileReportImpl profileReport = null; try { profileReport = profileReports.get().pop(); } catch (EmptyStackException ese) { throw new IllegalStateException("No task is being profiled in " + "this thread"); } // collect the final details about the report long runtime = stopTime - profileReport.actualStartTime; profileReport.runningTime = runtime; profileReport.tryCount = tryCount; boolean successful = t == null; profileReport.succeeded = successful; profileReport.throwable = t; // if this was a nested report, then merge all of the collected // data into the parent if (!profileReports.get().empty()) { profileReports.get().peek().merge(profileReport); } // Note that we're done modifying this report profileReport.finish(); // queue up the report to be reported to our listeners if (!queue.offer(profileReport)) { logger.log(Level.FINE, "ProfileCollector queue is full"); } // Update the task aggregate data boolean trans = profileReport.wasTaskTransactional(); if (successful) { long lagtime = profileReport.actualStartTime - profileReport.scheduledStartTime; taskStats.taskFinishedSuccess(trans, profileReport.readyCount, runtime, lagtime); } else { taskStats.taskFinishedFail(trans, profileReport.readyCount); } } /** * Package private method to notify listeners of a property change. * * @param event the change the listeners will be notified about */ void notifyListeners(PropertyChangeEvent event) { for (ProfileListener listener : listeners.keySet()) { listener.propertyChange(event); } } /** * Package private method to get the current profile report, * used by the operations, counters, and samples. * * @return the thread-local profile report for the currently running task */ ProfileReportImpl getCurrentProfileReport() { return profileReports.get().peek(); } /** * Private class that implements the long-running collector and reporter * of task data. The task blocks on the queue, and whenever there is a * report ready notifies all installed listeners. * <p> * NOTE: The commented-out code here may still be useful to make sure * that the single-threaded queue keeps up. It was originally used just * to to observe performance, and it's unclear whether it's worth * reporting anywhere, and where to report it. */ private class CollectorThread extends Thread { /*private volatile long queueSize = 0; private volatile long queueSamples = 0;*/ private boolean interrupted; public void run() { try { while (true) { synchronized (this) { if (interrupted) { return; } } ProfileReportImpl profileReport = queue.poll(); if (profileReport == null) { profileReport = queue.take(); } /*else { queueSize += queue.size(); queueSamples++; } double avgQueueSize = (queueSamples == 0) ? 0 : (double)queueSize / (double)queueSamples; double percentQueueNotEmpty = (double)queueSamples / (double)totalTasks; reportStr += " [ AvgStatQueueSize=" + avgQueueSize + " StatQueueNonEmpty=" + percentQueueNotEmpty + "% ]\n\n"; */ for (ProfileListener listener : listeners.keySet()) { try { listener.report(profileReport); } catch (Throwable t) { if (logger.isLoggable(Level.WARNING)) { logger.logThrow(Level.WARNING, t, "{0}: report method failed", listener.getClass()); } } } } } catch (InterruptedException ie) { } } /** * Modify interrupt to keep track of whether the thread has ever been * interrupted. That way we can have the thread exit if an interrupt * has occurred, even if something (say logging) catches an interrupt * and forgets to reset the interrupt status. */ @Override public void interrupt() { synchronized (this) { interrupted = true; } super.interrupt(); } } }