Skip to main content

tdscope

Command-line analyzer for Java (HotSpot) thread dumps. Point it at a directory of jstack / jcmd Thread.print / kill -3 output and it tells you:

  • which HTTP requests were in flight, how old they were and where in your code they spent time,
  • which stacks dominate (hotspots), grouped across all dumps,
  • which threads are stuck: same stack in N consecutive dumps,
  • who blocks whom: lock owners, their waiters and JVM-detected deadlocks,
  • who burns CPU between two dumps.

The CLI has no runtime dependencies (Python 3.10+ standard library only; tzdata on Windows, which has no time zone database), so it also runs on a locked-down production box. An optional interactive TUI with a timeline graph needs Textual.

Installation

pip install tdscope        # or: pipx install tdscope

From source:

git clone https://github.com/TBS093A/tdscope && cd tdscope
pip install -e ".[dev]"

Taking thread dumps

A single dump is a snapshot; most problems only show up in a series. Take several dumps a few seconds apart:

PID=$(pgrep -f my-app.jar)
for i in $(seq 1 6); do jcmd "$PID" Thread.print -l > "dump-$(date +%H%M%S).tdump"; sleep 10; done

jstack -l <pid> works the same way. Output of kill -3 written to a log file (e.g. stdout.log, catalina.out) can be fed in directly: tdscope finds the dumps inside the log and ignores everything else. Gzipped files (*.gz) are read transparently.

Usage

tdscope ANALYSIS [options] PATH [PATH ...]

PATH is a file or a directory (scanned recursively for *.dump *.tdump *.txt *.log *.out *.jstack *.gz, change with --glob), or - for stdin.

Analysis What it answers
summary thread count, states, HTTP requests and deadlocks per dump
requests HTTP request threads grouped by METHOD path: count, request age, observed span, top frames
frames unique frames matching --match with counts (where does my code show up?)
hotspots threads grouped by identical top N frames
stuck threads with an unchanged stack across --min-dumps consecutive dumps
locks contended monitors / java.util.concurrent locks with owner and waiters, deadlocks
cpu CPU consumed by each thread between consecutive dumps (JDK 11+)

Options shared by all analyses:

Option Meaning
-m, --match PATTERN keep threads having a frame that contains PATTERN (repeatable), e.g. -m com.mycompany.
-E, --regex treat --match patterns as regular expressions
-s, --state STATE keep threads in a java.lang.Thread.State, e.g. -s BLOCKED (repeatable)
-n, --name REGEX keep threads whose name matches
--http-only keep only threads serving an HTTP request
-t, --top N show only the first N results
--stack print an example stack for every result
--max-stack-lines N truncate printed stacks
-f json machine-readable output
-o FILE write the report to a file

Examples

What were the HTTP requests doing in my code (io.wcm. here, a common AEM library)?

tdscope requests dumps/ -m io.wcm. --tz Europe/Warsaw
GET /content/site/en.html
  seen 3x in 3 dump(s), 1 distinct request(s)
  request age:   avg 15.0s (min 5.0s, max 25.0s)
  observed span: 20.0s
  thread age:    avg 1200.5s (min 1200.5s, max 1200.5s)   (elapsed=, age of the pooled thread)
  thread cpu:    avg 9000.0ms (min 1000.0ms, max 17000.0ms)
  states:        RUNNABLE=3
  frames:
        3x  io.wcm.handler.url.impl.UrlHandlerImpl.externalize(UrlHandlerImpl.java:120)

Which of my frames appear most often, and with what full stack?

tdscope frames dumps/ -m com.mycompany. --top-only --stack --max-stack-lines 40

Threads stuck for at least 4 dumps, as JSON:

tdscope stuck dumps/ --min-dumps 4 -f json -o stuck.json

Who holds the lock everybody is waiting for?

tdscope locks dumps/ -t 5

Interactive TUI

pip install 'tdscope[tui]'
tdscope tui dumps/ --tz UTC

