001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.main;
029
030import java.io.ByteArrayOutputStream;
031import java.io.FileOutputStream;
032import java.lang.management.ManagementFactory;
033import java.lang.management.MonitorInfo;
034import java.lang.management.ThreadInfo;
035import java.lang.management.ThreadMXBean;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.LinkedHashMap;
039import java.util.List;
040import java.util.Map;
041import java.util.zip.ZipEntry;
042import java.util.zip.ZipOutputStream;
043
044import org.dom4j.Document;
045import org.dom4j.DocumentHelper;
046import org.dom4j.Element;
047import org.dom4j.io.OutputFormat;
048import org.dom4j.io.XMLWriter;
049
050import com.google.common.collect.Lists;
051
052/**
053 * Profiling thread used for the startup process.<p>
054 *
055 * Periodically creates thread dumps for a single thread, saves them zo a ZIP file, and also
056 * generates a summary XML files presenting the stack frames with with their sample counts as a tree
057 * structure.<p>
058 */
059public class CmsSingleThreadDumperThread extends Thread {
060
061    /**
062     * Node for the summary tree generated from the thread dumps.<p>
063     *
064     * The tree keeps track of sample counts for stack trace frames.
065     */
066    public static class SampleNode {
067
068        /** The map of children, with their keys used as map keys. */
069        private Map<Object, SampleNode> m_children = new LinkedHashMap<>();
070
071        /** The key for the node. */
072        private Object m_key;
073
074        /** The sample count. */
075        private long m_samples;
076
077        /**
078         * Creates a new node.<p>
079         *
080         * @param key the key of the node, identifying it among its siblings
081         */
082        public SampleNode(Object key) {
083
084            m_key = key;
085        }
086
087        /**
088         * Compare nodes by descending sample count.<p>
089         *
090         * @param a first node
091         * @param b second node
092         * @return the comparison result
093         */
094        public static int compareBySamplesDescending(SampleNode a, SampleNode b) {
095
096            return -Long.compare(a.getSamples(), b.getSamples());
097        }
098
099        /**
100         * Increment sample count for all nodes along the given path.<p>
101         *
102         * @param root the root node
103         * @param path a sequence of keys constituting a path in the tree
104         */
105        public static void incrementPath(SampleNode root, List<?> path) {
106
107            List<SampleNode> nodes = nodesForPath(root, path);
108            for (SampleNode node : nodes) {
109                node.increment();
110            }
111
112        }
113
114        /**
115         * Given a path consisting of a list of node keys, this method collects all nodes along that path,
116         * including the given root node, and creates nodes if they don't exist in the tree yet.<p>
117         *
118         * @param root the root of the tree
119         * @param path the path
120         *
121         * @return the sequence of nodes along the path
122         */
123        public static List<SampleNode> nodesForPath(SampleNode root, List<?> path) {
124
125            List<SampleNode> result = new ArrayList<>();
126            result.add(root);
127            SampleNode current = root;
128            for (Object key : path) {
129                current = current.getOrAddChild(key);
130                result.add(current);
131            }
132            return result;
133        }
134
135        /**
136         * Dumps the tree node to XML.<p>
137         *
138         * @param parent the parent XML to append the XML to
139         */
140        public void appendToXml(Element parent) {
141
142            Element element = parent.addElement("location").addAttribute("key", "" + m_key).addAttribute(
143                "samples",
144                "" + m_samples);
145            for (SampleNode child : m_children.values()) {
146                child.appendToXml(element);
147            }
148        }
149
150        /**
151         * Gets the key of the node.<p>
152         *
153         * @return the key of the node
154         */
155        public Object getKey() {
156
157            return m_key;
158        }
159
160        /**
161         * Gets the child node for the given key, creating it it it doesn't exist yet.<p>
162         *
163         * @param key the key
164         * @return the child for the key
165         */
166        public SampleNode getOrAddChild(Object key) {
167
168            SampleNode child = m_children.get(key);
169            if (child == null) {
170                child = new SampleNode(key);
171                m_children.put(key, child);
172            }
173            return child;
174        }
175
176        /**
177         * Returns the sample count.<p>
178         *
179         * @return the sample count
180         */
181        public long getSamples() {
182
183            return m_samples;
184        }
185
186        /**
187         * Check if this node has children.<p>
188         *
189         * @return true if this node has children
190         */
191        public boolean hasChildren() {
192
193            return m_children.size() > 0;
194        }
195
196        /**
197         * Increments the sample count for this node.<p>
198         */
199        public void increment() {
200
201            m_samples += 1;
202        }
203
204        /**
205         * Sorts the children of this node by descending sample count.<p>
206         */
207        public void sortChildren() {
208
209            ArrayList<SampleNode> children = new ArrayList<>(m_children.values());
210            children.sort(SampleNode::compareBySamplesDescending);
211            m_children.clear();
212            for (SampleNode node : children) {
213                m_children.put(node.getKey(), node);
214            }
215        }
216
217        /**
218         * Recursively sorts this node's and all its descendants' children.<p>
219         */
220        public void sortTree() {
221
222            sortChildren();
223            for (SampleNode child : m_children.values()) {
224                child.sortTree();
225            }
226        }
227
228    }
229
230    /** The file name to save the zip file to. */
231    private String m_filename;
232
233    /** The file name for the summary file. */
234    private String m_summaryFilename;
235
236    /** The id of the thread to monitor. */
237    private long m_threadId;
238
239    /** The thread management bean. */
240    private ThreadMXBean m_threadMx;
241
242    /**
243     * Creates a new instance.<p>
244     *
245     * @param filename the name of the zip file to generate
246     * @param summaryFilename the name of the summary file to generate
247     * @param id the id of the thread to monitor
248     */
249    public CmsSingleThreadDumperThread(String filename, String summaryFilename, long id) {
250
251        super("" + CmsSingleThreadDumperThread.class.getName());
252        m_threadMx = ManagementFactory.getThreadMXBean();
253        m_threadId = id;
254        m_filename = filename;
255        m_summaryFilename = summaryFilename;
256        // If startup process fails, we don't want this thread to hang around
257        setDaemon(true);
258    }
259
260    /**
261     * @see java.lang.Thread#run()
262     */
263    @Override
264    public void run() {
265
266        int count = 1;
267        SampleNode root = new SampleNode("ROOT");
268
269        try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(m_filename))) {
270            while (!isInterrupted()) {
271                ThreadInfo info = m_threadMx.getThreadInfo(m_threadId, Integer.MAX_VALUE);
272                List<StackTraceElement> path = Lists.reverse(Arrays.asList(info.getStackTrace()));
273                SampleNode.incrementPath(root, path);
274                StringBuffer buffer = new StringBuffer();
275                buffer.append(formatThreadInfo(info));
276                byte[] dumpData = buffer.toString().getBytes("UTF-8");
277                ZipEntry entry = new ZipEntry("dump_" + count + ".txt");
278                zip.putNextEntry(entry);
279                zip.write(dumpData);
280                count += 1;
281                Thread.sleep(10);
282            }
283
284        } catch (InterruptedException e) {
285            return;
286        } catch (Exception e) {
287            e.printStackTrace();
288        } finally {
289            saveSummaryXml(root);
290        }
291    }
292
293    /**
294     * Formats the thread information.<p>
295     *
296     * @param info thread information bean
297     * @return the formatted thread info
298     */
299    String formatThreadInfo(ThreadInfo info) {
300
301        StringBuilder sb = new StringBuilder(
302            "\"" + info.getThreadName() + "\"" + " Id=" + info.getThreadId() + " " + info.getThreadState());
303        if (info.getLockName() != null) {
304            sb.append(" on " + info.getLockName());
305        }
306        if (info.getLockOwnerName() != null) {
307            sb.append(" owned by \"" + info.getLockOwnerName() + "\" Id=" + info.getLockOwnerId());
308        }
309        if (info.isSuspended()) {
310            sb.append(" (suspended)");
311        }
312        if (info.isInNative()) {
313            sb.append(" (in native)");
314        }
315        sb.append('\n');
316        int i = 0;
317        for (; (i < info.getStackTrace().length); i++) {
318            StackTraceElement ste = info.getStackTrace()[i];
319            sb.append("\tat " + ste.toString());
320            sb.append('\n');
321            if ((i == 0) && (info.getLockInfo() != null)) {
322                Thread.State ts = info.getThreadState();
323                switch (ts) {
324                    case BLOCKED:
325                        sb.append("\t-  blocked on " + info.getLockInfo());
326                        sb.append('\n');
327                        break;
328                    case WAITING:
329                        sb.append("\t-  waiting on " + info.getLockInfo());
330                        sb.append('\n');
331                        break;
332                    case TIMED_WAITING:
333                        sb.append("\t-  waiting on " + info.getLockInfo());
334                        sb.append('\n');
335                        break;
336                    default:
337                }
338            }
339
340            for (MonitorInfo mi : info.getLockedMonitors()) {
341                if (mi.getLockedStackDepth() == i) {
342                    sb.append("\t-  locked " + mi);
343                    sb.append('\n');
344                }
345            }
346        }
347        return sb.toString();
348
349    }
350
351    /**
352     * Saves the stack trace summary tree starting from the given root node to an XML file.<p>
353     *
354     * @param root the root node
355     */
356    private void saveSummaryXml(SampleNode root) {
357
358        root.sortTree();
359        Document doc = DocumentHelper.createDocument();
360        Element rootElem = doc.addElement("root");
361        root.appendToXml(rootElem);
362        OutputFormat outformat = OutputFormat.createPrettyPrint();
363        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
364        outformat.setEncoding("UTF-8");
365        try {
366            XMLWriter writer = new XMLWriter(buffer, outformat);
367            writer.write(doc);
368            writer.flush();
369            try (FileOutputStream fos = new FileOutputStream(m_summaryFilename)) {
370                fos.write(buffer.toByteArray());
371            }
372        } catch (Exception e) {
373            e.printStackTrace();
374        }
375    }
376
377}