Skip to content

Commit d810c5a

Browse files
committed
Add threads command support to MAT CLI
- Implemented the `threads` command to analyze thread information from heap dumps. - Added `ThreadsResult` and `ThreadsResultBuilder` to process and summarize thread details. - Introduced `ThreadsResultSerializer` for both JSON and text serialization formats. - Updated CLI help and test suites to include and validate the `threads` command functionality.
1 parent fcc9c05 commit d810c5a

12 files changed

Lines changed: 656 additions & 5 deletions

File tree

plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliArgumentParser.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,9 @@ else if (command.requiresSnapshot() && heapFile == null)
508508

509509
private int defaultLimit(CliCommand command, String queryCommand)
510510
{
511+
if (command == CliCommand.THREADS)
512+
return Integer.MAX_VALUE;
513+
511514
if (command == CliCommand.QUERY)
512515
{
513516
String queryIdentifier = queryIdentifier(queryCommand);

plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommand.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
public enum CliCommand
1313
{
1414
SUMMARY("summary", true, false, false, false), //$NON-NLS-1$
15+
THREADS("threads", true, false, false, false), //$NON-NLS-1$
1516
HISTOGRAM("histogram", true, false, false, false), //$NON-NLS-1$
1617
TOP_CONSUMERS("top-consumers", true, false, false, false), //$NON-NLS-1$
1718
PATH2GC("path2gc", true, false, false, false), //$NON-NLS-1$

plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandCatalog.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,27 @@ private static Map<CliCommand, CommandDefinition> definitions()
211211
"summary object with snapshot-wide counters and heap metadata.",
212212
Arrays.asList("summary.path", "summary.heapFormat", "summary.numberOfObjects",
213213
"summary.numberOfClasses", "summary.usedHeapSize")));
214+
definitions.put(CliCommand.THREADS, new CommandDefinition(CliCommand.THREADS,
215+
"Show a best-effort thread report from the heap dump, including state, retained heap, and stack traces when available.",
216+
"mat-cli threads <heap> [--limit N] [--format text|json]", true,
217+
Collections.singletonList("heap"),
218+
Arrays.asList(option("--limit", "N", false,
219+
"Limit threads returned. Defaults to all threads."),
220+
option("--format", "text|json", false, "Select text or JSON output.")),
221+
Arrays.asList(output("default", "text", "threads",
222+
"Thread report with overview plus per-thread stack sections."),
223+
output("default", "json", "threads",
224+
"Structured thread report JSON."),
225+
output("agent", "json", "threads",
226+
"Stable thread report schema for agents.")),
227+
Arrays.asList("histogram <heap> --agent", "top-consumers <heap> --agent"),
228+
"thread report payload with a summary, best-effort notice, and one entry per returned thread.",
229+
Arrays.asList("notice", "summary.totalThreads", "summary.returnedThreads",
230+
"summary.stackAvailableThreads", "summary.stateAvailableThreads", "threads[]",
231+
"threads[].name", "threads[].technicalName", "threads[].objectAddress",
232+
"threads[].state", "threads[].retainedBytes", "threads[].stackAvailable",
233+
"threads[].stackUnavailableReason", "threads[].stackFrames[]",
234+
"threads[]._context.objectId")));
214235
definitions.put(CliCommand.HISTOGRAM, new CommandDefinition(CliCommand.HISTOGRAM,
215236
"Group objects by class and report shallow heap plus approximate retained heap.",
216237
"mat-cli histogram <heap> [--limit N] [--format text|json]", true,

plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliCommandExecutor.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ private CliExecution execute(CliArguments arguments, ISnapshot snapshot, IProgre
9898
{
9999
case SUMMARY:
100100
return CliExecution.summary(SnapshotSummary.from(snapshot.getSnapshotInfo()));
101+
case THREADS:
102+
return CliExecution.result(new ThreadsResultBuilder().build(snapshot, listener, arguments.getLimit()));
101103
case HISTOGRAM:
102104
RefinedResultBuilder histogram = SnapshotQuery.lookup("histogram", snapshot).refine(listener); //$NON-NLS-1$
103105
histogram.addDefaultContextDerivedColumn(RetainedSizeDerivedData.APPROXIMATE);
@@ -331,8 +333,9 @@ private void validateResult(IResult result) throws CliException
331333
if (result == null)
332334
throw CliException.execution("Query returned no result", null); //$NON-NLS-1$
333335

334-
if (!(result instanceof TextResult || result instanceof IResultTable || result instanceof IResultTree
335-
|| result instanceof IResultPie || result instanceof CompositeResult || result instanceof Spec))
336+
if (!(result instanceof TextResult || result instanceof ThreadsResult || result instanceof IResultTable
337+
|| result instanceof IResultTree || result instanceof IResultPie
338+
|| result instanceof CompositeResult || result instanceof Spec))
336339
{
337340
throw CliException.unsupported("Unsupported result type: " + result.getClass().getName()); //$NON-NLS-1$
338341
}

plugins/org.eclipse.mat.cli/src/org/eclipse/mat/cli/internal/CliHelp.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static String generalHelp()
2626
help.append(" mat-cli --help\n\n"); //$NON-NLS-1$
2727
help.append("Commands:\n"); //$NON-NLS-1$
2828
help.append(" summary <heap>\n"); //$NON-NLS-1$
29+
help.append(" threads <heap> [--limit N] [--format text|json]\n"); //$NON-NLS-1$
2930
help.append(" histogram <heap> [--limit N] [--format text|json]\n"); //$NON-NLS-1$
3031
help.append(" top-consumers <heap> [--limit N] [--depth N] [--format text|json]\n"); //$NON-NLS-1$
3132
help.append(" path2gc <heap> --object 0x... [--limit N] [--depth N] [--format text|json]\n"); //$NON-NLS-1$
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Eclipse Memory Analyzer Project.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*******************************************************************************/
10+
package org.eclipse.mat.cli.internal;
11+
12+
import java.util.Collections;
13+
import java.util.List;
14+
15+
import org.eclipse.mat.query.IResult;
16+
import org.eclipse.mat.query.ResultMetaData;
17+
18+
public final class ThreadsResult implements IResult
19+
{
20+
public static final String NOTICE = "best-effort from heap dump, not a full jstack equivalent"; //$NON-NLS-1$
21+
public static final String UNAVAILABLE = "unavailable"; //$NON-NLS-1$
22+
public static final String STACK_UNAVAILABLE_REASON = "No thread stack information present in heap dump"; //$NON-NLS-1$
23+
24+
public static final class Summary
25+
{
26+
private final int totalThreads;
27+
private final int returnedThreads;
28+
private final int stackAvailableThreads;
29+
private final int stateAvailableThreads;
30+
31+
public Summary(int totalThreads, int returnedThreads, int stackAvailableThreads, int stateAvailableThreads)
32+
{
33+
this.totalThreads = totalThreads;
34+
this.returnedThreads = returnedThreads;
35+
this.stackAvailableThreads = stackAvailableThreads;
36+
this.stateAvailableThreads = stateAvailableThreads;
37+
}
38+
39+
public int getTotalThreads()
40+
{
41+
return totalThreads;
42+
}
43+
44+
public int getReturnedThreads()
45+
{
46+
return returnedThreads;
47+
}
48+
49+
public int getStackAvailableThreads()
50+
{
51+
return stackAvailableThreads;
52+
}
53+
54+
public int getStateAvailableThreads()
55+
{
56+
return stateAvailableThreads;
57+
}
58+
}
59+
60+
public static final class ThreadEntry
61+
{
62+
private final int objectId;
63+
private final String name;
64+
private final String technicalName;
65+
private final String objectAddress;
66+
private final String state;
67+
private final long retainedBytes;
68+
private final boolean stackAvailable;
69+
private final String stackUnavailableReason;
70+
private final List<String> stackFrames;
71+
72+
public ThreadEntry(int objectId, String name, String technicalName, String objectAddress, String state,
73+
long retainedBytes, boolean stackAvailable, String stackUnavailableReason,
74+
List<String> stackFrames)
75+
{
76+
this.objectId = objectId;
77+
this.name = name;
78+
this.technicalName = technicalName;
79+
this.objectAddress = objectAddress;
80+
this.state = state;
81+
this.retainedBytes = retainedBytes;
82+
this.stackAvailable = stackAvailable;
83+
this.stackUnavailableReason = stackUnavailableReason;
84+
this.stackFrames = stackFrames == null ? Collections.<String>emptyList()
85+
: Collections.unmodifiableList(stackFrames);
86+
}
87+
88+
public int getObjectId()
89+
{
90+
return objectId;
91+
}
92+
93+
public String getName()
94+
{
95+
return name;
96+
}
97+
98+
public String getTechnicalName()
99+
{
100+
return technicalName;
101+
}
102+
103+
public String getObjectAddress()
104+
{
105+
return objectAddress;
106+
}
107+
108+
public String getState()
109+
{
110+
return state;
111+
}
112+
113+
public long getRetainedBytes()
114+
{
115+
return retainedBytes;
116+
}
117+
118+
public boolean isStackAvailable()
119+
{
120+
return stackAvailable;
121+
}
122+
123+
public String getStackUnavailableReason()
124+
{
125+
return stackUnavailableReason;
126+
}
127+
128+
public List<String> getStackFrames()
129+
{
130+
return stackFrames;
131+
}
132+
}
133+
134+
private final String notice;
135+
private final Summary summary;
136+
private final List<ThreadEntry> threads;
137+
private final boolean truncated;
138+
139+
public ThreadsResult(String notice, Summary summary, List<ThreadEntry> threads, boolean truncated)
140+
{
141+
this.notice = notice;
142+
this.summary = summary;
143+
this.threads = threads == null ? Collections.<ThreadEntry>emptyList() : Collections.unmodifiableList(threads);
144+
this.truncated = truncated;
145+
}
146+
147+
public String getNotice()
148+
{
149+
return notice;
150+
}
151+
152+
public Summary getSummary()
153+
{
154+
return summary;
155+
}
156+
157+
public List<ThreadEntry> getThreads()
158+
{
159+
return threads;
160+
}
161+
162+
public boolean isTruncated()
163+
{
164+
return truncated;
165+
}
166+
167+
public ResultMetaData getResultMetaData()
168+
{
169+
return null;
170+
}
171+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2026 Eclipse Memory Analyzer Project.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License 2.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*******************************************************************************/
10+
package org.eclipse.mat.cli.internal;
11+
12+
import java.util.ArrayList;
13+
import java.util.Collections;
14+
import java.util.List;
15+
16+
import org.eclipse.mat.SnapshotException;
17+
import org.eclipse.mat.query.Column;
18+
import org.eclipse.mat.query.IContextObject;
19+
import org.eclipse.mat.query.IResult;
20+
import org.eclipse.mat.query.IResultTree;
21+
import org.eclipse.mat.snapshot.ISnapshot;
22+
import org.eclipse.mat.snapshot.model.IObject;
23+
import org.eclipse.mat.snapshot.model.IStackFrame;
24+
import org.eclipse.mat.snapshot.model.IThreadStack;
25+
import org.eclipse.mat.snapshot.query.SnapshotQuery;
26+
import org.eclipse.mat.util.IProgressListener;
27+
28+
final class ThreadsResultBuilder
29+
{
30+
private static final String STATE_FALLBACK_LABEL = "State"; //$NON-NLS-1$
31+
32+
public ThreadsResult build(ISnapshot snapshot, IProgressListener listener, int limit) throws Exception
33+
{
34+
IResult result = SnapshotQuery.lookup("thread_overview", snapshot).execute(listener); //$NON-NLS-1$
35+
if (result == null)
36+
{
37+
return new ThreadsResult(ThreadsResult.NOTICE, new ThreadsResult.Summary(0, 0, 0, 0),
38+
Collections.<ThreadsResult.ThreadEntry>emptyList(), false);
39+
}
40+
if (!(result instanceof IResultTree))
41+
throw new SnapshotException("thread_overview returned unsupported result type: " + result.getClass().getName()); //$NON-NLS-1$
42+
43+
IResultTree tree = (IResultTree) result;
44+
List<?> elements = tree.getElements();
45+
List<ThreadsResult.ThreadEntry> rows = new ArrayList<ThreadsResult.ThreadEntry>();
46+
int stateColumn = findStateColumn(tree.getColumns());
47+
int limitToApply = Math.max(0, limit);
48+
boolean truncated = false;
49+
int stackAvailableThreads = 0;
50+
int stateAvailableThreads = 0;
51+
52+
for (Object element : elements)
53+
{
54+
if (listener.isCanceled())
55+
throw new IProgressListener.OperationCanceledException();
56+
57+
if (rows.size() >= limitToApply)
58+
{
59+
truncated = true;
60+
break;
61+
}
62+
63+
IContextObject context = tree.getContext(element);
64+
if (context == null || context.getObjectId() < 0)
65+
continue;
66+
67+
int objectId = context.getObjectId();
68+
IObject threadObject = snapshot.getObject(objectId);
69+
String state = stateValue(tree, element, stateColumn);
70+
ThreadsResult.ThreadEntry entry = toEntry(snapshot, threadObject, state);
71+
rows.add(entry);
72+
if (!ThreadsResult.UNAVAILABLE.equals(state))
73+
stateAvailableThreads++;
74+
if (entry.isStackAvailable())
75+
stackAvailableThreads++;
76+
}
77+
78+
ThreadsResult.Summary summary = new ThreadsResult.Summary(elements.size(), rows.size(), stackAvailableThreads,
79+
stateAvailableThreads);
80+
return new ThreadsResult(ThreadsResult.NOTICE, summary, rows, truncated);
81+
}
82+
83+
private ThreadsResult.ThreadEntry toEntry(ISnapshot snapshot, IObject threadObject, String state)
84+
{
85+
String name = threadObject.getClassSpecificName();
86+
if (name == null || name.length() == 0)
87+
name = threadObject.getTechnicalName();
88+
89+
List<String> stackFrames = new ArrayList<String>();
90+
boolean stackAvailable = false;
91+
String stackUnavailableReason = ThreadsResult.STACK_UNAVAILABLE_REASON;
92+
try
93+
{
94+
IThreadStack stack = snapshot.getThreadStack(threadObject.getObjectId());
95+
if (stack != null)
96+
{
97+
for (IStackFrame frame : stack.getStackFrames())
98+
{
99+
stackFrames.add(frame.getText());
100+
}
101+
stackAvailable = true;
102+
stackUnavailableReason = null;
103+
}
104+
}
105+
catch (SnapshotException e)
106+
{
107+
if (e.getLocalizedMessage() != null && e.getLocalizedMessage().length() > 0)
108+
stackUnavailableReason = e.getLocalizedMessage();
109+
}
110+
catch (RuntimeException e)
111+
{
112+
if (e.getLocalizedMessage() != null && e.getLocalizedMessage().length() > 0)
113+
stackUnavailableReason = e.getLocalizedMessage();
114+
}
115+
116+
return new ThreadsResult.ThreadEntry(threadObject.getObjectId(), name, threadObject.getTechnicalName(),
117+
"0x" + Long.toHexString(threadObject.getObjectAddress()), state, //$NON-NLS-1$
118+
threadObject.getRetainedHeapSize(), stackAvailable, stackUnavailableReason, stackFrames);
119+
}
120+
121+
private int findStateColumn(Column[] columns)
122+
{
123+
if (columns == null)
124+
return -1;
125+
126+
for (int ii = 0; ii < columns.length; ii++)
127+
{
128+
String label = columns[ii].getLabel();
129+
if (STATE_FALLBACK_LABEL.equalsIgnoreCase(label))
130+
return ii;
131+
}
132+
return -1;
133+
}
134+
135+
private String stateValue(IResultTree tree, Object row, int stateColumn)
136+
{
137+
if (stateColumn < 0)
138+
return ThreadsResult.UNAVAILABLE;
139+
140+
Object value = tree.getColumnValue(row, stateColumn);
141+
if (value == null)
142+
return ThreadsResult.UNAVAILABLE;
143+
144+
String text = String.valueOf(value).trim();
145+
return text.length() == 0 ? ThreadsResult.UNAVAILABLE : text;
146+
}
147+
}

0 commit comments

Comments
 (0)