/** * Copyright 2016 Alexey Ragozin * * 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.gridkit.jvmtool.stacktrace.analytics.flame; import java.io.IOException; import java.io.Writer; import java.util.Arrays; import java.util.Comparator; import java.util.Locale; import java.util.SortedMap; import java.util.TreeMap; import org.gridkit.jvmtool.stacktrace.GenericStackElement; import org.gridkit.jvmtool.stacktrace.StackFrame; import org.gridkit.jvmtool.stacktrace.StackFrameList; public abstract class AbstractFlameCharter { private static final GenericStackElement[] ROOT = new GenericStackElement[0]; private static final Locale SVG_LOCALE; static { SVG_LOCALE = Locale.ROOT; } private Node root; private FlameColorPicker colorPicker = new DefaultColorPicker(); public AbstractFlameCharter() { root = new Node(ROOT, comparator()); } protected abstract Comparator<GenericStackElement> comparator(); public void setColorPicker(FlameColorPicker cp) { this.colorPicker = cp; } public void feed(StackFrameList trace) { Node node = root; ++node.totalCount; for(int i = trace.depth(); i > 0; --i) { StackFrame f = trace.frameAt(i - 1).withoutSource(); Node c = node.child(f); ++c.totalCount; node = c; } ++node.terminalCount; } public void renderSVG(String title, int width, Writer writer) throws IOException { int topm = 24; int bm = 0; int frameheight = 16; int threashold = (int)(1.5d * root.totalCount / width); int maxDepth = calculateMaxDepth(root, threashold); int height = maxDepth * frameheight + topm + bm; appendHeader(width, height, writer); format(writer, "<rect x=\"0.0\" y=\"0\" width=\"%d\" height=\"%d\" fill=\"url(#background)\"/>\n", width, height); format(writer, "<text text-anchor=\"middle\" x=\"%d\" y=\"%d\" font-size=\"17\" font-family=\"Verdana\" fill=\"rgb(0,0,0)\" >%s</text>\n", width/2, topm, title); appendChildNodes(writer, root, 0, width, height - frameheight, frameheight, threashold); format(writer, "</svg>"); } private int calculateMaxDepth(Node node, int threshold) { if (node.totalCount < threshold) { return 0; } else { int max = 0; for(Node n: node.children.values()) { max = Math.max(max, calculateMaxDepth(n, threshold)); } return max + 1; } } private void appendChildNodes(Writer writer, Node node, int xoffs, int width, int height, int frameheight, int threshold) throws IOException { int x = xoffs; // x += node.terminalCount / 2; for(Node child: node.children.values()) { if (child.totalCount > threshold) { renderNode(writer, child, x, height, width, frameheight); appendChildNodes(writer, child, x, width, height - frameheight, frameheight, threshold); } x += child.totalCount; } if (node.terminalCount > threshold) { renderSmoke(writer, x, node.terminalCount, height, width, frameheight); } } private void renderNode(Writer writer, Node node, int x, int height, int width, int frameheight) throws IOException { double rx = (double)(width) * x / root.totalCount; double rw = (double)(width) * node.totalCount / root.totalCount; double ry = height; double rh = frameheight; int c = colorPicker.pickColor(node.path); int cr = (0xFF) & (c >> 16); int cg = (0xFF) & (c >> 8); int cb = (0xFF) & (c >> 0); // Node box format(writer, "<g class=\"fbar\">\n"); format(writer, "<title>%s (%d samples, %.2f%%)</title>\n", escape(describe(node)), node.totalCount, 100d * node.totalCount / root.totalCount); format(writer, "<rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" fill=\"rgb(%d,%d,%d)\" rx=\"2\" ry=\"2\"/>\n", rx, ry, rw, rh, cr, cg, cb); format(writer, "<text x=\"%.1f\" y=\"%.1f\" fill=\"rgb(0,0,0)\">%s</text>\n", rx + 10, ry + frameheight - 3, escape(trimStr(describe(node), (int)(rw - 10) / 7))); format(writer, "</g>\n"); // if (node.terminalCount == node.totalCount) { // renderSmoke(writer, x, node.terminalCount, height, width, frameheight); // } } private void renderSmoke(Writer writer, int x, int samples, int height, int width, int frameheight) throws IOException { double rx = (double)(width) * x / root.totalCount; double rw = (double)(width) * samples / root.totalCount; double ry = height; double rh = frameheight; format(writer, "<g>\n"); format(writer, "<title>%d samples, %.2f%%</title>", samples, 100d * samples / root.totalCount); format(writer, "<rect x=\"%.1f\" y=\"%.1f\" width=\"%.1f\" height=\"%.1f\" fill=\"rgb(20,20,20)\" rx=\"1\" ry=\"1\"/>\n", rx, ry + rh / 2, rw, 3f); format(writer, "</g>\n"); } private String trimStr(String describe, int len) { if (len < 3) { return ""; } StringBuilder sb = new StringBuilder(); int n = Math.min(describe.length(), len); boolean trimed = describe.length() > len; for(int i = 0; i != n; ++i) { if (trimed && i > n - 3) { sb.append('.'); } else { sb.append(describe.charAt(i)); } } return sb.toString(); } protected String escape(String text) { return text .replace((CharSequence)"<", "<") .replace((CharSequence)">", ">"); } protected abstract String describe(Node node); private void format(Writer writer, String format, Object... args) throws IOException { writer.append(String.format(SVG_LOCALE, format, args)); } protected void appendHeader(int width, int height, Writer writer) throws IOException { format(writer, "<?xml version=\"1.0\" standalone=\"no\"?>\n"); format(writer, "<svg version=\"1.1\" width=\"%d\" height=\"%d\" onload=\"init(evt)\" viewBox=\"0 0 %d %d\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n" , width, height, width, height); format(writer, "<defs>"); format(writer, " <linearGradient id=\"background\" y1=\"0\" y2=\"1\" x1=\"0\" x2=\"0\">\n"); format(writer, " <stop stop-color=\"#eeeeee\" offset=\"5%%\"/>\n"); format(writer, " <stop stop-color=\"#eeeeb0\" offset=\"95%%\"/>\n"); format(writer, " </linearGradient>\n"); format(writer, "</defs>\n"); format(writer, "<style type=\"text/css\">\n"); format(writer, " text { font-size:12px; font-family:Verdana }\n"); format(writer, " .fbar:hover { stroke:black; stroke-width:0.5; cursor:pointer; }\n"); format(writer, "</style>\n"); } static class Node { GenericStackElement[] path; int totalCount; int terminalCount; Comparator<GenericStackElement> comparator; SortedMap<GenericStackElement, Node> children; public Node(GenericStackElement[] path, Comparator<GenericStackElement> comparator) { this.path = path; this.comparator = comparator; this.children = new TreeMap<GenericStackElement, Node>(comparator); } public Node child(GenericStackElement f) { Node c = children.get(f); if (c == null) { GenericStackElement[] npath = Arrays.copyOf(path, path.length + 1); npath[path.length] = f; c = new Node(npath, comparator); children.put(f, c); } return c; } public GenericStackElement element() { return path[path.length - 1]; } @Override public String toString() { return path.length == 0 ? "<root>" : path[path.length - 1].toString(); } } }