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