Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,73 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.3] - 2026-08-24

Report readability pass. 1.1.2 fixed what the report *claimed*; this release
fixes what it *shows*. Two themes: every ranking that named a bean but not its
counterpart now answers "which one?" without opening the graph, and three charts
replace the numbers you had to hold in your head. No API, schema, or
configuration changes.

### Added

- **Coupling quadrant (Smells tab).** A fan-out vs fan-in scatter of every bean,
so the shape of the graph is visible instead of two top-10 tables: god beans
climb the left edge, shotgun-surgery risks run along the bottom, and the
dashed I = 0.8 line marks the instability threshold the tables use. Dot size is
how many beans share a position, framework beans are dimmed (with a
**Hide framework beans** toggle), and a single-bean dot opens that bean in the
graph. Axes are square-root scaled so a 400-bean tail stays readable; every
tick is a real count.
- **Pareto curve (Timing tab).** "How few beans you would have to fix" — the
cumulative share of measured bean-instantiation time, with the 80% knee
marked. On start.spring.io: 37 of 283 beans. The caption states plainly that a
bean's time includes the beans its constructor triggers, so the total counts
nested instantiation more than once — it is a ranking, not a wall-clock budget.
- **Startup-time trend verdicts (Timing tab).** The v1.1.0 sparkline is now a
two-axis chart: startup time plus a dashed bean-count line, and a coloured band
on every interval that crosses the `startup-time` gate's own thresholds
(default 500ms AND 20%) — amber when the app also grew, green when it got
faster, red when the slowdown is **unexplained by bean count**. The bean-count
line is drawn in segments so it never spans a run that predates the field, and
such an interval reads amber ("this run pair has no bean count"), never red.
The caption names the thresholds it used and states that a flat bean count with
rising startup does not prove a code regression — a bigger dataset, a slower
runner or a cold cache draw the same line.
- **`beanCount` in each `trendHistory[]` entry**, the same number the
dependencies section reports. Without it the trend chart cannot tell a
regression from an app that simply grew. Older entries stay valid; the chart
handles a missing count rather than guessing.
- **Cycles tab drill-down.** Each cycle now lists its members in order and the
`@Lazy` edge that breaks it, so the fix does not require re-reading the graph.
- **jsdom render check** (`tools/render-check.js`). The report template is one
top-down script, so a `const` declared below a tab's render is in temporal dead
zone for it — and `node --check` passes such code. The harness renders every
tab against a report JSON and fails on the first console error; it caught three
of these during this release.

### Changed

- **Fan-in / fan-out rankings answer "who".** Every row expands to the beans on
the other end of the coupling, instead of a count you had to trace by hand.
- **Slow startup steps name their bean.** A `spring.beans.instantiate` step used
to show only the step name; the bean it instantiated is now its own column.
- **Critical path chips show cumulative position.** Each bean carries its own
instantiation time in bold and how far into the chain it sits in dim text, so
the expensive segment is visible at a glance.
- **Orphan and proxied bean names are listed**, not just counted — a count with
no names cannot be acted on.

### Fixed

- **The proxy card claimed a count it had not verified.** Beans not instantiated
at report time cannot be inspected for a proxy; the card now states how many
were skipped rather than implying the scan was complete.
- **The histogram caption called the wrong number "the threshold".** It labelled
its fixed 100ms bucket as the slow-bean threshold, which is configurable and
was 50ms on the run being described. It now prints the real
`slow-bean-threshold-ms` alongside the bucket count.

## [1.1.2] - 2026-08-24

Precision pass on report accuracy: four ways WireDoctor reported something the
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 🩺 WireDoctor

