/*******************************************************************************
 * Copyright (C) 2014, 2015 Google Inc 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:
 *     Steve Foreman (Google) - initial API and implementation
 *     Marcus Eng (Google)
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.ui.internal.monitoring;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.ui.monitoring.IUiFreezeEventLogger;
import org.eclipse.ui.monitoring.PreferenceConstants;
import org.eclipse.ui.monitoring.StackSample;
import org.eclipse.ui.monitoring.UiFreezeEvent;

/**
 * Event loop monitoring thread. Detects events that take long time to process, collects stack
 * traces of the UI thread during processing of those events, and logs the long events to the error
 * log.
 */
public class EventLoopMonitorThread extends Thread {
	private static final int EVENT_HISTORY_SIZE = 100;
	private static final String EXTENSION_ID = "org.eclipse.ui.monitoring.logger"; //$NON-NLS-1$
	private static final String NEW_LINE_AND_BULLET = "\n* "; //$NON-NLS-1$
	private static final String TRACE_EVENT_MONITOR = "/debug/event_monitor"; //$NON-NLS-1$
	private static final String TRACE_PREFIX = "Event Loop Monitor"; //$NON-NLS-1$
	private static final Tracer tracer =
			Tracer.create(TRACE_PREFIX, PreferenceConstants.PLUGIN_ID + TRACE_EVENT_MONITOR);

	/* NOTE: All time-related values in this class are in milliseconds. */

	/**
	 * Helper object for passing preference-based arguments by name to the constructor, making
	 * the code more readable compared to a large parameter list of integers and booleans.
	 */
	public static class Parameters {
		/** Events that took longer than the specified duration are logged as warnings. */
		public int longEventWarningThreshold;
		/** Events that took longer than the specified duration are logged as errors. */
		public int longEventErrorThreshold;
		/**
		 * Events that took longer than the specified duration are reported as deadlocks without
		 * waiting for the event to finish.
		 */
		public long deadlockThreshold;
		/** Maximum number of stack samples to log */
		public int maxStackSamples;
		/** If true, log freeze events to the Eclipse error log on the local machine. */
		public boolean logToErrorLog;
		/** @see org.eclipse.ui.monitoring.PreferenceConstants#UI_THREAD_FILTER */
		public String uiThreadFilter;
		/** @see org.eclipse.ui.monitoring.PreferenceConstants#NONINTERESTING_THREAD_FILTER */
		public String noninterestingThreadFilter;

		/**
		 * Checks if the values of parameters for UI responsiveness monitoring are valid.
		 *
		 * @throws IllegalArgumentException if the parameter values are invalid or inconsistent.
		 */
		public void checkParameters() throws IllegalArgumentException {
			StringBuilder problems = new StringBuilder();
			if (longEventWarningThreshold <= 0) {
				problems.append(NEW_LINE_AND_BULLET +
						NLS.bind(Messages.EventLoopMonitorThread_warning_threshold_error_1,
								longEventWarningThreshold));
			}
			if (longEventErrorThreshold < longEventWarningThreshold) {
				problems.append(NEW_LINE_AND_BULLET +
						NLS.bind(Messages.EventLoopMonitorThread_error_threshold_too_low_error_2,
								longEventErrorThreshold, longEventWarningThreshold));
			}
			if (deadlockThreshold <= 0) {
				problems.append(NEW_LINE_AND_BULLET +
						NLS.bind(Messages.EventLoopMonitorThread_deadlock_error_1,
								deadlockThreshold));
			} else if (deadlockThreshold <= longEventErrorThreshold) {
				problems.append(NEW_LINE_AND_BULLET +
						NLS.bind(Messages.EventLoopMonitorThread_deadlock_threshold_too_low_error_2,
								deadlockThreshold, longEventErrorThreshold));
			}

			if (problems.length() != 0) {
				throw new IllegalArgumentException(
						NLS.bind(Messages.EventLoopMonitorThread_invalid_argument_error_1,
								problems.toString()));
			}
		}
	}

	/**
	 * Describes the state of the event loop. Visible for testing.
	 */
	private class EventLoopState implements Listener {
		/**
		 * The number of {@link SWT#PreEvent PreEvent}s minus the number of
		 * {@link SWT#PostEvent PostEvent}s since the last
		 * {@link SWT#PreExternalEventDispatch PreExternalEventDispatch}.
		 */
		private int nestingLevel;

