blob: 559de2ecd625123442eb6c78a35f9b316706ccde [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2013 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Lars Vogel <Lars.Vogel@gmail.com> - Adds generic type arguments https://bugs.eclipse.org/412836
*******************************************************************************/
package org.eclipse.core.runtime;
import java.io.PrintWriter;
import java.util.*;
import org.eclipse.core.internal.runtime.InternalPlatform;
import org.eclipse.core.internal.runtime.PerformanceStatsProcessor;
/**
* PerformanceStats collects and aggregates timing data about events such as
* a builder running, an editor opening, etc. This data is collected for the
* purpose of performance analysis, and is not intended to be used as
* a generic event tracking and notification system.
* <p>
* Each performance event can have an associated maximum acceptable
* duration that is specified in the platform debug options file (.options).
* Events that take longer than this maximum are logged as errors. Along
* with option file entries for each debug event, there are some global debug
* options for enabling or disabling performance event gathering and reporting.
* See the "org.eclipse.core.runtime/perf*" debug options in the .options file
* for the org.eclipse.core.runtime plugin for more details.
* </p><p>
* A performance event can optionally have additional context information
* ({@link #getContext}). This information is only stored in the case
* of a performance failure, and can be used to provide further diagnostic
* information that can help track down the cause of the failure.
* </p><p>
* Performance events and performance failures are batched up and periodically
* sent to interested performance event listeners.
* </p><p>
* This class is not intended to be subclassed or instantiated by clients.
* </p>
* @since 3.1
*/
public class PerformanceStats {
/**
* A performance listener is periodically notified after performance events occur
* or after events fail.
* <p>
* This class is intended to be subclassed.
* </p>
*
* @see PerformanceStats#addListener(PerformanceStats.PerformanceListener)
*/
public static abstract class PerformanceListener {
/**
* Creates a new listener.
*/
protected PerformanceListener() {
super();
}
/**
* Notifies than an event exceeded the maximum duration for that event type.
* <p>
* This default implementation does nothing. Subclasses may override.
* </p>
*
* @param event The event that failed
* @param duration The duration of the failed event, in milliseconds
*/
public void eventFailed(PerformanceStats event, long duration) {
//default implementation does nothing
}
/**
* Notifies that an event occurred. Notification might not occur
* in the same thread or near the time of the actual event.
* <p>
* This default implementation does nothing. Subclasses may override.
* </p>
*
* @param event The event that occurred
*/
public void eventsOccurred(PerformanceStats[] event) {
//default implementation does nothing
}
}
/**
* An empty stats object that is returned when tracing is turned off
*/
private static final PerformanceStats EMPTY_STATS = new PerformanceStats("", ""); //$NON-NLS-1$ //$NON-NLS-2$
/**
* Constant indicating whether or not tracing is enabled
*/
public static final boolean ENABLED;
/**
* A constant indicating that the timer has not been started.
*/
private static final long NOT_STARTED = -1;
/**
* All known event statistics.
*/
private final static Map<PerformanceStats, PerformanceStats> statMap =
Collections.synchronizedMap(new HashMap<PerformanceStats,PerformanceStats>());
/**
* Maximum allowed durations for each event.
* Maps String (event name) -&gt; Long (threshold)
*/
private final static Map<String, Long> thresholdMap = Collections.synchronizedMap(new HashMap<String, Long>());
/**
* Whether non-failure statistics should be retained.
*/
private static final boolean TRACE_SUCCESS;
/**
* An identifier that can be used to figure out who caused the event. This is
* typically a string representation of the object whose code was running when
* the event occurred or a <code>String</code> describing the event.
*/
private String blame;
/**
* The id of the plugin that defined the blame object for this event, or
* <code>null</code> if it could not be determined.
*/
private String blamePluginId;
/**
* An optional context for the event (may be <code>null</code>).
* The context can provide extra information about an event, such as the
* name of a project being built, or the input of an editor being opened.
*/
private String context;
/**
* The starting time of the current occurrence of this event.
*/
private long currentStart = NOT_STARTED;
/**
* The symbolic name of the event that occurred. This is usually the name of
* the debug option for this event.
*/
private String event;
/**
* Whether this is a performance failure event
*/
private boolean isFailure;
/**
* The total number of times this event has occurred.
*/
private int runCount = 0;
/**
* The total time in milliseconds taken by all occurrences of this event.
*/
private long runningTime = 0;
static {
ENABLED = InternalPlatform.getDefault().getBooleanOption(Platform.PI_RUNTIME + "/perf", false);//$NON-NLS-1$
//turn these on by default if the global trace flag is turned on
TRACE_SUCCESS = InternalPlatform.getDefault().getBooleanOption(Platform.PI_RUNTIME + "/perf/success", ENABLED); //$NON-NLS-1$
}
/**
* Adds a listener that is notified when performance events occur. If
* an equal listener is already installed, it will be replaced.
*
* @param listener The listener to be added
* @see #removeListener(PerformanceStats.PerformanceListener)
*/
public static void addListener(PerformanceListener listener) {
if (ENABLED)
PerformanceStatsProcessor.addListener(listener);
}
/**
* Discards all known performance event statistics.
*/
public static void clear() {
statMap.clear();
}
/**
* Returns all performance event statistics.
*
* @return An array of known performance event statistics. The array
* will be empty if there are no recorded statistics.
*/
public static PerformanceStats[] getAllStats() {
return statMap.values().toArray(new PerformanceStats[statMap.size()]);
}
/**
* Returns the stats object corresponding to the given parameters.
* A stats object is created and added to the global list of events if it did not
* already exist.
*
* @param eventName A symbolic event name. This is usually the name of
* the debug option for this event. An example event name from
* the org.eclipse.core.resources plugin describing a build event might look like:
* <code>"org.eclipse.core.resources/perf/building"</code>"
* @param blameObject The blame for the event. This is typically the object
* whose code was running when the event occurred. If a blame object cannot
* be obtained, a <code>String</code> describing the event should be supplied
*/
public static PerformanceStats getStats(String eventName, Object blameObject) {
if (!ENABLED || eventName == null || blameObject == null)
return EMPTY_STATS;
PerformanceStats newStats = new PerformanceStats(eventName, blameObject);
if (!TRACE_SUCCESS)
return newStats;
//use existing stats object if available
PerformanceStats oldStats = statMap.get(newStats);
if (oldStats != null)
return oldStats;
statMap.put(newStats, newStats);
return newStats;
}
/**
* Returns whether monitoring of a given performance event is enabled.
* <p>
* For frequent performance events, the result of this method call should
* be cached by the caller to minimize overhead when performance monitoring
* is turned off. It is not possible for enablement to change during the life
* of this invocation of the platform.
* </p>
*
* @param eventName The name of the event to determine enablement for
* @return <code>true</code>If the performance event with the given
* name is enabled, and <code>false</code> otherwise.
*/
public static boolean isEnabled(String eventName) {
if (!ENABLED)
return false;
String option = Platform.getDebugOption(eventName);
return option != null && !"false".equalsIgnoreCase(option) && !"-1".equalsIgnoreCase(option); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Prints all statistics to the standard output.
*/
public static void printStats() {
if (!ENABLED)
return;
PrintWriter writer = new PrintWriter(System.out);
PerformanceStatsProcessor.printStats(writer);
writer.flush();
}
/**
* Writes all statistics using the provided writer
*
* @param out The writer to print stats to.
*/
public static void printStats(PrintWriter out) {
if (!ENABLED)
return;
PerformanceStatsProcessor.printStats(out);
}
/**
* Removes an event listener. Has no effect if an equal
* listener object is not currently registered.
*
* @param listener The listener to remove
* @see #addListener(PerformanceStats.PerformanceListener)
*/
public static void removeListener(PerformanceListener listener) {
if (ENABLED)
PerformanceStatsProcessor.removeListener(listener);
}
/**
* Removes statistics for a given event and blame
*
* @param eventName The name of the event to remove
* @param blameObject The blame for the event to remove
*/
public static void removeStats(String eventName, Object blameObject) {
synchronized (statMap) {
for (Iterator<PerformanceStats> it = statMap.keySet().iterator(); it.hasNext();) {
PerformanceStats stats = it.next();
if (stats.getEvent().equals(eventName) && stats.getBlame().equals(blameObject))
it.remove();
}
}
}
/**
* Creates a new PerformanceStats object. Private to prevent client instantiation.
*/
private PerformanceStats(String event, Object blame) {
this(event, blame, null);
}
/**
* Creates a new PerformanceStats object. Private to prevent client instantiation.
*/
private PerformanceStats(String event, Object blameObject, String context) {
this.event = event;
this.blame = blameObject instanceof String ? (String) blameObject : blameObject.getClass().getName();
this.blamePluginId = InternalPlatform.getDefault().getBundleId(blameObject);
this.context = context;
}
/**
* Adds an occurrence of this event to the cumulative counters. This method
* can be used as an alternative to <code>startRun</code> and <code>endRun</code>
* for clients that want to track the context and execution time separately.
*
* @param elapsed The elapsed time of the new occurrence in milliseconds
* @param contextName The context for the event to return, or <code>null</code>.
* The context optionally provides extra information about an event, such as the
* name of a project being built, or the input of an editor being opened.
*/
public void addRun(long elapsed, String contextName) {
if (!ENABLED)
return;
runCount++;
runningTime += elapsed;
if (elapsed > getThreshold(event))
PerformanceStatsProcessor.failed(createFailureStats(contextName, elapsed), blamePluginId, elapsed);
if (TRACE_SUCCESS)
PerformanceStatsProcessor.changed(this);
}
/**
* Creates a stats object representing a performance failure
*
* @param contextName The failure context information.
* @param elapsed The elapsed time in milliseconds
* @return The failure stats
*/
private PerformanceStats createFailureStats(String contextName, long elapsed) {
PerformanceStats failedStat = new PerformanceStats(event, blame, contextName);
PerformanceStats old = statMap.get(failedStat);
if (old == null)
statMap.put(failedStat, failedStat);
else
failedStat = old;
failedStat.isFailure = true;
failedStat.runCount++;
failedStat.runningTime += elapsed;
return failedStat;
}
/**
* Stops timing the occurrence of this event that was started by the previous
* call to <code>startRun</code>. The event is automatically added to
* the cumulative counters for this event and listeners are notified.
* <p>
* Note that this facility guards itself against runs that start but fail to stop,
* so it is not necessary to call this method from a finally block. Tracking
* performance of failure cases is generally not of interest.
* </p>
*
* @see #startRun()
*/
public void endRun() {
if (!ENABLED || currentStart == NOT_STARTED)
return;
addRun(System.currentTimeMillis() - currentStart, context);
currentStart = NOT_STARTED;
}
/* (non-Javadoc)
* @see java.lang.Object#equals()
*/
@Override
public boolean equals(Object obj) {
//count and time are not considered part of equality
if (!(obj instanceof PerformanceStats))
return false;
PerformanceStats that = (PerformanceStats) obj;
return this.event.equals(that.event) && this.getBlameString().equals(that.getBlameString())
&& Objects.equals(this.context, that.context);
}
/**
* Returns an object that can be used to figure out who caused the event,
* or a string describing the cause of the event.
*
* @return The blame for this event
*/
public Object getBlame() {
return blame;
}
/**
* Returns a string describing the blame for this event.
*
* @return A string describing the blame.
*/
public String getBlameString() {
return blame;
}
/**
* Returns the optional event context, such as the input of an editor, or the target project
* of a build event.
*
* @return The context, or <code>null</code> if there is none
*/
public String getContext() {
return context;
}
/**
* Returns the symbolic name of the event that occurred.
*
* @return The name of the event.
*/
public String getEvent() {
return event;
}
/**
* Returns the total number of times this event has occurred.
*
* @return The number of occurrences of this event.
*/
public int getRunCount() {
return runCount;
}
/**
* Returns the total execution time in milliseconds for all occurrences
* of this event.
*
* @return The total running time in milliseconds.
*/
public long getRunningTime() {
return runningTime;
}
/**
* Returns the performance threshold for this event.
*/
private long getThreshold(String eventName) {
Long value = thresholdMap.get(eventName);
if (value == null) {
String option = InternalPlatform.getDefault().getOption(eventName);
if (option != null) {
try {
value = Long.valueOf(option);
} catch (NumberFormatException e) {
//invalid option, just ignore
}
}
if (value == null)
value = Long.valueOf(Long.MAX_VALUE);
thresholdMap.put(eventName, value);
}
return value.longValue();
}
@Override
public int hashCode() {
//count and time are not considered part of equality
int hash = event.hashCode() * 37 + getBlameString().hashCode();
if (context != null)
hash = hash * 37 + context.hashCode();
return hash;
}
/**
* Returns whether this performance event represents a performance failure.
*
* @return <code>true</code> if this is a performance failure, and
* <code>false</code> otherwise.
*/
public boolean isFailure() {
return isFailure;
}
/**
* Resets count and running time for this particular stats event.
*/
public void reset() {
runningTime = 0;
runCount = 0;
}
/**
* Starts timing an occurrence of this event. This is a convenience method,
* fully equivalent to <code>startRun(null)</code>.
*/
public void startRun() {
if (ENABLED)
startRun(null);
}
/**
* Starts timing an occurrence of this event. The event should be stopped
* by a subsequent call to <code>endRun</code>.
*
* @param contextName The context for the event to return, or <code>null</code>.
* The context optionally provides extra information about an event, such as the
* name of a project being built, or the input of an editor being opened.
* @see #endRun
*/
public void startRun(String contextName) {
if (!ENABLED)
return;
this.context = contextName;
this.currentStart = System.currentTimeMillis();
}
/**
* For debugging purposes only.
*/
@Override
public String toString() {
StringBuilder result = new StringBuilder("PerformanceStats("); //$NON-NLS-1$
result.append(event);
result.append(',');
result.append(blame);
if (context != null) {
result.append(',');
result.append(context);
}
result.append(')');
return result.toString();
}
}