avro-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ph...@apache.org
Subject svn commit: r980173 [1/4] - in /avro/trunk: ./ lang/java/ lang/java/src/java/org/apache/avro/ipc/stats/ lang/java/src/java/org/apache/avro/ipc/stats/static/ lang/java/src/java/org/apache/avro/ipc/stats/templates/ lang/java/src/test/java/org/apache/avro...
Date Wed, 28 Jul 2010 19:28:54 GMT
Author: philz
Date: Wed Jul 28 19:28:53 2010
New Revision: 980173

URL: http://svn.apache.org/viewvc?rev=980173&view=rev
Log:
AVRO-587. Add Charts and Templating to Stats View
(Contributed by Patrick Wendell)

Added:
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StaticServlet.java
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServer.java
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.css
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.js
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/g.bar.js
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/jquery-1.4.2.min.js
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/jquery.tipsy.js
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/protovis-r3.2.js
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/tipsy.css
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/tipsy.js
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/templates/
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/templates/statsview.vm
Modified:
    avro/trunk/CHANGES.txt
    avro/trunk/lang/java/build.xml
    avro/trunk/lang/java/ivy.xml
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsPlugin.java
    avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServlet.java
    avro/trunk/lang/java/src/test/java/org/apache/avro/ipc/stats/TestStatsPluginAndServlet.java
    avro/trunk/share/rat-excludes.txt

Modified: avro/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/avro/trunk/CHANGES.txt?rev=980173&r1=980172&r2=980173&view=diff
==============================================================================
--- avro/trunk/CHANGES.txt (original)
+++ avro/trunk/CHANGES.txt Wed Jul 28 19:28:53 2010
@@ -37,6 +37,10 @@ Avro 1.4.0 (unreleased)
     types when no specific class is available.  (cutting)
 
   IMPROVEMENTS
+
+    AVRO-587. Add Charts and Templating to Stats View
+    (Patrick Wendell via philz)
+
     AVRO-584. Update Histogram for Stats Plugin
     (Patrick Wendell via philz)
 

Modified: avro/trunk/lang/java/build.xml
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/build.xml?rev=980173&r1=980172&r2=980173&view=diff
==============================================================================
--- avro/trunk/lang/java/build.xml (original)
+++ avro/trunk/lang/java/build.xml Wed Jul 28 19:28:53 2010
@@ -41,11 +41,16 @@
   <property name="java.src.dir" value="${src.dir}/java"/>  	
   <property name="build.dir" value="${basedir}/build"/>
   <property name="lib.dir" value="${basedir}/lib"/>
+  <property name="template.dir" value="${src.dir}/java/org/apache/avro/ipc/stats/templates"/>
+  <property name="static.dir" value="${src.dir}/java/org/apache/avro/ipc/stats/static"/>
+
 
   <property name="build.classes" value="${build.dir}/classes"/>
   <property name="build.doc" value="${build.dir}/doc"/>
   <property name="build.javadoc" value="${build.doc}/api/java"/>
   <property name="build.javadoc.log" value="${build.dir}/javadoc.log"/>
+  <property name="build.template.dir" value="${build.classes}/org/apache/avro/ipc/stats/templates"/>
+  <property name="build.static.dir" value="${build.classes}/org/apache/avro/ipc/stats/static"/>
 
   <property name="test.count" value="100"/>
   <property name="test.junit.output.format" value="plain"/>
@@ -169,6 +174,12 @@
       <fileset file="${basedir}/../../NOTICE.txt"/>
       <fileset file="${basedir}/../../share/VERSION.txt"/>
     </copy>
+    <copy todir="${build.template.dir}">
+      <fileset dir="${template.dir}" includes="**/**"/>
+    </copy>
+    <copy todir="${build.static.dir}">
+      <fileset dir="${static.dir}" includes="**/**"/>
+    </copy>
   </target>
 
   <target name="ivy-download" unless="ivy.jar.exists" depends="init">

Modified: avro/trunk/lang/java/ivy.xml
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/ivy.xml?rev=980173&r1=980172&r2=980173&view=diff
==============================================================================
--- avro/trunk/lang/java/ivy.xml (original)
+++ avro/trunk/lang/java/ivy.xml Wed Jul 28 19:28:53 2010
@@ -45,6 +45,7 @@
         rev="2.2"/>
     <dependency org="org.mortbay.jetty" name="jetty"
         rev="6.1.22"/>
+    <dependency org="org.apache.velocity" name="velocity" rev="1.6.4"/>
     <dependency org="junit" name="junit" rev="4.8.1" conf="test->default"/>
     <dependency org="checkstyle" name="checkstyle" rev="5.0"
         conf="test->default"/>

Added: avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StaticServlet.java
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StaticServlet.java?rev=980173&view=auto
==============================================================================
--- avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StaticServlet.java (added)
+++ avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StaticServlet.java Wed Jul 28
19:28:53 2010
@@ -0,0 +1,28 @@
+package org.apache.avro.ipc.stats;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.mortbay.jetty.servlet.DefaultServlet;
+import org.mortbay.resource.Resource;
+
+/**
+ * Very simple servlet class capable of serving static files.
+ */
+public class StaticServlet extends DefaultServlet {
+  public Resource getResource(String pathInContext) {
+    // Take only last slice of the URL as a filename, so we can adjust path. 
+    // This also prevents mischief like '../../foo.css'
+    String[] parts = pathInContext.split("/");
+    String filename =  parts[parts.length - 1];
+
+    try {
+      URL resource = getClass().getClassLoader().getResource(
+          "org/apache/avro/ipc/stats/static/" + filename);
+      if (resource == null) { return null; }
+      return Resource.newResource(resource);
+    } catch (IOException e) {
+      return null;
+    }
+  }
+} 

