Phase 1 of GWT Dashboard. This includes an interface in gwt-dev for posting to a dashboard (default implementation is a no-op). Added calls to this interface in a couple of places. Created factory class that can provide the no-op implementation or a real implementation (based on system property). Updated unit test for SpeedTracerLogger to include a test that verifies that it is posting events to dashboard as expected.

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

Review by: tobyr@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10159 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserChannelServer.java b/dev/core/src/com/google/gwt/dev/shell/BrowserChannelServer.java
index f81101b..a9a7324 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserChannelServer.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserChannelServer.java
@@ -19,6 +19,7 @@
 import com.google.gwt.core.ext.TreeLogger.HelpInfo;
 import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.ExceptionOrReturnValue;
 import com.google.gwt.dev.shell.JsValue.DispatchObject;
+import com.google.gwt.dev.util.log.dashboard.DashboardNotifierFactory;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -106,6 +107,8 @@
   private static Map<String, byte[]> iconCache = new HashMap<String, byte[]>();
   
   private static final Object cacheLock = new Object();
+  
+  private DevModeSession devModeSession;
 
   private final SessionHandlerServer handler;
 
@@ -164,6 +167,13 @@
   }
 
   /**
+   * Returns the {@code DevModeSession} representing this browser connection.
+   */
+  public DevModeSession getDevModeSession() {
+    return devModeSession;
+  }
+
+  /**
    * @return the table of Java objects which have been sent to the browser.
    */
   public ServerObjectsTable getJavaObjectsExposedInBrowser() {
@@ -507,6 +517,9 @@
     Thread.currentThread().setName(
         "Code server for " + moduleName + " from " + userAgent + " on " + url
         + " @ " + sessionKey);
+
+    createDevModeSession();
+
     logger = handler.loadModule(this, moduleName, userAgent, url,
         tabKey, sessionKey, iconBytes);
     if (logger == null) {
@@ -631,6 +644,17 @@
         break;
     }
   }
