Most developers know that J2EE development is hard. With multi-tiered, distributed applications, it is hard to isolate and resolve performance bottlenecks, identify untested code, diagnose the source of memory leaks, and detect intermittent failures. A runtime problem determination solution can help with these challenges.
Runtime problem determination encompasses several techniques, including:
Memory profiling and leak analysis
Performance profiling and analysis
Thread analysis and debugging
Code coverage analysis
In this article, I'll discuss the technologies that IBM Rational Application Developer (formerly named Websphere Application Developer, or WSAD) offers for executing these techniques.
A comprehensive IDE that enables fast development and deployment of Web, Web services, Java, J2EE, and portal applications, Rapid Application Developer, or RAD, is powered by the Eclipse open source platform and supports multi-vendor runtime environments. 1 It helps developers improve code quality with automated tools for applying coding standard reviews, component and Web Service unit testing, and multi-tier runtime analysis.
The runtime problem determination technologies available in RAD are built on top of Hyades, an integrated, Eclipse-based, test, trace, and monitoring environment. Accessible through RAD's profiling and logging perspective, they provide views to help users analyze J2EE and J2SE applications for problems related to memory leaks, performance bottlenecks, and thread bottlenecks -- all possible reasons for poor runtime performance. Other views help users locate gaps in code coverage during profiling runs. And RAD's Probekit provides a framework for writing and inserting Java code fragments in existing code to investigate specific runtime problems. The profiling dialog box is integrated with the run dialog, and pre-defined profiling sets are available to users, as shown in Figure 1.
Figure 1: Pre-defined profiling sets in Rational Application Developer (RAD)
Let's discuss some of the runtime problem determination technologies available in RAD in more detail.
Memory leaks often occur in Java when objects hold on to references inappropriately. Such leaks can degrade performance and eventually cause a program to crash. RAD's profiling and logging perspective includes views for analyzing your application's memory usage and detecting these leaks through advanced algorithms that compare two heap dumps. You can define a profiling configuration to capture these dumps either manually or automatically, at times you specify in the profiling set. You can also import heap dumps in multiple formats. 2
RAD offers the following views for memory leak analysis:
Leak Candidates view: Identifies the objects most likely responsible for memory leaks.
Object Reference Graph, Object References, and Object Details views: Provide information about objects holding onto memory.
Statistical views: Display basic profiling statistics for packages, methods, classes, and instances.
The snapshot in Figure 2 highlights a memory leak (String object is leaking), along with the leak's container type (Vector object), and root (TestThreeTierQueue object). With such highly specific information, users can more readily isolate and fix memory leaks. The Object Reference Graph view and the Object Details view also highlight counts for the ten objects with the greatest leakage, which aids in manual analysis.
Figure 2: RAD views highlight specific memory leak information.
Performance bottlenecks are code segments that prevent an application from running as fast as it should. The profiling and logging perspective includes a number of views that profile runtime performance and help you detect bottlenecks:
Performance Call Graph and Method Details views (see Figure 3)
Package Statistics, Class Statistics, and Method Statistics views
Method Invocation, Method Invocation Table, Execution Flow, Execution Flow Table, and UML2 Sequence Diagram views
In the Performance Call Graph view (Figure 3), the line's thickness indicates the amount of time the application spends along that code path. In this case, it is obvious that the application is spending too much time in the Thread.sleep() method. The Method Detail view confirms this, showing that Thread.sleep() consumes 85 percent of descendant time for the highlighted method. Figure 3 also shows how the Call Graph view highlights the top ten methods by base time consumption.
Figure 3: RAD Performance Call Graph view
Thread bottlenecks, such as contentions and deadlocks, can slow your application or bring it to a halt. The profiling and logging perspective includes tools with views that help you detect and resolve these thread problems:
- Thread View and UML2 Sequence Diagram views, especially the UML2 Object Interactions view
- Execution Flow and Execution Flow Table views
In the Thread View, vertical arrows point from a thread requesting a lock to the thread that holds the lock. A single arrow indicates a thread contention: One thread is waiting for another thread to release a lock. If multiple arrows show a circular lock-request pattern -- threads prevent each other from running because they are waiting for each other to release a lock -- then you have a deadlock. You can hover over vertical arrows to get details on the locks being held.
The Thread View also provides synchronization with the UML2 Sequence Diagram view from Hyades. In the Thread View list, select the thread holding the lock that you want to investigate; the UML2 Object Interactions view will show the selected thread's object interactions, including the lock and requests for it. Then, positioning the Current Time indicator over the arrow for the request you want to investigate will synchronize both the UML2 Object Interactions view and the call stack profiling resource in the profiling monitor.
Figure 4: UML2 Sequence Diagram view in RAD
Probes are Java code fragments you can use to monitor specific characteristics of an application's runtime behavior. RAD offers the Probekit tool for applying this technique. A Probekit file can contain one or more probes, and each probe can contain one or more probe fragments. You can specify which probes you want to use on a given program, and when you want the probe fragments to be executed. Probe fragments are assembled into a set of Java methods, which are then compiled. When a probe is compiled, its probe code fragments are combined with standard boilerplate to create Java source code for a new class. The functions generated from the probe fragments appear as static methods of the generated probe class.
When you apply probes, the byte code insertion (BCI) engine refers to the list of probes and their target patterns and inserts calls to probe fragment methods into the target programs. The process of inserting call statements into target methods is referred to as "instrumentation." The data items requested by a probe fragment (for example, the method name and arguments) are passed as arguments. One benefit of this system is that a probe can be inserted into a large number of target methods with small overhead. The probes can be inserted at method entry, method exit, call-site, exception catch clause, and so forth. Figure 5 shows a probe file with the entry fragment highlighted and opened in the editor.
Figure 5: A source file view in the RAD Probekit displaying a method data fragment
The RAD profiling and logging perspective includes views for identifying untested lines and methods in your code. When you profile a run to analyze performance or thread bottlenecks, you can also monitor coverage to ensure that you have exercised all the appropriate parts of your application, using two types of views:
- Coverage Detail views, available only if you collect both method and line coverage:
- The Coverage Navigator view, which lists the application's classes and methods with their coverage level
- The Annotated Source view, which shows coverage annotations on a copy of your source code
- The Coverage Statistics view
Figure 6: The Annotated Source view in RAD
Using the Annotated Source view, you can see graphs and charts showing coverage percentages of classes, source files, or even entire packages, across multiple runs. This is immensely useful during component testing, as you can identify untested code and check it before it is shipped. Figure 7 shows two files that have not been tested because they contain classes that were never loaded at runtime. In addition, only 63 percent of the file ThreeTierQueue.java has been tested, which is a cause for concern.
Figure 7: RAD Annotated Source view revealing test coverage issues
Probekit collects line-level coverage data using probes, aggregates it using a runtime library, and sends it to the Eclipse-based RAD viewer for display at regular intervals.
In addition to the technologies described above, RAD ships with a log and trace analyzer, an interface that provides a single point of operation to deal with logs and traces produced by various components of a deployed system. Linking the tracing and logging tooling helps bridge the gap between problem determination and both application and middleware debugging.
By capturing and correlating end-to-end events in the distributed stack of a customer application, this tool provides a more structured analysis of distributed application problems. It also makes debugging and resolving system problems easier and faster. With the log and trace analyzer, you can import various log files as well as symptom databases against which to analyze and correlate the log files. In addition, a generic log adapter allows you to process application log files and transform their contents into a common base event format, which provides a standard for logging, management, problem determination, and autonomic computing. Using a consistent format facilitates communication between tools that support the same goals. With common base event objects, you can develop a common prescriptive event in a consistent format and develop tools to support your goals.
Rational Application Developer, or RAD, offers several new technologies to help developers pinpoint the source of runtime performance- and memory-related problems. It provides trace-based analysis for performance bottlenecks and lock contention between threads. Memory analysis allows users to pinpoint memory leaks in Java and J2EE applications. Developers can also get detailed line-level code coverage for components by using the coverage views, and they can use Probekit to create custom probes for debugging or logging. The goal of all these technologies is to make the lives of J2EE developers a little easier.
1These include AIX, HP-UX, z/OS, OS/400, Red Hat Linux, Sun Solaris, SuSe Linux, and Windows 2000/2003/XP, to name a few.
2IBM® heap dump format, Hprof heap dump format, heap dump format for the IBM® OS/390, and Hyades Optimized heap dump format.
Software engineering manager Tanuj Vohra manages the IBM Rational PurifyPlus family of runtime-analysis tools. He is also keenly interested in management and leadership development practices. Before joining Rational as a software engineer in 1997, he held positions at Citibank, N.A., and Computer Maintenance Corporation, India. He earned a bachelors degree in computer science and engineering before leaving India, and then a masters degree in computer science from Michigan Technological University.