mjprof is a command line monadic java profiler.
mjprof is a monadic thread dump analysis tool set. It is a fancy way to say it analyzes jstack output using a series of simple composable building blocks (monads).
So, You are out there in the wild vs a production machine. All you have in hand is the "poor man's profiler" jstack. You take one, two, three jstack thread dumps and then you need to look at them manually inside an editor, vi, less, etc.... If you have done it enough times, you probably know that it is a lot of manual work. Especially when you have thousands of threads in your process.
mjprof reads thread dumps from one or more data sources and writes to the standard output. An example of a data source can be the standard input.
The following will filter out all the threads which are not in RUNNABLE state:
jstack -l pid | ./mjprof.sh contains/state,RUNNABLE/
The commands passed to mjprof consists of several building blocks, monads from now on, concatenated with , (comma) While the monads are relatively simple, mixing and matching them can yield a very thorough analysis, while still allowing you to focus on the data you need. Monad parameters are wrapped with / (instead of () {} or [] which are special chars in the shell) and seperated by ,
Data sources generate thread dumps and feed them into mjprof. The default data source is stdin. When no data source is specified stdin will be used. When more than one data source is specified the thread dumps generated by all of them will be fed into mjprof.
- jmx/host:port|MainClass|pid,[count],[sleep],[username],[passwd]/ - Generate dumps via JMX
- jmxc/host:port|MainClass|pid,[count],[sleep],[username],[passwd]/ - Generate thread dumps via JMX and collect sper thread CPU
- path/path/ - Read thread dump from file
- stdin - Read thread dumps from standard input
- visualvm/path/ - Read profiling session from xml export of VisualVM
- gui/[title],[maxInvocations]/ - Display current thread dump in a GUI window
- snapshot/[filename]/ - Write to a file
- stdout - Writes current stream of thread dumps to stdout
- contains/attr,value/ - Returns only threads which contain the string in certain attribute (regexp not supported)
- -contains/attr,value/ - Returns only threads which do not contain the string (regexp not supported)
- -at - Eliminates the 'at' from the beginning of stack frames
- bottom/int/ - Returns at most n bottom stack frames of the stack
- -fn - Eliminates file name and line from stack frames
- frame/string/ - Eliminates stack frames from all stacks which do not contain string.
- -frame/string/ - Eliminates stack frames from all stacks which contain string.
- -namesuffix - Trim the last number from thread names helps to group thread pool threads together
- noop - Does nothing
- -pkg - Eliminates package name from stack frames
- -prop/attr/ - Removes a certain attribute e.g. eliminate/stack/
- top/int/ - Returns at most n top stack frames of the stack
- trimbelow/string/ - Trim all stack frames below the first occurrence of string
- group/[attr]/ - Group a single thread dump by an attribute. If not attribute is specified all dump is merged
- merge/attribute/ - Combine all dumps to a single one merge based on thread id attribute
- sort/attr/ - Sorts based on an attribute
- -sort/string/ - Sorts based on an attribute (descending order)
Fixed: sort/-sort used to throw a NullPointerException and crash the whole run when any
thread lacked the sorted attribute (e.g. mjprof path/dump.txt/.sort/state/ on an ordinary dump).
Threads missing the attribute now sort last, in both ascending and descending order, instead of
crashing.
-
count - counts number of threads
-
ctree - combine all stack traces with colors (UNIX Terminal)
-
flat - Shows flat histogram of the profiles
-
list - lists the possible stack trace attributes
-
tree - combine all stack traces
-
help -Prints this message
Macros:
- blocked - contains/state,BLOCKED/
- jvm - ncontains/stack,at/
- -jvm - contains/stack,at/.ncontains/name,RMI TCP /.ncontains/name,Reference Handle/.ncontains/name,Finalizer/.ncontains/name,JMX server connection timeout/.ncontains/name,RMI Scheduler/
- locks - stackkeep/- /
- -locks - stackelim/- /
- parking - contains/state,TIMED_WAITING/.contains/stack,sun.misc.Unsafe.park/
- running - contains/state,RUNNABLE/
- sleeping - contains/state,TIMED_WAITING/
- waiting - contains/state,WAITING/
- withstack - contains/stack,at/
Properties may change from one dump to another and the can also be eliminated by mjstack. Following is the list of usual properties
- status - The status of the thread
- nid - Native thread id ( a number)
- name - Name of thread
- state - State of thread
- los - The locked ownable synchronizers part of the stack trace
- daemon - Whether the thread is a daemon or not
- tid - The thread id (a number)
- prio - Thread priority, a number
- stack - The actual stack trace
- cpu_ns - cpu consumed in nano seconds
- wall_ms - wall time in ms for the time frame cpu was recorded
- %cpu - %cpu of the thread info
You can also get the actual list of properties bu using the list monad.
jstack -l pid | ./mjprof list
jstack Original output:
jstack -l 38515 > mystack.txt
Keep only thread which their names contain ISCREAM:
jstack -l 38515 | mjprof contains/name,ISCREAM/
Sort them by state
cat mystack.txt | mjprof contains/name,ISCREAM/.sort/state/
Eliminate the Locked Ownable Synchronizers Section
jstack -l 38515 | mjprof contains/name,ISCREAM/.sort/state/.eliminate/los/
Shorten stack traces to include only 10 last stack frames
mjprof jstack/MyAppMainClass/.contains/name,ISCREAM/.sort/state/.eliminate/los/.keeptop/10/
Count threads
mjprof jstack/38515/.contains/name,ISCREAM/.sort/state/.eliminate/los/.keeptop/10/.count
mjprof-core is usable as a library. Add the dependency:
<dependency>
<groupId>com.performizeit</groupId>
<artifactId>mjprof-core</artifactId>
<version>1.1.0</version>
</dependency>Not on Maven Central yet. There is no publishing configuration for mjprof-core, so the
snippet above will not resolve from a fresh checkout. Until it is published, build and install it
into your local ~/.m2 repository yourself:
git clone https://github.com/AdoptOpenJDK/mjprof.git
cd mjprof
mvn -pl mjprof-core -am install
Stages are plugin instances, so their constructor arguments are typed and checked by the compiler:
List<ThreadInfo> runnable = Pipeline.fromFile("dump.txt")
.contains(Attr.STATE, "RUNNABLE")
.sort(Attr.NAME)
.threads();Anything without a convenience method — including plugins from your own jars — goes through map:
Pipeline.fromStdin()
.map(new MergedCallees("java.net.SocketInputStream.read"))
.collect();Sample on your own schedule and merge later:
List<ThreadDump> samples = new ArrayList<>();
samples.add(Pipeline.fromJmxWithCpu("localhost:9010", 1, 0, null, null).collectOne());
// ... more samples, on your cadence ...
List<ThreadInfo> profile = Pipeline.fromDumps(samples)
.merge(Attr.TID) // sums cpu_ns, merges stack trees
.withoutNameSuffix()
.group(Attr.NAME)
.sortDesc(Attr.CPU_NS)
.threads();Notes:
- Collection runs on the calling thread. A source configured for several samples blocks for its own sleep interval.
- Failures throw
MjprofExceptionrather than yielding an empty result. group(Attr.STACK)andmerge(Attr.STACK)are rejected: stack profiles have no value equality. Usemerge(Attr.TID)thentree().- Dumps written as jstack text keep
count,cpu_ns,wall_msand%cpu. But JMX-sourced dumps do not re-split when concatenated — their header is not date-prefixed — so several JMX samples in one file read back as one dump. UsefromDumpsfor self-collected samples. - The expression DSL, macros and the
supported_monads.txtplugin registry live inmjprof-cli, not in the library.
mjprof uses maven for compilation. In order to build mjprof use the following command line:
mvn clean package
This builds both modules and produces a self-contained runnable jar at
mjprof-cli/target/mjprof-cli-1.1.0-jar-with-dependencies.jar. Run it with
java -jar mjprof-cli/target/mjprof-cli-1.1.0-jar-with-dependencies.jar or via the launcher script
in mjprof-cli/src/main/scripts/mjprof. A native binary (via GraalVM, mvn -Pnative package) is
named mjprof.
mjprof monadic capabilities can be extended with plugins. If you feel something is missing you can write your own plugin.
We will appreciate if you will contribute it back to the community.
A new plugin belongs in mjprof-core (the plugin implementations live there; mjprof-cli only
discovers and wires them up). A plugin is a Java class annotated with
com.performizeit.mjprof.core.api.Plugin. The annotation requires "name" (the plugin name, must be
unique), "category" (a PluginCategory value identifying which plugin-type interface it
implements), and "params" (an array of @Param, describing the expected arguments); "description"
is optional and used for the synopsis. For example:
import com.performizeit.mjprof.core.api.Param;
import com.performizeit.mjprof.core.api.Plugin;
import com.performizeit.mjprof.core.api.PluginCategory;
@Plugin(name = "group", params = {@Param(type = String.class)},
category = PluginCategory.DUMP_REDUCER, description = "Group by an attribute")No registration code is needed beyond the annotation: the build's process-classes phase
regenerates supported_monads.txt from every annotated class it finds on the classpath.
There are several plugin types mappers filters terminals etc... for each type you will need to implement a different interface.
Implement JStackMapper interface which includes a single method
ThreadInfo map(ThreadInfo stck)
Implement JStackFilter interface which includes a single method
boolean filter(ThreadIndo stck)
Implement JStackFilter interface which includes a single method
void addStackDump(JStackDump jsd)
Implement JStackComparator interface which includes a single method
int compare(ThreadInfo o1, ThreadInfo o2)
In order to install the plugin just drop your jar which contains plugin implementation into the 'plugins' directory inside mjprof installation directory.
This repo ships a project .claude/settings.json that declares the caveman, ponytail, and
superpowers Claude Code marketplaces and enables their plugins. Enabling does not install:
external plugins only load after each user installs them once. In the repo, run:
/plugin install ponytail@ponytail
/plugin install caveman@caveman
/plugin install superpowers@superpowers-marketplace
Then /reload-plugins (or restart Claude Code). After that the committed settings keep the
plugins enabled automatically, so the install is a one-time step per user.