diff --git a/README.md b/README.md index 021ccd6..2edfcde 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,11 @@ The Java Thread Dump Analyzer is licensed under the copyright belongs to Spotify AB. ## TODO +* List top RUNNING methods before everything else. I have a thread + dump taken on a system that was swapping basically getting the GC + stuck. With a list of top RUNNING methods it should be possible to + see this from the analysis. + * Make the Thread class parse held locks, waited-for locks and waited-for condition variables from the thread dump. diff --git a/analyze.js b/analyze.js index 23da3e6..0476ed7 100644 --- a/analyze.js +++ b/analyze.js @@ -16,7 +16,7 @@ limitations under the License. /* global document */ -var EMPTY_STACK = " \n"; +var EMPTY_STACK = " "; // This method is called from HTML so we need to tell JSHint it's not unused function analyzeTextfield() { // jshint ignore: line @@ -86,22 +86,16 @@ function Thread(line) { if (line.match(FRAME) === null) { return false; } - this._frames.push(line); + this.frames.push(line); return true; }; this.toStackString = function() { - var string = ""; - for (var i = 0; i < this._frames.length; i++) { - var frame = this._frames[i]; - string += frame + '\n'; - } - - if (string === '') { + if (this.frames.length === 0) { return EMPTY_STACK; } - return string; + return this.frames.join('\n'); }; this.toHeaderString = function() { @@ -156,28 +150,7 @@ function Thread(line) { return undefined; } - this._frames = []; -} - -function toStackWithHeadersString(stack, threads) { - var string = ''; - if (threads.length > 4) { - string += "" + threads.length + " threads with this stack:\n"; - } - - // Print thread headers for this stack in alphabetic order - var headers = []; - for (var k = 0; k < threads.length; k++) { - headers.push(threads[k].toHeaderString()); - } - headers.sort(); - for (var l = 0; l < headers.length; l++) { - string += headers[l] + '\n'; - } - - string += stack; - - return string; + this.frames = []; } function StringCounter() { @@ -252,7 +225,10 @@ function Analyzer(text) { } }; - this.toString = function() { + // Returns an array [{threads:, stackFrames:,} ...]. The threads: + // field contains an array of Threads. The stackFrames contain an + // array of strings + this._toThreadsAndStacks = function() { // Map stacks to which threads have them var stacksToThreads = {}; for (var i = 0; i < this.threads.length; i++) { @@ -301,12 +277,55 @@ function Analyzer(text) { // Iterate over stacks and for each stack, print first all // threads that have it, and then the stack itself. - var asString = ""; - asString += "" + this.threads.length + " threads found:\n"; + var threadsAndStacks = []; for (var j = 0; j < stacks.length; j++) { var currentStack = stacks[j]; var threads = stacksToThreads[currentStack]; - asString += '\n' + toStackWithHeadersString(currentStack, threads); + + threads.sort(function(a, b){ + var aHeader = a.toHeaderString(); + var bHeader = b.toHeaderString(); + if (aHeader > bHeader) { + return 1; + } + if (aHeader < bHeader) { + return -1; + } + return 0; + }); + + threadsAndStacks.push({ + threads: threads, + stackFrames: currentStack.split('\n') + }); + } + + return threadsAndStacks; + }; + + this.toString = function() { + var threadsAndStacks = this._toThreadsAndStacks(); + + var asString = ""; + asString += "" + this.threads.length + " threads found:\n"; + for (var i = 0; i < threadsAndStacks.length; i++) { + var currentThreadsAndStack = threadsAndStacks[i]; + var stackFrames = currentThreadsAndStack.stackFrames; + var threads = currentThreadsAndStack.threads; + + asString += '\n'; + + if (threads.length > 4) { + asString += "" + threads.length + " threads with this stack:\n"; + } + + for (var j = 0; j < threads.length; j++) { + asString += threads[j].toHeaderString() + '\n'; + } + + for (var k = 0; k < stackFrames.length; k++) { + asString += stackFrames[k] + "\n"; + } } return asString; diff --git a/tests.js b/tests.js index 3f420fe..6c890a9 100644 --- a/tests.js +++ b/tests.js @@ -89,19 +89,78 @@ QUnit.test( "multiline thread name", function(assert) { assert.equal(threads.length, 1); var threadLines = threads[0].toString().split('\n'); - assert.equal(threadLines.length, 3); - assert.equal(threadLines[0], '"line 1, line 2": runnable'); - assert.equal(threadLines[1], ' '); - assert.equal(threadLines[2], ''); + assert.deepEqual(threadLines, [ + '"line 1, line 2": runnable', + ' ' + ]); // Test the Analyzer's toString() method as well now that we have an Analyzer var analysisLines = analyzer.toString().split('\n'); - assert.equal(analysisLines.length, 5); - assert.equal(analysisLines[0], "1 threads found:"); - assert.equal(analysisLines[1], ""); - assert.equal(analysisLines[2], '"line 1, line 2": runnable'); - assert.equal(analysisLines[3], ' '); - assert.equal(analysisLines[4], ""); + assert.deepEqual(analysisLines, [ + "1 threads found:", + "", + '"line 1, line 2": runnable', + " ", + "" + ]); +}); + +QUnit.test( "analyze stackless thread", function(assert) { + var threadDump = '"thread name" prio=10 tid=0x00007f16a118e000 nid=0x6e5a runnable [0x00007f18b91d0000]'; + var analyzer = new Analyzer(threadDump); + var threads = analyzer.threads; + assert.equal(threads.length, 1); + var thread = threads[0]; + + var analysisResult = analyzer._toThreadsAndStacks(); + assert.deepEqual(analysisResult, [{ + threads: [thread], + stackFrames: [" "] + }]); +}); + +QUnit.test( "analyze single thread", function(assert) { + var threadDump = [ + '"thread name" prio=10 tid=0x00007f16a118e000 nid=0x6e5a runnable [0x00007f18b91d0000]', + ' at fluff' + ].join('\n'); + var analyzer = new Analyzer(threadDump); + var threads = analyzer.threads; + assert.equal(threads.length, 1); + var thread = threads[0]; + + var analysisResult = analyzer._toThreadsAndStacks(); + assert.deepEqual(analysisResult, [{ + threads: [thread], + stackFrames: [" at fluff"] + }]); +}); + +QUnit.test( "analyze two threads with same stack", function(assert) { + // Thread dump with zebra before aardvark + var threadDump = [ + '"zebra thread" prio=10 tid=0x00007f16a118e000 nid=0x6e5a runnable [0x00007f18b91d0000]', + ' at fluff', + "", + '"aardvark thread" prio=10 tid=0x00007f16a118e000 nid=0x6e5a runnable [0x00007f18b91d0000]', + ' at fluff' + ].join('\n'); + + var analyzer = new Analyzer(threadDump); + + var threads = analyzer.threads; + assert.equal(threads.length, 2); + var zebra = threads[0]; + assert.equal(zebra.name, "zebra thread"); + var aardvark = threads[1]; + assert.equal(aardvark.name, "aardvark thread"); + + var analysisResult = analyzer._toThreadsAndStacks(); + assert.deepEqual(analysisResult, [{ + // Make sure the aardvark comes before the zebra + threads: [aardvark, zebra], + stackFrames: [" at fluff"] + }]); }); QUnit.test( "thread stack", function(assert) { @@ -115,11 +174,11 @@ QUnit.test( "thread stack", function(assert) { // When adding stack frames we should just ignore unsupported // lines, and the end result should contain only supported data. var threadLines = thread.toString().split('\n'); - assert.equal(threadLines.length, 4); - assert.equal(threadLines[0], '"Thread name": sleeping'); - assert.equal(threadLines[1], " at java.security.AccessController.doPrivileged(Native Method)"); - assert.equal(threadLines[2], " at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:353)"); - assert.equal(threadLines[3], ""); + assert.deepEqual(threadLines, [ + '"Thread name": sleeping', + " at java.security.AccessController.doPrivileged(Native Method)", + " at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:353)" + ]); }); function unescapeHtml(escaped) { @@ -132,10 +191,10 @@ QUnit.test( "full dump analysis", function(assert) { var input = document.getElementById("sample-input").innerHTML; var expectedOutput = unescapeHtml(document.getElementById("sample-analysis").innerHTML); var analyzer = new Analyzer(input); - assert.equal(analyzer.toString(), expectedOutput); + assert.deepEqual(analyzer.toString().split('\n'), expectedOutput.split('\n')); var expectedIgnores = document.getElementById("sample-ignored").innerHTML; - assert.equal(analyzer.toIgnoresString(), expectedIgnores); + assert.deepEqual(analyzer.toIgnoresString().split('\n'), expectedIgnores.split('\n')); }); QUnit.test("extract regex from string", function(assert) {