More‎ > ‎

YACA-Monitor 3.0 - Do you Know What Happens in Your Java EE Application Server?

BY MARKUS SPRUNCK


As a software developer you spend a large portion of your time with the attempt to understand existing legacy source code, frameworks, medium level design and the typical call dependencies in a software.

Debugging application to find out the call graph and control flow is extremely time consuming. This is especially true for enterprise applications. You set a break-point and analyze the call stack to find the way across all the layers and components of the program. Java EE application server use proxies and long calling chains.

Getting Started in 3 Minutes

  1. Download ZIP from Github and unzip yaca-monitor-jee-master.zip

  2. Just copy the file ./yaca-monitor-jee-master\yaca-monitor-jee-test\target\yaca-monitor-jee.war into autodeploy folder of your application server (e.g. Glassfish, Wildfly, WebSphere Application Server V8.5 Liberty Profile) 

  3. Open URL http://<your-domain>:<your-port>/yaca-monitor-jee/  

Implementation - Environment

The new version 3.0 of YACA-Monitor is a Java EE 6 Web Application that visualizes the current activities in a running server. Just drop the yaca-monitor-jee.war into auto deploy folder of your application server, start the monitor and see the call graphs of your applications. The call graph is rendered with WebGL in 3D, based on Barnes-Hut N-Body-Simulation algorithm.

Development:
Project for yaca-monitor-jee-test.war - Java EE 6 Web Application (yaca-monitor-jee.war) with the project facets:
  • Dynamic Web Module 3.0
  • Java 1.7
  • JavaScript 1.0
Project for yaca-monitor-jee-test.war - is a minimal test-client with the project facets:
  • Dynamic Web Module 3.0
  • Java 1.7
  • JavaScript 1.0
  • Java Server Faces 2.0
The application has two parts:
  • A web interface which renders the model data and 
  • Java code that collects data from call stacks of all threads and provides a RESTful service interface.

Implementation - Source Code

The class AnalyserResource (lines 18-87) is the boundary to the YACA web client.  This simple REST interface offers the all services which are needed to start, stop and reset the analysis. 

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/**
 * REST interface for YACA-Monitor web page.
 */
@Path("/analyser")
public class AnalyserResource {

	private static final Logger LOGGER = Logger.getLogger(AnalyserResource.class.getName());

	private Model model;

	@GET
	@Path("model")
	@Produces(MediaType.APPLICATION_JSON)
	public String getJSONModel() {
		return getModel().getJSONPModel();
	}

	@PUT
	@Path("filter")
	@Consumes(MediaType.TEXT_PLAIN)
	@Produces(MediaType.TEXT_PLAIN)
	public String setFilter(final String message) {
		if (null != getModel()) {
			final String filter = null == message ? "" : message;
			getModel().setFilter(filter);
			getModel().reset();
			return "Boundary - Analyser filter put '" + filter + "'";
		} else {
			return "ERROR";
		}
	}

	@DELETE
	@Path("filter")
	@Produces(MediaType.TEXT_PLAIN)
	public String deleteFilter() {
		getModel().setFilter("");
		return "Boundary - Analyser filter delete";
	}

	@PUT
	@Path("tasks/task")
	@Produces(MediaType.TEXT_PLAIN)
	public String start() {
		getModel().getExecutorService().execute(new Analyser(getModel()));
		return "Boundary - Analyser new task created";
	}

	@DELETE
	@Path("tasks")
	@Produces(MediaType.TEXT_PLAIN)
	public String stop() {
		getModel().setToBeStopped(true);
		return "Boundary - Analyser all tasks marked to be deleted";
	}

	private Model getModel() {
		if (null == model) {
			try {
				final InitialContext ic = new InitialContext();
				model = (Model) ic.lookup("java:module/Model");
			} catch (final NamingException e) {
				LOGGER.log(Level.SEVERE, "Can't find Model bean -> {0}.", e.getMessage());
			}
		}
		return model;
	}

}

The class Analyser (lines 8-74) makes the analysis of the call stack and stores the result in the model. 

  8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/*
 * Runs dynamic code analysis.
 */
