SLF4J extensions
SLF4J extensions are packaged within slf4j-ext.jar which ships with SLF4J.
Profilers
What is a profiler?
According to wikipedia, profiling is the investigation of a program's behavior using information gathered as the program runs, i.e. it is a form of dynamic program analysis, as opposed to static code analysis. The usual goal of performance analysis is to determine which parts of a program to optimize for speed or memory usage.
SLF4J profilers, a.k.a. poor man's profilers, will help the developer gather performance data. Essentially, a profiler consists of one or more stopwatches. Stopwatches are driven (started/stopped) by statements in the source code. An example should make the point clearer.
Basic example
Example: Using the profiler: BasicProfilerDemo[omitted] 32 public class BasicProfilerDemo { 33 34 public static void main(String[] args) { 35 // create a profiler called "BASIC" 36 Profiler profiler = new Profiler("BASIC"); 37 profiler.start("A"); 38 doA(); 39 40 profiler.start("B"); 41 doB(); 42 43 profiler.start("OTHER"); 44 doOther(); 45 profiler.stop().print(); 46 } [omitted]
Running the above example will output the following output.
+ Profiler [BASIC] |-- elapsed time [A] 220.487 milliseconds. |-- elapsed time [B] 2499.866 milliseconds. |-- elapsed time [OTHER] 3300.745 milliseconds. |-- Total [BASIC] 6022.568 milliseconds.
Instantiating a profiler starts a global stopwatch. Each call to
the start() method starts a new and named stopwatch. In addition to
starting a named stopwatch, the start() method also causes the
previous stopwatch to stop. Thus, the call to
profiler.start("A")
starts a stopwatch named "A". The
subsequent call to profiler.start("B")
starts
stopwatch "B" and simultaneously stops the stopwatch named
"A". Invoking the stop()
on a profiler method stops
the last stopwatch as well as the global stopwatch which was
started when the profiler was instantiated.
Profiler nesting
Profilers can also be nested. By nesting profilers, it is possible to measure a task which itself has subtasks that need to be timed and measured.
Starting a nested profiler will stop any previously started stopwatch or nested profiler associated with the parent profiler.
Often times, the subtask is implemented by a different class as
the class hosting the parent profiler. Using the
ProfilerRegistry
is a convenient way of passing a
nested profiler to an object outside the current object. Each
thread has its own profiler registry which can be retrieved by
invoking the getThreadContextInstance()
method.
33 public class NestedProfilerDemo { 34 35 public static void main(String[] args) { 36 // create a profiler called "DEMO" 37 Profiler profiler = new Profiler("DEMO"); 38 39 // register this profiler in the thread context's profiler registry 40 ProfilerRegistry profilerRegistry = ProfilerRegistry.getThreadContextInstance(); 41 profiler.registerWith(profilerRegistry); 42 43 // start a stopwatch called "RANDOM" 44 profiler.start("RANDOM"); 45 RandomIntegerArrayGenerator riaGenerator = new RandomIntegerArrayGenerator(); 46 int n = 1000*1000; 47 int[] randomArray = riaGenerator.generate(n); 48 49 // create and start a nested profiler called "SORT_AND_PRUNE" 50 // By virtue of its parent-child relationship with the "DEMO" 51 // profiler, and the previous registration of the parent profiler, 52 // this nested profiler will be automatically registered 53 // with the thread context's profiler registry 54 profiler.startNested(SortAndPruneComposites.NESTED_PROFILER_NAME); 55 56 SortAndPruneComposites pruner = new SortAndPruneComposites(randomArray); 57 pruner.sortAndPruneComposites(); 58 59 // stop and print the "DEMO" printer 60 profiler.stop().print(); 61 } 62 }
Here is the relevant excerpt from the SortAndPruneComposites class.
[omitted] 6 public class SortAndPruneComposites { 7 8 static String NESTED_PROFILER_NAME = "SORT_AND_PRUNE"; 9 10 final int[] originalArray; 11 final int originalArrrayLength; 12 13 public SortAndPruneComposites(int[] randomArray) { 14 this.originalArray = randomArray; 15 this.originalArrrayLength = randomArray.length; 16 17 } 18 19 public int[] sortAndPruneComposites() { 20 // retrieve previously registered profiler named "SORT_AND_PRUNE" 21 ProfilerRegistry profilerRegistry = ProfilerRegistry.getThreadContextInstance(); 22 Profiler sortProfiler = profilerRegistry.get(NESTED_PROFILER_NAME); 23 24 // start a new stopwatch called SORT 25 sortProfiler.start("SORT"); 26 int[] sortedArray = sort(); 27 // start a new stopwatch called PRUNE_COMPOSITES 28 sortProfiler.start("PRUNE_COMPOSITES"); 29 int result[] = pruneComposites(sortedArray); 30 31 return result; 32 } [omitted]
On a Dual-Core Intel CPU clocked at 3.2 GHz, running the
ProfilerDemo
application yields the following output:
+ Profiler [DEMO] |-- elapsed time [RANDOM] 70.524 milliseconds. |---+ Profiler [SORT_AND_PRUNE] |-- elapsed time [SORT] 665.281 milliseconds. |-- elapsed time [PRUNE_COMPOSITES] 5695.515 milliseconds. |-- Subtotal [SORT_AND_PRUNE] 6360.866 milliseconds. |-- elapsed time [SORT_AND_PRUNE] 6360.866 milliseconds. |-- Total [DEMO] 6433.922 milliseconds.
From the above, we learn that generating 1'000'000 random integers takes 70 ms, sorting them 665 ms, and pruning the composite (non-prime) integers 5695 ms, for a grand total of 6433 ms. Given that pruning composites takes most of the CPU effort, any future optimizations efforts would be directed at the pruning part.
With just a few well-placed profiler calls we were able to identify hot-spots in our application. Also note that passing a profiler to a target class could be achieved by registering it in a profiler registry and then retrieving it in the target class.
Printing using a logger
Invoking profiler.print
will always print the
output on the console. If you wish to leave the profiler code in
production, then you probably need more control over the output
destination. This can be accomplished by associating a logger of
your choice with a profiler.
After you have associated a logger with a profiler, you would
invoke the log()
method instead of print()
previously, as the next example illustrates.
[omitted] 17 public class NestedProfilerDemo2 { 18 19 static Logger logger = LoggerFactory.getLogger(NestedProfilerDemo2.class); 20 21 public static void main(String[] args) { 22 Profiler profiler = new Profiler("DEMO"); 23 // associate a logger with the profiler 24 profiler.setLogger(logger); 25 26 ProfilerRegistry profilerRegistry = ProfilerRegistry.getThreadContextInstance(); 27 profiler.registerWith(profilerRegistry); 28 29 profiler.start("RANDOM"); 30 RandomIntegerArrayGenerator riaGenerator = new RandomIntegerArrayGenerator(); 31 int n = 10*1000; 32 int[] randomArray = riaGenerator.generate(n); 33 34 profiler.startNested(SortAndPruneComposites.NESTED_PROFILER_NAME); 35 36 SortAndPruneComposites pruner = new SortAndPruneComposites(randomArray); 37 pruner.sortAndPruneComposites(); 38 39 // stop and log 40 profiler.stop().log(); 41 } 42 }
The output generated by this example will depend on the logging
environment, but should be very similar to the output generated by
the previous NestedProfilerDemo
example.
The log() method logs at level DEBUG using a marker named "PROFILER".
If your logging system supports markers, e.g. logback, you could specifically enable or disable output generated by SLF4J profilers. Here is logback configuration file disabling output for any logging event bearing the "PROFILER" marker, even if the logger used by the profiler is enabled for the debug level.
logback configuration disabling logging from profilers, and only profilers<configuration> <turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter"> <Marker>PROFILER</Marker> <OnMatch>DENY</OnMatch> </turboFilter> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%-5level %logger{36} - %msg%n</Pattern> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="STDOUT" /> </root> </configuration>