A k9s-style terminal UI on top of the same analyses. One key per analysis opens a form with its options. Results appear in a table with the full report of the selected row. A : command line takes CLI arguments, and results export to text or JSON. The timeline graph plots threads over time (count per group, or the duration of every thread), colored by pool, family, code, state or request, with hover details and a self-contained HTML export.

tdscope TUI timeline graph

TUI guide with screenshots and all keys

How to read the numbers

  • elapsed= is the age of the thread, not of the request. Servlet containers reuse pooled threads, so a request thread may be hours old while serving a 50 ms request. tdscope reports it as thread age and never uses it as a request duration.
  • Request age is computed only when the container writes the request start time (epoch millis) into the thread name, as Apache Sling / AEM does, e.g. qtp123-45 [1700000000000] GET /content/page.html HTTP/1.1 or, on AEM as a Cloud Service, <client ip> [1700000000000] POST /path HTTP/1.1. Dump timestamps are written in the JVM's local time without a zone, so pass the JVM's zone with --tz when it differs from the machine running tdscope. If the dump time appears to precede the request start, tdscope flags it and suggests --tz; a zone that is wrong in the other direction cannot be detected and makes ages too long.
  • Observed span needs no clock at all: it is the time between the first and the last dump in which the very same request (same thread, same name) was seen.
  • Cross-dump analyses (stuck, cpu, observed span) compare dumps of the same JVM process only. The JVM is recognised by the native address of start-up threads such as "Reference Handler", so dumps of several nodes can be analysed together.
  • stuck ignores idle threads by default: it considers RUNNABLE/BLOCKED threads and threads serving a request, minus threads "running" in well-known idle native frames (selectors, accept(), ...). Background threads blocked in a socket read (HTTP/2 connection readers, long polling) are skipped as well; add --include-network-wait to hunt for outbound calls without a read timeout. Request threads are always kept.

Supported formats

HotSpot / OpenJDK thread dumps from JDK 8 to JDK 21+: jstack, jstack -l, jcmd <pid> Thread.print [-l] and kill -3 / SIGQUIT output, including the JDK 19+ header format (#1 [12345] ... nid=12345), Locked ownable synchronizers sections, JVM deadlock reports and CRLF line endings. Virtual-thread dumps (jcmd Thread.dump_to_file) and IBM/OpenJ9 javacores are not supported yet.

Using it as a library

from tdscope import ThreadFilter, load
from tdscope import analysis

dumps = load(["dumps/"])
for group in analysis.hotspots(dumps, ThreadFilter(states=["BLOCKED"]), depth=8)[:3]:
    print(group.snapshots, group.signature[0])

Development

See CONTRIBUTING.md for the setup, the pre-commit hooks and the quality gate, and docs/RELEASING.md for the CI/CD pipelines and releases.

pip install -e ".[dev,tui]" pre-commit && pre-commit install
pytest

Test fixtures in tests/fixtures/ are synthetic. Please never commit real thread dumps: they contain host names, URLs and sometimes user data.

License

MIT

Metadata

Release files for tdscope 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tdscope 0.1.0
File Size Uploaded
tdscope-0.1.0.tar.gz 73.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tdscope 0.1.0
File Interpreter ABI Platform
tdscope-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 123.4 kB

Release files / tdscope-0.1.0.tar.gz

Download URL tdscope-0.1.0.tar.gz
Size 73.7 kB
Tags Source
SHA-256 checksum
How to use checksums
1390b4f19e949f1d0932aa4927c048e4659e3cce54cfb99c5360d6ee6c4ba0d6
BLAKE2b-256 checksum
How to use checksums
15710769f2ba447fb80d2c352cf4d2273ca52a34ace5a350bfd9462986154c4c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release files / tdscope-0.1.0-py3-none-any.whl

Download URL tdscope-0.1.0-py3-none-any.whl
Size 49.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f88f908ccd99f74d46f342d3d0a6002afb9a2d80bcb1b07d33fbe3ac258f4a4c
BLAKE2b-256 checksum
How to use checksums
7a55599499f67f2fd74ea1522be4473b3ec5418e52afe9da3d1b5160f1d39288
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page