From 96af09bdaa341d44719f7aa948c5f455cc03fb48 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Tue, 18 Nov 2014 15:43:29 +0100 Subject: [PATCH 1/8] Rewrite Analyser.toString() Split it into one part that lists threads with stacktraces, and one part that turns that into a string representation. This is a first step towards turning the analysis part of the web page into HTML. Fails one test though, needs debugging. --- analyze.js | 63 ++++++++++++++++++++++++++++++++---------------------- tests.js | 13 +++++------ 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/analyze.js b/analyze.js index 23da3e6..449d28e 100644 --- a/analyze.js +++ b/analyze.js @@ -159,27 +159,6 @@ function Thread(line) { 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; -} - function StringCounter() { this.addString = function(string) { if (!this._stringsToCounts.hasOwnProperty(string)) { @@ -252,7 +231,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 +283,43 @@ 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); + 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\n"; + for (var i = 0; i < threadsAndStacks.length; i++) { + var currentThreadsAndStack = threadsAndStacks[i]; + var stackFrames = currentThreadsAndStack.stackFrames; + var threads = currentThreadsAndStack.threads; + + if (threads.length > 4) { + asString += "" + threads.length + " threads with this stack:\n"; + } + + var headers = []; + for (var j = 0; j < threads.length; j++) { + headers.push(threads[j].toHeaderString()); + } + headers.sort(); + asString += headers.join('\n') + '\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..ab7cfd6 100644 --- a/tests.js +++ b/tests.js @@ -96,12 +96,13 @@ QUnit.test( "multiline thread name", function(assert) { // 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.equal(analysisLines, [ + "1 threads found:", + "", + '"line 1, line 2": runnable', + " ", + "" + ]); }); QUnit.test( "thread stack", function(assert) { From c9ec0fb7124fad428f09536fdae359fc544736e5 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Wed, 19 Nov 2014 21:55:41 +0100 Subject: [PATCH 2/8] Make multiline test diff more readable --- tests.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests.js b/tests.js index ab7cfd6..feab83a 100644 --- a/tests.js +++ b/tests.js @@ -89,10 +89,11 @@ 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.equal(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'); From 30bdf20184435e93967cbd8adb8a05057d566d0c Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 20 Nov 2014 07:41:55 +0100 Subject: [PATCH 3/8] TODO: List top RUNNING methods first --- README.md | 5 +++++ 1 file changed, 5 insertions(+) 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. From e0036e30266ddad959b08d32c722ae8243b66a54 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 20 Nov 2014 08:07:58 +0100 Subject: [PATCH 4/8] Improve unit test string diffs Validate full analysis test output vs an array of lines rather than a big string. This makes empty lines properly highlighted on the results web page. --- tests.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests.js b/tests.js index feab83a..1100dad 100644 --- a/tests.js +++ b/tests.js @@ -134,10 +134,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.equal(analyzer.toString().split('\n'), expectedOutput.split('\n')); var expectedIgnores = document.getElementById("sample-ignored").innerHTML; - assert.equal(analyzer.toIgnoresString(), expectedIgnores); + assert.equal(analyzer.toIgnoresString().split('\n'), expectedIgnores.split('\n')); }); QUnit.test("extract regex from string", function(assert) { From 1907af4dd0a4f89bd5abf41953a0bea10c51e3b1 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 20 Nov 2014 08:49:31 +0100 Subject: [PATCH 5/8] Add more analysis tests --- analyze.js | 17 ++++++----------- tests.js | 52 +++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/analyze.js b/analyze.js index 449d28e..062d72f 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,7 +150,7 @@ function Thread(line) { return undefined; } - this._frames = []; + this.frames = []; } function StringCounter() { @@ -320,6 +314,7 @@ function Analyzer(text) { for (var k = 0; k < stackFrames.length; k++) { asString += stackFrames[k] + "\n"; } + asString += '\n'; } return asString; diff --git a/tests.js b/tests.js index 1100dad..fb520b2 100644 --- a/tests.js +++ b/tests.js @@ -89,15 +89,14 @@ QUnit.test( "multiline thread name", function(assert) { assert.equal(threads.length, 1); var threadLines = threads[0].toString().split('\n'); - assert.equal(threadLines, [ + 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, [ + assert.deepEqual(analysisLines, [ "1 threads found:", "", '"line 1, line 2": runnable', @@ -106,6 +105,37 @@ QUnit.test( "multiline thread name", function(assert) { ]); }); +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( "thread stack", function(assert) { var header = '"Thread name" prio=10 tid=0x00007f1728056000 nid=0x1347 sleeping[0x00007f169cdcb000]'; var thread = new Thread(header); @@ -117,11 +147,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) { @@ -134,10 +164,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().split('\n'), expectedOutput.split('\n')); + assert.deepEqual(analyzer.toString().split('\n'), expectedOutput.split('\n')); var expectedIgnores = document.getElementById("sample-ignored").innerHTML; - assert.equal(analyzer.toIgnoresString().split('\n'), expectedIgnores.split('\n')); + assert.deepEqual(analyzer.toIgnoresString().split('\n'), expectedIgnores.split('\n')); }); QUnit.test("extract regex from string", function(assert) { From 70f0f5ff3b2d28ee15cdc0f5554663aab85ae318 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 20 Nov 2014 08:55:19 +0100 Subject: [PATCH 6/8] Test that we sort threads by name when analysing --- tests.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests.js b/tests.js index fb520b2..162d974 100644 --- a/tests.js +++ b/tests.js @@ -136,6 +136,32 @@ QUnit.test( "analyze single thread", function(assert) { }]); }); +QUnit.test( "analyze two threads with same stack", function(assert) { + 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"); + + // This test validates that threads with the same stack are sorted by name + var analysisResult = analyzer._toThreadsAndStacks(); + assert.deepEqual(analysisResult, [{ + threads: [aardvark, zebra], + stackFrames: [" at fluff"] + }]); +}); + QUnit.test( "thread stack", function(assert) { var header = '"Thread name" prio=10 tid=0x00007f1728056000 nid=0x1347 sleeping[0x00007f169cdcb000]'; var thread = new Thread(header); From a6f6fe40db35fb3e4612b5ed78d0defb93045934 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 20 Nov 2014 08:57:26 +0100 Subject: [PATCH 7/8] Drop superfluous newline at end of analysis --- analyze.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/analyze.js b/analyze.js index 062d72f..1ace169 100644 --- a/analyze.js +++ b/analyze.js @@ -294,12 +294,14 @@ function Analyzer(text) { var threadsAndStacks = this._toThreadsAndStacks(); var asString = ""; - asString += "" + this.threads.length + " threads found:\n\n"; + 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"; } @@ -314,7 +316,6 @@ function Analyzer(text) { for (var k = 0; k < stackFrames.length; k++) { asString += stackFrames[k] + "\n"; } - asString += '\n'; } return asString; From 1421ea304ca423c047c84edff90c1f3b565e9d13 Mon Sep 17 00:00:00 2001 From: Johan Walles Date: Thu, 20 Nov 2014 09:23:31 +0100 Subject: [PATCH 8/8] Make the Analyzer sort threads by header contents --- analyze.js | 18 ++++++++++++++---- tests.js | 3 ++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/analyze.js b/analyze.js index 1ace169..0476ed7 100644 --- a/analyze.js +++ b/analyze.js @@ -281,6 +281,19 @@ function Analyzer(text) { for (var j = 0; j < stacks.length; j++) { var currentStack = stacks[j]; var threads = stacksToThreads[currentStack]; + + 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') @@ -306,12 +319,9 @@ function Analyzer(text) { asString += "" + threads.length + " threads with this stack:\n"; } - var headers = []; for (var j = 0; j < threads.length; j++) { - headers.push(threads[j].toHeaderString()); + asString += threads[j].toHeaderString() + '\n'; } - headers.sort(); - asString += headers.join('\n') + '\n'; for (var k = 0; k < stackFrames.length; k++) { asString += stackFrames[k] + "\n"; diff --git a/tests.js b/tests.js index 162d974..6c890a9 100644 --- a/tests.js +++ b/tests.js @@ -137,6 +137,7 @@ QUnit.test( "analyze single thread", function(assert) { }); 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', @@ -154,9 +155,9 @@ QUnit.test( "analyze two threads with same stack", function(assert) { var aardvark = threads[1]; assert.equal(aardvark.name, "aardvark thread"); - // This test validates that threads with the same stack are sorted by name var analysisResult = analyzer._toThreadsAndStacks(); assert.deepEqual(analysisResult, [{ + // Make sure the aardvark comes before the zebra threads: [aardvark, zebra], stackFrames: [" at fluff"] }]);