		/**
		 * The stack of nesting levels. The current nesting level is pushed to the stack on
		 * {@link SWT#PreExternalEventDispatch PreExternalEventDispatch} event and popped from
		 * the stack on {@link SWT#PostExternalEventDispatch PostExternalEventDispatch} event.
		 */
		private int[] nestingLevelStack = new int[64];
		private int nestingLevelStackSize;

		@Override
		public void handleEvent(Event event) {
			/*
			 * Freeze monitoring involves seeing long intervals between PreEvent/PostEvent messages.
			 * For example:
			 * 1) Log if a top-level or nested dispatch takes too long (interval is between PreEvent
			 *    and PostEvent).
			 * 2) Log if preparation before popping up a dialog takes too long (interval is between
			 *    two PreEvent messages).
			 * 3) Log if processing after dismissing a dialog takes too long (interval is between
			 *    two PostEvent messages).
			 * 4) Log if there is a long delay between nested calls (interval is between PostEvent
			 *    and PreEvent). This could happen after a dialog is dismissed, the code does too
			 *    much processing on the UI thread, and then pops up another dialog.
			 * 5) Don't log for long delays between top-level events (interval is between PostEvent
			 *    and PreEvent at the top level), which should involve sleeping.
			 *
			 * Tracking of PreExternalEventDispatch/PostExternalEventDispatch events allows us to
			 * handle items 4 and 5 above since we can tell if a long delay between an PostEvent and
			 * a PreEvent are due to an idle state (e.g. in Display.sleep()) or a UI freeze.
			 *
			 * Since an idle system can potentially block in an external event loop for a long time,
			 * we need to avoid logging long delays during that time. The eventStartOrResumeTime
			 * field is set to zero when the thread is in an external event loop so that deadlock
			 * logging can be avoided for this case.
			 */
			switch (event.type) {
			case SWT.PreEvent:
				if (!doesEventIndicateResponsiveUI(event.detail)) {
					break;  // Ignore events that may be produced during a UI freeze.
				}
				nestingLevel++;
				if (eventHistory != null) {
					eventHistory.recordEvent(event.type, event.detail, nestingLevel);
				}
				// Log a long interval, start the timer.
				handleEventTransition(true, true);
				break;
			case SWT.PostEvent:
				if (!doesEventIndicateResponsiveUI(event.detail)) {
					break;  // Ignore events that may be produced during a UI freeze.
				}
				if (--nestingLevel < 0) {
					// This may happen if some PreEvent events had occurred before we
					// started listening to SWT events.
					nestingLevel = 0;
				}
				if (eventHistory != null) {
					eventHistory.recordEvent(event.type, event.detail, nestingLevel);
				}
				// Log a long interval, start the timer if inside another event.
				handleEventTransition(true, nestingLevel > 0);
				break;
			case SWT.PreExternalEventDispatch:
				saveAndResetNestingLevel();
				if (eventHistory != null) {
					eventHistory.recordEvent(event.type, event.detail, nestingLevel);
				}
				// Log a long interval, stop the timer.
				handleEventTransition(true, false);
				break;
			case SWT.PostExternalEventDispatch:
				restoreNestingLevel();
				if (eventHistory != null) {
					eventHistory.recordEvent(event.type, event.detail, nestingLevel);
				}
				// Don't log a long interval, start the timer if inside another event.
				handleEventTransition(false, nestingLevel > 0);
				break;
			default:
				break;
			}
		}

		/**
		 * Returns {@code true} if dispatching of an event of the given type indicates that the UI
		 * is responsive. Events that may be produced during UI freezes are irrelevant to UI
		 * responsiveness monitoring.
		 */
		private boolean doesEventIndicateResponsiveUI(int eventType) {
			switch (eventType) {
			case SWT.Skin:
			case SWT.MeasureItem:
			case SWT.Dispose:
				return false;
			default:
				return true;
			}
		}