public class Analyser implements Runnable {

	private static final Logger LOGGER = Logger.getLogger(Analyser.class.getName());

	private final Model model;

	public Analyser(final Model model) {
		model.setToBeStopped(false);
		this.model = model;
	}

	@Override
	public void run() {
		LOGGER.info("Analyser " + Thread.currentThread().getName());

		final List<Node> entryList = new ArrayList<>(10);
		do {
			// Update filter
			final Pattern pattern = Pattern.compile(model.getFilter());

			// Get current threads
			final ThreadGroup currentGroup = Thread.currentThread().getThreadGroup();
			final Thread[] threads = new Thread[currentGroup.activeCount()];
			currentGroup.enumerate(threads);

			// Process all threads
			for (final Thread thread : threads) {

				// Analyse just active threads
				if ((null != thread) && Thread.State.RUNNABLE.equals(thread.getState())) {

					// Read all entries of current stack trace
					for (final StackTraceElement trace : thread.getStackTrace()) {
						final String clazz = trace.getClassName();
						final String method = trace.getMethodName();
						final String alias = String.format("%s.%s", clazz, method);
						final String filterValue = String.format("%s:%d", alias, 
                                                        thread.getId());

						// Test filter
						if (pattern.matcher(filterValue).find()) {
							final int index = clazz.lastIndexOf('.');
							final String clazzShort = clazz.substring(index + 1, 
                                                        
                                                                clazz.length());
							final Node node = new Node(clazzShort, method, alias);
							entryList.add(node);
						}
					}

					// Add nodes in the case a link exists
					if (entryList.size() > 1) {
						model.append(entryList);
						entryList.clear();
					}
				}
			}
		} while (!model.isToBeStopped());

		LOGGER.log(Level.INFO, "Analyser {0} stopped", Thread.currentThread().getName());
	}

}

The class Model (line 19-278) stores all analysis results and creates JSON file format for the client.

 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
/**
 * Stores data from code analysis and provides the result in the JSON format.
 */
@Startup
@Singleton
public class Model {

	private static final Logger LOGGER = Logger.getLogger(Model.class.getName());

	private static final int EXPECTED_NUMBER_OF_CLUSTERS = 5000;

	private static final int EXPECTED_NUMBER_OF_LINKS = 10000;

	private static final int EXPECTED_NUMBER_OF_NODES = 5000;

	/**
	 * The list clusterIds is used to find the correct ids for the creation of
	 * the JSON file
	 */
	private final List<String> clusterIds = new ArrayList<String>(EXPECTED_NUMBER_OF_CLUSTERS);

	/**
	 * The list clusterIds is used to find the correct ids for the creation of
	 * the JSON file
	 */
	private final List<String> linkIds = new ArrayList<String>(EXPECTED_NUMBER_OF_LINKS);

	/**
	 * The map links all created links
	 */
	private final Map<String, String> links = new HashMap<String, String>(EXPECTED_NUMBER_OF_LINKS);

	/**
	 * The list nodeIds is used to find the correct ids for the creation of the
	 * JSON file
	 */
	private final List<String> nodeIds = new ArrayList<String>(EXPECTED_NUMBER_OF_NODES);

	/**
	 * The map stores all created nodes
	 */
	private final Map<String, String> nodes = new HashMap<String, String>(EXPECTED_NUMBER_OF_NODES);

	/**
	 * The list is used to count the finding of each link
	 */
	private final Map<String, Long> nodesCount = new HashMap<String, Long>(EXPECTED_NUMBER_OF_NODES);

	/**
	 * Just for logging purposes
	 */
	private long numberOfUpdates = 0L;

	/**
	 * This filter is used in analyser-task
	 */
	private String filter = "";

	/**
	 * This flag is used to stop analyser-task
	 */
	private boolean isToBeStopped = false;

	/**
	 * This thread pool is used to run the analyser-task
	 */
	private static final ExecutorService POOL = Executors.newCachedThreadPool();

	// //////////////////////////////////////////////////////////////////////////
	// Public