Modified: avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsPlugin.java
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsPlugin.java?rev=980173&r1=980172&r2=980173&view=diff
==============================================================================
--- avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsPlugin.java (original)
+++ avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsPlugin.java Wed Jul 28 19:28:53
2010
@@ -17,8 +17,11 @@
  */
 package org.apache.avro.ipc.stats;
 
+import java.nio.ByteBuffer;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
@@ -32,14 +35,15 @@ import org.apache.avro.ipc.stats.Stopwat
 
 /**
  * Collects count and latency statistics about RPC calls.  Keeps
- * data for every method.
+ * data for every method. Can be added to a Requestor (client)
+ * or Responder (server). 
  *
  * This uses milliseconds as the standard unit of measure
  * throughout the class, stored in floats.
  */
 public class StatsPlugin extends RPCPlugin {
   /** Static declaration of histogram buckets. */
-  static final Segmenter<String, Float> DEFAULT_SEGMENTER =
+  static final Segmenter<String, Float> LATENCY_SEGMENTER =
     new Histogram.TreeMapSegmenter<Float>(new TreeSet<Float>(Arrays.asList(
             0f,
            25f,
@@ -57,28 +61,74 @@ public class StatsPlugin extends RPCPlug
         60000f, // 1 minute
        600000f)));
 
+  static final Segmenter<String, Integer> PAYLOAD_SEGMENTER =
+    new Histogram.TreeMapSegmenter<Integer>(new TreeSet<Integer>(Arrays.asList(
+            0,
+           25,
+           50,
+           75,
+          100,
+          200,
+          300,
+          500,
+          750,
+         1000, // 1 k
+         2000,
+         5000,
+        10000,
+        50000, 
+       100000)));
+  
   /** Per-method histograms.
-   * Must be accessed while holding a lock on methodTimings. */
+   * Must be accessed while holding a lock. */
   Map<Message, FloatHistogram<?>> methodTimings =
     new HashMap<Message, FloatHistogram<?>>();
 
+  Map<Message, IntegerHistogram<?>> sendPayloads =
+    new HashMap<Message, IntegerHistogram<?>>();
+  
+  Map<Message, IntegerHistogram<?>> receivePayloads =
+    new HashMap<Message, IntegerHistogram<?>>();
+  
   /** RPCs in flight. */
   ConcurrentMap<RPCContext, Stopwatch> activeRpcs =
     new ConcurrentHashMap<RPCContext, Stopwatch>();
   private Ticks ticks;
 
-  private Segmenter<?, Float> segmenter;
+  /** How long I've been alive */
+  public Date startupTime = new Date();
+  
+  private Segmenter<?, Float> floatSegmenter;
+  private Segmenter<?, Integer> integerSegmenter;
 
   /** Construct a plugin with custom Ticks and Segmenter implementations. */
-  StatsPlugin(Ticks ticks, Segmenter<?, Float> segmenter) {
-    this.segmenter = segmenter;
+  StatsPlugin(Ticks ticks, Segmenter<?, Float> floatSegmenter, 
+      Segmenter<?, Integer> integerSegmenter) {
+    this.floatSegmenter = floatSegmenter;
+    this.integerSegmenter = integerSegmenter;
     this.ticks = ticks;
   }
 
   /** Construct a plugin with default (system) ticks, and default
    * histogram segmentation. */
   public StatsPlugin() {
-    this(Stopwatch.SYSTEM_TICKS, DEFAULT_SEGMENTER);
+    this(Stopwatch.SYSTEM_TICKS, LATENCY_SEGMENTER, PAYLOAD_SEGMENTER);
+  }
+  
+  /**
+   * Helper to get the size of an RPC payload.
+   */
+  private int getPayloadSize(List<ByteBuffer> payload) {
+    if (payload == null) {
+      return 0;
+    }
+    
+    int size = 0;
+    for (ByteBuffer bb: payload) {
+      size = size + bb.limit();
+    }
+    
+    return size;
   }
 
   @Override
@@ -86,15 +136,65 @@ public class StatsPlugin extends RPCPlug
     Stopwatch t = new Stopwatch(ticks);
     t.start();
     this.activeRpcs.put(context, t);
+    
+    synchronized(receivePayloads) {
+      IntegerHistogram<?> h = receivePayloads.get(context.getMessage());
+      if (h == null) {
+        h = createNewIntegerHistogram();
+        receivePayloads.put(context.getMessage(), h);
+      }
+      h.add(getPayloadSize(context.getRequestPayload()));
+    }
   }
-
+  
   @Override
   public void serverSendResponse(RPCContext context) {
     Stopwatch t = this.activeRpcs.remove(context);
     t.stop();
     publish(context, t);
+    
+    synchronized(sendPayloads) {
+      IntegerHistogram<?> h = sendPayloads.get(context.getMessage());
+      if (h == null) {
+        h = createNewIntegerHistogram();
+        sendPayloads.put(context.getMessage(), h);
+      }
+      h.add(getPayloadSize(context.getResponsePayload()));
+    }
   }
-
+  
+  @Override
+  public void clientSendRequest(RPCContext context) {
+    Stopwatch t = new Stopwatch(ticks);
+    t.start();
+    this.activeRpcs.put(context, t);
+    
+    synchronized(sendPayloads) {
+      IntegerHistogram<?> h = sendPayloads.get(context.getMessage());
+      if (h == null) {
+        h = createNewIntegerHistogram();
+       sendPayloads.put(context.getMessage(), h);
+      }
+      h.add(getPayloadSize(context.getRequestPayload()));
+    }
+  }
+  
+  @Override
+  public void clientReceiveResponse(RPCContext context) {
+    Stopwatch t = this.activeRpcs.remove(context);
+    t.stop();
+    publish(context, t);
+    
+    synchronized(receivePayloads) {
+      IntegerHistogram<?> h = receivePayloads.get(context.getMessage());
+      if (h == null) {
+        h = createNewIntegerHistogram();
+        receivePayloads.put(context.getMessage(), h);
+      }
+      h.add(getPayloadSize(context.getRequestPayload()));
+    }
+  }
+  
   /** Adds timing to the histograms. */
   private void publish(RPCContext context, Stopwatch t) {
     Message message = context.getMessage();
@@ -102,7 +202,7 @@ public class StatsPlugin extends RPCPlug
     synchronized(methodTimings) {
       FloatHistogram<?> h = methodTimings.get(context.getMessage());
       if (h == null) {
-        h = createNewHistogram();
+        h = createNewFloatHistogram();
         methodTimings.put(context.getMessage(), h);
       }
       h.add(nanosToMillis(t.elapsedNanos()));
@@ -110,10 +210,15 @@ public class StatsPlugin extends RPCPlug
   }
 
   @SuppressWarnings("unchecked")
-  private FloatHistogram<?> createNewHistogram() {
-    return new FloatHistogram(segmenter);
+  private FloatHistogram<?> createNewFloatHistogram() {
+    return new FloatHistogram(floatSegmenter);
   }
 
+  @SuppressWarnings("unchecked")
+  private IntegerHistogram<?> createNewIntegerHistogram() {
+    return new IntegerHistogram(integerSegmenter);
+  }
+  
   /** Converts nanoseconds to milliseconds. */
   static float nanosToMillis(long elapsedNanos) {
     return elapsedNanos / 1000000.0f;

Added: avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServer.java
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServer.java?rev=980173&view=auto
==============================================================================
--- avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServer.java (added)
+++ avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServer.java Wed Jul 28 19:28:53
2010
@@ -0,0 +1,54 @@
+package org.apache.avro.ipc.stats;
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.servlet.Context;
+import org.mortbay.jetty.servlet.ServletHolder;
+
+/* This is a server that displays live information from a StatsPlugin.
+ * 
+ *  Typical usage is as follows:
+ *    StatsPlugin plugin = new StatsPlugin(); 
+ *    requestor.addPlugin(plugin);
+ *    StatsServer server = new StatsServer(plugin, 8080);
+ *    
+ *  */
+public class StatsServer {
+  Server httpServer;
+  StatsPlugin plugin;
+  
+  /* Start a stats server on the given port, 
+   * responsible for the given plugin. */
+  public StatsServer(StatsPlugin plugin, int port) throws Exception {
+    this.httpServer = new Server(port);
+    this.plugin = plugin;
+    
+    Context staticContext = new Context(httpServer, "/static");
+    staticContext.addServlet(new ServletHolder(new StaticServlet()), "/");
+    
+    Context context = new Context(httpServer, "/");
+    context.addServlet(new ServletHolder(new StatsServlet(plugin)), "/");
+    
+    httpServer.start();
+  }
+  
+  /* Stops this server. */
+  public void stop() throws Exception {
+    this.httpServer.stop();
+  }
+}

Modified: avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServlet.java
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServlet.java?rev=980173&r1=980172&r2=980173&view=diff
==============================================================================
--- avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServlet.java (original)
+++ avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/StatsServlet.java Wed Jul 28 19:28:53
2010
@@ -19,12 +19,25 @@ package org.apache.avro.ipc.stats;
 
 import java.io.IOException;
 import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
 import java.util.Map.Entry;
 
 import javax.servlet.ServletException;
+import javax.servlet.UnavailableException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.exception.ParseErrorException;
+import org.apache.velocity.exception.ResourceNotFoundException;
 
 import org.apache.avro.Protocol.Message;
 import org.apache.avro.ipc.RPCContext;
@@ -36,71 +49,217 @@ import org.apache.avro.ipc.RPCContext;
  * This class follows the same synchronization conventions
  * as StatsPlugin, to avoid requiring StatsPlugin to serve
  * a copy of the data.
- */
+ */ 
 public class StatsServlet extends HttpServlet {
   private final StatsPlugin statsPlugin;
+  private VelocityEngine velocityEngine;
+  private static final SimpleDateFormat FORMATTER = 
+    new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");
 
-  public StatsServlet(StatsPlugin statsPlugin) {
+  public StatsServlet(StatsPlugin statsPlugin) throws UnavailableException {
     this.statsPlugin = statsPlugin;
+    this.velocityEngine = new VelocityEngine();
+    
+    // These two properties tell Velocity to use its own classpath-based loader
+    velocityEngine.addProperty("resource.loader", "class");
+    velocityEngine.addProperty("class.resource.loader.class",
+        "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
+  }
+  
+  /* Helper class to store per-message data which is passed to templates.
+   * 
+   * The template expects a list of charts, each of which is parameterized by
+   * map key-value string attributes. */
+  public class RenderableMessage { // Velocity brakes if not public
+    public String name;
+    public int numCalls;
+    public ArrayList<HashMap<String, String>> charts;
+    
+    public RenderableMessage(String name) {
+      this.name = name;
+      this.charts = new ArrayList<HashMap<String, String>>();
+    }
+    
+    public ArrayList<HashMap<String, String>> getCharts() {
+      return this.charts;
+    }
+    
+    public String getname() {
+      return this.name;
+    }
+    
+    public int getNumCalls() {
+      return this.numCalls;
+    }
   }
 
+  /* Surround each string in an array with
+   * quotation marks and escape existing quotes.
+   * 
+   * This is useful when we have an array of strings that we want to turn into
+   * a javascript array declaration. 
+   */
+  protected static List<String> escapeStringArray(List<String> input) {
+    for (int i = 0; i < input.size(); i++) {
+      input.set(i, "\"" + input.get(i).replace("\"", "\\\"") + "\"");
+    }
+    return input;
+  }
+  
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse resp)
       throws ServletException, IOException {
     resp.setContentType("text/html");
-    writeStats(resp.getWriter());
+    String url = req.getRequestURL().toString();
+    String[] parts = url.split("//")[1].split("/");
+    
+    try {
+      writeStats(resp.getWriter()); 
+    }
+    catch (Exception e) {
+      e.printStackTrace();
+    }
   }
 
   void writeStats(Writer w) throws IOException {
-    w.append("<html><head><title>Avro RPC Stats</title></head>");
-    w.append("<body><h1>Avro RPC Stats</h1>");
-
-    w.append("<h2>Active RPCs</h2>");
-    w.append("<ol>");
-    for (Entry<RPCContext, Stopwatch> rpc : this.statsPlugin.activeRpcs.entrySet())
{
-      writeActiveRpc(w, rpc.getKey(), rpc.getValue());
+    VelocityContext context = new VelocityContext();
+    context.put("title", "Avro RPC Stats"); 
+    
+    ArrayList<String> rpcs = new ArrayList<String>();  // in flight rpcs
+    
+    ArrayList<RenderableMessage> messages = 
+      new ArrayList<RenderableMessage>();
+    
+    for (Entry<RPCContext, Stopwatch> rpc : 
+         this.statsPlugin.activeRpcs.entrySet()) {
+      rpcs.add(renderActiveRpc(rpc.getKey(), rpc.getValue()));
     }
-    w.append("</ol>");
-
-    w.append("<h2>Per-method Timing</h2>");
+    
+    // Get set of all seen messages
+    Set<Message> keys = null;
     synchronized(this.statsPlugin.methodTimings) {
-      for (Entry<Message, FloatHistogram<?>> e :
-        this.statsPlugin.methodTimings.entrySet()) {
-        writeMethod(w, e.getKey(), e.getValue());
+       keys = this.statsPlugin.methodTimings.keySet();
+    
+      for (Message m: keys) {
+        messages.add(renderMethod(m));
       }
     }
-    w.append("</body></html>");
+    
+    context.put("inFlightRpcs", rpcs);
+    context.put("messages", messages);
+    
+    context.put("currTime", FORMATTER.format(new Date()));
+    context.put("startupTime", FORMATTER.format(statsPlugin.startupTime));
+    
+    Template t;
+    try {
+      t = velocityEngine.getTemplate(
+          "org/apache/avro/ipc/stats/templates/statsview.vm");
+    } catch (ResourceNotFoundException e) {
+      throw new IOException();
+    } catch (ParseErrorException e) {
+      throw new IOException();
+    } catch (Exception e) {
+      throw new IOException();
+    }
+    t.merge(context, w);
   }
 
-  private void writeActiveRpc(Writer w, RPCContext rpc, Stopwatch stopwatch) throws IOException
{
-    w.append("<li>").append(rpc.getMessage().getName()).append(": ");
-    w.append(formatMillis(StatsPlugin.nanosToMillis(stopwatch.elapsedNanos())));
-    w.append("</li>");
-  }
-
-  private void writeMethod(Writer w, Message message, FloatHistogram<?> hist) throws
IOException {
-    w.append("<h3>").append(message.getName()).append("</h3>");
-    w.append("<p>Number of calls: ");
-    w.append(Integer.toString(hist.getCount()));
-    w.append("</p><p>Average Duration: ");
-    w.append(formatMillis(hist.getMean()));
-    w.append("</p>");
-    w.append("</p><p>Std Dev: ");
-    w.append(formatMillis(hist.getUnbiasedStdDev()));
-    w.append("</p>");
-
-    w.append("<dl>");
-
-    for (Histogram.Entry<?> e : hist.entries()) {
-      w.append("<dt>");
-      w.append(e.bucket.toString());
-      w.append("</dt>");
-      w.append("<dd>").append(Integer.toString(e.count)).append("</dd>");
-      w.append("</dt>");
-    }
-    w.append("</dl>");
+  private String renderActiveRpc(RPCContext rpc, Stopwatch stopwatch) 
+      throws IOException {
+    String out = new String();
+    out += rpc.getMessage().getName() + ": " + 
+        formatMillis(StatsPlugin.nanosToMillis(stopwatch.elapsedNanos()));
+    return out;
   }
 
+  
+  private RenderableMessage renderMethod(Message message) {
+    RenderableMessage out = new RenderableMessage(message.getName());
+    
+    synchronized(this.statsPlugin.methodTimings) {
+      FloatHistogram<?> hist = this.statsPlugin.methodTimings.get(message);
+      out.numCalls = hist.getCount();
+      
+      HashMap<String, String> latencyBar = new HashMap<String, String>();
+      // Fill in chart attributes for velocity
+      latencyBar.put("type", "bar");
+      latencyBar.put("title", "All-Time Latency");
+      latencyBar.put("units", "ms");
+      latencyBar.put("numCalls", Integer.toString(hist.getCount()));
+      latencyBar.put("avg", Float.toString(hist.getMean()));
+      latencyBar.put("stdDev", Float.toString(hist.getUnbiasedStdDev()));
+      latencyBar.put("labelStr", 
+          Arrays.toString(hist.getSegmenter().getBoundaryLabels().toArray()));
+      latencyBar.put("boundaryStr",
+          Arrays.toString(escapeStringArray(hist.getSegmenter().
+              getBucketLabels()).toArray()));
+      latencyBar.put("dataStr", Arrays.toString(hist.getHistogram())); 
+      out.charts.add(latencyBar);
+      
+      HashMap<String, String> latencyDot = new HashMap<String, String>();
+      latencyDot.put("title", "Latency");
+      latencyDot.put("type", "dot");
+      latencyDot.put("dataStr", 
+          Arrays.toString(hist.getRecentAdditions().toArray()));
+      out.charts.add(latencyDot);
+    }
+    
+    synchronized(this.statsPlugin.sendPayloads) {
+      IntegerHistogram<?> hist = this.statsPlugin.sendPayloads.get(message);
+      HashMap<String, String> latencyBar = new HashMap<String, String>();
+      // Fill in chart attributes for velocity
+      latencyBar.put("type", "bar");
+      latencyBar.put("title", "All-Time Send Payload");
+      latencyBar.put("units", "ms");
+      latencyBar.put("numCalls", Integer.toString(hist.getCount()));
+      latencyBar.put("avg", Float.toString(hist.getMean()));
+      latencyBar.put("stdDev", Float.toString(hist.getUnbiasedStdDev()));
+      latencyBar.put("labelStr", 
+          Arrays.toString(hist.getSegmenter().getBoundaryLabels().toArray()));
+      latencyBar.put("boundaryStr",
+          Arrays.toString(escapeStringArray(hist.getSegmenter().
+              getBucketLabels()).toArray()));
+      latencyBar.put("dataStr", Arrays.toString(hist.getHistogram())); 
+      out.charts.add(latencyBar);
+      
+      HashMap<String, String> latencyDot = new HashMap<String, String>();
+      latencyDot.put("title", "Send Payload");
+      latencyDot.put("type", "dot");
+      latencyDot.put("dataStr", 
+          Arrays.toString(hist.getRecentAdditions().toArray()));
+      out.charts.add(latencyDot);
+    }
+    
+    synchronized(this.statsPlugin.receivePayloads) {
+      IntegerHistogram<?> hist = this.statsPlugin.receivePayloads.get(message);
+      HashMap<String, String> latencyBar = new HashMap<String, String>();
+      // Fill in chart attributes for velocity
+      latencyBar.put("type", "bar");
+      latencyBar.put("title", "All-Time Receive Payload");
+      latencyBar.put("units", "ms");
+      latencyBar.put("numCalls", Integer.toString(hist.getCount()));
+      latencyBar.put("avg", Float.toString(hist.getMean()));
+      latencyBar.put("stdDev", Float.toString(hist.getUnbiasedStdDev()));
+      latencyBar.put("labelStr", 
+          Arrays.toString(hist.getSegmenter().getBoundaryLabels().toArray()));
+      latencyBar.put("boundaryStr",
+          Arrays.toString(escapeStringArray(hist.getSegmenter().
+              getBucketLabels()).toArray()));
+      latencyBar.put("dataStr", Arrays.toString(hist.getHistogram())); 
+      out.charts.add(latencyBar);
+      
+      HashMap<String, String> latencyDot = new HashMap<String, String>();
+      latencyDot.put("title", "Recv Payload");
+      latencyDot.put("type", "dot");
+      latencyDot.put("dataStr", 
+          Arrays.toString(hist.getRecentAdditions().toArray()));
+      out.charts.add(latencyDot);
+    }
+    
+    return out;
+  }
+  
   private CharSequence formatMillis(float millis) {
     return String.format("%.0fms", millis);
   }

Added: avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.css
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.css?rev=980173&view=auto
==============================================================================
--- avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.css (added)
+++ avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.css Wed Jul 28 19:28:53
2010
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+table#charts_table tr {
+  padding-right: 10px;
+  padding-left: 10px;
+}
\ No newline at end of file

Added: avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.js
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.js?rev=980173&view=auto
==============================================================================
--- avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.js (added)
+++ avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/avro.js Wed Jul 28 19:28:53
2010
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+function makeDotChart(yVals) {
+    var xVals = pv.range(1, yVals.length + 1);
+    var data = new Array();
+    var dotColors = pv.Colors.category20().range();
+    
+    for (i = 0; i < yVals.length; i = i + 1) {
+      data[i] = {x: xVals[i], y: yVals[i]};
+    }
+
+	/* Sizing and scales. */
+	var w = 200,
+	    h = 250,
+	    x = pv.Scale.linear(0, Math.max.apply(Math, xVals)).range(0, w),
+	    y = pv.Scale.linear(0, Math.max.apply(Math, yVals)).range(0, h),
+	    c = pv.Scale.linear(1, 20).range("orange", "brown");
+	
+	/* The root panel. */
+	var vis = new pv.Panel()
+	    .width(w)
+	    .height(h)
+	    .bottom(20)
+	    .left(50)
+	    .right(10)
+	    .top(5);
+	
+	/* Y-axis and ticks. */
+	vis.add(pv.Rule)
+	    .data(y.ticks())
+	    .bottom(y)
+	    .strokeStyle(function(d) d ? "#eee" : "#000")
+	  .anchor("left").add(pv.Label)
+	    .text(y.tickFormat);
+
+	
+	/* The dot plot! */
+	vis.add(pv.Panel)
+	    .data(data)
+	    .add(pv.Dot)
+	    .left(function(d) x(d.x))
+	    .bottom(function(d) y(d.y))
+	    .strokeStyle(function(d) dotColors[d.x % 20])
+	    .fillStyle(function() this.strokeStyle().alpha(1))
+	    .title(function(d) d.y)
+	    .event("mouseover", pv.Behavior.tipsy({gravity: "n", 
+	      fade: false, delayIn: 0}));
+	vis.render();
+}
+
+
+function makeBarChart(labels, boundries, data) {
+	var w = 200,
+	    h = 250,
+	    x = pv.Scale.ordinal(pv.range(data.length)).splitBanded(0, w, 4/5),
+	    y = pv.Scale.linear(0, Math.max.apply(Math, data)).range(0, h),
+	    i = -1,
+	    c = pv.Scale.linear(1, 5, 20).range("green", "yellow", "red");
+
+	var vis = new pv.Panel()
+	    .width(w)
+	    .height(h)
+	    .bottom(20)
+	    .left(40)
+	    .right(5)
+	    .top(30);
+	
+	var bar = vis.add(pv.Bar)
+	    .data(data)
+	    .left(function(){ return x(this.index); })
+	    .width(10)
+	    .bottom(0)
+	    .height(y)
+	    .fillStyle(function(d) "#1f77b4")
+	    .title(function() { return boundries[this.index]; })
+	    .event("mouseover", pv.Behavior.tipsy({gravity: "n", 
+	      fade: false, delayIn: 0}));
+	
+	bar.anchor("bottom").add(pv.Label)
+    	.textMargin(5)
+		.textBaseline("top")
+		.text(function() (this.index % 4 == 0) ? labels[this.index]: "");		
+	
+	vis.add(pv.Rule)
+	    .data(y.ticks())
+	    .bottom(function(d) Math.round(y(d)) - .5)
+	    .strokeStyle(function(d) d ? "rgba(255,255,255,.3)" : "#000")
+	  .add(pv.Rule)
+	    .left(0)
+	    .width(5)
+	    .strokeStyle("#000")
+	  .anchor("left").add(pv.Label)
+	    .text(function(d) d.toFixed(1));
+	vis.render();
+}
\ No newline at end of file

Added: avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/g.bar.js
URL: http://svn.apache.org/viewvc/avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/g.bar.js?rev=980173&view=auto
==============================================================================
--- avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/g.bar.js (added)
+++ avro/trunk/lang/java/src/java/org/apache/avro/ipc/stats/static/g.bar.js Wed Jul 28 19:28:53
2010
@@ -0,0 +1,386 @@
+/*!
+ * g.Raphael 0.4.1 - Charting library, based on Raphaël
+ *
+ * Copyright (c) 2009 Dmitry Baranovskiy (http://g.raphaeljs.com)
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
+ */
+Raphael.fn.g.barchart = function (x, y, width, height, values, opts) {
+    opts = opts || {};
+    var type = {round: "round", sharp: "sharp", soft: "soft"}[opts.type] || "square",
+        gutter = parseFloat(opts.gutter || "20%"),
+        chart = this.set(),
+        bars = this.set(),
+        covers = this.set(),
+        covers2 = this.set(),
+        total = Math.max.apply(Math, values),
+        stacktotal = [],
+        paper = this,
+        multi = 0,
+        colors = opts.colors || this.g.colors,
+        len = values.length;
+    if (this.raphael.is(values[0], "array")) {
+        total = [];
+        multi = len;
+        len = 0;
+        for (var i = values.length; i--;) {
+            bars.push(this.set());
+            total.push(Math.max.apply(Math, values[i]));
+            len = Math.max(len, values[i].length);
+        }
+        if (opts.stacked) {
+            for (var i = len; i--;) {
+                var tot = 0;
+                for (var j = values.length; j--;) {
+                    tot +=+ values[j][i] || 0;
+                }
+                stacktotal.push(tot);
+            }
+        }
+        for (var i = values.length; i--;) {
+            if (values[i].length < len) {
+                for (var j = len; j--;) {
+                    values[i].push(0);
+                }
+            }
+        }
+        total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
+    }
+    
+    total = (opts.to) || total;
+    var barwidth = width / (len * (100 + gutter) + gutter) * 100,
+        barhgutter = barwidth * gutter / 100,
+        barvgutter = opts.vgutter == null ? 20 : opts.vgutter,
+        stack = [],
+        X = x + barhgutter,
+        Y = (height - 2 * barvgutter) / total;
+    if (!opts.stretch) {
+        barhgutter = Math.round(barhgutter);
+        barwidth = Math.floor(barwidth);
+    }
+    !opts.stacked && (barwidth /= multi || 1);
+    for (var i = 0; i < len; i++) {
+        stack = [];
+        for (var j = 0; j < (multi || 1); j++) {
+            var h = Math.round((multi ? values[j][i] : values[i]) * Y),
+                top = y + height - barvgutter - h,
+                bar = this.g.finger(Math.round(X + barwidth / 2), top + h, barwidth, h, true,
type).attr({stroke: "none", fill: colors[multi ? j : i]});
+            if (multi) {
+                bars[j].push(bar);
+            } else {
+                bars.push(bar);
+            }
+            bar.y = top;
+            bar.x = Math.round(X + barwidth / 2);
+            bar.w = barwidth;
+            bar.h = h;
+            bar.value = multi ? values[j][i] : values[i];
+            if (!opts.stacked) {
+                X += barwidth;
+            } else {
+                stack.push(bar);
+            }
+        }
+        if (opts.stacked) {
+            var cvr;
+            covers2.push(cvr = this.rect(stack[0].x - stack[0].w / 2, y, barwidth, height).attr(this.g.shim));
+            cvr.bars = this.set();
+            var size = 0;
+            for (var s = stack.length; s--;) {
+                stack[s].toFront();
+            }
+            for (var s = 0, ss = stack.length; s < ss; s++) {
+                var bar = stack[s],
+                    cover,
+                    h = (size + bar.value) * Y,
+                    path = this.g.finger(bar.x, y + height - barvgutter - !!size * .5, barwidth,
h, true, type, 1);
+                cvr.bars.push(bar);
+                size && bar.attr({path: path});
+                bar.h = h;
+                bar.y = y + height - barvgutter - !!size * .5 - h;
+                covers.push(cover = this.rect(bar.x - bar.w / 2, bar.y, barwidth, bar.value
* Y).attr(this.g.shim));
+                cover.bar = bar;
+                cover.value = bar.value;
+                size += bar.value;
+            }
+            X += barwidth;
+        }
+        X += barhgutter;
+    }
+    covers2.toFront();
+    X = x + barhgutter;
+    if (!opts.stacked) {
+        for (var i = 0; i < len; i++) {
+            for (var j = 0; j < (multi || 1); j++) {
+                var cover;
+                covers.push(cover = this.rect(Math.round(X), y + barvgutter, barwidth, height
- barvgutter).attr(this.g.shim));
+                cover.bar = multi ? bars[j][i] : bars[i];
+                cover.value = cover.bar.value;
+                X += barwidth;
+            }
+            X += barhgutter;
+        }
+    }
+    chart.label = function (labels, isBottom) {
+        labels = labels || [];
+        this.labels = paper.set();
+        var L, l = -Infinity;
+        if (opts.stacked) {
+            for (var i = 0; i < len; i++) {
+                var tot = 0;
+                for (var j = 0; j < (multi || 1); j++) {
+                    tot += multi ? values[j][i] : values[i];
+                    if (j == multi - 1) {
+                        var label = paper.g.labelise(labels[i], tot, total);
+                        L = paper.g.text(bars[i * (multi || 1) + j].x, y + height - barvgutter
/ 2, label).insertBefore(covers[i * (multi || 1) + j]);
+                        var bb = L.getBBox();
+                        if (bb.x - 7 < l) {
+                            L.remove();
+                        } else {
+                            this.labels.push(L);
+                            l = bb.x + bb.width;
+                        }
+                    }
+                }
+            }
+        } else {
+            for (var i = 0; i < len; i++) {
+                for (var j = 0; j < (multi || 1); j++) {
+                    var label = paper.g.labelise(multi ? labels[j] && labels[j][i]
: labels[i], multi ? values[j][i] : values[i], total);
+                    L = paper.g.text(bars[i * (multi || 1) + j].x, isBottom ? y + height
- barvgutter / 2 : bars[i * (multi || 1) + j].y - 10, label).insertBefore(covers[i * (multi
|| 1) + j]);
+                    var bb = L.getBBox();
+                    if (bb.x - 7 < l) {
+                        L.remove();
+                    } else {
+                        this.labels.push(L);
+                        l = bb.x + bb.width;
+                    }
+                }
+            }
+        }
+        return this;
+    };
+    chart.hover = function (fin, fout) {
+        covers2.hide();
+        covers.show();
+        covers.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.hoverColumn = function (fin, fout) {
+        covers.hide();
+        covers2.show();
+        fout = fout || function () {};
+        covers2.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.click = function (f) {
+        covers2.hide();
+        covers.show();
+        covers.click(f);
+        return this;
+    };
+    chart.each = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers.length; i--;) {
+            f.call(covers[i]);
+        }
+        return this;
+    };
+    chart.eachColumn = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers2.length; i--;) {
+            f.call(covers2[i]);
+        }
+        return this;
+    };
+    chart.clickColumn = function (f) {
+        covers.hide();
+        covers2.show();
+        covers2.click(f);
+        return this;
+    };
+    chart.push(bars, covers, covers2);
+    chart.bars = bars;
+    chart.covers = covers;
+    return chart;
+};
+Raphael.fn.g.hbarchart = function (x, y, width, height, values, opts) {
+    opts = opts || {};
+    var type = {round: "round", sharp: "sharp", soft: "soft"}[opts.type] || "square",
+        gutter = parseFloat(opts.gutter || "20%"),
+        chart = this.set(),
+        bars = this.set(),
+        covers = this.set(),
+        covers2 = this.set(),
+        total = Math.max.apply(Math, values),
+        stacktotal = [],
+        paper = this,
+        multi = 0,
+        colors = opts.colors || this.g.colors,
+        len = values.length;
+    if (this.raphael.is(values[0], "array")) {
+        total = [];
+        multi = len;
+        len = 0;
+        for (var i = values.length; i--;) {
+            bars.push(this.set());
+            total.push(Math.max.apply(Math, values[i]));
+            len = Math.max(len, values[i].length);
+        }
+        if (opts.stacked) {
+            for (var i = len; i--;) {
+                var tot = 0;
+                for (var j = values.length; j--;) {
+                    tot +=+ values[j][i] || 0;
+                }
+                stacktotal.push(tot);
+            }
+        }
+        for (var i = values.length; i--;) {
+            if (values[i].length < len) {
+                for (var j = len; j--;) {
+                    values[i].push(0);
+                }
+            }
+        }
+        total = Math.max.apply(Math, opts.stacked ? stacktotal : total);
+    }
+    
+    total = (opts.to) || total;
+    var barheight = Math.floor(height / (len * (100 + gutter) + gutter) * 100),
+        bargutter = Math.floor(barheight * gutter / 100),
+        stack = [],
+        Y = y + bargutter,
+        X = (width - 1) / total;
+    !opts.stacked && (barheight /= multi || 1);
+    for (var i = 0; i < len; i++) {
+        stack = [];
+        for (var j = 0; j < (multi || 1); j++) {
+            var val = multi ? values[j][i] : values[i],
+                bar = this.g.finger(x, Y + barheight / 2, Math.round(val * X), barheight
- 1, false, type).attr({stroke: "none", fill: colors[multi ? j : i]});
+            if (multi) {
+                bars[j].push(bar);
+            } else {
+                bars.push(bar);
+            }
+            bar.x = x + Math.round(val * X);
+            bar.y = Y + barheight / 2;
+            bar.w = Math.round(val * X);
+            bar.h = barheight;
+            bar.value = +val;
+            if (!opts.stacked) {
+                Y += barheight;
+            } else {
+                stack.push(bar);
+            }
+        }
+        if (opts.stacked) {
+            var cvr = this.rect(x, stack[0].y - stack[0].h / 2, width, barheight).attr(this.g.shim);
+            covers2.push(cvr);
+            cvr.bars = this.set();
+            var size = 0;
+            for (var s = stack.length; s--;) {
+                stack[s].toFront();
+            }
+            for (var s = 0, ss = stack.length; s < ss; s++) {
+                var bar = stack[s],
+                    cover,
+                    val = Math.round((size + bar.value) * X),
+                    path = this.g.finger(x, bar.y, val, barheight - 1, false, type, 1);
+                cvr.bars.push(bar);
+                size && bar.attr({path: path});
+                bar.w = val;
+                bar.x = x + val;
+                covers.push(cover = this.rect(x + size * X, bar.y - bar.h / 2, bar.value
* X, barheight).attr(this.g.shim));
+                cover.bar = bar;
+                size += bar.value;
+            }
+            Y += barheight;
+        }
+        Y += bargutter;
+    }
+    covers2.toFront();
+    Y = y + bargutter;
+    if (!opts.stacked) {
+        for (var i = 0; i < len; i++) {
+            for (var j = 0; j < (multi || 1); j++) {
+                var cover = this.rect(x, Y, width, barheight).attr(this.g.shim);
+                covers.push(cover);
+                cover.bar = multi ? bars[j][i] : bars[i];
+                cover.value = cover.bar.value;
+                Y += barheight;
+            }
+            Y += bargutter;
+        }
+    }
+    chart.label = function (labels, isRight) {
+        labels = labels || [];
+        this.labels = paper.set();
+        for (var i = 0; i < len; i++) {
+            for (var j = 0; j < multi; j++) {
+                var  label = paper.g.labelise(multi ? labels[j] && labels[j][i] :
labels[i], multi ? values[j][i] : values[i], total);
+                var X = isRight ? bars[i * (multi || 1) + j].x - barheight / 2 + 3 : x +
5,
+                    A = isRight ? "end" : "start",
+                    L;
+                this.labels.push(L = paper.g.text(X, bars[i * (multi || 1) + j].y, label).attr({"text-anchor":
A}).insertBefore(covers[0]));
+                if (L.getBBox().x < x + 5) {
+                    L.attr({x: x + 5, "text-anchor": "start"});
+                } else {
+                    bars[i * (multi || 1) + j].label = L;
+                }
+            }
+        }
+        return this;
+    };
+    chart.hover = function (fin, fout) {
+        covers2.hide();
+        covers.show();
+        fout = fout || function () {};
+        covers.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.hoverColumn = function (fin, fout) {
+        covers.hide();
+        covers2.show();
+        fout = fout || function () {};
+        covers2.mouseover(fin).mouseout(fout);
+        return this;
+    };
+    chart.each = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers.length; i--;) {
+            f.call(covers[i]);
+        }
+        return this;
+    };
+    chart.eachColumn = function (f) {
+        if (!Raphael.is(f, "function")) {
+            return this;
+        }
+        for (var i = covers2.length; i--;) {
+            f.call(covers2[i]);
+        }
+        return this;
+    };
+    chart.click = function (f) {
+        covers2.hide();
+        covers.show();
+        covers.click(f);
+        return this;
+    };
+    chart.clickColumn = function (f) {
+        covers.hide();
+        covers2.show();
+        covers2.click(f);
+        return this;
+    };
+    chart.push(bars, covers, covers2);
+    chart.bars = bars;
+    chart.covers = covers;
+    return chart;
+};



Mime
View raw message