[![Maven Central](https://img.shields.io/maven-central/v/io.github.ddsha441981/wiredoctor-autoconfigure.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.ddsha441981/wiredoctor-autoconfigure) [![CI Tests](https://github.com/ddsha441981/wiredoctor/actions/workflows/compat.yml/badge.svg)](https://github.com/ddsha441981/wiredoctor/actions/workflows/compat.yml) [![Tests](https://img.shields.io/badge/Tests-253%20passed-success.svg)](https://github.com/ddsha441981/wiredoctor/actions/workflows/compat.yml) [![License](https://img.shields.io/badge/License-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE) [![Awesome Java](https://awesome.re/mentioned-badge.svg)](https://github.com/akullpp/awesome-java#architecture)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.ddsha441981/wiredoctor-autoconfigure.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.ddsha441981/wiredoctor-autoconfigure) [![CI Tests](https://github.com/ddsha441981/wiredoctor/actions/workflows/compat.yml/badge.svg)](https://github.com/ddsha441981/wiredoctor/actions/workflows/compat.yml) [![Tests](https://img.shields.io/badge/Tests-259%20passed-success.svg)](https://github.com/ddsha441981/wiredoctor/actions/workflows/compat.yml) [![License](https://img.shields.io/badge/License-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE) [![Awesome Java](https://awesome.re/mentioned-badge.svg)](https://github.com/akullpp/awesome-java#architecture)

> *"Your bean graph has a story. WireDoctor reads it."*

Expand Down Expand Up @@ -80,7 +80,7 @@ Notes:
| [Upgrade Guard](https://ddsha441981.github.io/wiredoctor/upgrade-guard.html) | Catching silent autoconfiguration changes across Boot upgrades |
| [Ghost Detector](https://ddsha441981.github.io/wiredoctor/ghost-detector.html) | Passive candidates + opt-in first-touch tracking, and their trust postures |
| [Thread Distribution](https://ddsha441981.github.io/wiredoctor/thread-distribution.html) | Per-thread bean map with donut chart (v1.1.0) |
| [Startup Time Trend](https://ddsha441981.github.io/wiredoctor/startup-time-trend.html) | trendHistory in baseline + sparkline (v1.1.0) |
| [Startup Time Trend](https://ddsha441981.github.io/wiredoctor/startup-time-trend.html) | trendHistory in baseline + trend chart with verdict bands (v1.1.3) |
| [Security posture](https://ddsha441981.github.io/wiredoctor/security-posture.html) | What the reports expose, offline-only network behavior |
| [Known Limitations](https://ddsha441981.github.io/wiredoctor/known-limitations.html) | Honest heuristics and what the tool cannot guarantee |

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Run your app once — `wiredoctor-report.json` and `wiredoctor-report.html` appe
| [Upgrade Guard](upgrade-guard.html) | Catching silent autoconfiguration changes across Boot upgrades |
| [Ghost Detector](ghost-detector.html) | Passive candidates + opt-in first-touch tracking, and their trust postures |
| [Thread Distribution](thread-distribution.html) | Per-thread bean map with donut chart (v1.1.0) |
| [Startup Time Trend](startup-time-trend.html) | trendHistory in baseline + sparkline (v1.1.0) |
| [Startup Time Trend](startup-time-trend.html) | trendHistory in baseline + trend chart with verdict bands (v1.1.3) |
| [Security posture](security-posture.html) | What the reports expose, offline-only network behavior |
| [Known Limitations](known-limitations.html) | Honest heuristics and what the tool cannot guarantee |

Expand Down
16 changes: 15 additions & 1 deletion docs/report-tour.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ A guided walkthrough of the WireDoctor HTML console, tab by tab. All screenshots

The report is a single self-contained `wiredoctor-report.html` — the graph library is inlined at generation time, so it renders completely offline. Just open it in a browser.

The screenshots below were captured on 0.7.1. Everything they show is still there; the charts added in v1.1.3 (coupling quadrant, Pareto curve, trend verdict bands) are described in the Smells and Timing sections but are not in these images yet.

---

## Overview tab
Expand Down Expand Up @@ -67,6 +69,15 @@ Architecture smells computed on the **live resolved graph** — what Spring actu

- **High fan-in · coupling hotspots**: beans the most others depend on. A change here ripples widest — here `AzureTokenCredentialAutoConfiguration` (8 dependents) and `initializrMetadataProvider` (6) top the list.
- **High fan-out · shotgun surgery risk**: beans that depend on the most others — they break when any of their many dependencies change.
- **Who is on the other end (v1.1.3)**: every row in both tables expands to the actual beans it is coupled to, so "fan-in 6" no longer means tracing six edges in the graph by hand.

Above the tables sits the **coupling quadrant** (v1.1.3) — a fan-out vs fan-in scatter of every bean, so you see the shape the two top-10 lists hide:

- **Up the left edge**: god beans — high fan-in, low fan-out. Many beans depend on them, so a change ripples widest.
- **Along the bottom**: shotgun-surgery risks — high fan-out, low fan-in.
- The dashed **I = 0.8** line is the same instability threshold the *Unstable beans* table uses; everything below it is unstable.
- Dot size is how many beans share that exact position, framework beans are dimmed (with a **Hide framework beans** toggle), and clicking a dot that holds a single bean opens it in the Graph tab.
- Axes are square-root scaled so a 400-bean tail stays readable — but every tick is a real count, not a bucket.

---

Expand All @@ -76,9 +87,12 @@ Architecture smells computed on the **live resolved graph** — what Spring actu

Real measured startup numbers from `BufferingApplicationStartup` — no reflection heuristics:

- **Slowest startup steps**: the Boot lifecycle phases, with `spring.context.refresh` (4,619ms) at the top and individual `spring.beans.instantiate` steps below.
- **How few beans you would have to fix (v1.1.3)**: a Pareto curve of cumulative bean-instantiation time with the 80% knee marked — on a start.spring.io run, **37 of 283 beans** carry 80% of it. The caption is explicit that a bean's time includes the beans its constructor triggers, so the total counts nested instantiation more than once: it is a ranking of where to look, not a wall-clock budget.
- **Slowest startup steps**: the Boot lifecycle phases, with `spring.context.refresh` (4,619ms) at the top and individual `spring.beans.instantiate` steps below. Since v1.1.3 the bean each `instantiate` step created is its own column, so a step no longer names a phase without naming what it built.
- **Slow bean instantiation**: every bean over the `slow-bean-threshold-ms` (default 100ms), ranked. On this run, `bomRangesInfoContributor` (315ms) and `initializrMetadataProvider` (313ms) lead.

The **startup time trend** chart closes the tab: startup time plus a dashed bean-count line, with a coloured band on every interval that crosses the `startup-time` gate's thresholds — red when a slowdown is *unexplained by bean count*, amber when the app also grew, green when it got faster. It only has data from the second `baseline-write=true` run onward; see the [Startup Time Trend guide](startup-time-trend.html) for the verdict rules and the caveats they carry.

Since v0.7.1, this tab also hosts the **Performance Gates card**: each gate (startup-time, slow-bean, new-cycle, condition-changed) with its threshold, actual value, and a PASS/FAIL/NOT RUN verdict chip — plus `not armed` tags and a CI hint when gates aren't configured. This is the UI counterpart of `wiredoctor.fail-on` CI gating (see the [Performance Gates guide](performance-gates.html)).

---
Expand Down
3 changes: 3 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ nav_order: 13

| Version | Date | What |
|---------|------|------|
| v1.1.3 | 2026-08-24 | **Report readability** — coupling quadrant, Pareto curve, trend verdict bands, drill-downs on every ranking |
| v1.1.2 | 2026-08-24 | Report accuracy — type-collected beans are not ghosts, baseline diff noise masked |
| v1.1.1 | 2026-08-08 | Log format fix |
| v1.1.0 | 2026-08-22 | **Longitudinal Visibility** — startup time trend with sparkline, thread distribution with donut chart, EnvironmentPostProcessor migration, foreign ApplicationStartup respect |
| v1.0.0 | 2026-08 | **Stability Contract** — frozen JSON schema (`schemaVersion: 1`), frozen config property names, performance budget (< 5s on 1k beans), zero-intrusion guarantee |
| v0.10.0 | 2026-07-29 | Graph timing heat + critical path chips |
Expand Down
30 changes: 23 additions & 7 deletions docs/startup-time-trend.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ title: Startup Time Trend
nav_order: 10
---

# Startup Time Trend — track creep over time (v1.1.0)
# Startup Time Trend — track creep over time (v1.1.0, chart rebuilt in v1.1.3)

Every baseline write appends a timestamped snapshot to `trendHistory[]` inside `wiredoctor-baseline.json`. The Timing tab renders a **sparkline** so you can see startup-time creep before the gate trips.
Every baseline write appends a timestamped snapshot to `trendHistory[]` inside `wiredoctor-baseline.json`. The Timing tab renders it as a chart so you can see startup-time creep before the gate trips — and, since v1.1.3, says whether a slowdown is explained by the app having grown.

---

Expand All @@ -14,28 +14,44 @@ Every baseline write appends a timestamped snapshot to `trendHistory[]` inside `
When WireDoctor runs with `wiredoctor.baseline-write=true`, before writing the baseline:

1. Read the existing baseline JSON (if present) and extract `trendHistory[]`
2. Append a new entry: `{timestamp, totalStartupMs, slowBeanCount}`
2. Append a new entry: `{timestamp, totalStartupMs, slowBeanCount, beanCount}`
3. Cap at `wiredoctor.trend-history-size` (default: 30 entries, `0` = unlimited)
4. Trim oldest entries if over cap
5. Write the updated baseline

The `totalStartupMs` field is omitted from entries when it was `null` (pre-v0.7.0 baselines where `ApplicationReadyEvent.getTimeTaken()` was unavailable). This lets the trend grow organically across Boot upgrades.

`beanCount` (v1.1.3) is the same number the report's dependencies section shows. Entries written before v1.1.3 have no `beanCount`, and that is handled rather than guessed — see the verdict rules below.

Example `trendHistory` in `wiredoctor-baseline.json`:

```json
{
"trendHistory": [
{ "timestamp": 1723900800000, "totalStartupMs": 3420, "slowBeanCount": 2 },
{ "timestamp": 1723987200000, "totalStartupMs": 3580, "slowBeanCount": 3 },
{ "timestamp": 1724073600000, "totalStartupMs": 4759, "slowBeanCount": 5 }
{ "timestamp": 1723900800000, "totalStartupMs": 3420, "slowBeanCount": 2, "beanCount": 388 },
{ "timestamp": 1723987200000, "totalStartupMs": 3580, "slowBeanCount": 3, "beanCount": 391 },
{ "timestamp": 1724073600000, "totalStartupMs": 4759, "slowBeanCount": 5, "beanCount": 391 }
]
}
```

## In the HTML report

The Timing tab shows a **sparkline chart** of `totalStartupMs` over time. Each point is a baseline write. The chart makes gradual creep visible — a 200ms drift per week becomes obvious before the `startup-time` gate trips on a single bad run.
The Timing tab shows a two-axis chart: `totalStartupMs` as a solid line (left axis) and `beanCount` as a dashed line (right axis). Each point is a baseline write, so gradual creep becomes visible — a 200ms drift per week is obvious long before the `startup-time` gate trips on a single bad run.

Any interval that crosses the **`startup-time` gate's own thresholds** (both must hold — default `500ms` AND `20%`; see [Performance gates](performance-gates.html)) gets a coloured band, and the caption below states the verdict for the latest interval:

| Band | Meaning |
|------|---------|
| 🟥 red | slower, and the bean count barely moved — **unexplained by bean count** |
| 🟧 amber | slower, and the app grew by ≥1% more beans |
| 🟧 amber | slower, but one of the two runs predates `beanCount`, so nothing can be ruled explained or not |
| 🟩 green | faster by the same margins |
| no band | inside the gate's thresholds |

Two things the chart deliberately does not claim. Both axes span the observed range rather than starting at zero, so the line exaggerates small absolute changes — read the numbers, not the slope. And a flat bean count with rising startup is **not** proof of a code regression: a bigger dataset, a slower CI runner or a cold cache draw exactly the same line. The band says where to look, not what is wrong.

The dashed bean-count line is drawn in segments, so it never bridges a run that recorded no count.

**The sparkline only appears in reports produced by a `baseline-write=true` run, and only from the second write onward.** `trendHistory[]` lives in the baseline file, not in the per-run report: a normal diff/gate run does not read it back, so its report shows the "Need at least 2 baseline writes" placeholder even when the baseline already holds 30 entries. To see the trend, look at the report from your baseline-refresh job (a nightly CI run is the natural place), or read `trendHistory[]` out of `wiredoctor-baseline.json` directly.

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.ddsha441981</groupId>
<artifactId>wiredoctor-parent</artifactId>
<version>1.1.1</version>
<version>1.1.3</version>
<packaging>pom</packaging>

<name>WireDoctor</name>
Expand Down
Loading
Loading