001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.plugins.profiler;
018import static org.opends.messages.PluginMessages.*;
019import static org.opends.server.util.StaticUtils.*;
020
021import java.io.File;
022import java.util.List;
023import java.util.Set;
024
025import org.forgerock.i18n.LocalizableMessage;
026import org.forgerock.i18n.slf4j.LocalizedLogger;
027import org.forgerock.opendj.config.server.ConfigException;
028import org.forgerock.opendj.config.server.ConfigurationChangeListener;
029import org.forgerock.opendj.server.config.meta.PluginCfgDefn;
030import org.forgerock.opendj.server.config.server.PluginCfg;
031import org.forgerock.opendj.server.config.server.ProfilerPluginCfg;
032import org.opends.server.api.plugin.DirectoryServerPlugin;
033import org.opends.server.api.plugin.PluginResult;
034import org.opends.server.api.plugin.PluginType;
035import org.forgerock.opendj.config.server.ConfigChangeResult;
036import org.forgerock.opendj.ldap.DN;
037import org.opends.server.types.DirectoryConfig;
038import org.opends.server.util.TimeThread;
039
040/**
041 * This class defines a Directory Server startup plugin that will register
042 * itself as a configurable component that can allow for a simple sample-based
043 * profiling mechanism within the Directory Server.  When profiling is enabled,
044 * the server will periodically (e.g., every few milliseconds) retrieve all the
045 * stack traces for all threads in the server and aggregates them so that they
046 * can be analyzed to see where the server is spending all of its processing
047 * time.
048 */
049public final class ProfilerPlugin
050       extends DirectoryServerPlugin<ProfilerPluginCfg>
051       implements ConfigurationChangeListener<ProfilerPluginCfg>
052{
053  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
054
055  /**
056   * The value to use for the profiler action when no action is necessary.
057   */
058  public static final String PROFILE_ACTION_NONE = "none";
059
060
061
062  /**
063   * The value to use for the profiler action when it should start capturing
064   * information.
065   */
066  public static final String PROFILE_ACTION_START = "start";
067
068
069
070  /**
071   * The value to use for the profiler action when it should stop capturing
072   * data and write the information it has collected to disk.
073   */
074  public static final String PROFILE_ACTION_STOP = "stop";
075
076
077
078  /**
079   * The value to use for the profiler action when it should stop capturing
080   * data and discard any information that has been collected.
081   */
082  public static final String PROFILE_ACTION_CANCEL = "cancel";
083
084
085
086  /** The DN of the configuration entry for this plugin. */
087  private DN configEntryDN;
088
089  /** The current configuration for this plugin. */
090  private ProfilerPluginCfg currentConfig;
091
092  /** The thread that is actually capturing the profile information. */
093  private ProfilerThread profilerThread;
094
095
096
097  /**
098   * Creates a new instance of this Directory Server plugin.  Every plugin must
099   * implement a default constructor (it is the only one that will be used to
100   * create plugins defined in the configuration), and every plugin constructor
101   * must call <CODE>super()</CODE> as its first element.
102   */
103  public ProfilerPlugin()
104  {
105    super();
106
107  }
108
109
110
111  /** {@inheritDoc} */
112  @Override
113  public final void initializePlugin(Set<PluginType> pluginTypes,
114                                     ProfilerPluginCfg configuration)
115         throws ConfigException
116  {
117    configuration.addProfilerChangeListener(this);
118
119    currentConfig = configuration;
120    configEntryDN = configuration.dn();
121
122
123    // Make sure that this plugin is only registered as a startup plugin.
124    if (pluginTypes.isEmpty())
125    {
126      throw new ConfigException(ERR_PLUGIN_PROFILER_NO_PLUGIN_TYPES.get(configEntryDN));
127    }
128    else
129    {
130      for (PluginType t : pluginTypes)
131      {
132        if (t != PluginType.STARTUP)
133        {
134          throw new ConfigException(ERR_PLUGIN_PROFILER_INVALID_PLUGIN_TYPE.get(configEntryDN, t));
135        }
136      }
137    }
138
139
140    // Make sure that the profile directory exists.
141    File profileDirectory = getFileForPath(configuration.getProfileDirectory());
142    if (!profileDirectory.exists() || !profileDirectory.isDirectory())
143    {
144      LocalizableMessage message = WARN_PLUGIN_PROFILER_INVALID_PROFILE_DIR.get(
145          profileDirectory.getAbsolutePath(), configEntryDN);
146      throw new ConfigException(message);
147    }
148  }
149
150
151
152  /** {@inheritDoc} */
153  @Override
154  public final void finalizePlugin()
155  {
156    currentConfig.removeProfilerChangeListener(this);
157
158    // If the profiler thread is still active, then cause it to dump the
159    // information it has captured and exit.
160    synchronized (this)
161    {
162      if (profilerThread != null)
163      {
164        profilerThread.stopProfiling();
165
166        String filename = currentConfig.getProfileDirectory() + File.separator +
167                          "profile." + TimeThread.getGMTTime();
168        try
169        {
170          profilerThread.writeCaptureData(filename);
171        }
172        catch (Exception e)
173        {
174          logger.traceException(e);
175
176          logger.error(ERR_PLUGIN_PROFILER_CANNOT_WRITE_PROFILE_DATA, configEntryDN, filename,
177                  stackTraceToSingleLineString(e));
178        }
179      }
180    }
181  }
182
183
184
185  /** {@inheritDoc} */
186  @Override
187  public final PluginResult.Startup doStartup()
188  {
189    ProfilerPluginCfg config = currentConfig;
190
191    // If the profiler should be started automatically, then do so now.
192    if (config.isEnableProfilingOnStartup())
193    {
194      profilerThread = new ProfilerThread(config.getProfileSampleInterval());
195      profilerThread.start();
196    }
197
198    return PluginResult.Startup.continueStartup();
199  }
200
201
202
203  /** {@inheritDoc} */
204  @Override
205  public boolean isConfigurationAcceptable(PluginCfg configuration,
206                                           List<LocalizableMessage> unacceptableReasons)
207  {
208    ProfilerPluginCfg config = (ProfilerPluginCfg) configuration;
209    return isConfigurationChangeAcceptable(config, unacceptableReasons);
210  }
211
212
213
214  /** {@inheritDoc} */
215  @Override
216  public boolean isConfigurationChangeAcceptable(
217                      ProfilerPluginCfg configuration,
218                      List<LocalizableMessage> unacceptableReasons)
219  {
220    boolean configAcceptable = true;
221    DN cfgEntryDN = configuration.dn();
222
223    // Make sure that the plugin is only registered as a startup plugin.
224    if (configuration.getPluginType().isEmpty())
225    {
226      unacceptableReasons.add(ERR_PLUGIN_PROFILER_NO_PLUGIN_TYPES.get(cfgEntryDN));
227      configAcceptable = false;
228    }
229    else
230    {
231      for (PluginCfgDefn.PluginType t : configuration.getPluginType())
232      {
233        if (t != PluginCfgDefn.PluginType.STARTUP)
234        {
235          unacceptableReasons.add(ERR_PLUGIN_PROFILER_INVALID_PLUGIN_TYPE.get(cfgEntryDN, t));
236          configAcceptable = false;
237          break;
238        }
239      }
240    }
241
242
243    // Make sure that the profile directory exists.
244    File profileDirectory = getFileForPath(configuration.getProfileDirectory());
245    if (!profileDirectory.exists() || !profileDirectory.isDirectory())
246    {
247      unacceptableReasons.add(WARN_PLUGIN_PROFILER_INVALID_PROFILE_DIR.get(
248          profileDirectory.getAbsolutePath(), cfgEntryDN));
249      configAcceptable = false;
250    }
251
252    return configAcceptable;
253  }
254
255
256
257  /**
258   * Applies the configuration changes to this change listener.
259   *
260   * @param configuration
261   *          The new configuration containing the changes.
262   * @return Returns information about the result of changing the
263   *         configuration.
264   */
265  @Override
266  public ConfigChangeResult applyConfigurationChange(
267                                 ProfilerPluginCfg configuration)
268  {
269    final ConfigChangeResult ccr = new ConfigChangeResult();
270
271    currentConfig = configuration;
272
273    // See if we need to perform any action.
274    switch (configuration.getProfileAction())
275    {
276      case START:
277        // See if the profiler thread is running.  If so, then don't do
278        // anything.  Otherwise, start it.
279        synchronized (this)
280        {
281          if (profilerThread == null)
282          {
283            profilerThread =
284                 new ProfilerThread(configuration.getProfileSampleInterval());
285            profilerThread.start();
286
287            ccr.addMessage(INFO_PLUGIN_PROFILER_STARTED_PROFILING.get(configEntryDN));
288          }
289          else
290          {
291            ccr.addMessage(INFO_PLUGIN_PROFILER_ALREADY_PROFILING.get(configEntryDN));
292          }
293        }
294        break;
295
296      case STOP:
297        // See if the profiler thread is running.  If so, then stop it and write
298        // the information captured to disk.  Otherwise, don't do anything.
299        synchronized (this)
300        {
301          if (profilerThread == null)
302          {
303            ccr.addMessage(INFO_PLUGIN_PROFILER_NOT_RUNNING.get(configEntryDN));
304          }
305          else
306          {
307            profilerThread.stopProfiling();
308
309            ccr.addMessage(INFO_PLUGIN_PROFILER_STOPPED_PROFILING.get(configEntryDN));
310
311            String filename =
312                 getFileForPath(
313                      configuration.getProfileDirectory()).getAbsolutePath() +
314                 File.separator + "profile." + TimeThread.getGMTTime();
315
316            try
317            {
318              profilerThread.writeCaptureData(filename);
319
320              ccr.addMessage(INFO_PLUGIN_PROFILER_WROTE_PROFILE_DATA.get(configEntryDN, filename));
321            }
322            catch (Exception e)
323            {
324              logger.traceException(e);
325
326              ccr.addMessage(ERR_PLUGIN_PROFILER_CANNOT_WRITE_PROFILE_DATA.get(
327                  configEntryDN, filename, stackTraceToSingleLineString(e)));
328              ccr.setResultCode(DirectoryConfig.getServerErrorResultCode());
329            }
330
331            profilerThread = null;
332          }
333        }
334        break;
335
336      case CANCEL:
337        // See if the profiler thread is running.  If so, then stop it but don't
338        // write anything to disk.  Otherwise, don't do anything.
339        synchronized (this)
340        {
341          if (profilerThread == null)
342          {
343            ccr.addMessage(INFO_PLUGIN_PROFILER_NOT_RUNNING.get(configEntryDN));
344          }
345          else
346          {
347            profilerThread.stopProfiling();
348
349            ccr.addMessage(INFO_PLUGIN_PROFILER_STOPPED_PROFILING.get(configEntryDN));
350
351            profilerThread = null;
352          }
353        }
354        break;
355    }
356
357    return ccr;
358  }
359}