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.util;
029
030import org.opencms.main.CmsLog;
031import org.opencms.main.OpenCms;
032
033import java.util.ArrayList;
034import java.util.Collections;
035import java.util.List;
036import java.util.Map;
037import java.util.concurrent.ConcurrentHashMap;
038import java.util.concurrent.TimeUnit;
039
040import org.apache.commons.lang3.RandomStringUtils;
041import org.apache.commons.logging.Log;
042
043import com.google.common.collect.ComparisonChain;
044
045/**
046 * Helper class for building diagnostics code that tracks potentially long running operations.<p>
047 * Is used with the try-with-resources syntax.  (I_CmsCloseable c = CmsTaskWatcher.get().openTask(description)) { ... }
048 */
049public class CmsTaskWatcher {
050
051    /**
052     * Entry for a single task.
053     */
054    static class Entry {
055
056        /** Label. */
057        private String m_label;
058        /** Start time. */
059        private long m_startTime;
060
061        /** Id. */
062        private String m_id;
063
064        /** Thread name. */
065        private String m_threadName;
066
067        /**
068         * Creates a new entry.
069         *
070         * @param threadName the thread name
071         * @param label the label
072         * @param startTime the start time
073         */
074        public Entry(String threadName, String label, long startTime) {
075
076            m_id = RandomStringUtils.randomAlphanumeric(10);
077            m_threadName = threadName;
078            m_label = label;
079            m_startTime = startTime;
080        }
081
082        /**
083         * Gets the age of the entry in milliseconds.
084         *
085         * @return the age in milliseconds
086         */
087        public long getAge() {
088
089            return System.currentTimeMillis() - m_startTime;
090        }
091
092        /**
093         * Gets the id.
094         *
095         * @return the id
096         */
097        public String getId() {
098
099            return m_id;
100        }
101
102        /**
103         * Gets the label.
104         *
105         * @return the label
106         */
107        public String getLabel() {
108
109            return m_label;
110        }
111
112        /**
113         * Gets the start tiem
114         *
115         * @return the start time
116         */
117        public long getStartTime() {
118
119            return m_startTime;
120        }
121
122        /**
123         * Gets the thread name.
124         *
125         * @return the thread name
126         */
127        public String getThreadName() {
128
129            return m_threadName;
130        }
131
132        /**
133         * @see java.lang.Object#toString()
134         */
135        @Override
136        public String toString() {
137
138            return "" + (System.currentTimeMillis() - m_startTime) + " " + m_threadName + " / " + m_label;
139        }
140
141    }
142
143    /** Task watcher log channel. */
144    private static final Log TASKWATCHER = CmsLog.getLog("taskwatcher");
145
146    /** Singleton instance. */
147    private static CmsTaskWatcher m_instance = new CmsTaskWatcher();
148
149    /**
150     * The map of task entries.
151     */
152    private ConcurrentHashMap<String, Entry> m_map = new ConcurrentHashMap<>();
153
154    /**
155     * Gets the singleton instance.
156     * @return the singleton instance
157     */
158    public static CmsTaskWatcher get() {
159
160        return m_instance;
161    }
162
163    /**
164     * Initializes the logging job.
165     */
166    public static void initialize() {
167
168        OpenCms.getExecutor().scheduleWithFixedDelay(() -> {
169            try {
170                String report = get().getReport(5000);
171                if (report.length() > 0) {
172                    TASKWATCHER.error("Report:\n");
173                    TASKWATCHER.error(report);
174                }
175            } catch (Exception e) {
176                // ignore
177            }
178
179        }, 5000, 5000, TimeUnit.MILLISECONDS);
180    }
181
182    /**
183     * Gets the map of entries.
184     *
185     * @return the map of entries.
186     */
187    public Map<String, Entry> getMap() {
188
189        return Collections.unmodifiableMap(m_map);
190    }
191
192    /**
193     * Gets a report as a string.
194     *
195     * @param minAge minimum age of entries to show in the report
196     *
197     * @return the report as a string
198     */
199    public String getReport(long minAge) {
200
201        List<Entry> entries = new ArrayList<>(m_map.values());
202        StringBuilder builder = new StringBuilder();
203        Collections.sort(
204            entries,
205            (
206                a,
207                b) -> ComparisonChain.start().compare(a.getStartTime(), b.getStartTime()).compare(
208                    a.getThreadName(),
209                    b.getThreadName()).compare(a.getLabel(), b.getLabel()).result());
210        for (Entry entry : entries) {
211            if (entry.getAge() > minAge) {
212                builder.append(entry.toString());
213                builder.append("\n");
214            }
215        }
216        return builder.toString();
217
218    }
219
220    /**
221     * Adds a new task and returns a Closeable (to be used in a try-with-resources block) which removes the task entry again when closed.
222     *
223     * @param label the label for the task entry
224     * @return the closeable
225     */
226    public I_CmsCloseable openTask(String label) {
227
228        Entry entry = new Entry(Thread.currentThread().getName(), label, System.currentTimeMillis());
229        final String id = entry.getId();
230        m_map.put(id, entry);
231        return () -> {
232            m_map.remove(id);
233        };
234    }
235
236}