Skip to content

Commit

Permalink
- Added top threads view
Browse files Browse the repository at this point in the history
- Processes can now be killed with Del/Backspace
- Fixed error when running app from another directory
- Showing max heap and non-heap
- Showing thread count
  • Loading branch information
ajermakovics committed Feb 8, 2017
1 parent 2f9b511 commit 547569f
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 97 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@ jvm-top lets you monitor your Java/Scala/.. server applications from the termina

To install:
1. Download the [release](https://github.com/ajermakovics/jvm-mon/releases)
2. Unzip or untar
3. Run `./bin/jvm-mon`
2. Unzip/untar
3. Run `./bin/jvm-mon` from extracted directory

Usage:
- Select a JVM process and press `Enter` to monitor it
- Press `Esc` to exit
- Press `q` or `ctrl+c` to exit
- Press `Del` or `Backspace` to kill a process

# What is available

Currently it shows:
- List of running JVM processes
- Cpu and GC load
- Heap size and usage
- Top threads with cpu usage

# Building from source

Expand Down
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ mainClassName = 'JvmMon'
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
// mavenLocal()
}

dependencies {
compile 'com.eclipsesource.j2v8:j2v8_macosx_x86_64:4.6.0'
compile 'com.eclipsesource.j2v8:j2v8_linux_x86_64:4.6.0'
compile 'com.github.ajermakovics:jvmtop:0.8.1'
compile 'com.github.ajermakovics:jvmtop:0.8.2'
testCompile group: 'junit', name: 'junit', version: '4.11'
}

Expand All @@ -37,6 +38,8 @@ startScripts {
doLast {
def unixScriptFile = file getUnixScript()
unixScriptFile.text = unixScriptFile.text.replace('$APP_HOME/lib/tools.jar', '$JAVA_HOME/lib/tools.jar')
unixScriptFile.text = unixScriptFile.text.replace('exec ', 'cd $APP_HOME;\nexec ')
unixScriptFile.text = unixScriptFile.text.replace('exec ', '[ -f $JAVA_HOME/lib/tools.jar ] || echo \'$JAVA_HOME/lib/tools.jar not found\';\nexec ')
}
}

Expand Down
129 changes: 64 additions & 65 deletions src/dist/jvm-mon.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,10 @@ var table = grid.set(0, 0, 1, 2, contrib.table,
, selectedBg: 'blue'
, interactive: true
, label: 'JVM Processes'
// , width: '30%'
// , height: '30%'
, border: {type: "line", fg: "cyan"}
, columnSpacing: 4 //in chars
, columnWidth: [6, 16, 10, 10, 10, 10] /*in chars*/ })


var line = grid.set(0, 2, 1, 2, contrib.line,
{
label: "CPU %",
Expand Down Expand Up @@ -56,41 +53,53 @@ var memLine = grid.set(1, 2, 1, 2, contrib.line,
}
})

var cpuDonut = grid.set(1, 0, 1, 1, contrib.donut, {
label: 'CPU',
radius: 20,
arcWidth: 4,
remainColor: 'black',
yPadding: 2,
data: [ ]
});

var bar = grid.set(1, 1, 1, 1, contrib.stackedBar,
{ label: 'Heap (MB)'
, barWidth: 6
, barSpacing: 6
, xOffset: 5
//, maxValue: 15
// , height: "40%"
// , width: "50%"
, barBgColor: [ 'red', 'cyan']})
var threadTable = grid.set(1, 0, 1, 2, contrib.table,
{ keys: false
, fg: 'white'
, selectedFg: 'white'
, selectedBg: 'blue'
, interactive: false
, label: 'Threads'
, border: {type: "line", fg: "cyan"}
, columnSpacing: 4 //in chars
, columnWidth: [6, 16, 12, 10, 10] /*in chars*/ })
// ["TID", "Name", "State", "CPU", "Total CPU"], data:[]};

var prompt = blessed.question({
parent: screen,
border: 'line',
height: 'shrink',
width: 'half',
top: 'center',
left: 'center',
label: ' {blue-fg}Question{/blue-fg} (Enter=Ok, Esc=Cancel)',
tags: true,
keys: true,
vi: true
});

//----------------------
screen.append(table)
screen.append(cpuDonut)
screen.append(bar)
screen.append(threadTable)
screen.append(line)
screen.append(memLine)
screen.append(prompt);

//allow control the table with the keyboard
table.focus()

