StackVisualizer.java

/*
 * Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Additionally licensed with:
 *
 * 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 org.spf4j.stackmonitor;

import com.google.common.html.HtmlEscapers;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import gnu.trove.map.TMap;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import org.spf4j.base.Methods;
import org.spf4j.base.StackSamples;
import org.spf4j.base.avro.Method;

/**
 * utility class to generate svg and html out of stack samples.
 *
 * @author zoly
 */
@SuppressFBWarnings({"CBX_CUSTOM_BUILT_XML", "PREDICTABLE_RANDOM"})
public final class StackVisualizer {

  private static final String[] COLORS = {"#CCE01B",
    "#DDE01B", "#EEE01B", "#FFE01B", "#FFD01B",
    "#FFC01B", "#FFA01B", "#FF901B", "#FF801B",
    "#FF701B", "#FF601B", "#FF501B", "#FF401B"};

  private StackVisualizer() {
  }

  public static void generateHtmlTable(final Writer writer, final Method m,
          final StackSamples node, final int tableWidth, final int maxDepth) throws IOException {
    TMap<Method, ? extends StackSamples> subNodes = node.getSubNodes();
    writer.append("<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" "
            + "style=\"overflow:hidden;table-layout:fixed;width:").
            append(Integer.toString(tableWidth)).append("px\">\n");
    int totalSamples = node.getSampleCount();

    if (maxDepth > 0 && !subNodes.isEmpty()) {
      writer.append("<tr style=\"height:1em\">");
      for (Map.Entry<Method, ? extends StackSamples> entry : subNodes.entrySet()) {
        int width = entry.getValue().getSampleCount() * tableWidth / totalSamples;
        writer.append("<td style=\"vertical-align:bottom; width:").
                append(Integer.toString(width)).append("px\">");
        generateHtmlTable(writer, entry.getKey(), entry.getValue(), width, maxDepth - 1);
        writer.append("</td>");
      }
      writer.append("<td></td>");
      writer.append("</tr>\n");
    }
    writer.append("<tr style=\"height:1em\" ><td ");
    if (!subNodes.isEmpty()) {
      writer.append("colspan=\"").append(Integer.toString(subNodes.size() + 1)).append("\" ");
    }
    writer.append(" title=\"");
    Methods.writeHtml(m, writer);
    writer.append(":");
    writer.append(Integer.toString(node.getSampleCount()));
    writer.append("\" style=\"overflow:hidden;width:100%;vertical-align:bottom ;background:").
            append(COLORS[(int) (Math.random() * COLORS.length)]).append("\">");
    Methods.writeHtml(m, writer);
    writer.append(":");
    writer.append(Integer.toString(totalSamples));
    writer.append("</td></tr>\n");

    writer.append("</table>\n");
  }