		private void saveAndResetNestingLevel() {
			if (nestingLevelStackSize < nestingLevelStack.length) {
				nestingLevelStack[nestingLevelStackSize++] = nestingLevel;
				nestingLevel = 0;
			} else {
				MonitoringPlugin.logError(
						NLS.bind(Messages.EventLoopMonitorThread_max_event_loop_depth_exceeded_1,
						nestingLevelStack.length), null);
				shutdown();
			}
		}

		private void restoreNestingLevel() {
			if (nestingLevelStackSize > 0) {
				nestingLevel = nestingLevelStack[--nestingLevelStackSize];
			} else {
				// This may happen if some PreExternalEventDispatch events had occurred before we
				// started listening to SWT events.
				nestingLevel = 0;
			}
		}
	}

	/**
	 * Circular buffer recording SWT events. Used for tracing.
	 */
	private static class EventHistory {
		private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); //$NON-NLS-1$

		private static class EventInfo {
			long timestamp;
			int eventType;
			int detail;
			int nestingLevel;
		}

		private final EventInfo[] buffer;
		private int start; // Index of the first recorded event.
		private int size;  // Number of recorded events.

		EventHistory(int capacity) {
			buffer = new EventInfo[capacity];
			for (int i = 0; i < capacity; i++) {
				buffer[i] = new EventInfo();
			}
		}

		synchronized void recordEvent(int eventType, int detail, int nestingLevel) {
			int j = (start + size) % buffer.length;
			EventInfo event = buffer[j];
			event.timestamp = System.currentTimeMillis();
			event.eventType = eventType;
			event.detail = detail;
			event.nestingLevel = nestingLevel;
			if (size < buffer.length) {
				size++;
			} else if (++start >= buffer.length) {
				start = 0;
			}
		}