screen.key(['escape', 'q', 'C-c'], function(ch, key) {
screen.key(['q', 'C-c'], function(ch, key) {
return process.exit(0);
});

screen.key(['space'], function(ch, key) {
addData(getData());
screen.key(['delete', 'backspace'], function(ch, key) {
var vm = table.vms[table.rows.selected];
var pid = vm.Id
prompt.ask('Kill ' + vm.DisplayName.trunc(16) + ' (' + pid + ')?', function (err, val) {
if(val)
process.kill(pid, 'SIGTERM')
})
prompt.focus()
});

String.prototype.trunc = function(n){
Expand All @@ -102,12 +111,12 @@ var startTime = Math.floor(Date.now() / 1000);
table.rows.on("select", function (item) {
table.selectedItem = table.rows.getItemIndex(item);
var vm = table.vms[table.selectedItem];
renderVmCharts(vm.Id);
renderVmStats(vm);
table.selectedVmId = vm.Id;
renderVmCharts(vm);
renderThreads();
screen.render();
});

// set initial data
screen.render()

function addData(vms) {
Expand Down Expand Up @@ -138,40 +147,39 @@ function addData(vms) {

if(vms.length) {
var selectedVm = vms[table.selectedItem || 0]
renderVmCharts(selectedVm.Id)
renderVmStats(selectedVm);
renderVmCharts(selectedVm)
renderThreads(selectedVm.threads)
table.selectedVmId = selectedVm.Id
threadTable.setLabel('Threads (' + selectedVm.ThreadCount + ') ')
}

screen.render()
}

function formatTime(time) {
var sec = time%60;
return Math.floor(time/60) + ':' + ((sec<10)?'0':'') + sec;
function renderThreads(threads) {
var tableData = {headers:["TID", "Name", "State", "CPU", "TotalCPU"], data:[]};

if(threads) {
for(var i = 0; i < threads.length; i++) {
var th = threads[i];
var row = [th.TID, th.name.trunc(16), th.state.trunc(11), pct(th.cpu), pct(th.totalCpu)];
tableData.data.push(row)
}
}

threadTable.setData(tableData);
}

function renderVmCharts(vmId) {
var vmHist = hist[vmId]
function renderVmCharts(vm) {
var vmHist = hist[vm.Id]
var cpuData = {title: 'CPU', x: times, y: vmHist.cpu, style: {line: 'yellow'}};
var gcData = {title: 'GC', x: times, y: vmHist.gc, style: {line: 'blue'}};
line.setData([gcData, cpuData]);

var heapSizeData = {title: 'Size', x: times, y: vmHist.size, style: {line: 'red'}};
var heapUsageData = {title: 'Usage', x: times, y: vmHist.used, style: {line: 'cyan'}};
var heapUsageData = {title: 'Used ' + fmt(vm.HeapUsed), x: times, y: vmHist.used, style: {line: 'cyan'}};
memLine.setData([heapSizeData, heapUsageData]);
}

function renderVmStats(vm) {
cpuDonut.setData([{percent: vm.CpuLoad, label: 'CPU', 'color': 'red'}]);

var freeHeap = vm.HeapSize - vm.HeapUsed;

bar.setData({
barCategory: ['Heap']
, stackedCategory: ['Used', 'Free']
, data:
[ [Math.floor(vm.HeapUsed/1024/1024), Math.floor(freeHeap/1024/1024)] ]
})
memLine.setLabel('Heap (MB), max=' + fmt(vm.HeapMax) + ', nonHeap=' + fmt(vm.NonHeapUsed));
}

function fmt(bytes, decimals) {
Expand All @@ -187,20 +195,11 @@ function pct(num) {
return (num * 100).toFixed(2) + ' %'
}

function formatTime(time) {
var sec = time%60;
return Math.floor(time/60) + ':' + ((sec<10)?'0':'') + sec;
}

setInterval(function() {
addData(getData());
addData(getData(table.selectedVmId || 0));
}, refreshDelay)

// "Id"
// "DisplayName"
// "HeapUsed"
// "HeapSize"
// "HeapMax"
// "NonHeapUsed"
// "NonHeapMax"
// "CpuLoad"
// "GcLoad"
// "VMVersion"
// "OSUser"
// "ThreadCount"
// "hasDeadlockThreads"
101 changes: 73 additions & 28 deletions src/main/java/JvmMon.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import com.eclipsesource.v8.*;
import com.jvmtop.monitor.VMInfo;
import com.jvmtop.monitor.VMInfoState;
import com.jvmtop.view.VMDetailView;
import com.jvmtop.view.VMOverviewView;

import java.io.IOError;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;

import static com.eclipsesource.v8.utils.V8ObjectUtils.toV8Array;

public class JvmMon implements JavaCallback {

static NodeJS nodeJS = NodeJS.createNodeJS(); //nodeJS.require(new File("blessed-contrib"));
static NodeJS nodeJS = NodeJS.createNodeJS();
static V8 v8 = nodeJS.getRuntime();

public static void main(String[] args) throws Exception {
Expand All @@ -19,7 +25,7 @@ public static void main(String[] args) throws Exception {
}

JvmMon jvmMon = new JvmMon();
jvmMon.update();

nodeJS.getRuntime().registerJavaMethod(jvmMon, "getData");
nodeJS.getRuntime().executeScript("var refreshDelay = 1000");

Expand All @@ -33,38 +39,76 @@ public static void main(String[] args) throws Exception {
}

private VMOverviewView vmOverviewView = new VMOverviewView(0);
private VMDetailView vmDetailView;

JvmMon() throws Exception {
update(0);
vmDetailView = new VMDetailView(vmOverviewView.getVMInfoList().get(0));
}

public void update() throws Exception {
public void update(int vmId) throws Exception {
vmOverviewView.updateVMs(vmOverviewView.scanForNewVMs());

if(vmId != 0 && vmId != vmDetailView.getVmId())
vmDetailView = new VMDetailView(vmOverviewView.getVMInfo(vmId));
}

private V8Array getVmStats() throws Exception {
V8Array res = new V8Array(v8);

vmOverviewView.getVMInfoList().stream()
List<V8Object> vmStats = vmOverviewView.getVMInfoList().stream()
.filter(vm -> vm.getState() == VMInfoState.ATTACHED)
.map(this::toJsObject)
.forEach(res::push);
.map(this::toVmJsObject)
.map(vmJs -> maybeAddThreads(vmJs, vmDetailView))
.collect(Collectors.toList());

return toV8Array(v8, vmStats);
}

private V8Object maybeAddThreads(V8Object vmJs, VMDetailView view) {
if(vmJs.getInteger("Id") == view.getVmId()) {
try {
List<V8Object> threads = view.getTopThreads().stream()
.map(this::toThreadJs)
.collect(Collectors.toList());
vmJs.add("threads", toV8Array(v8, threads));
} catch (Exception e) {
throw new IOError(e);
}
}
return vmJs;
}

return res;
private V8Object toThreadJs(VMDetailView.ThreadStats ts) {
V8Object threadJs = new V8Object(v8);
threadJs.add("TID", ts.TID);
threadJs.add("name", ts.name);
threadJs.add("state", ts.state.toString());
threadJs.add("cpu", ts.cpu);
threadJs.add("totalCpu", ts.totalCpu);
threadJs.add("blockedBy", ts.blockedBy);
return threadJs;
}

private V8Object toJsObject(VMInfo vm) {
V8Object res = new V8Object(v8);
res.add("Id", vm.getId());
res.add("DisplayName", displayName(vm));
res.add("HeapUsed", vm.getHeapUsed());
res.add("HeapMax", vm.getHeapMax());
res.add("HeapSize", vm.getHeapSize());
res.add("NonHeapUsed", vm.getNonHeapUsed());
res.add("NonHeapMax", vm.getNonHeapMax());
res.add("CpuLoad", vm.getCpuLoad());
res.add("GcLoad", vm.getGcLoad());
res.add("VMVersion", vm.getVMVersion());
res.add("OSUser", vm.getOSUser());
res.add("ThreadCount", vm.getThreadCount());
res.add("hasDeadlockThreads", vm.hasDeadlockThreads());
return res;
private V8Object toVmJsObject(VMInfo vm) {
V8Object vmJs = new V8Object(v8);
vmJs.add("Id", vm.getId());
vmJs.add("DisplayName", displayName(vm));
vmJs.add("HeapUsed", vm.getHeapUsed());
vmJs.add("HeapMax", vm.getHeapMax());
vmJs.add("HeapSize", vm.getHeapSize());
vmJs.add("NonHeapUsed", vm.getNonHeapUsed());
vmJs.add("NonHeapMax", vm.getNonHeapMax());
vmJs.add("CpuLoad", vm.getCpuLoad());
vmJs.add("GcLoad", vm.getGcLoad());
vmJs.add("VMVersion", vm.getVMVersion());
vmJs.add("OSUser", vm.getOSUser());
vmJs.add("ThreadCount", vm.getThreadCount());
vmJs.add("hasDeadlockThreads", vm.hasDeadlockThreads());
return vmJs;
}

private V8Object addThreadStats(V8Object vm) {
vm.getInteger("Id");
return vm;
}

private String displayName(VMInfo vm) {
Expand All @@ -77,11 +121,12 @@ private String displayName(VMInfo vm) {
@Override
public Object invoke(V8Object receiver, V8Array parameters) {
try {
update();
int vmId = parameters.getInteger(0);
update(vmId);
return getVmStats();
} catch (Exception e) {
e.printStackTrace();
return new V8Array(v8);
throw new IOError(e);
}
}

}

0 comments on commit 547569f

Please sign in to comment.