+  
+  /**
+   * Creates the {@code DevModeSession} that represents the current browser
+   * connection, sets it as the "default" session for the current thread, and
+   * notifies a GWT Dashboard.
+   */
+  private void createDevModeSession() {
+    devModeSession = new DevModeSession(moduleName, userAgent);
+    DevModeSession.setSessionForCurrentThread(devModeSession);
+    DashboardNotifierFactory.getNotifier().devModeSession(devModeSession);
+  }
 
   /**
    * Create the requested transport and return the appropriate information so
diff --git a/dev/core/src/com/google/gwt/dev/shell/DevModeSession.java b/dev/core/src/com/google/gwt/dev/shell/DevModeSession.java
new file mode 100644
index 0000000..0f32d7a
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/shell/DevModeSession.java
@@ -0,0 +1,68 @@
+/*
+ * 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.shell;
+
+/**
+ * Represents a session between devmode and a browser plugin. A session is
+ * essentially a socket connection, established by the plugin when a GWT module
+ * is started in devmode and used to communicate invocations of Java from
+ * JavaScript and vice-versa.
+ */
+public class DevModeSession {
+
+  private static final ThreadLocal<DevModeSession> sessionForCurrentThread =
+      new ThreadLocal<DevModeSession>();
+
+  /**
+   * Gets the devmode session for the current thread. If a thread is not
+   * associated with a devmode session, via
+   * <code>setSessionForCurrentThread()</code>, then this will return null.
+   */
+  public static DevModeSession getSessionForCurrentThread() {
+    return sessionForCurrentThread.get();
+  }
+
+  /**
+   * Sets the devmode session for the current thread.
+   */
+  static void setSessionForCurrentThread(DevModeSession session) {
+    sessionForCurrentThread.set(session);
+  }
+
+  private String moduleName;
+  private String userAgent;
+
+  /**
+   * Creates a new instance.
+   * 
+   * @param moduleName the name of the GWT module for this session
+   * @param userAgent the User agent field provided by the browser for this
+   *          session
+   */
+  DevModeSession(String moduleName, String userAgent) {
+    this.moduleName = moduleName;
+    this.userAgent = userAgent;
+  }
+
+  public String getModuleName() {
+    return moduleName;
+  }
+
+  public String getUserAgent() {
+    return userAgent;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/shell/OophmSessionHandler.java b/dev/core/src/com/google/gwt/dev/shell/OophmSessionHandler.java
index c529093..64b6963 100644
--- a/dev/core/src/com/google/gwt/dev/shell/OophmSessionHandler.java
+++ b/dev/core/src/com/google/gwt/dev/shell/OophmSessionHandler.java
@@ -101,7 +101,8 @@
   @Override
   public ExceptionOrReturnValue invoke(BrowserChannelServer channel,
       Value thisVal, int methodDispatchId, Value[] args) {
-    Event jsToJavaCallEvent = SpeedTracerLogger.start(DevModeEventType.JS_TO_JAVA_CALL);
+    Event jsToJavaCallEvent =
+        SpeedTracerLogger.start(channel.getDevModeSession(), DevModeEventType.JS_TO_JAVA_CALL);
     ServerObjectsTable localObjects = channel.getJavaObjectsExposedInBrowser();
     ModuleSpace moduleSpace = moduleMap.get(channel);
     ModuleHandle moduleHandle = moduleHandleMap.get(channel);
@@ -183,7 +184,9 @@
   public synchronized TreeLogger loadModule(BrowserChannelServer channel,
       String moduleName, String userAgent, String url, String tabKey,
       String sessionKey, byte[] userAgentIcon) {
-    Event moduleInit = SpeedTracerLogger.start(DevModeEventType.MODULE_INIT, "Module Name", moduleName);
+    Event moduleInit =
+        SpeedTracerLogger.start(channel.getDevModeSession(), DevModeEventType.MODULE_INIT,
+            "Module Name", moduleName);
     ModuleHandle moduleHandle = host.createModuleLogger(moduleName, userAgent,
         url, tabKey, sessionKey, channel, userAgentIcon);
     TreeLogger logger = moduleHandle.getLogger();
diff --git a/dev/core/src/com/google/gwt/dev/util/log/dashboard/DashboardNotifier.java b/dev/core/src/com/google/gwt/dev/util/log/dashboard/DashboardNotifier.java
new file mode 100644
index 0000000..8b9f128
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/log/dashboard/DashboardNotifier.java
@@ -0,0 +1,49 @@
+/*
+ * 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.dashboard;
+
+import com.google.gwt.dev.shell.DevModeSession;
+import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
+
+/**
+ * Sends information to a dashboard service. The dashboard service collects
+ * information from GWT runtime and compiler instrumentation.
+ */
+public interface DashboardNotifier {
+  // First: Compiler related entry points
+
+  // TODO(jhumphries) Add interface methods for collecting data from the
+  // compiler
+
+  // Second: Dev-Mode related entry points
+
+  /**
+   * Records a top-level event to the dashboard.
+   */
+  void devModeEvent(DevModeSession session, Event event);
+
+  /**
+   * Records a new module/session.
+   */
+  void devModeSession(DevModeSession session);
+
+  // Third: Test related entry points
+
+  // TODO(jhumphries) Add interface methods for collecting data from automated
+  // tests
+
+}
diff --git a/dev/core/src/com/google/gwt/dev/util/log/dashboard/DashboardNotifierFactory.java b/dev/core/src/com/google/gwt/dev/util/log/dashboard/DashboardNotifierFactory.java
new file mode 100644
index 0000000..74b49d3
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/log/dashboard/DashboardNotifierFactory.java
@@ -0,0 +1,88 @@
+/*
+ * 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.dashboard;
+
+/**
+ * Gets an instance of {@link DashboardNotifier} for sending data to a GWT
+ * dashboard.
+ */
+public class DashboardNotifierFactory {
+  private static final NoOpDashboardNotifier defaultNotifier = new NoOpDashboardNotifier();
+  private static DashboardNotifier theNotifier;
+
+  static {
+    setNotifier(createNotifier(System.getProperty("gwt.dashboard.notifierClass")));
+  }
+
+  /**
+   * Determines whether notifications to a GWT Dashboard are enabled. Returns
+   * true if the current notifier is <strong>not</strong> the default "no-op"
+   * instance.
+   */
+  public static boolean areNotificationsEnabled() {
+    return theNotifier != defaultNotifier;
+  }
+
+  /**
+   * Returns an instance of {@code DashboardNotifier} for sending data to a GWT
+   * dashboard.
+   */
+  public static DashboardNotifier getNotifier() {
+    return theNotifier;
+  }
+
+  /**
+   * Creates a {@code DashboardNotifier} from a given class name. The object is
+   * instantiated via reflection.
+   * 
+   * @return the new notifier instance if the creation was successful or null on
+   *         failure
+   */
+  static DashboardNotifier createNotifier(String className) {
+    // Create the instance!
+    DashboardNotifier notifier = null;
+    if (className != null) {
+      try {
+        Class<?> clazz = Class.forName(className);
+        notifier = (DashboardNotifier) clazz.newInstance();
+      } catch (Exception e) {
+        // print error and skip dashboard notifications...
+        new Exception("Unexpected failure while trying to load dashboard class: " + className
+            + ". Notifications to the dashboard will be disabled.", e).printStackTrace();
+        return null;
+      }
+    }
+    return notifier;
+  }
+
+  /**
+   * Defines the {@code DashboardNotifier} returned by this factory. Exposed for
+   * unit testing purposes (to support mock notifier objects). If set to null, a
+   * default notifier (whose methods do nothing) will be returned by subsequent
+   * calls to {@code getNotifier}, not null.
+   */
+  static void setNotifier(DashboardNotifier notifier) {
+    theNotifier = notifier == null ? defaultNotifier : notifier;
+  }
+
+  /**
+   * Prevents this class from being instantiated. Instead use static method
+   * {@link #getNotifier()}.
+   */
+  private DashboardNotifierFactory() {
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/util/log/dashboard/NoOpDashboardNotifier.java b/dev/core/src/com/google/gwt/dev/util/log/dashboard/NoOpDashboardNotifier.java
new file mode 100644
index 0000000..111660f
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/log/dashboard/NoOpDashboardNotifier.java
@@ -0,0 +1,38 @@
+/*
+ * 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.dashboard;
+
+import com.google.gwt.dev.shell.DevModeSession;
+import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
+
+/**
+ * Implements the <code>DashboardNotifier</code> interface but does not actually
+ * do anything.
+ */
+public class NoOpDashboardNotifier implements DashboardNotifier {
+
+  @Override
+  public void devModeEvent(DevModeSession sesion, Event event) {
+    // do nothing
+  }
+
+  @Override
+  public void devModeSession(DevModeSession session) {
+    // do nothing
+  }
+
+}
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 7cdb2a4..f4cd953 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
@@ -17,7 +17,9 @@
 
 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;
@@ -60,13 +62,14 @@
   private static final String defaultFormatString = 
     System.getProperty("gwt.speedtracerformat");
   
-  // Use cummulative multi-threaded process cpu time instead of wall time
+  // 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
-  private static final boolean logThreadCpuTime = 
-    getBooleanProperty("gwt.speedtracer.logThreadCpuTime");
+  // 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 =
@@ -81,6 +84,15 @@
   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.
    */
@@ -88,38 +100,46 @@
     protected final EventType type;
     List<Event> children;
     List<String> data;
-    long durationNanos;
-    final long startTimeNanos;
+    DevModeSession devModeSession;
+    
+    long elapsedDurationNanos;
+    long elapsedStartTimeNanos;
+
+    long processCpuDurationNanos;
+    long processCpuStartTimeNanos;
+
+    long threadCpuDurationNanos;
+    long threadCpuStartTimeNanos;
 
     Event() {
       if (enabled) {
-        timeKeeper.resetTimeBase();
-        this.startTimeNanos = timeKeeper.normalizedTimeNanos();
+        threadCpuTimeKeeper.resetTimeBase();
+        recordStartTime();
         this.data = Lists.create();
         this.children = Lists.create();
       } else {
-        this.startTimeNanos = 0L;
+        this.processCpuStartTimeNanos = 0L;
+        this.threadCpuStartTimeNanos = 0L;
+        this.elapsedStartTimeNanos = 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) {
+
+    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.startTimeNanos = startTimeNanos;
+      this.devModeSession = session;
     }
-
+    
     /**
      * @param data key/value pairs to add to JSON object.
      */
@@ -129,7 +149,7 @@
         this.data = Lists.addAll(this.data, data);
       }
     }
-
+    
     /**
      * Signals the end of the current event.
      */
@@ -137,19 +157,76 @@
       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(startTimeNanos);
+      double startMs = convertToMilliseconds(getStartTimeNanos());
       json.put("time", startMs);
-      double durationMs = convertToMilliseconds(durationNanos);
+      double durationMs = convertToMilliseconds(getDurationNanos());
       json.put("duration", durationMs);
 
       JsonObject jsonData = JsonObject.create();
@@ -166,6 +243,49 @@
 
       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();
+      }
+    }
   }
 
   /**
@@ -210,21 +330,68 @@
     }
   }
   
-  private interface NormalizedTimeKeeper {
-    long normalizedTimeNanos();
-    void resetTimeBase();
-    long zeroTimeMillis();
-  }
+  /**
+   * 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 DefaultNormalizedTimeKeeper implements NormalizedTimeKeeper {
+  private class ElapsedNormalizedTimeKeeper {
 
     private final long zeroTimeNanos;
     private final long zeroTimeMillis;
 
-    public DefaultNormalizedTimeKeeper() {
+    public ElapsedNormalizedTimeKeeper() {
       zeroTimeNanos = System.nanoTime();
       zeroTimeMillis = (long) convertToMilliseconds(zeroTimeNanos);
     }
@@ -233,19 +400,16 @@
       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.
+   * time, since it is cumulative over the multiple threads of a process.
    */
-  private class ProcessNormalizedTimeKeeper implements NormalizedTimeKeeper {
+  private class ProcessNormalizedTimeKeeper {
     private final OperatingSystemMXBean osMXBean;
     private final Method getProcessCpuTimeMethod;
     private final long zeroTimeNanos;
@@ -256,7 +420,7 @@
         osMXBean = ManagementFactory.getOperatingSystemMXBean();
         /* 
          * Find this method by reflection, since it's part of the Sun
-         * implementation for OperatingSystemMXBean, and we can't alwayws assume
+         * implementation for OperatingSystemMXBean, and we can't always assume
          * that com.sun.management.OperatingSystemMXBean will be available.
          */
         getProcessCpuTimeMethod = 
@@ -276,16 +440,13 @@
         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
@@ -295,7 +456,7 @@
    * 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 class ThreadNormalizedTimeKeeper {
 
     private final ThreadMXBean threadMXBean;
     private final ThreadLocal<Long> resettableTimeBase = new ThreadLocal<Long>();
@@ -409,7 +570,7 @@
     JsonObject toJson() {
       JsonObject json = JsonObject.create();
       json.put("type", 11);
-      double startMs = convertToMilliseconds(startTimeNanos);
+      double startMs = convertToMilliseconds(getStartTimeNanos());
       json.put("time", startMs);
       json.put("duration", 0.0);
       JsonObject jsonData = JsonObject.create();
@@ -461,14 +622,35 @@
   /**
    * 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
-   * @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 closed by the caller
+   * @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(type, 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) {
@@ -496,91 +678,81 @@
 
   private final boolean enabled;
   
-  private final DummyEvent dummyEvent;
+  private final DummyEvent dummyEvent = new DummyEvent();
 
-  private final BlockingQueue<Event> eventsToWrite;
+  private BlockingQueue<Event> eventsToWrite;
 
+  private final boolean fileLoggingEnabled;
+  
   private CountDownLatch flushLatch;
 
-  private final Event flushSentinel;
+  private Event flushSentinel;
 
-  private final Format outputFormat;
+  private Format outputFormat;
 
-  private final ThreadLocal<Stack<Event>> pendingEvents;
+  private ThreadLocal<Stack<Event>> pendingEvents;
   
-  private final CountDownLatch shutDownLatch;
+  private CountDownLatch shutDownLatch;
 
-  private final Event shutDownSentinel;
+  private Event shutDownSentinel;
 
-  private final List<GarbageCollectorMXBean> gcMXBeans;
+  private List<GarbageCollectorMXBean> gcMXBeans;
   
-  private final Map<String, Long> lastGcTimes;
+  private Map<String, Long> lastGcTimes;
 
-  private final NormalizedTimeKeeper timeKeeper;
+  private final ElapsedNormalizedTimeKeeper elapsedTimeKeeper = new ElapsedNormalizedTimeKeeper();
+
+  private final ProcessNormalizedTimeKeeper processCpuTimeKeeper =
+      new ProcessNormalizedTimeKeeper();
+
+  private final ThreadNormalizedTimeKeeper threadCpuTimeKeeper = new ThreadNormalizedTimeKeeper();
 
   /**
    * 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();
-    timeKeeper = initTimeKeeper();
-    gcMXBeans = null;
-    lastGcTimes = null;
     shutDownSentinel = new DummyEvent();
     flushSentinel = new DummyEvent();
     shutDownLatch = new CountDownLatch(1);
-    dummyEvent = null;
   }
 
   private SpeedTracerLogger() {
-    // Enabled flag (will be true if logFile is non-null)
-    this.enabled = logFile != null;
-    
+    fileLoggingEnabled = logFile != null;
+    enabled = fileLoggingEnabled || DashboardNotifierFactory.areNotificationsEnabled();
+
     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;
+      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);
       }
-      
-      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();
+
+      pendingEvents = initPendingEvents();
     }
   }
 
@@ -607,7 +779,13 @@
   }
   
   void addGcEvents(Event refEvent) {
-    for (java.lang.management.GarbageCollectorMXBean gcMXBean : gcMXBeans) {
+    // 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();
@@ -617,26 +795,23 @@
       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;
+        Event gcEvent =
+            new GcEvent(refEvent, gcName, gcMXBean.getCollectionCount(), 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;
+    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) {
@@ -644,8 +819,6 @@
       return;
     }
 
-    long endTimeNanos = timeKeeper.normalizedTimeNanos();
-    
     if (data.length % 2 == 1) {
       throw new IllegalArgumentException("Unmatched data argument");
     }
@@ -656,17 +829,14 @@
           "Tried to end an event that never started!");
     }
     Event currentEvent = threadPendingEvents.pop();
-
-    assert (endTimeNanos >= currentEvent.startTimeNanos);
-    currentEvent.durationNanos = endTimeNanos - currentEvent.startTimeNanos;
-
+    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();
-      assert (endTimeNanos >= currentEvent.startTimeNanos);
-      currentEvent.durationNanos = endTimeNanos - currentEvent.startTimeNanos;
+      currentEvent.updateDuration();
     }
 
     if (threadPendingEvents.isEmpty() && currentEvent != event) {
@@ -685,7 +855,12 @@
     }
     
     if (threadPendingEvents.isEmpty()) {
-      eventsToWrite.add(currentEvent);
+      if (fileLoggingEnabled) {
+        eventsToWrite.add(currentEvent);
+      }
+
+      DashboardNotifierFactory.getNotifier().devModeEvent(currentEvent.getDevModeSession(),
+          currentEvent);
     }
   }
 
@@ -695,6 +870,10 @@
    * thread.
    */
   void flush() {
+    if (!fileLoggingEnabled) {
+      return;
+    }
+    
     try {
       // Wait for the other thread to drain the queue.
       flushLatch = new CountDownLatch(1);
@@ -705,7 +884,7 @@
     }
   }
 
-  Event startImpl(EventType type, String... data) {
+  Event startImpl(DevModeSession session, EventType type, String... data) {
     if (!enabled) {
       return dummyEvent;
     }
@@ -719,15 +898,19 @@
     if (!threadPendingEvents.isEmpty()) {
       parent = threadPendingEvents.peek();
     } else {
-      // start new time base for top-level events
-      timeKeeper.resetTimeBase();
+      // reset the thread CPU time base for top-level events (so events can be
+      // properly sequenced chronologically)
+      threadCpuTimeKeeper.resetTimeBase();
     }
     
-    Event newEvent = new Event(parent, type, data);
+    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) {
-      newEvent.addData("baseTime", "" + timeKeeper.zeroTimeMillis());
+      long baseTime = logProcessCpuTime ? processCpuTimeKeeper.zeroTimeMillis()
+          : (logThreadCpuTime ? threadCpuTimeKeeper.zeroTimeMillis()
+              : elapsedTimeKeeper.zeroTimeMillis());
+      newEvent.addData("baseTime", "" + baseTime);
     }
     threadPendingEvents.push(newEvent);
     return newEvent;
@@ -742,16 +925,6 @@
     };
   }
   
-  private NormalizedTimeKeeper initTimeKeeper() {
-    if (logProcessCpuTime) {
-      return new ProcessNormalizedTimeKeeper();
-    } else if (logThreadCpuTime) {
-      return new ThreadNormalizedTimeKeeper();
-    } else {
-      return new DefaultNormalizedTimeKeeper();
-    } 
-  }
-  
   private BlockingQueue<Event> openDefaultLogWriter() {
     Writer writer = null;
     if (enabled) {
diff --git a/dev/core/test/com/google/gwt/dev/shell/BrowserChannelServerTest.java b/dev/core/test/com/google/gwt/dev/shell/BrowserChannelServerTest.java
index eb90b23..2653712 100644
--- a/dev/core/test/com/google/gwt/dev/shell/BrowserChannelServerTest.java
+++ b/dev/core/test/com/google/gwt/dev/shell/BrowserChannelServerTest.java
@@ -208,6 +208,10 @@
     assertNull(handler.getSessionKey());
     assertNull(handler.getUserAgentIcon());
     assertEquals(MessageType.RETURN, type);
+    DevModeSession session = server.getDevModeSession();
+    assertNotNull(session);
+    assertEquals("testModule", session.getModuleName());
+    assertEquals("userAgent", session.getUserAgent());
     ReturnMessage.receive(client);
     QuitMessage.send(client);
     server.waitForClose();
@@ -246,6 +250,10 @@
     assertEquals("session", handler.getSessionKey());
     assertNull(handler.getUserAgentIcon());
     assertEquals(MessageType.RETURN, type);
+    DevModeSession session = server.getDevModeSession();
+    assertNotNull(session);
+    assertEquals("testModule", session.getModuleName());
+    assertEquals("userAgent", session.getUserAgent());
     ReturnMessage.receive(client);
     QuitMessage.send(client);
     server.waitForClose();
@@ -296,6 +304,10 @@
       assertEquals(iconBytes[i], receivedIcon[i]);
     }
     assertEquals(MessageType.RETURN, type);
+    DevModeSession session = server.getDevModeSession();
+    assertNotNull(session);
+    assertEquals("testModule", session.getModuleName());
+    assertEquals("userAgent", session.getUserAgent());
     ReturnMessage.receive(client);
     QuitMessage.send(client);
     server.waitForClose();
diff --git a/dev/core/test/com/google/gwt/dev/shell/DevModeSessionTest.java b/dev/core/test/com/google/gwt/dev/shell/DevModeSessionTest.java
new file mode 100644
index 0000000..d18f3ce
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/shell/DevModeSessionTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.shell;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests the DevModeSession class.
+ */
+public class DevModeSessionTest extends TestCase {
+  public void testConstructor() {
+    String moduleName = "test module name";
+    String userAgent = "test user agent";
+    DevModeSession session = new DevModeSession(moduleName, userAgent);
+    assertEquals("Constructor failed to initialize moduleName", session.getModuleName(), moduleName);
+    assertEquals("Constructor failed to initialize userAgent", session.getUserAgent(), userAgent);
+  }
+
+  public void testSetSessionForCurrentThread() {
+    DevModeSession session = new DevModeSession("test", "test");
+    // call method
+    DevModeSession.setSessionForCurrentThread(session);
+    // verify
+    assertTrue(DevModeSession.getSessionForCurrentThread() == session);
+    // tear-down
+    DevModeSession.setSessionForCurrentThread(null);
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/shell/DevModeSessionTestUtil.java b/dev/core/test/com/google/gwt/dev/shell/DevModeSessionTestUtil.java
new file mode 100644
index 0000000..8654565
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/shell/DevModeSessionTestUtil.java
@@ -0,0 +1,20 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.google.gwt.dev.shell;
+
+/**
+ * Provides public method for accessing package-private members of
+ * <code>DevModeSession</code> for testing.
+ * 
+ * @author jhumphries@google.com (Joshua Humphries)
+ */
+public class DevModeSessionTestUtil {
+  public static DevModeSession createSession(String moduleName, String userAgent,
+      boolean setForCurrentThread) {
+    DevModeSession session = new DevModeSession(moduleName, userAgent);
+    if (setForCurrentThread) {
+      DevModeSession.setSessionForCurrentThread(session);
+    }
+    return session;
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/util/log/dashboard/DashboardNotifierFactoryTest.java b/dev/core/test/com/google/gwt/dev/util/log/dashboard/DashboardNotifierFactoryTest.java
new file mode 100644
index 0000000..44df0fa
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/util/log/dashboard/DashboardNotifierFactoryTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.dashboard;
+
+import com.google.gwt.dev.shell.DevModeSession;
+import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests the DashboardNotifierFactory class.
+ */
+public class DashboardNotifierFactoryTest extends TestCase {
+
+  public void testSetNotifier() {
+    // create test notifier instance
+    DashboardNotifier obj = new DashboardNotifier() {
+      @Override
+      public void devModeEvent(DevModeSession session, Event event) {
+        // no need to do anything
+      }
+
+      @Override
+      public void devModeSession(DevModeSession session) {
+        // no need to do anything
+      }
+    };
+    // call method
+    DashboardNotifierFactory.setNotifier(obj);
+    // verify it worked
+    assertTrue("Notifier is not set correctly!", DashboardNotifierFactory.getNotifier() == obj);
+    assertTrue("Setting notifier failed to enable notifications!", DashboardNotifierFactory
+        .areNotificationsEnabled());
+  }
+
+  public void testClearNotifier() {
+    // clearing the notifier should use a "no-op" instance and disable
+    // notifications
+    DashboardNotifierFactory.setNotifier(null);
+    // verify it worked
+    assertTrue("Notifier is of wrong type!",
+        DashboardNotifierFactory.getNotifier() instanceof NoOpDashboardNotifier);
+    assertFalse("Resetting notifier failed to disable notifications!", DashboardNotifierFactory
+        .areNotificationsEnabled());
+  }
+
+  public void testCreateNotifier() {
+    DashboardNotifier notifier =
+        DashboardNotifierFactory.createNotifier(SpeedTracerLoggerTestMockNotifier.class.getName());
+    assertNotNull("Notifier could not be created!", notifier);
+    assertTrue(notifier instanceof SpeedTracerLoggerTestMockNotifier);
+  }
+
+  public void testCreateNotifierBadClass() {
+    assertNull(DashboardNotifierFactory.createNotifier("this.is.not.a.valid.Notifier"));
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/util/log/dashboard/SpeedTracerLoggerTestMockNotifier.java b/dev/core/test/com/google/gwt/dev/util/log/dashboard/SpeedTracerLoggerTestMockNotifier.java
new file mode 100644
index 0000000..889a357
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/util/log/dashboard/SpeedTracerLoggerTestMockNotifier.java
@@ -0,0 +1,61 @@
+/*
+ * 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.dashboard;
+
+import com.google.gwt.dev.shell.DevModeSession;
+import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
+
+import junit.framework.Assert;
+
+import java.util.LinkedList;
+
+/**
+ * Mock object for testing SpeedTracerLogger
+ */
+public class SpeedTracerLoggerTestMockNotifier implements DashboardNotifier {
+  /**
+   * Activates this mock object. After calling this, the notifier factory will
+   * be setup so that dashboard notifications are enabled and the notifier
+   * instance returned is an instance of this class
+   */
+  public static SpeedTracerLoggerTestMockNotifier enable() {
+    SpeedTracerLoggerTestMockNotifier ret = new SpeedTracerLoggerTestMockNotifier();
+    DashboardNotifierFactory.setNotifier(ret);
+    return ret;
+  }
+
+  /**
+   * This keeps track of events
+   */
+  private LinkedList<Event> eventSeq = new LinkedList<Event>();
+
+  @Override
+  public void devModeEvent(DevModeSession session, Event event) {
+    eventSeq.add(event);
+  }
+
+  @Override
+  public void devModeSession(DevModeSession session) {
+    // always raise exception here - this method shouldn't be invoked from
+    // SpeedTracerLogger
+    Assert.assertTrue("SpeedTracerLogger should not be calling DashboardNotifier.devModeSession()",
+        false);
+  }
+
+  public LinkedList<Event> getEventSequence() {
+    return eventSeq;
+  }
+}
diff --git a/dev/core/test/com/google/gwt/dev/util/log/speedtracer/SpeedTracerLoggerTest.java b/dev/core/test/com/google/gwt/dev/util/log/speedtracer/SpeedTracerLoggerTest.java
index 7e08850..7ed6395 100644
--- a/dev/core/test/com/google/gwt/dev/util/log/speedtracer/SpeedTracerLoggerTest.java
+++ b/dev/core/test/com/google/gwt/dev/util/log/speedtracer/SpeedTracerLoggerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2010 Google Inc.
+ * 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
@@ -18,6 +18,9 @@
 import com.google.gwt.dev.json.JsonArray;
 import com.google.gwt.dev.json.JsonException;
 import com.google.gwt.dev.json.JsonObject;
+import com.google.gwt.dev.shell.DevModeSession;
+import com.google.gwt.dev.shell.DevModeSessionTestUtil;
+import com.google.gwt.dev.util.log.dashboard.SpeedTracerLoggerTestMockNotifier;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.EventType;
 import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Format;
@@ -30,6 +33,8 @@
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.io.Writer;
+import java.util.LinkedList;
+import java.util.Properties;
 
 /**
  * Tests the SpeedTracerLogger class.
@@ -44,10 +49,12 @@
       this.color = color;
     }
 
+    @Override
     public String getColor() {
       return color;
     }
 
+    @Override
     public String getName() {
       return name;
     }
@@ -66,7 +73,7 @@
     @Override
     public void run() {
       for (int i = 0; i < MAX_EVENT_LOGS; i++) {
-        Event e = logger.startImpl(event);
+        Event e = logger.startImpl(null, event);
         logger.endImpl(e);
       }
     }
@@ -83,10 +90,10 @@
   public void testSpeedTracerLogger() throws IOException, JsonException {
     Writer writer = new StringWriter();
     SpeedTracerLogger logger = new SpeedTracerLogger(writer, Format.HTML);
-    Event dummyOneEvent = logger.startImpl(dummyOne);
-    Event dummyTwoEvent = logger.startImpl(dummyTwo);
+    Event dummyOneEvent = logger.startImpl(null, dummyOne);
+    Event dummyTwoEvent = logger.startImpl(null, dummyTwo);
     logger.endImpl(dummyTwoEvent);
-    Event dummyThreeEvent = logger.startImpl(dummyThree);
+    Event dummyThreeEvent = logger.startImpl(null, dummyThree);
     logger.endImpl(dummyThreeEvent);
     logger.endImpl(dummyOneEvent);
     logger.flush();
@@ -113,7 +120,7 @@
       JsonException {
     Writer writer = new StringWriter();
     SpeedTracerLogger logger = new SpeedTracerLogger(writer, Format.HTML);
-    Event dummyOneEvent = logger.startImpl(dummyOne, "extraStart", "valueStart");
+    Event dummyOneEvent = logger.startImpl(null, dummyOne, "extraStart", "valueStart");
     logger.addDataImpl("extraMiddle", "valueMiddle");
     logger.endImpl(dummyOneEvent, "extraEnd", "valueEnd");
     logger.flush();
@@ -133,11 +140,11 @@
   public void testSpeedTracerLoggerMultiple() throws IOException, JsonException {
     Writer writer = new StringWriter();
     SpeedTracerLogger logger = new SpeedTracerLogger(writer, Format.HTML);
-    Event dummyOneEvent = logger.startImpl(dummyOne);
+    Event dummyOneEvent = logger.startImpl(null, dummyOne);
     logger.endImpl(dummyOneEvent);
-    Event dummyTwoEvent = logger.startImpl(dummyTwo);
+    Event dummyTwoEvent = logger.startImpl(null, dummyTwo);
     logger.endImpl(dummyTwoEvent);
-    Event dummyThreeEvent = logger.startImpl(dummyThree);
+    Event dummyThreeEvent = logger.startImpl(null, dummyThree);
     logger.endImpl(dummyThreeEvent);
     logger.flush();
 
@@ -199,7 +206,7 @@
   public void testSpeedTracerLoggerMarkTimeline() throws IOException, JsonException {
    Writer writer = new StringWriter();
     SpeedTracerLogger logger = new SpeedTracerLogger(writer, Format.RAW);
-    Event dummyOneEvent = logger.startImpl(dummyOne);
+    Event dummyOneEvent = logger.startImpl(null, dummyOne);
     logger.markTimelineImpl("Test Message");
     dummyOneEvent.end();
     logger.flush();
@@ -222,7 +229,7 @@
   public void testSpeedTracerLoggerRaw() throws IOException, JsonException {
    Writer writer = new StringWriter();
     SpeedTracerLogger logger = new SpeedTracerLogger(writer, Format.RAW);
-    Event dummyOneEvent = logger.startImpl(dummyOne);
+    Event dummyOneEvent = logger.startImpl(null, dummyOne);
     dummyOneEvent.end();
     logger.flush();
 
@@ -261,4 +268,74 @@
     }
     return jsonReader;
   }
+  
+  public void testSpeedTracerWhenOnlyDashboardEnabled() {
+    // backup system properties before making changes to them
+    Properties props = (Properties) System.getProperties().clone();
+
+    try {
+      // no logging to file!
+      System.clearProperty("gwt.speedtracerlog");
+      // we don't capture GC events in dashboard, so setting this will allow us
+      // to confirm that they *don't* show up in dashboard notices
+      System.setProperty("gwt.speedtracer.logGcTime", "yes");
+
+      // now enable the mock dashboard notifier
+      SpeedTracerLoggerTestMockNotifier notifier = SpeedTracerLoggerTestMockNotifier.enable();
+
+      // create "sessions"
+      DevModeSession session1 = DevModeSessionTestUtil.createSession("test1", "test", true);
+      DevModeSession session2 = DevModeSessionTestUtil.createSession("test2", "test", false);
+
+      // expected values (used in final assertions below)
+      LinkedList<Event> expectedEvents = new LinkedList<Event>();
+      LinkedList<DevModeSession> expectedSessions = new LinkedList<DevModeSession>();
+
+      Event evt1, evt2;
+
+      // test events with no session specified
+      evt1 = SpeedTracerLogger.start(DevModeEventType.MODULE_INIT, "k1", "v1", "k2", "v2");
+      // also test that child events aren't posted (only top-level events)
+      evt2 = SpeedTracerLogger.start(DevModeEventType.CLASS_BYTES_REWRITE);
+      evt2.end();
+      evt1.end();
+      // expect only first event
+      expectedEvents.add(evt1);
+      expectedSessions.add(session1); // event should get "default" session
+
+      // now with session specified
+      evt1 = SpeedTracerLogger.start(session2, DevModeEventType.JAVA_TO_JS_CALL, "k1", "v1");
+      // also test that child events aren't posted (only top-level events)
+      evt2 = SpeedTracerLogger.start(DevModeEventType.CREATE_UI);
+      evt2.end();
+      evt1.end();
+      // expect only first event
+      expectedEvents.add(evt1);
+      expectedSessions.add(session2);
+
+      evt1 = SpeedTracerLogger.start(session1, DevModeEventType.JS_TO_JAVA_CALL, "k1", "v1");
+      evt1.end();
+      expectedEvents.add(evt1);
+      expectedSessions.add(session1);
+
+      // Finally, assert that the events and corresponding sessions sent to the
+      // notifier are exactly as expected
+      assertEquals("Events posted to dashboard do not match expected events!", expectedEvents,
+          notifier.getEventSequence());
+
+      // Collect sessions associated with each event
+      LinkedList<DevModeSession> actualSessions = new LinkedList<DevModeSession>();
+      for (Event event : notifier.getEventSequence()) {
+        actualSessions.add(event.getDevModeSession());
+      }
+
+      // and confirm the sessions are correct
+      assertEquals("Events posted to dashboard are associated with incorrect sessions!",
+          expectedSessions, actualSessions);
+
+    } finally {
+      // restore system properties
+      System.setProperties(props);
+    }
+  }
 }