001    /*
002     * $Id: ProfileStatistic.java,v 1.24 2014/05/10 21:43:38 oboehm Exp $
003     *
004     * Copyright (c) 2008 by Oliver Boehm
005     *
006     * Licensed under the Apache License, Version 2.0 (the "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *   http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express orimplied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     *
018     * (c)reated 22.12.2008 by oliver (ob@oasd.de)
019     */
020    package patterntesting.runtime.monitor;
021    
022    import java.io.*;
023    import java.lang.management.ManagementFactory;
024    import java.lang.reflect.*;
025    import java.util.*;
026    
027    import javax.management.*;
028    import javax.management.openmbean.*;
029    
030    import org.aspectj.lang.Signature;
031    import org.slf4j.*;
032    
033    import patterntesting.annotation.check.runtime.MayReturnNull;
034    import patterntesting.runtime.annotation.DontProfileMe;
035    import patterntesting.runtime.jmx.MBeanHelper;
036    import patterntesting.runtime.util.*;
037    
038    /**
039     * This is constructed as a thin layer around com.jamonapi.MonitorFactory for
040     * the needs of patterntesting. The reason for this layer is that sometimes you
041     * want to minimize the use of other libraries. So this implementation
042     * provides also an implementation if the JaMon library is missing.
043     *
044     * @see com.jamonapi.MonitorFactory
045     * @author <a href="boehm@javatux.de">oliver</a>
046     * @since 22.12.2008
047     * @version $Revision: 1.24 $
048     */
049    public class ProfileStatistic extends Thread implements ProfileStatisticMBean {
050    
051        private static final ProfileStatistic instance;
052        private static final Logger log = LoggerFactory.getLogger(ProfileStatistic.class);
053    
054        private final ObjectName mbeanName;
055        private final SimpleProfileMonitor rootMonitor;
056    
057        /** Is JaMon library available?. */
058        protected static final boolean jamonAvailable;
059    
060        /**
061         * ProfileStatistic *must* be initialized after isJamonAvailable attribute
062         * is set. Otherwise you'll get a NullPointerException after MBean
063         * registration.
064         */
065        static {
066            jamonAvailable = Environment.isJamonAvailable();
067            instance = new ProfileStatistic("root");
068        }
069    
070        /**
071         * Gets the single instance of ProfileStatistic.
072         *
073         * @return single instance of ProfileStatistic
074         */
075        public static ProfileStatistic getInstance() {
076            return instance;
077        }
078    
079        /**
080         * Instantiates a new profile statistic.
081         *
082         * @param rootLabel the root label
083         */
084        protected ProfileStatistic(final String rootLabel) {
085            this.rootMonitor = new SimpleProfileMonitor(rootLabel);
086            this.mbeanName = this.registerAsMBean();
087        }
088    
089        private ObjectName registerAsMBean() {
090            ObjectName name = createObjectName();
091            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
092            try {
093                server.registerMBean(this, name);
094            } catch (InstanceAlreadyExistsException ex) {
095                log.info(getMBeanName() + " was already registered.", ex);
096            } catch (MBeanRegistrationException ex) {
097                log.info(getMBeanName() + " cannot be registered.", ex);
098            } catch (NotCompliantMBeanException ex) {
099                log.info(getMBeanName() + " is not a compliant MBean.", ex);
100            }
101            log.info("{} successful registered as MBean", name);
102            return name;
103        }
104    
105        private ObjectName createObjectName() {
106            String name = MBeanHelper.getMBeanName(this);
107            ObjectName objectName = null;
108            try {
109                objectName = new ObjectName(name);
110            } catch (MalformedObjectNameException e) {
111                log.info("can't create object name + '" + name + "'");
112            }
113            return objectName;
114        }
115    
116        /**
117         * You can register the instance as shutdown hook. If the VM is
118         * terminated the profile values are logged and dumped to a CSV file in the
119         * tmp directory.
120         *
121         * @see #logStatistic()
122         * @see #dumpStatistic()
123         */
124        public static void addAsShutdownHook() {
125            addAsShutdownHook(instance);
126        }
127    
128        /**
129         * Adds the given instance (hook) as shutdown hook.
130         *
131         * @param hook the hook
132         */
133        protected static void addAsShutdownHook(final ProfileStatistic hook) {
134            Runtime.getRuntime().addShutdownHook(hook);
135            if (log.isDebugEnabled()) {
136                log.debug(hook + " registered as shutdown hook");
137            }
138        }
139    
140        /**
141         * We can't reset all ProfileMonitors - we must keep the empty
142         * monitors with 0 hits to see which methods or constructors are
143         * never called.
144         */
145        public void reset() {
146            synchronized(ProfileStatistic.class) {
147                List<String> labels = new ArrayList<String>();
148                ProfileMonitor[] monitors = getMonitors();
149                for (int i = 0; i < monitors.length; i++) {
150                    if (monitors[i].getHits() == 0) {
151                        labels.add(monitors[i].getLabel());
152                    }
153                }
154                if (jamonAvailable) {
155                    JamonMonitorFactory.addMonitors(labels);
156                } else {
157                    rootMonitor.addChildren(labels);
158                }
159            }
160        }
161    
162        /**
163         * Resets the root monitor.
164         */
165        protected void resetRootMonitor() {
166            if (jamonAvailable) {
167                JamonMonitorFactory.reset(this.rootMonitor);
168            } else {
169                this.rootMonitor.reset();
170            }
171        }
172    
173        /**
174         * For each constructor and for each method of the given class a
175         * ProfileMonitor is initialized. This is done to be able to find
176         * constructors and methods which are are never used (i.e. their
177         * hit count is zero).
178         *
179         * @param cl the given class
180         */
181        public void init(final Class<?> cl) {
182            if (log.isTraceEnabled()) {
183                log.trace("initializing monitors for " + cl + "...");
184            }
185            instance.init(cl, cl.getMethods());
186            instance.init(cl.getConstructors());
187        }
188    
189        /**
190         * Only methods of the given class but not the methods of the superclass
191         * are initialized for profiling.
192         *
193         * @param cl
194         * @param methods
195         */
196        private void init(final Class<?> cl, final Method[] methods) {
197            for (int i = 0; i < methods.length; i++) {
198                Class<?> declaring = methods[i].getDeclaringClass();
199                if (cl.equals(declaring)) {
200                    init(methods[i]);
201                } else if (log.isTraceEnabled()) {
202                    log.trace(methods[i] + " not defined in " + cl
203                            + " -> no monitor initialized");
204                }
205            }
206        }
207    
208        private void init(final Method method) {
209            if (method.getAnnotation(DontProfileMe.class) != null) {
210                if (log.isTraceEnabled()) {
211                    log.trace("@DontProfileMe " + method + " is ignored");
212                }
213                return;
214            }
215            Signature sig = SignatureHelper.getAsSignature(method);
216            ProfileMonitor mon = getMonitor(sig);
217            if (log.isTraceEnabled()) {
218                log.trace(mon + " initialized");
219            }
220        }
221    
222        private void init(final Constructor<?>[] ctors) {
223            for (int i = 0; i < ctors.length; i++) {
224                init(ctors[i]);
225            }
226        }
227    
228        private void init(final Constructor<?> ctor) {
229            if (ctor.getAnnotation(DontProfileMe.class) != null) {
230                if (log.isTraceEnabled()) {
231                    log.trace("@DontProfileMe " + ctor + " is ignored");
232                }
233                return;
234            }
235            Signature sig = SignatureHelper.getAsSignature(ctor);
236            ProfileMonitor mon = getMonitor(sig);
237            if (log.isTraceEnabled()) {
238                log.trace(mon + " initialized");
239            }
240        }
241    
242        /**
243         * Gets the MBean name of the registered {@link ProfileStatistic} bean.
244         *
245         * @return the MBean name
246         */
247        public ObjectName getMBeanName() {
248            return this.mbeanName;
249        }
250    
251    
252    
253        /////   business logic (measurement, statistics and more)   ///////////////
254    
255        /**
256         * This method is called when the PerformanceMonitor is registered as
257         * shutdown hook.
258         *
259         * @see java.lang.Thread#run()
260         */
261        @Override
262        public void run() {
263            dumpStatistic();
264        }
265    
266        /**
267         * Start.
268         *
269         * @param sig the sig
270         *
271         * @return the profile monitor
272         */
273        public static ProfileMonitor start(final Signature sig) {
274            return instance.startProfileMonitorFor(sig);
275        }
276    
277        /**
278         * Start profile monitor for the given signature.
279         *
280         * @param sig the signature
281         * @return the profile monitor
282         */
283        public ProfileMonitor startProfileMonitorFor(final Signature sig) {
284            return this.startProfileMonitorFor(SignatureHelper.getAsString(sig));
285        }
286    
287        /**
288         * Start profile monitor for the given signature.
289         *
290         * @param sig the signature
291         * @return the profile monitor
292         */
293        public ProfileMonitor startProfileMonitorFor(final String sig) {
294            ProfileMonitor mon = null;
295            if (jamonAvailable) {
296                mon = JamonMonitorFactory.getMonitor(sig, rootMonitor);
297            } else {
298                SimpleProfileMonitor parent = this.getSimpleProfileMonitor(sig);
299                mon = new SimpleProfileMonitor(sig, parent);
300            }
301            mon.start();
302            return mon;
303        }
304    
305        private synchronized ProfileMonitor getMonitor(final Signature sig) {
306            if (jamonAvailable) {
307                return JamonMonitorFactory.getMonitor(sig, this.rootMonitor);
308            } else {
309                return getSimpleProfileMonitor(sig);
310            }
311        }
312    
313        private SimpleProfileMonitor getSimpleProfileMonitor(final Signature sig) {
314            return this.getSimpleProfileMonitor(SignatureHelper.getAsString(sig));
315        }
316    
317        private SimpleProfileMonitor getSimpleProfileMonitor(final String sig) {
318            SimpleProfileMonitor monitor = rootMonitor.getMonitor(sig);
319            if (monitor == null) {
320                monitor = new SimpleProfileMonitor(sig, rootMonitor);
321            }
322            return monitor;
323        }
324    
325        private ProfileMonitor[] getMonitors() {
326            if (jamonAvailable) {
327                return JamonMonitorFactory.getMonitors(this.rootMonitor);
328            } else {
329                return rootMonitor.getMonitors();
330            }
331        }
332    
333        /**
334         * Gets the sorted monitors.
335         *
336         * @return monitors sorted after total time (descending order)
337         */
338        protected final ProfileMonitor[] getSortedMonitors() {
339            ProfileMonitor[] monitors = getMonitors();
340            Arrays.sort(monitors);
341            return monitors;
342        }
343    
344        private ProfileMonitor getMaxHitsMonitor() {
345            ProfileMonitor[] monitors = getMonitors();
346            ProfileMonitor max = new SimpleProfileMonitor();
347            for (int i = 0; i < monitors.length; i++) {
348                if (monitors[i].getHits() >= max.getHits()) {
349                    max = monitors[i];
350                }
351            }
352            return max;
353        }
354    
355        /**
356         * Gets the max hits.
357         *
358         * @return the max hits
359         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxHits()
360         */
361        public int getMaxHits() {
362            return getMaxHitsMonitor().getHits();
363        }
364    
365        /**
366         * Gets the max hits label.
367         *
368         * @return the max hits label
369         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxHitsLabel()
370         */
371        public String getMaxHitsLabel() {
372            return getMaxHitsMonitor().getLabel();
373        }
374    
375        /**
376         * Gets the max hits statistic.
377         *
378         * @return the max hits statistic
379         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxHitsStatistic()
380         */
381        public String getMaxHitsStatistic() {
382            return getMaxHitsMonitor().toShortString();
383        }
384    
385        private ProfileMonitor getMaxTotalMonitor() {
386            ProfileMonitor[] monitors = getMonitors();
387            ProfileMonitor max = new SimpleProfileMonitor();
388            for (int i = 0; i < monitors.length; i++) {
389                if (monitors[i].getTotal() >= max.getTotal()) {
390                    max = monitors[i];
391                }
392            }
393            return max;
394        }
395    
396        /**
397         * Gets the max total.
398         *
399         * @return the max total
400         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxTotal()
401         */
402        public double getMaxTotal() {
403            return getMaxTotalMonitor().getTotal();
404        }
405    
406        /**
407         * Gets the max total label.
408         *
409         * @return the max total label
410         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxTotalLabel()
411         */
412        public String getMaxTotalLabel() {
413            return getMaxTotalMonitor().getLabel();
414        }
415    
416        /**
417         * Gets the max total statistic.
418         *
419         * @return the max total statistic
420         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxTotalStatistic()
421         */
422        public String getMaxTotalStatistic() {
423            return getMaxTotalMonitor().toShortString();
424        }
425    
426        private ProfileMonitor getMaxAvgMonitor() {
427            ProfileMonitor[] monitors = getMonitors();
428            ProfileMonitor max = monitors[0];
429            double maxValue = 0.0;
430            for (int i = 0; i < monitors.length; i++) {
431                double value = monitors[i].getAvg();
432                if (!Double.isNaN(value) && (value > maxValue)) {
433                    maxValue = value;
434                    max = monitors[i];
435                }
436            }
437            return max;
438        }
439    
440        /**
441         * Gets the root monitor.
442         *
443         * @return the root monitor
444         */
445        protected ProfileMonitor getRootMonitor() {
446            return this.rootMonitor;
447        }
448    
449        /**
450         * Gets the max avg.
451         *
452         * @return the max avg
453         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxAvg()
454         */
455        public double getMaxAvg() {
456            return getMaxAvgMonitor().getAvg();
457        }
458    
459        /**
460         * Gets the max avg label.
461         *
462         * @return the max avg label
463         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxAvgLabel()
464         */
465        public String getMaxAvgLabel() {
466            return getMaxAvgMonitor().getLabel();
467        }
468    
469        /**
470         * Gets the max avg statistic.
471         *
472         * @return the max avg statistic
473         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxAvgStatistic()
474         */
475        public String getMaxAvgStatistic() {
476            return getMaxAvgMonitor().toShortString();
477        }
478    
479        private ProfileMonitor getMaxMaxMonitor() {
480            ProfileMonitor[] monitors = getMonitors();
481            ProfileMonitor max = new SimpleProfileMonitor();
482            for (int i = 0; i < monitors.length; i++) {
483                if (monitors[i].getMax() >= max.getMax()) {
484                    max = monitors[i];
485                }
486            }
487            return max;
488        }
489    
490        /**
491         * Gets the max max.
492         *
493         * @return the max max
494         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxMax()
495         */
496        public double getMaxMax() {
497            return getMaxMaxMonitor().getMax();
498        }
499    
500        /**
501         * Gets the max max label.
502         *
503         * @return the max max label
504         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxMaxLabel()
505         */
506        public String getMaxMaxLabel() {
507            return getMaxMaxMonitor().getLabel();
508        }
509    
510        /**
511         * Gets the max max statistic.
512         *
513         * @return the max max statistic
514         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getMaxMaxStatistic()
515         */
516        public String getMaxMaxStatistic() {
517            return getMaxMaxMonitor().toShortString();
518        }
519    
520        /**
521         * Gets the statistics.
522         *
523         * @return the statistics
524         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#getStatistics()
525         */
526        @SuppressWarnings("rawtypes")
527        public TabularData getStatistics() {
528            try {
529                String[] itemNames = { "Label", "Units", "Hits", "Avg", "Total",
530                        "Min", "Max" };
531                String[] itemDescriptions = { "method name", "time unit (e.g. ms)",
532                        "number of hits", "average time", "total time",
533                        "minimal time", "maximal time" };
534                OpenType[] itemTypes = { SimpleType.STRING, SimpleType.STRING,
535                        SimpleType.INTEGER, SimpleType.DOUBLE, SimpleType.DOUBLE,
536                        SimpleType.DOUBLE, SimpleType.DOUBLE };
537                CompositeType rowType = new CompositeType("propertyType",
538                        "property entry", itemNames, itemDescriptions, itemTypes);
539                TabularDataSupport data = MBeanHelper.createTabularDataSupport(rowType, itemNames);
540                ProfileMonitor[] monitors = getSortedMonitors();
541                if (monitors == null) {
542                    log.warn("can't find monitors");
543                    return null;
544                }
545                for (int i = 0; i < monitors.length; i++) {
546                    Map<String, Object> map = new HashMap<String, Object>();
547                    map.put("Label", monitors[i].getLabel());
548                    map.put("Units", monitors[i].getUnits());
549                    map.put("Hits", monitors[i].getHits());
550                    map.put("Avg", monitors[i].getAvg());
551                    map.put("Total", monitors[i].getTotal());
552                    map.put("Min", monitors[i].getMin());
553                    map.put("Max", monitors[i].getMax());
554                    CompositeDataSupport compData = new CompositeDataSupport(
555                            rowType, map);
556                    data.put(compData);
557                }
558                return data;
559            } catch (OpenDataException e) {
560                log.error("can't create TabularData for log settings", e);
561                return null;
562            }
563        }
564    
565        /**
566         * Log statistic.
567         *
568         * @see patterntesting.runtime.monitor.ProfileStatisticMBean#logStatistic()
569         */
570        public void logStatistic() {
571            log.info("----- Profile Statistic -----");
572            ProfileMonitor[] monitors = getSortedMonitors();
573            for (ProfileMonitor profMon : monitors) {
574                log.info("{}", profMon);
575            }
576        }
577    
578        /**
579         * Dump statistic to a file in the temporary directory. Since 1.4.2 the
580         * filename is no longer "profile###.csv", but begins with the classname.
581         * The reason is located in the subclass SqlStatistic - now you can see
582         * which CSV file belongs to which statistic.
583         *
584         * @see ProfileStatisticMBean#dumpStatistic()
585         */
586        public void dumpStatistic() {
587            try {
588                File dumpFile = File.createTempFile(this.getClass().getSimpleName(), ".csv");
589                dumpStatisticTo(dumpFile);
590            } catch (IOException ioe) {
591                log.warn("Cannot dump statistic.", ioe);
592            }
593        }
594    
595        /**
596         * Dump statistic to the given file
597         *
598         * @param dumpFile the dump file
599         * @throws IOException Signals that an I/O exception has occurred.
600         */
601        public void dumpStatisticTo(final File dumpFile) throws IOException {
602            ProfileMonitor[] monitors = getSortedMonitors();
603            if (monitors.length == 0) {
604                log.debug("no profiling data available");
605                return;
606            }
607            BufferedWriter writer = new BufferedWriter(new FileWriter(dumpFile));
608            writer.write(monitors[0].toCsvHeadline());
609            writer.newLine();
610            for (ProfileMonitor profMon : monitors) {
611                writer.write(profMon.toCsvString());
612                writer.newLine();
613            }
614            writer.close();
615            log.info("profiling data dumped to " + dumpFile);
616        }
617    
618        /**
619         * Do you want to look for the monitor of a given method? Use this
620         * method here.
621         *
622         * @param clazz the clazz
623         * @param method the method name, including parameter
624         * e.g. "getProfileMonitor(Class,String)"
625         *
626         * @return monitor of the given class or null
627         */
628        @MayReturnNull
629        public ProfileMonitor getProfileMonitor(final Class<?> clazz, final String method) {
630            return this.getProfileMonitor(clazz.getName() + "." + method);
631        }
632    
633        /**
634         * Do you want to look for the monitor of a given method? Use this method
635         * here.
636         *
637         * @param signature e.g. "hello.World(String[])"
638         *
639         * @return monitor for the given signature or null
640         * @since 1.4.2
641         */
642        @MayReturnNull
643        public ProfileMonitor getProfileMonitor(final Signature signature) {
644            return this.getProfileMonitor(SignatureHelper.getAsString(signature));
645        }
646    
647        /**
648         * Do you want to look for the monitor of a given method? Use this
649         * method here.
650         *
651         * @param signature e.g. "hello.World(String[])"
652         *
653         * @return monitor for the given signature or null
654         */
655        @MayReturnNull
656        public ProfileMonitor getProfileMonitor(final String signature) {
657            for (ProfileMonitor profMon : this.getMonitors()) {
658                if (signature.equals(profMon.getLabel())) {
659                    return profMon;
660                }
661            }
662            log.trace("no ProfileMonitor for " + signature + " found");
663            return null;
664        }
665    
666        /**
667         * To string.
668         *
669         * @return the string
670         * @see java.lang.Thread#toString()
671         */
672        @Override
673        public String toString() {
674            return this.mbeanName.toString();
675        }
676    
677    }