	/**
	 * Method updateLinks analyzes the call stack of all threads and collects
	 * the data in the class ResultData.
	 *
	 * @param entryList
	 *            all entries to be added
	 */
	@Lock(LockType.WRITE)
	public void append(final List<Node> entryList) {
		numberOfUpdates++;
		final int maxIndex = entryList.size() - 1;
		if (maxIndex > 0) {
			for (int i = 0; i < maxIndex; i++) {
				add(entryList.get(i), entryList.get(i + 1));
			}
		}
	}

	public ExecutorService getExecutorService() {
		return POOL;
	}

	@Lock(LockType.READ)
	public String getFilter() {
		return filter;
	}

	@Lock(LockType.READ)
	public String getJSONPModel() {

		final String NL = System.getProperty("line.separator");

		final StringBuffer fw = new StringBuffer();

		fw.append("{").append(NL);

		// Add all nodes
		boolean isFrist = true;
		fw.append("\"nodes\":[").append(NL);
		for (final String key : nodeIds) {
			if (nodes.containsKey(key)) {
				fw.append(isFrist ? "" : "," + NL).append("");
				isFrist = false;
				fw.append(String.format(Locale.ENGLISH, nodes.get(key), nodesCount.get(key)));
			}
		}
		fw.append(NL).append("],").append(NL);

		// Add all links
		fw.append("\"links\":[").append(NL);
		isFrist = true;
		for (final String key : linkIds) {
			if (links.containsKey(key)) {
				fw.append(isFrist ? "" : "," + NL).append("");
				isFrist = false;
				fw.append(links.get(key));
			}
		}

		fw.append(NL).append("]}").append(NL);

		if (LOGGER.isLoggable(Level.FINE)) {
			final StringBuffer message = new StringBuffer(200);
			message.append("updates=").append(numberOfUpdates);
			message.append(", clusters=").append(clusterIds.size());
			message.append(", nodes=").append(nodeIds.size());
			message.append(", links=").append(links.size());
			LOGGER.log(Level.FINE, message.toString());
		}

		resetCouters();

		return fw.toString();
	}

	@PostConstruct
	public void initialize() {
		LOGGER.log(Level.INFO, "Model initialized ({0})", Thread.currentThread().getName());
	}

	@Lock(LockType.READ)
	public boolean isToBeStopped() {
		return isToBeStopped;
	}

	@Lock(LockType.WRITE)
	public void reset() {
		LOGGER.info("Reset Model");
		this.resetCouters();
		nodes.clear();
		nodeIds.clear();
		clusterIds.clear();
		linkIds.clear();
		links.clear();
		nodesCount.clear();
	}

	@Lock(LockType.WRITE)
	public void setFilter(final String filter) {
		if (filter.isEmpty()) {
			LOGGER.log(Level.INFO, "Model set empty filter");
		} else {
			LOGGER.log(Level.INFO, "Model set filter={0}", filter);
		}
		this.filter = filter;
	}

	@Lock(LockType.WRITE)
	public void setToBeStopped(final boolean isToBeStopped) {
		this.isToBeStopped = isToBeStopped;
	}

	// //////////////////////////////////////////////////////////////////////////
	// Private

	private void resetCouters() {
		for (final String key : nodesCount.keySet()) {
			nodesCount.put(key, 0L);
		}
		numberOfUpdates = 0L;
	}

	private String getClusterKey(final Node item) {
		return item.getClassName();
	}

	private String getNodeKey(final Node item) {
		return item.getAlias();
	}

	private void incrementNodeCount(final String keyNode) {
		long value = 0L;
		if (nodesCount.containsKey(keyNode)) {
			value = nodesCount.get(keyNode) + 1L;
		}
		nodesCount.put(keyNode, value);
	}