		synchronized String extractAndClear() {
			StringBuilder buf = new StringBuilder();
			for (int i = 0; i < size; i++) {
				int j = (start + i) % buffer.length;
				EventInfo eventInfo = buffer[j];
				buf.append(TIME_FORMAT.format(Instant.ofEpochMilli(eventInfo.timestamp)));
				buf.append(": "); //$NON-NLS-1$
				switch (eventInfo.eventType) {
				case SWT.PreEvent:
					buf.append("PreEvent"); //$NON-NLS-1$
					break;
				case SWT.PostEvent:
					buf.append("PostEvent"); //$NON-NLS-1$
					break;
				case SWT.PreExternalEventDispatch:
					buf.append("PreExternalEventDispatch"); //$NON-NLS-1$
					break;
				case SWT.PostExternalEventDispatch:
					buf.append("PostExternalEventDispatch"); //$NON-NLS-1$
					break;
				default:
					buf.append("Event "); //$NON-NLS-1$
					buf.append(eventInfo.eventType);
				}
				buf.append(' ');
				buf.append(eventInfo.detail);
				buf.append(" nesting level: "); //$NON-NLS-1$
				buf.append(eventInfo.nestingLevel);
				buf.append('\n');
			}
			size = 0;
			return buf.toString();
		}
	}

	// Accessed only by the UI thread. */
	private final EventLoopState eventLoopState = new EventLoopState();

	/*
	 * Tracks when the current event was started, or if the event has nested {@link Event#sendEvent}
	 * calls, then the time when the most recent nested call returns and the current event is
	 * resumed.
	 *
	 * Accessed by both the UI and the monitoring thread. Updated by the UI thread and read by the
	 * polling thread. Changing this in the UI thread causes the polling thread to reset its stalled
	 * event state. The UI thread sets this value to zero to indicate a sleep state and to
	 * a positive value to represent a dispatched state.
	 */
	private volatile long eventStartOrResumeTime;

	// Accessed by both the UI and monitoring threads.
	private final int longEventWarningThreshold;
	private final AtomicBoolean cancelled = new AtomicBoolean(false);
	private final AtomicReference<LongEventInfo> eventToPublish =
			new AtomicReference<>(null);

	// Accessed only by the monitoring thread.
	private final List<IUiFreezeEventLogger> externalLoggers =
			new ArrayList<>();
	private DefaultUiFreezeEventLogger defaultLogger;
	private final Display display;
	private final FilterHandler uiThreadFilter;
	private final FilterHandler noninterestingThreadFilter;
	private final int longEventErrorThreshold;
	private final long sampleInterval;
	private final long allThreadsSampleInterval;
	private final int maxStackSamples;
	private final int maxLoggedStackSamples;
	private final long deadlockThreshold;
	private final long uiThreadId;
	private final Object sleepMonitor;
	private final boolean logToErrorLog;
	private EventHistory eventHistory;
	private ThreadMXBean threadMXBean;
	private boolean dumpLockedMonitors;
	private boolean dumpLockedSynchronizers;
	private long monitoringThreadId;

	/**
	 * Initializes the static state of the monitoring thread.
	 *
	 * @param args parameters derived from preferences
	 * @throws IllegalArgumentException if monitoring thread cannot be initialized due to an error
	 */
	public EventLoopMonitorThread(Parameters args) throws IllegalArgumentException {
		super("Event Loop Monitor"); //$NON-NLS-1$

		if (tracer != null) {
			eventHistory = new EventHistory(EVENT_HISTORY_SIZE);
		}

		Assert.isNotNull(args);

		args.checkParameters();

		setDaemon(true);
		setPriority(NORM_PRIORITY + 1);
		display = getDisplay();
		uiThreadId = this.display.getThread().getId();
		longEventWarningThreshold = Math.max(args.longEventWarningThreshold, 3);
		longEventErrorThreshold = Math.max(args.longEventErrorThreshold, longEventWarningThreshold);
		maxLoggedStackSamples = Math.max(args.maxStackSamples, 0);
		maxStackSamples = 2 * maxLoggedStackSamples;
		sampleInterval = longEventWarningThreshold * 2 / 3;
		allThreadsSampleInterval = longEventErrorThreshold * 2 / 3;
		deadlockThreshold = args.deadlockThreshold;
		logToErrorLog = args.logToErrorLog;
		uiThreadFilter = new FilterHandler(args.uiThreadFilter);
		noninterestingThreadFilter = new FilterHandler(args.noninterestingThreadFilter);
		sleepMonitor = new Object();
	}

	/**
	 * Shuts down the monitoring thread. Must be called on the display thread.
	 */
	public void shutdown() throws SWTException {
		cancelled.set(true);
		if (!display.isDisposed()) {
			display.removeListener(SWT.PreEvent, eventLoopState);
			display.removeListener(SWT.PostEvent, eventLoopState);
			display.removeListener(SWT.PreExternalEventDispatch, eventLoopState);
			display.removeListener(SWT.PostExternalEventDispatch, eventLoopState);
		}
		wakeUp();
	}

	/**
	 * For testing only.
	 */
	final void handleEvent(Event event) {
		eventLoopState.handleEvent(event);
	}

	// Called on the UI thread!
	private void handleEventTransition(boolean attemptToLogLongDelay, boolean startEventTimer) {
		/*
		 * On transition between events or sleeping/wake up, we need to reset the delay tracking
		 * state and possibly publish a long delay message. Updating eventStartOrResumeTime causes
		 * the polling thread to reset its stack traces, so it should always be changed *after*
		 * the event is published. The indeterminacy of threading may cause the polling thread to
		 * see both changes or only the (first) eventToPublish change, but the only difference is
		 * a small window where, if an additional stack trace was scheduled to be sampled, a bogus
		 * stack trace sample will be appended to the end of the samples. This bogus sample is
		 * removed before logging.
		 */
		long currTime = getTimestamp();
		if (attemptToLogLongDelay) {
			long startTime = eventStartOrResumeTime;
			if (startTime != 0) {
				int duration = (int) (currTime - startTime);
				if (duration >= longEventWarningThreshold) {
					LongEventInfo info = new LongEventInfo(startTime, duration);
					eventToPublish.set(info);
					wakeUp();
				}
			}
		}
		eventStartOrResumeTime = startEventTimer ? currTime : 0;
	}

	@Override
	public void run() {
		if (logToErrorLog) {
			defaultLogger = new DefaultUiFreezeEventLogger(longEventErrorThreshold);
		}

		loadLoggerExtensions();

		if (!logToErrorLog && externalLoggers.isEmpty()) {
			MonitoringPlugin.logWarning(Messages.EventLoopMonitorThread_logging_disabled_error);
		}

		monitoringThreadId = Thread.currentThread().getId();
		threadMXBean = ManagementFactory.getThreadMXBean();
		dumpLockedMonitors = threadMXBean.isObjectMonitorUsageSupported();
		dumpLockedSynchronizers = threadMXBean.isSynchronizerUsageSupported();
		boolean contentionMonitoringSupported = threadMXBean.isThreadContentionMonitoringSupported();

		/*
		 * If this event loop starts in the middle of a UI freeze, it will succeed in capturing
		 * the portion of that UI freeze that it sees.
		 *
		 * Our timer resolution is, at best, 1 millisecond so we can never try to catch events of
		 * a duration less than that.
		 */
		boolean resetStalledEventState = true;

		long deadlockTimerStart = 0;

		final long pollingNyquistDelay = sampleInterval / 2;
		long pollingDelay = 0; // Immediately updated by resetStalledEventState.
		long grabStackSampleAt = 0; // Immediately updated by resetStalledEventState.
		long lastEventStartOrResumeTime = 0; // Immediately updated by resetStalledEventState.

		StackSample[] stackSamples = new StackSample[maxStackSamples];
		int numSamples = 0;

		boolean starvedAwake = false;
		boolean starvedAsleep = false;

		boolean dumpAllThreads = false;

		// Register for events
		display.asyncExec(this::registerDisplayListeners);

		long currTime = getTimestamp();

		while (!cancelled.get()) {
			long sleepFor;
			if (resetStalledEventState) {
				long eventTime = eventStartOrResumeTime;
				deadlockTimerStart = eventTime;
				if (eventTime == 0) {
					eventTime = currTime;
				}
				grabStackSampleAt = eventTime + sampleInterval;
				numSamples = 0;
				starvedAwake = false;
				starvedAsleep = false;
				if (dumpAllThreads) {
					// Stop capturing stacks of all threads.
					dumpAllThreads = false;
					if (contentionMonitoringSupported) {
						threadMXBean.setThreadContentionMonitoringEnabled(false);
					}
				}
				pollingDelay = sampleInterval;
				sleepFor = pollingNyquistDelay;
				resetStalledEventState = false;
			} else if (lastEventStartOrResumeTime == 0) {
				sleepFor = pollingNyquistDelay;
			} else {
				sleepFor = Math.min(pollingNyquistDelay, Math.max(1, grabStackSampleAt - currTime));
			}

			// Allow the discarded stack samples to be garbage collected.
			for (int i = numSamples; i < stackSamples.length && stackSamples[i] != null; i++) {
				stackSamples[i] = null;
			}

			// This is the top of the polling loop.
			long sleepAt = getTimestamp();

			/*
			 * Check for starvation outside of sleeping. If we sleep or process much longer than
			 * expected (e.g. > threshold/2 longer), then the polling thread has been starved and
			 * it's very likely that the UI thread has been as well. Starvation freezes do not have
			 * useful information, so don't log them.
			 */
			long awakeDuration = currTime - sleepAt;
			boolean starvedAwakeCurrentCycle = awakeDuration > (sleepFor + longEventWarningThreshold / 2);
			if (starvedAwakeCurrentCycle) {
				starvedAwake = true;
			}
			sleepForMillis(sleepFor);
			currTime = getTimestamp();
			long currEventStartOrResumeTime = eventStartOrResumeTime;
			long sleepDuration = currTime - sleepAt;
			boolean starvedAsleepCurrentCycle = sleepDuration > (sleepFor + longEventWarningThreshold / 2);
			if (starvedAsleepCurrentCycle) {
				starvedAsleep = true;
			}
			boolean starved = starvedAsleepCurrentCycle || starvedAwakeCurrentCycle;

			/*
			 * If after sleeping we see that a new event has been dispatched, mark that we should
			 * update the stalled event state. Otherwise, check if we have surpassed our threshold
			 * and collect a stack trace.
			 */
			if (lastEventStartOrResumeTime != currEventStartOrResumeTime || starved) {
				resetStalledEventState = true;
				if (tracer != null && starved) {
					if (starvedAwakeCurrentCycle) {
						tracer.trace(String.format(
								"Starvation detected! Polling loop took a significant amount of threshold: %dms", //$NON-NLS-1$
								awakeDuration));
					}

					if (starvedAsleepCurrentCycle) {
						tracer.trace(String.format(
								"Starvation detected! Expected to sleep for %dms but actually slept for %dms", //$NON-NLS-1$
								sleepFor, sleepDuration));
					}
				}
			} else if (lastEventStartOrResumeTime != 0) {
				if (!dumpAllThreads && currTime >= lastEventStartOrResumeTime + allThreadsSampleInterval) {
					// Start capturing stacks of all threads.
					dumpAllThreads = true;
					if (contentionMonitoringSupported) {
						threadMXBean.setThreadContentionMonitoringEnabled(true);
					}
				}

				if (deadlockTimerStart != 0) {
					long totalDuration = currTime - deadlockTimerStart;
					if (totalDuration >= deadlockThreshold) {
						if (numSamples > maxLoggedStackSamples) {
							decimate(stackSamples, numSamples, maxLoggedStackSamples);
							numSamples = maxLoggedStackSamples;
						}
						if (uiThreadFilter.shouldLogEvent(stackSamples, numSamples, uiThreadId)) {
							logEvent(new UiFreezeEvent(deadlockTimerStart, totalDuration,
									Arrays.copyOf(stackSamples, numSamples),
									true, starvedAwake, starvedAsleep));
							deadlockTimerStart = 0; // Don't log potential deadlock more than once.
						}
					}
				}

				// Collect additional stack traces if enough time has elapsed.
				if (maxStackSamples > 0 && currTime >= grabStackSampleAt) {
					if (numSamples == maxStackSamples) {
						numSamples = maxStackSamples / 2;
						decimate(stackSamples, maxStackSamples, numSamples);
					}

					ThreadInfo[] threadStacks = captureThreadStacks(dumpAllThreads);
					stackSamples[numSamples++] = new StackSample(getTimestamp(), threadStacks);
					if (numSamples == maxStackSamples) {
						pollingDelay *= 2; // Reduce polling frequency.
					}
					grabStackSampleAt += pollingDelay;
				}
			}

			// If a stalled event has finished, publish it and mark that the information should
			// be reset.
			LongEventInfo eventSnapshot = eventToPublish.getAndSet(null);
			if (eventSnapshot != null) {
				long eventEnd = eventSnapshot.start + eventSnapshot.duration;
				// Remove stack samples collected after the end of the event.
				while (numSamples > 0 && eventEnd <= stackSamples[numSamples - 1].getTimestamp()) {
					--numSamples;
				}
				if (numSamples > maxLoggedStackSamples) {
					// Remove the last stack sample if it is too close to the end of the event.
					if (eventEnd - stackSamples[numSamples - 1].getTimestamp() < sampleInterval) {
						--numSamples;
					}
				}

				if (numSamples > maxLoggedStackSamples) {
					decimate(stackSamples, numSamples, maxLoggedStackSamples);
					numSamples = maxLoggedStackSamples;
				}

				if (uiThreadFilter.shouldLogEvent(stackSamples, numSamples, uiThreadId)) {
					logEvent(new UiFreezeEvent(eventSnapshot.start, eventSnapshot.duration,
							Arrays.copyOf(stackSamples, numSamples),
							false, starvedAwake, starvedAsleep));
				}

				resetStalledEventState = true;
			}

			lastEventStartOrResumeTime = currEventStartOrResumeTime;
		}
	}

	private ThreadInfo[] captureThreadStacks(boolean dumpAllThreads) {
		if (dumpAllThreads) {
			ThreadInfo[] threadStacks =
					threadMXBean.dumpAllThreads(dumpLockedMonitors, dumpLockedSynchronizers);
			// Remove the info for the monitoring thread.
			int index = 0;
			for (ThreadInfo thread : threadStacks) {
				long threadId = thread.getThreadId();
				// Skip the stack trace of the event loop monitoring thread.
				if (threadId != monitoringThreadId) {
					if (threadId == uiThreadId) {
						// Swap the UI thread to first slot in the array if it is not there already.
						if (index != 0) {
							ThreadInfo uiThread = thread;
							thread = threadStacks[0];
							threadStacks[0] = uiThread;
						}
					} else if (!isInteresting(thread)) {
						continue; // Skip the non-interesting thread.
					}
					threadStacks[index++] = thread;
				}
			}
			return Arrays.copyOf(threadStacks, index);
		} else {
			return new ThreadInfo[] { threadMXBean.getThreadInfo(uiThreadId, Integer.MAX_VALUE) };
		}
	}

	/**
	 * A thread is considered interesting if its stack trace includes at least one frame not
	 * matching any of the methods in {@link #noninterestingThreadFilter}.
	 */
	private boolean isInteresting(ThreadInfo thread) {
		for (StackTraceElement element : thread.getStackTrace()) {
			if (!noninterestingThreadFilter.matchesFilter(element)) {
				return true;
			}
		}
		return false;
	}

	private static Display getDisplay() throws IllegalStateException {
		Display display = Display.getDefault();
		if (display == null) {
			throw new IllegalStateException(Messages.EventLoopMonitorThread_display_was_null);
		}

		return display;
	}

	// VisibleForTesting
	protected long getTimestamp() {
		return System.currentTimeMillis();
	}

	// VisibleForTesting
	protected void sleepForMillis(long milliseconds) {
		if (milliseconds > 0) {
			try {
				synchronized (sleepMonitor) {
					// Spurious wake ups are OK; they will just burn a few extra CPU cycles.
					sleepMonitor.wait(milliseconds);
				}
			} catch (InterruptedException e) {
				// Wake up.
			}
		}
	}

	private void loadLoggerExtensions() {
		IConfigurationElement[] configElements =
				Platform.getExtensionRegistry().getConfigurationElementsFor(EXTENSION_ID);

		for (IConfigurationElement element : configElements) {
			try {
				Object object = element.createExecutableExtension("class"); //$NON-NLS-1$
				if (object instanceof IUiFreezeEventLogger) {
					externalLoggers.add((IUiFreezeEventLogger) object);
				} else {
					MonitoringPlugin.logWarning(NLS.bind(
							Messages.EventLoopMonitorThread_invalid_logger_type_error_4,
							new Object[] { object.getClass().getName(),
									IUiFreezeEventLogger.class.getClass().getSimpleName(),
									EXTENSION_ID, element.getContributor().getName() }));
				}
			} catch (CoreException e) {
				MonitoringPlugin.logError(e.getMessage(), e);
			}
		}
	}

	private void registerDisplayListeners() {
		display.addListener(SWT.PreEvent, eventLoopState);
		display.addListener(SWT.PostEvent, eventLoopState);
		display.addListener(SWT.PreExternalEventDispatch, eventLoopState);
		display.addListener(SWT.PostExternalEventDispatch, eventLoopState);
	}

	/**
	 * Reduces number of samples by weeding some of them out. The remaining samples are chosen to
	 * represent the whole range of samples with a slight preference given to later samples that
	 * are more likely to contain stacks of all threads.
	 *
	 * @param samples the array of samples
	 * @param fromSize the number of samples to choose from in the array
	 * @param toSize the number of samples to select
	 */
	private static void decimate(StackSample[] samples, int fromSize, int toSize) {
		for (int i = 0; i < toSize; ++i) {
			int j = ((i + 1) * fromSize - 1) / toSize;
			samples[i] = samples[j];
		}
	}

	private void wakeUp() {
		synchronized (sleepMonitor) {
			sleepMonitor.notify();
		}
	}

	/**
	 * Writes a UI freeze event to the log.
	 */
	private void logEvent(UiFreezeEvent event) {
		if (tracer != null) {
			tracer.trace("Logging " + event + "Prior events:\n" + eventHistory.extractAndClear()); //$NON-NLS-1$//$NON-NLS-2$
		}

		if (logToErrorLog) {
			defaultLogger.log(event);
		}

		for (int i = 0; i < externalLoggers.size(); i++) {
			IUiFreezeEventLogger currentLogger = externalLoggers.get(i);
			try {
				currentLogger.log(event);
			} catch (Throwable t) {
				externalLoggers.remove(i);
				i--;
				MonitoringPlugin.logError(NLS.bind(
						Messages.EventLoopMonitorThread_external_exception_error_1,
						currentLogger.getClass().getName()), t);
			}
		}
	}
}
