/* * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.util.log.speedtracer; import com.google.gwt.dev.json.JsonArray; import com.google.gwt.dev.json.JsonObject; import com.google.gwt.dev.shell.DevModeSession; import com.google.gwt.dev.util.collect.Lists; import com.google.gwt.dev.util.log.dashboard.DashboardNotifierFactory; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.lang.management.OperatingSystemMXBean; import java.lang.management.ThreadMXBean; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; /** * Logs performance metrics for internal development purposes. The output is * formatted so it can be opened directly in the SpeedTracer Chrome extension. * This class formats events using SpeedTracer's custom event feature. The html * file output can be viewed by using Chrome to open the file on a Chrome * browser that has the SpeedTracer extension installed. * * <p> * Enable logging by setting the system property {@code gwt.speedtracerlog} to * the output file path. * </p> * */ public final class SpeedTracerLogger { private static final Logger log = Logger.getLogger(SpeedTracerLogger.class.getName()); // Log file name (logging is enabled if this is non-null) private static final String logFile = System.getProperty("gwt.speedtracerlog"); // Allow a system property to override the default output format private static final String defaultFormatString = System.getProperty("gwt.speedtracerformat"); // Use cumulative multi-threaded process cpu time instead of wall time private static final boolean logProcessCpuTime = getBooleanProperty("gwt.speedtracer.logProcessCpuTime"); // Use per thread cpu time instead of wall time. If logProcessCpuTime is set, // then this can remain false - we only need one or the other. private static final boolean logThreadCpuTime = getBooleanProperty("gwt.speedtracer.logThreadCpuTime"); // Turn on logging summarizing gc time during an event private static final boolean logGcTime = getBooleanProperty("gwt.speedtracer.logGcTime"); // Turn on logging estimating overhead used for speedtracer logging. private static final boolean logOverheadTime = getBooleanProperty("gwt.speedtracer.logOverheadTime"); // Disable logging of JSNI calls and callbacks to reduce memory usage where // the heap is already tight. private static final boolean jsniCallLoggingEnabled = !getBooleanProperty("gwt.speedtracer.disableJsniLogging"); static { // verify configuration if (logProcessCpuTime && logThreadCpuTime) { throw new RuntimeException("System properties are misconfigured: " + "Specify one or the other of 'gwt.speedtracer.logProcessCpuTime' " + "or 'gwt.speedtracer.logThreadCpuTime', not both."); } } /** * Represents a node in a tree of SpeedTracer events. */ public class Event { protected final EventType type; List<Event> children; List<String> data; DevModeSession devModeSession; long elapsedDurationNanos; long elapsedStartTimeNanos; long processCpuDurationNanos; long processCpuStartTimeNanos; long threadCpuDurationNanos; long threadCpuStartTimeNanos; Event() { if (enabled) { if (logThreadCpuTime) { threadCpuTimeKeeper.resetTimeBase(); } recordStartTime(); this.data = Lists.create(); this.children = Lists.create(); } else { this.processCpuStartTimeNanos = 0L; this.threadCpuStartTimeNanos = 0L; this.elapsedStartTimeNanos = 0L; this.data = null; this.children = null; } this.type = null; } Event(DevModeSession session, Event parent, EventType type, String... data) { if (parent != null) { parent.children = Lists.add(parent.children, this); } this.type = type; assert (data.length % 2 == 0); recordStartTime(); this.data = Lists.create(data); this.children = Lists.create(); this.devModeSession = session; } /** * @param data key/value pairs to add to JSON object. */ public void addData(String... data) { if (data != null) { assert (data.length % 2 == 0); this.data = Lists.addAll(this.data, data); } } /** * Signals the end of the current event. */ public void end(String... data) { endImpl(this, data); } public DevModeSession getDevModeSession() { return devModeSession; } /** * Returns the event duration, in nanoseconds, for the log file. Depending * on system properties, this will measured in elapsed time, process CPU * time, or thread CPU time. */ public long getDurationNanos() { return logProcessCpuTime ? processCpuDurationNanos : (logThreadCpuTime ? threadCpuDurationNanos : elapsedDurationNanos); } public long getElapsedDurationNanos() { return this.elapsedDurationNanos; } public long getElapsedStartTimeNanos() { return this.elapsedStartTimeNanos; } /** * Returns the event start time, normalized in nanoseconds, for the log * file. Depending on system properties, this will be normalized based on * elapsed time, process CPU time, or thread CPU time. */ public long getStartTimeNanos() { return logProcessCpuTime ? processCpuStartTimeNanos : (logThreadCpuTime ? threadCpuStartTimeNanos : elapsedStartTimeNanos); } public EventType getType() { return type; } @Override public String toString() { return type.getName(); } /** * Extends the durations of the current event by the durations of the * specified event. */ void extendDuration(Event refEvent) { elapsedDurationNanos += refEvent.elapsedDurationNanos; processCpuDurationNanos += refEvent.processCpuDurationNanos; threadCpuDurationNanos += refEvent.threadCpuDurationNanos; } /** * Sets the start time of this event to start immediately after the * specified event ends. */ void setStartsAfter(Event refEvent) { elapsedStartTimeNanos = refEvent.elapsedStartTimeNanos + refEvent.elapsedDurationNanos; processCpuStartTimeNanos = refEvent.processCpuStartTimeNanos + refEvent.processCpuDurationNanos; threadCpuStartTimeNanos = refEvent.threadCpuStartTimeNanos + refEvent.threadCpuDurationNanos; } JsonObject toJson() { JsonObject json = JsonObject.create(); json.put("type", -2); json.put("typeName", type.getName()); json.put("color", type.getColor()); double startMs = convertToMilliseconds(getStartTimeNanos()); json.put("time", startMs); double durationMs = convertToMilliseconds(getDurationNanos()); json.put("duration", durationMs); JsonObject jsonData = JsonObject.create(); for (int i = 0; i < data.size(); i += 2) { jsonData.put(data.get(i), data.get(i + 1)); } json.put("data", jsonData); JsonArray jsonChildren = JsonArray.create(); for (Event child : children) { jsonChildren.add(child.toJson()); } json.put("children", jsonChildren); return json; } /** * Records the duration of this event based on the current time and the * event's recorded start time. */ void updateDuration() { long elapsedEndTimeNanos = elapsedTimeKeeper.normalizedTimeNanos(); assert (elapsedEndTimeNanos >= elapsedStartTimeNanos); elapsedDurationNanos = elapsedEndTimeNanos - elapsedStartTimeNanos; // don't bother making expensive time keeping method calls unless // necessary if (logProcessCpuTime) { long processCpuEndTimeNanos = processCpuTimeKeeper.normalizedTimeNanos(); assert (processCpuEndTimeNanos >= processCpuStartTimeNanos); processCpuDurationNanos = processCpuEndTimeNanos - processCpuStartTimeNanos; } else if (logThreadCpuTime) { long threadCpuEndTimeNanos = threadCpuTimeKeeper.normalizedTimeNanos(); assert (threadCpuEndTimeNanos >= threadCpuStartTimeNanos); threadCpuDurationNanos = threadCpuEndTimeNanos - threadCpuStartTimeNanos; } } /** * Marks the start time for this event. Three different time measurements * are used: * <ol> * <li>Elapsed (wall-clock) time</li> * <li>Process CPU time</li> * <li>Thread CPU time</li> * </ol> */ private void recordStartTime() { elapsedStartTimeNanos = elapsedTimeKeeper.normalizedTimeNanos(); // don't bother making expensive time keeping method calls unless // necessary if (logProcessCpuTime) { processCpuStartTimeNanos = processCpuTimeKeeper.normalizedTimeNanos(); } else if (logThreadCpuTime) { threadCpuStartTimeNanos = threadCpuTimeKeeper.normalizedTimeNanos(); } } } /** * Enumerated types for logging events implement this interface. */ public interface EventType { String getColor(); String getName(); } static enum Format { /** * Standard SpeedTracer log that includes JSON wrapped in HTML that will * launch a SpeedTracer monitor session. */ HTML, /** * Only the JSON data without any HTML wrappers. */ RAW } /** * A dummy implementation to do nothing if logging has not been turned on. */ private class DummyEvent extends Event { @Override public void addData(String... data) { // do nothing } @Override public void end(String... data) { // do nothing } @Override public String toString() { return "Dummy"; } } /** * Provides functionality specific to garbage collection events. */ private class GcEvent extends Event { private Event refEvent; /** * Constructs an event that represents garbage collection metrics. * * @param refEvent the event during which the garbage collections took place * @param gcType the garbage collector type * @param collectionCount the total number of collections for this garbage * collector type * @param durationNanos the total elapsed time spent in garbage collection * during the span of {@code refEvent} */ GcEvent(Event refEvent, String gcType, long collectionCount, long durationNanos) { super(null, null, SpeedTracerEventType.GC, "Collector Type", gcType, "Cumulative Collection Count", Long.toString(collectionCount)); this.refEvent = refEvent; // GarbageCollectorMXBean can only provide elapsed time, so that's all we // record this.elapsedDurationNanos = durationNanos; } /** * Returns elapsed duration since that is the only duration we can measure * for garbage collection events. */ @Override public long getDurationNanos() { return getElapsedDurationNanos(); } /** * Returns a start time so that this event ends with its {@code refEvent}. */ @Override public long getElapsedStartTimeNanos() { return refEvent.getElapsedStartTimeNanos() + refEvent.getElapsedDurationNanos() - getElapsedDurationNanos(); } /** * Returns a start time so that this event ends with its {@code refEvent}. */ @Override public long getStartTimeNanos() { return refEvent.getStartTimeNanos() + refEvent.getDurationNanos() - getDurationNanos(); } } /** * Time keeper which uses wall time. */ private class ElapsedNormalizedTimeKeeper { private final long zeroTimeNanos; private final long zeroTimeMillis; public ElapsedNormalizedTimeKeeper() { zeroTimeNanos = System.nanoTime(); zeroTimeMillis = (long) convertToMilliseconds(zeroTimeNanos); } public long normalizedTimeNanos() { return System.nanoTime() - zeroTimeNanos; } public long zeroTimeMillis() { return zeroTimeMillis; } } /** * Time keeper which uses process cpu time. This can be greater than wall * time, since it is cumulative over the multiple threads of a process. */ private class ProcessNormalizedTimeKeeper { private final OperatingSystemMXBean osMXBean; private final Method getProcessCpuTimeMethod; private final long zeroTimeNanos; private final long zeroTimeMillis; public ProcessNormalizedTimeKeeper() { try { osMXBean = ManagementFactory.getOperatingSystemMXBean(); /* * Find this method by reflection, since it's part of the Sun * implementation for OperatingSystemMXBean, and we can't always assume * that com.sun.management.OperatingSystemMXBean will be available. */ getProcessCpuTimeMethod = osMXBean.getClass().getMethod("getProcessCpuTime"); getProcessCpuTimeMethod.setAccessible(true); zeroTimeNanos = (Long) getProcessCpuTimeMethod.invoke(osMXBean); zeroTimeMillis = (long) convertToMilliseconds(zeroTimeNanos); } catch (Exception ex) { throw new RuntimeException(ex); } } public long normalizedTimeNanos() { try { return (Long) getProcessCpuTimeMethod.invoke(osMXBean) - zeroTimeNanos; } catch (Exception ex) { throw new RuntimeException(ex); } } public long zeroTimeMillis() { return zeroTimeMillis; } } /** * Time keeper which uses per thread cpu time. It is assumed that individual * events logged will be single threaded, and that top-level events will call * {@link #resetTimeBase()} prior to logging time. The resettable time base * is needed since each individual thread starts its timing at 0, regardless * of when the thread is created. So we reset the time base at the beginning * of an event, so that we can generate a chronologically representative * output, although the relation to wall time is actually compressed within * a logged event (thread cpu time does not include wait time, etc.). */ private class ThreadNormalizedTimeKeeper { private final ThreadMXBean threadMXBean; private final ThreadLocal<Long> resettableTimeBase = new ThreadLocal<Long>(); private final long zeroTimeNanos; private final long zeroTimeMillis; public ThreadNormalizedTimeKeeper() { threadMXBean = ManagementFactory.getThreadMXBean(); if (!threadMXBean.isCurrentThreadCpuTimeSupported()) { throw new RuntimeException("Current thread cpu time not supported"); } zeroTimeNanos = System.nanoTime(); zeroTimeMillis = (long) convertToMilliseconds(zeroTimeNanos); } public long normalizedTimeNanos() { return threadMXBean.getCurrentThreadCpuTime() + resettableTimeBase.get(); } public void resetTimeBase() { /* * Since all threads start individually at time 0L, we use this to * offset each event's time so we can generate chronological output. */ resettableTimeBase.set(System.nanoTime() - zeroTimeNanos - threadMXBean.getCurrentThreadCpuTime()); } public long zeroTimeMillis() { return zeroTimeMillis; } } /** * Initializes the singleton on demand. */ private static class LazySpeedTracerLoggerHolder { public static SpeedTracerLogger singleton = new SpeedTracerLogger(); } /** * Thread that converts log requests to JSON in the background. */ private class LogWriterThread extends Thread { private static final int FLUSH_TIMER_MSECS = 10000; private final String fileName; private final BlockingQueue<Event> threadEventQueue; private final Writer writer; public LogWriterThread( Writer writer, String fileName, final BlockingQueue<Event> eventQueue) { super(); this.writer = writer; this.fileName = fileName; this.threadEventQueue = eventQueue; } @Override public void run() { long nextFlush = System.currentTimeMillis() + FLUSH_TIMER_MSECS; try { while (true) { Event event = threadEventQueue.poll(nextFlush - System.currentTimeMillis(), TimeUnit.MILLISECONDS); if (event == null) { // ignore. } else if (event == shutDownSentinel) { break; } else if (event == flushSentinel) { writer.flush(); flushLatch.countDown(); } else { JsonObject json = event.toJson(); json.write(writer); writer.write('\n'); } if (System.currentTimeMillis() >= nextFlush) { writer.flush(); nextFlush = System.currentTimeMillis() + FLUSH_TIMER_MSECS; } } // All queued events have been written. if (outputFormat.equals(Format.HTML)) { writer.write("</div></body></html>\n"); } writer.close(); } catch (InterruptedException ignored) { } catch (IOException e) { log.log(Level.SEVERE, "Unable to write to gwt.speedtracerlog '" + (fileName == null ? "" : fileName) + "'", e); } finally { shutDownLatch.countDown(); } } } /** * Records a LOG_MESSAGE type of SpeedTracer event. */ private class MarkTimelineEvent extends Event { public MarkTimelineEvent(Event parent) { super(); if (parent != null) { parent.children = Lists.add(parent.children, this); } } @Override JsonObject toJson() { JsonObject json = JsonObject.create(); json.put("type", 11); double startMs = convertToMilliseconds(getStartTimeNanos()); json.put("time", startMs); json.put("duration", 0.0); JsonObject jsonData = JsonObject.create(); for (int i = 0; i < data.size(); i += 2) { jsonData.put(data.get(i), data.get(i + 1)); } json.put("data", jsonData); return json; } } /** * Annotate the current event on the top of the stack with more information. * The method expects key, value pairs, so there must be an even number of * parameters. * * @param data JSON property, value pair to add to current event. */ public static void addData(String... data) { SpeedTracerLogger.get().addDataImpl(data); } /** * Create a new global instance. Force the zero time to be recorded and the * log to be opened if the default logging is turned on with the <code> * -Dgwt.speedtracerlog</code> VM property. * * This method is only intended to be called once. */ public static void init() { get(); } /** * Returns true if JSNI calls and callbacks are being logged. */ public static boolean jsniCallLoggingEnabled() { return jsniCallLoggingEnabled; } /** * Adds a LOG_MESSAGE SpeedTracer event to the log. This represents a single * point in time and has a special representation in the SpeedTracer UI. */ public static void markTimeline(String message) { SpeedTracerLogger.get().markTimelineImpl(message); } /** * Signals that a new event has started. You must end each event for each * corresponding call to {@code start}. You may nest timing calls. * * <p> * Has the same effect as calling * {@link #start(DevModeSession, EventType, String...) * start(DevModeSession.getSessionForCurrentThread(), type, data)}. * * @param type the type of event * @param data a set of key-value pairs (each key is followed by its value) * that contain additional information about the event * @return an Event object to be ended by the caller */ public static Event start(EventType type, String... data) { return SpeedTracerLogger.get().startImpl(DevModeSession.getSessionForCurrentThread(), type, data); } /** * Signals that a new event has started. You must end each event for each * corresponding call to {@code start}. You may nest timing calls. * * @param session the devmode session with which this event is associated or * null if no devmode session is active * @param type the type of event * @param data a set of key-value pairs (each key is followed by its value) that * contain additional information about the event * @return an Event object to be ended by the caller */ public static Event start(DevModeSession session, EventType type, String... data) { return SpeedTracerLogger.get().startImpl(session, type, data); } private static double convertToMilliseconds(long nanos) { return nanos / 1000000.0d; } /** * For accessing the logger as a singleton, you can retrieve the global * instance. It is prudent, but not necessary to first initialize the * singleton with a call to {@link #init()} to set the base time. * * @return the current global {@link SpeedTracerLogger} instance. */ private static SpeedTracerLogger get() { return LazySpeedTracerLoggerHolder.singleton; } private static boolean getBooleanProperty(String propName) { try { return System.getProperty(propName) != null; } catch (RuntimeException ruEx) { return false; } } private final boolean enabled; private final DummyEvent dummyEvent = new DummyEvent(); private BlockingQueue<Event> eventsToWrite; private final boolean fileLoggingEnabled; private CountDownLatch flushLatch; private Event flushSentinel; private Format outputFormat; private ThreadLocal<Stack<Event>> pendingEvents; private CountDownLatch shutDownLatch; private Event shutDownSentinel; private List<GarbageCollectorMXBean> gcMXBeans; private Map<String, Long> lastGcTimes; private final ElapsedNormalizedTimeKeeper elapsedTimeKeeper = new ElapsedNormalizedTimeKeeper(); private final ProcessNormalizedTimeKeeper processCpuTimeKeeper = (logProcessCpuTime) ? new ProcessNormalizedTimeKeeper() : null; private final ThreadNormalizedTimeKeeper threadCpuTimeKeeper = (logThreadCpuTime) ? new ThreadNormalizedTimeKeeper() : null; /** * Constructor intended for unit testing. * * @param writer alternative {@link Writer} to send speed tracer output. */ SpeedTracerLogger(Writer writer, Format format) { enabled = true; fileLoggingEnabled = true; outputFormat = format; eventsToWrite = openLogWriter(writer, ""); pendingEvents = initPendingEvents(); shutDownSentinel = new DummyEvent(); flushSentinel = new DummyEvent(); shutDownLatch = new CountDownLatch(1); } private SpeedTracerLogger() { fileLoggingEnabled = logFile != null; enabled = fileLoggingEnabled || DashboardNotifierFactory.areNotificationsEnabled(); if (enabled) { if (fileLoggingEnabled) { // Allow a system property to override the default output format Format format = Format.HTML; if (defaultFormatString != null) { for (Format value : Format.values()) { if (value.name().toLowerCase().equals(defaultFormatString.toLowerCase())) { format = value; break; } } } outputFormat = format; eventsToWrite = openDefaultLogWriter(); shutDownSentinel = new DummyEvent(); flushSentinel = new DummyEvent(); shutDownLatch = new CountDownLatch(1); } if (logGcTime) { gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); lastGcTimes = new ConcurrentHashMap<String, Long>(); } pendingEvents = initPendingEvents(); } } public void addDataImpl(String... data) { Stack<Event> threadPendingEvents = pendingEvents.get(); if (threadPendingEvents.isEmpty()) { throw new IllegalStateException( "Tried to add data to an event that never started!"); } Event currentEvent = threadPendingEvents.peek(); currentEvent.addData(data); } public void markTimelineImpl(String message) { Stack<Event> threadPendingEvents = pendingEvents.get(); Event parent = null; if (!threadPendingEvents.isEmpty()) { parent = threadPendingEvents.peek(); } Event newEvent = new MarkTimelineEvent(parent); threadPendingEvents.push(newEvent); newEvent.end("message", message); } void addGcEvents(Event refEvent) { // we're not sending GC events to the dashboard, so we only record them // to file if (!fileLoggingEnabled) { return; } for (GarbageCollectorMXBean gcMXBean : gcMXBeans) { String gcName = gcMXBean.getName(); Long lastGcTime = lastGcTimes.get(gcName); long currGcTime = gcMXBean.getCollectionTime(); if (lastGcTime == null) { lastGcTime = 0L; } if (currGcTime > lastGcTime) { // create a new event long gcDurationNanos = (currGcTime - lastGcTime) * 1000000L; Event gcEvent = new GcEvent(refEvent, gcName, gcMXBean.getCollectionCount(), gcDurationNanos); eventsToWrite.add(gcEvent); lastGcTimes.put(gcName, currGcTime); } } } void addOverheadEvent(Event refEvent) { Event overheadEvent = new Event(refEvent.devModeSession, refEvent, SpeedTracerEventType.OVERHEAD); // measure the time between the end of refEvent and now overheadEvent.setStartsAfter(refEvent); overheadEvent.updateDuration(); refEvent.extendDuration(overheadEvent); } void endImpl(Event event, String... data) { if (!enabled) { return; } if (data.length % 2 == 1) { throw new IllegalArgumentException("Unmatched data argument"); } Stack<Event> threadPendingEvents = pendingEvents.get(); if (threadPendingEvents.isEmpty()) { throw new IllegalStateException( "Tried to end an event that never started!"); } Event currentEvent = threadPendingEvents.pop(); currentEvent.updateDuration(); while (currentEvent != event && !threadPendingEvents.isEmpty()) { // Missed a closing end for one or more frames! Try to sync back up. currentEvent.addData("Missed", "This event was closed without an explicit call to Event.end()"); currentEvent = threadPendingEvents.pop(); currentEvent.updateDuration(); } if (threadPendingEvents.isEmpty() && currentEvent != event) { currentEvent.addData( "Missed", "Fell off the end of the threadPending events"); } if (logGcTime) { addGcEvents(currentEvent); } currentEvent.addData(data); if (logOverheadTime) { addOverheadEvent(currentEvent); } if (threadPendingEvents.isEmpty()) { if (fileLoggingEnabled) { eventsToWrite.add(currentEvent); } DashboardNotifierFactory.getNotifier().devModeEventEnd(currentEvent.getDevModeSession(), currentEvent.getType().getName(), currentEvent.getElapsedStartTimeNanos(), currentEvent.getElapsedDurationNanos()); } } /** * Notifies the background thread to finish processing all data in the queue. * Blocks the current thread until the data is flushed in the Log Writer * thread. */ void flush() { if (!fileLoggingEnabled) { return; } try { // Wait for the other thread to drain the queue. flushLatch = new CountDownLatch(1); eventsToWrite.add(flushSentinel); flushLatch.await(); } catch (InterruptedException e) { // Ignored } } Event startImpl(DevModeSession session, EventType type, String... data) { if (!enabled) { return dummyEvent; } if (data.length % 2 == 1) { throw new IllegalArgumentException("Unmatched data argument"); } Stack<Event> threadPendingEvents = pendingEvents.get(); Event parent = null; if (!threadPendingEvents.isEmpty()) { parent = threadPendingEvents.peek(); } else { DashboardNotifierFactory.getNotifier().devModeEventBegin(); if (logThreadCpuTime) { // reset the thread CPU time base for top-level events (so events can be // properly sequenced chronologically) threadCpuTimeKeeper.resetTimeBase(); } } Event newEvent = new Event(session, parent, type, data); // Add a field to the top level event in order to track the base time // so we can re-normalize the data if (threadPendingEvents.size() == 0) { long baseTime = logProcessCpuTime ? processCpuTimeKeeper.zeroTimeMillis() : (logThreadCpuTime ? threadCpuTimeKeeper.zeroTimeMillis() : elapsedTimeKeeper.zeroTimeMillis()); newEvent.addData("baseTime", "" + baseTime); } threadPendingEvents.push(newEvent); return newEvent; } private ThreadLocal<Stack<Event>> initPendingEvents() { return new ThreadLocal<Stack<Event>>() { @Override protected Stack<Event> initialValue() { return new Stack<Event>(); } }; } private BlockingQueue<Event> openDefaultLogWriter() { Writer writer = null; if (enabled) { try { writer = new BufferedWriter(new FileWriter(logFile)); return openLogWriter(writer, logFile); } catch (IOException e) { log.log(Level.SEVERE, "Unable to open gwt.speedtracerlog '" + logFile + "'", e); } } return null; } private BlockingQueue<Event> openLogWriter( final Writer writer, final String fileName) { try { if (outputFormat.equals(Format.HTML)) { writer.write( "<HTML isdump=\"true\"><body>" + "<style>body {font-family:Helvetica; margin-left:15px;}</style>" + "<h2>Performance dump from GWT</h2>" + "<div>This file contains data that can be viewed with the " + "<a href=\"http://code.google.com/speedtracer\">SpeedTracer</a> " + "extension under the <a href=\"http://chrome.google.com/\">" + "Chrome</a> browser.</div><p><span id=\"info\">" + "(You must install the SpeedTracer extension to open this file)</span></p>" + "<div style=\"display: none\" id=\"traceData\" version=\"0.17\">\n"); } } catch (IOException e) { log.log(Level.SEVERE, "Unable to write to gwt.speedtracerlog '" + (fileName == null ? "" : fileName) + "'", e); return null; } final BlockingQueue<Event> eventQueue = new LinkedBlockingQueue<Event>(); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { try { // Wait for the other thread to drain the queue. eventQueue.add(shutDownSentinel); shutDownLatch.await(); } catch (InterruptedException e) { // Ignored } } }); // Background thread to write SpeedTracer events to log Thread logWriterWorker = new LogWriterThread(writer, fileName, eventQueue); // Lower than normal priority. logWriterWorker.setPriority( (Thread.MIN_PRIORITY + Thread.NORM_PRIORITY) / 2); /* * This thread must be daemon, otherwise shutdown hooks would never begin to * run, and an app wouldn't finish. */ logWriterWorker.setDaemon(true); logWriterWorker.setName("SpeedTracerLogger writer"); logWriterWorker.start(); return eventQueue; } }