	private void add(final Node targetEntry, final Node sourceEntry) {

		// add target cluster
		final String targetClusterKey = getClusterKey(targetEntry);
		if (!clusterIds.contains(targetClusterKey)) {
			clusterIds.add(targetClusterKey);
		}

		// add source cluster
		final String sourceClusterKey = getClusterKey(sourceEntry);
		if (!clusterIds.contains(sourceClusterKey)) {
			clusterIds.add(sourceClusterKey);
		}

		// add target node
		final String targetKey = getNodeKey(targetEntry);
		if (!nodeIds.contains(targetKey)) {
			nodeIds.add(targetKey);
		}
		String nodeString = "\t{\"id\":" + nodeIds.indexOf(targetKey) + ", \"clusterId\":"
				+ clusterIds.indexOf(targetClusterKey) + ", \"alias\":\""
				+ targetEntry.getClassName() + '.' + targetEntry.getMethodName()
				+ "\" , \"name\":\"" + targetEntry.getAlias() + "\", \"calls\": %d }";
		nodes.put(targetKey, nodeString);
		incrementNodeCount(targetKey);

		// add source node
		final String sourceKey = getNodeKey(sourceEntry);
		if (!nodeIds.contains(sourceKey)) {
			nodeIds.add(sourceKey);
		}
		nodeString = "\t{\"id\":" + nodeIds.indexOf(sourceKey) + ", \"clusterId\":"
				+ clusterIds.indexOf(sourceClusterKey) + ", \"alias\":\""
				+ sourceEntry.getClassName() + '.' + sourceEntry.getMethodName()
				+ "\" , \"name\":\"" + sourceEntry.getAlias() + "\", \"calls\": %d }";
		nodes.put(sourceKey, nodeString);
		incrementNodeCount(sourceKey);

		// add node link
		final String keyLink = targetKey + "<-" + sourceKey;
		if (!linkIds.contains(keyLink)) {
			linkIds.add(keyLink);
		}
		nodeString = "\t{\"id\":" + linkIds.indexOf(keyLink) + ", \"sourceId\":"
				+ nodeIds.indexOf(sourceKey) + ", \"targetId\":" + nodeIds.indexOf(targetKey)
				+ " }";
		links.put(keyLink, nodeString);

	}

}

The class Node is just a simple POJO used in the model class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.sw_engineering_candies.yaca.control;

/**
 * POJO for Model.
 */
class Node {

	private final String className;

	private final String methodName;

	private final String alias;

	public Node(final String className, final String methodName, final String alias) {
		this.className = className;
		this.methodName = methodName;
		this.alias = alias;
	}

	public String getAlias() {
		return alias;
	}

	public String getClassName() {
		return className;
	}

	public String getMethodName() {
		return methodName;
	}

}

Tested Environments

The YACA-Monitor 3.0 should work in every server that supports Java EE 6 Web Application (Dynamic Web Module 3.0, Java 1.7, JavaScript 1.0). It is is tested with the following Application Servers:
  • Glassfish 4.0
  • WebSphere Application Server V8.5 Liberty Profile
  • Wildfly 8.1
The needed web browser should support WebGL. The monitor is tested with the following web browsers:
  • Chrome 36
  • Firefox 30
Even though Internet Explorer 11 should support WebGL (see http://caniuse.com/webgl), the quality and performance of WebGL rendering is very poor (maybe this changes in the future). Additionally the IE showed problems with the export and import of model files. So, you should avoid to use Internet Explorer 11 in combination with YACA-Monitor 3.0.  

Known Issues:
  • Use browser zoom level 100% - the GUI layout is just correct with this zoom level
  • Export of parameters just updated with active import
Click to Export Model File stores the current model to a file (see Figure 2). You may drop this file into Drop 'Exported Model File'. When you drop a model file the import will be deactivated automatically. 

Check Import Options | Active to activate the update of the new model. The option Import Options | Filter reduces the scope of collected data. If you enter a value just methods with this pattern in the package, class or method name will be collected. 

Figure 2: Options of YACA Monitor Client 

The Filter Activity Index filters by activity. The value zero means 100% of nodes will be displayed and 100 means just the most active node will be shown. 

Filter RegEx Name shows all nodes independent the activity. Here you may use a regular expression. Check Filter RegEx Invert to see all the other nodes which doesn't match the regular expression. 

You may use Add Nodes options to add additional nodes in the call graph. All options in N-Body Simulation are used to arrange the 3D graph. Check N-Body Simulation | Run to start/stop the Barnes-Hut-Simulation.

Find Code on GitHub