  public static void generateSvg(final Writer writer, final Method m,
          final SampleNode node, final int x, final int y,
          final int width, final int maxDepth, final String idPfx) throws IOException {
    if (width < 200) {
      throw new IllegalArgumentException("width must be greater than 200, it is " + width);
    }
    writer.append("<svg width=\"" + width + "\" height= \"" + (15 * node.height() + 15) + "\" onload=\""
            + idPfx + "init(evt)\" >\n");
    writer.append("<script type=\"text/ecmascript\">\n"
            + "<![CDATA[\n"
            + "var " + idPfx + "tooltip;\n"
            + "var " + idPfx + "tooltip_bg;\n"
            + "function " + idPfx + "init(evt)\n"
            + "  {\n"
            + "    if ( window.svgDocument == null )\n"
            + "    {\n"
            + "      svgDocument = evt.target.ownerDocument;\n"
            + "    }\n"
            + "    " + idPfx + "tooltip = svgDocument.getElementById('" + idPfx + "tooltip'); "
            + idPfx + "tooltip_bg = svgDocument.getElementById('" + idPfx + "tooltip_bg');"
            + "  }\n"
            + "function " + idPfx + "ss(evt, mouseovertext, xx, yy)\n"
            + "{\n"
            + "  " + idPfx + "tooltip.setAttributeNS(null,\"x\",xx );\n"
            + "  " + idPfx + "tooltip.setAttributeNS(null,\"y\",yy+13 );\n"
            + "  " + idPfx + "tooltip.firstChild.data = mouseovertext;\n"
            + "  " + idPfx + "tooltip.setAttributeNS(null,\"visibility\",\"visible\");\n"
            + "length = " + idPfx + "tooltip.getComputedTextLength();\n"
            + idPfx + "tooltip_bg.setAttributeNS(null,\"width\",length+8);\n"
            + idPfx + "tooltip_bg.setAttributeNS(null,\"x\",xx);\n"
            + idPfx + "tooltip_bg.setAttributeNS(null,\"y\",yy);\n"
            + idPfx + "tooltip_bg.setAttributeNS(null,\"visibility\",\"visibile\");"
            + "}\n"
            + "function " + idPfx + "hh()\n"
            + "{\n"
            + "  " + idPfx + "tooltip.setAttributeNS(null,\"visibility\",\"hidden\");\n"
            + idPfx + "tooltip_bg.setAttributeNS(null,\"visibility\",\"hidden\");\n"
            + "}]]>"
            + "</script>");

    generateSubSvg(writer, m, node, x, y + 15, width - 100, maxDepth, idPfx);

    writer.append("<rect fill=\"rgb(255,255,255)\" id=\"" + idPfx + "tooltip_bg\"\n"
            + "      x=\"0\" y=\"0\" rx=\"4\" ry=\"4\"\n"
            + "      width=\"55\" height=\"17\" visibility=\"hidden\"/>");
    writer.append("<text font-size=\"12\" font-family=\"Verdana\" fill=\"rgb(0,0,0)\"  id=\""
            + idPfx + "tooltip\" x=\"0\" y=\"0\" visibility=\"hidden\">Tooltip</text>");
    writer.append("</svg>\n");
  }

  @SuppressFBWarnings("ISB_TOSTRING_APPENDING")
  public static void generateSubSvg(final Writer writer, final Method m, final StackSamples node,
          final int x, final int y, final int width, final int maxDepth, final String idPfx) throws IOException {

    Map<Method, ? extends StackSamples> subNodes = node.getSubNodes();

    int totalSamples = node.getSampleCount();
    String id = idPfx + "ix" + x + 'y' + y;
    String content = HtmlEscapers.htmlEscaper().escape(m.toString() + ':' + totalSamples);
    writer.append("<g onmouseover=\"" + idPfx + "ss(evt,'" + content + "'," + x + ", "
            + y + " )\" onmouseout=\"" + idPfx + "hh()\">");
    writer.append("<rect id=\"" + id + "\" x=\"" + x + "\" y=\"" + y + "\" width=\"" + width
            + "\" height=\"15\" fill=\""
            + COLORS[(int) (Math.random() * COLORS.length)] + "\"  />");

    writer.append("<text x=\"" + x + "\" y=\"" + (y + 13)
            + "\" font-size=\"12\" font-family=\"Verdana\" fill=\"rgb(0,0,0)\" "
            + " >");
    writer.append(content.substring(0, Math.min(width / 9, content.length())));
    writer.append("</text>\n");
    writer.append("</g>");

    if (maxDepth > 0 && !subNodes.isEmpty()) {
      int rx = 0;
      for (Map.Entry<Method, ? extends StackSamples> entry : subNodes.entrySet()) {
        int cwidth = (int) (((long) entry.getValue().getSampleCount()) * width / totalSamples);
        generateSubSvg(writer, entry.getKey(), entry.getValue(), rx + x, y + 15, cwidth, maxDepth - 1, idPfx);
        rx += cwidth;
      }

    }
  }
}