Enhancements for SpeedTracerLogging within gwt
Added support for logging/estimating garbage collection, process cpu time, per thread cpu time, and for overhead time for logging itself.

Review at http://gwt-code-reviews.appspot.com/1336803


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9674 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/util/log/speedtracer/SpeedTracerEventType.java b/dev/core/src/com/google/gwt/dev/util/log/speedtracer/SpeedTracerEventType.java
new file mode 100644
index 0000000..69a10a0
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/log/speedtracer/SpeedTracerEventType.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2011 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.util.log.speedtracer.SpeedTracerLogger.EventType;
+
+/**
+ * Represents a type of event whose performance is tracked while running
+ * {@link com.google.gwt.dev.DevMode}
+ */
+public enum SpeedTracerEventType implements EventType {
+  GC("Garbage Collection", "Plum"),
+  OVERHEAD("Speedtracer Overhead","Black");
+
+  final String cssColor;
+  final String name;
+
+  SpeedTracerEventType(String name, String cssColor) {
+    this.name = name;
+    this.cssColor = cssColor;
+  }
+
+  public String getColor() {
+    return cssColor;
+  }
+
+  public String getName() {
+    return name;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/util/log/speedtracer/SpeedTracerLogger.java b/dev/core/src/com/google/gwt/dev/util/log/speedtracer/SpeedTracerLogger.java
index 0fafa5f..0bca426 100644
--- a/dev/core/src/com/google/gwt/dev/util/log/speedtracer/SpeedTracerLogger.java
+++ b/dev/core/src/com/google/gwt/dev/util/log/speedtracer/SpeedTracerLogger.java
@@ -23,9 +23,16 @@
 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;
 
@@ -43,31 +50,68 @@
  *
  */
 public final class SpeedTracerLogger {
+  
+  // 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 cummulative 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
+  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");
 
   /**
    * Represents a node in a tree of SpeedTracer events.
    */
   public class Event {
     protected final EventType type;
-    List<Event> children = Lists.create();
+    List<Event> children;
     List<String> data;
     long durationNanos;
     final long startTimeNanos;
 
     Event() {
-      data = Lists.create();
-      this.startTimeNanos = normalizedTimeNanos();
+      if (enabled) {
+        timeKeeper.resetTimeBase();
+        this.startTimeNanos = timeKeeper.normalizedTimeNanos();
+        this.data = Lists.create();
+        this.children = Lists.create();
+      } else {
+        this.startTimeNanos = 0L;
+        this.data = null;
+        this.children = null;
+      }
       this.type = null;
     }
-
+    
     Event(Event parent, EventType type, String... data) {
+      this(timeKeeper.normalizedTimeNanos(), parent, type, data);
+    }
+    
+    Event(long startTimeNanos, Event parent, EventType type, String... data) {
       if (parent != null) {
         parent.children = Lists.add(parent.children, this);
       }
       this.type = type;
       assert (data.length % 2 == 0);
       this.data = Lists.create(data);
-      this.startTimeNanos = normalizedTimeNanos();
+      this.children = Lists.create();
+      this.startTimeNanos = startTimeNanos;
     }
 
     /**
@@ -92,10 +136,6 @@
       return type.getName();
     }
 
-    protected double convertToMilliseconds(long nanos) {
-      return nanos / 1000000.0d;
-    }
-
     JsonObject toJson() {
       JsonObject json = JsonObject.create();
       json.put("type", -2);
@@ -163,6 +203,125 @@
       return "Dummy";
     }
   }
+  
+  private interface NormalizedTimeKeeper {
+    long normalizedTimeNanos();
+    void resetTimeBase();
+    long zeroTimeMillis();
+  }
+
+  /*
+   * Time keeper which uses wall time.
+   */
+  private class DefaultNormalizedTimeKeeper implements NormalizedTimeKeeper {
+
+    private final long zeroTimeNanos;
+    private final long zeroTimeMillis;
+
+    public DefaultNormalizedTimeKeeper() {
+      zeroTimeNanos = System.nanoTime();
+      zeroTimeMillis = (long) convertToMilliseconds(zeroTimeNanos);
+    }
+
+    public long normalizedTimeNanos() {
+      return System.nanoTime() - zeroTimeNanos;
+    }
+    
+    public void resetTimeBase() {
+    }
+    
+    public long zeroTimeMillis() {
+      return zeroTimeMillis;
+    }
+  }
+
+  /*
+   * Time keeper which uses process cpu time.  This can be greater than wall
+   * time, since it is cummulative over the multiple threads of a process.
+   */
+  private class ProcessNormalizedTimeKeeper implements NormalizedTimeKeeper {
+    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 alwayws 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 void resetTimeBase() {
+    }
+    
+    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 implements NormalizedTimeKeeper {
+
+    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.
@@ -291,6 +450,10 @@
     return SpeedTracerLogger.get().startImpl(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
@@ -301,33 +464,38 @@
   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 DummyEvent dummyEvent = new DummyEvent();
-
-  private final BlockingQueue<Event> eventQueue = new LinkedBlockingQueue<
-      Event>();
+  private final boolean enabled;
+  
+  private final DummyEvent dummyEvent;
 
   private final BlockingQueue<Event> eventsToWrite;
 
   private CountDownLatch flushLatch;
 
-  private final Event flushSentinel = new Event();
+  private final Event flushSentinel;
 
   private final Format outputFormat;
 
-  private final ThreadLocal<Stack<Event>> pendingEvents = new ThreadLocal<
-      Stack<Event>>() {
-    @Override
-    protected Stack<Event> initialValue() {
-      return new Stack<Event>();
-    }
-  };
-  private final CountDownLatch shutDownLatch = new CountDownLatch(1);
+  private final ThreadLocal<Stack<Event>> pendingEvents;
+  
+  private final CountDownLatch shutDownLatch;
 
-  private final Event shutDownSentinel = new Event();
+  private final Event shutDownSentinel;
 
-  private final long zeroTimeNanos = System.nanoTime();
-  private final long zeroTimeMilliseconds = System.currentTimeMillis();
+  private final List<GarbageCollectorMXBean> gcMXBeans;
+  
+  private final Map<String, Long> lastGcTimes;
+
+  private final NormalizedTimeKeeper timeKeeper;
 
   /**
    * Constructor intended for unit testing.
@@ -335,24 +503,64 @@
    * @param writer alternative {@link Writer} to send speed tracer output.
    */
   SpeedTracerLogger(Writer writer, Format format) {
+    enabled = true;
     outputFormat = format;
     eventsToWrite = openLogWriter(writer, "");
+    pendingEvents = initPendingEvents();
+    timeKeeper = initTimeKeeper();
+    gcMXBeans = null;
+    lastGcTimes = null;
+    shutDownSentinel = new DummyEvent();
+    flushSentinel = new DummyEvent();
+    shutDownLatch = new CountDownLatch(1);
+    dummyEvent = null;
   }
 
   private SpeedTracerLogger() {
-    // Allow a system property to override the default output format
-    final String formatString = System.getProperty("gwt.speedtracerformat");
-    Format format = Format.HTML;
-    if (formatString != null) {
-      for (Format value : Format.values()) {
-        if (value.name().toLowerCase().equals(formatString.toLowerCase())) {
-          format = value;
-          break;
+    // Enabled flag (will be true if logFile is non-null)
+    this.enabled = logFile != null;
+    
+    if (enabled) {
+      // 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();
+      pendingEvents = initPendingEvents();
+      timeKeeper = initTimeKeeper();
+      
+      if (logGcTime) {
+        gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans();
+        lastGcTimes = new ConcurrentHashMap<String, Long>();
+      } else {
+        gcMXBeans = null;
+        lastGcTimes = null;
+      }
+      
+      shutDownSentinel = new DummyEvent();
+      flushSentinel = new DummyEvent();
+      shutDownLatch = new CountDownLatch(1);
+      dummyEvent = null;
+    } else {
+      outputFormat = null;
+      eventsToWrite = null;
+      pendingEvents = null;
+      timeKeeper = null;
+      gcMXBeans = null;
+      lastGcTimes = null;
+      shutDownSentinel = null;
+      flushSentinel = null;
+      shutDownLatch = null;
+      dummyEvent = new DummyEvent();
     }
-    outputFormat = format;
-    eventsToWrite = openDefaultLogWriter();
   }
 
   public void addDataImpl(String... data) {
@@ -376,17 +584,51 @@
     threadPendingEvents.push(newEvent);
     newEvent.end("message", message);
   }
+  
+  void addGcEvents(Event refEvent) {
+    for (java.lang.management.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;
+        long gcStartTimeNanos = refEvent.startTimeNanos + refEvent.durationNanos
+          - gcDurationNanos;
+        Event gcEvent = new Event(gcStartTimeNanos, null, 
+            SpeedTracerEventType.GC, "Collector Type", gcName, 
+            "Cummulative Collection Count", Long.toString(gcMXBean.getCollectionCount()));
+        gcEvent.durationNanos = gcDurationNanos;
+        eventsToWrite.add(gcEvent);
+        
+        lastGcTimes.put(gcName, currGcTime);
+      }
+    }
+  }
+  
+  void addOverheadEvent(Event refEvent) {
+    long overheadStartTime = refEvent.startTimeNanos + refEvent.durationNanos;
+    Event overheadEvent = 
+      new Event(overheadStartTime, refEvent, SpeedTracerEventType.OVERHEAD);
+    overheadEvent.durationNanos = 
+      timeKeeper.normalizedTimeNanos() - overheadStartTime;
+    refEvent.durationNanos += overheadEvent.durationNanos;
+  }
 
   void endImpl(Event event, String... data) {
-    if (eventsToWrite == null) {
+    if (!enabled) {
       return;
     }
 
+    long endTimeNanos = timeKeeper.normalizedTimeNanos();
+    
     if (data.length % 2 == 1) {
       throw new IllegalArgumentException("Unmatched data argument");
     }
-
-    long endTimeNanos = normalizedTimeNanos();
+    
     Stack<Event> threadPendingEvents = pendingEvents.get();
     if (threadPendingEvents.isEmpty()) {
       throw new IllegalStateException(
@@ -410,8 +652,17 @@
       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()) {
       eventsToWrite.add(currentEvent);
     }
@@ -424,7 +675,7 @@
     try {
       // Wait for the other thread to drain the queue.
       flushLatch = new CountDownLatch(1);
-      eventQueue.add(flushSentinel);
+      eventsToWrite.add(flushSentinel);
       flushLatch.await();
     } catch (InterruptedException e) {
       // Ignored
@@ -432,7 +683,7 @@
   }
 
   Event startImpl(EventType type, String... data) {
-    if (eventsToWrite == null) {
+    if (!enabled) {
       return dummyEvent;
     }
 
@@ -444,28 +695,43 @@
     Event parent = null;
     if (!threadPendingEvents.isEmpty()) {
       parent = threadPendingEvents.peek();
+    } else {
+      // start new time base for top-level events
+      timeKeeper.resetTimeBase();
     }
-
+    
     Event newEvent = new Event(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) {
-      newEvent.addData("baseTime", "" + zeroTimeMilliseconds);
+      newEvent.addData("baseTime", "" + timeKeeper.zeroTimeMillis());
     }
     threadPendingEvents.push(newEvent);
     return newEvent;
   }
 
-  private long normalizedTimeNanos() {
-    return System.nanoTime() - zeroTimeNanos;
+  private ThreadLocal<Stack<Event>> initPendingEvents() {
+    return new ThreadLocal<Stack<Event>>() {
+      @Override
+      protected Stack<Event> initialValue() {
+        return new Stack<Event>();
+      }
+    };
   }
-
+  
+  private NormalizedTimeKeeper initTimeKeeper() {
+    if (logProcessCpuTime) {
+      return new ProcessNormalizedTimeKeeper();
+    } else if (logThreadCpuTime) {
+      return new ThreadNormalizedTimeKeeper();
+    } else {
+      return new DefaultNormalizedTimeKeeper();
+    } 
+  }
+  
   private BlockingQueue<Event> openDefaultLogWriter() {
-    final String logFile = System.getProperty("gwt.speedtracerlog");
-
     Writer writer = null;
-    if (logFile != null) {
-
+    if (enabled) {
       try {
         writer = new BufferedWriter(new FileWriter(logFile));
         return openLogWriter(writer, logFile);
@@ -500,6 +766,8 @@
       return null;
     }
 
+    final BlockingQueue<Event> eventQueue = new LinkedBlockingQueue<Event>();
+    
     Runtime.getRuntime().addShutdownHook(new Thread() {
       @Override
       public void run() {