Skip to content

v1.1.3 β€” report readability: three charts and drill-downs on every ranking - #46

Merged
ddsha441981 merged 15 commits into
mainfrom
release/v1.1.3
Aug 24, 2026
Merged

v1.1.3 β€” report readability: three charts and drill-downs on every ranking#46
ddsha441981 merged 15 commits into
mainfrom
release/v1.1.3

Conversation

@ddsha441981

Copy link
Copy Markdown
Owner

What

Report readability pass. 1.1.2 fixed what the report claimed; this release fixes what it shows.

Three charts

  • Coupling quadrant (Smells) β€” fan-out vs fan-in scatter of every bean: god beans up the left edge, shotgun-surgery risks along the bottom, dashed I = 0.8 line, dot size = beans sharing a position, framework beans dimmed with a toggle, single-bean dot opens it in the graph. Square-root axes so a 400-bean tail stays readable.
  • Pareto curve (Timing) β€” cumulative share of measured bean-instantiation time with the 80% knee marked (37 of 283 beans on start.spring.io). Caption states that a bean's time includes the beans its constructor triggers, so the total counts nested instantiation more than once β€” a ranking, not a wall-clock budget.
  • Startup-time trend (Timing) β€” the v1.1.0 sparkline rebuilt: startup time + dashed bean-count line, coloured band on every interval crossing the startup-time gate's own thresholds (default 500ms AND 20%). Red = slowdown unexplained by bean count, amber = the app also grew (or one run predates beanCount), green = faster.

Drill-downs β€” fan-in / fan-out rows expand to the beans on the other end; slow startup steps name the bean they instantiated; critical-path chips show how far into the chain each bean sits; orphan and proxied beans are listed, not just counted; Cycles tab lists cycle members and the @Lazy edge that breaks it.

Fixes β€” the proxy card no longer implies it inspected beans that were never instantiated; the histogram caption no longer calls its fixed 100ms bucket "the slow-bean threshold" (that value is configurable, and was 50ms on the run described).

New: beanCount in each trendHistory[] entry β€” without it the trend chart cannot tell a regression from an app that simply grew. Pre-1.1.3 entries stay valid and are handled, not guessed.

Tested

  • 259 unit/integration tests green (mvn -pl wiredoctor-autoconfigure test), including two new baseline tests for beanCount persistence across writes.
  • tools/render-check.js β€” new jsdom harness. The template is one top-down script, so a const declared below a tab's render is in TDZ for it and node --check passes such code; the harness caught three of these during this release. Run across 13 report JSONs (empty graph, no timings, single-bean graph, flat fan-in, five trend shapes, two real apps).
  • Real runs, real browser: demo app with a 7-entry baseline (bands verified against deliberately manipulated runs β€” 908/927/1318/943/1341/921/1329ms with Β±3 beans), spring-petclinic (462 beans, gate FAIL path), and start.spring.io (394 beans / 435 edges, ForkJoinPool thread tags, 2-entry trend). Screenshotted per tab in headless chromium β€” no console errors.

Not in this PR

No API, schema (schemaVersion: 1) or configuration changes. Chart animation was considered and deliberately skipped β€” the charts are static SVG built in one pass; adding motion to the same file as this diff doubles the review surface. Install snippets stay at 1.1.0 because that is what Maven Central serves.

The overview KPI printed "0 CGLIB / 0 JDK" as fact while the JSON already
recorded notInstantiatedSkipped β€” on the petclinic run, 3 beans that were
never instantiated and therefore never inspected.

Renders only when the count is > 0.
lazySuggestions had zero references in the template: the only actionable
advice WireDoctor produces was console- and JSON-only since 0.3.0. The HTML
showed cycles as a number and a graph filter, never the bean names.

New sidebar tab (third, with a red count badge), one card per cycle:
- the chain, first bean repeated so it reads as a loop
- every member clickable through to that node in the graph
- the @lazy cut with the lowest downstreamImpact, alternatives dimmed below
- no suggestion -> says only that, never that the cycle is unbreakable

The overview KPI now links to the tab.
startupSlowestSteps carries tags {beanName, beanType, threadName}; the table
used none of them, so the twelve slowest rows read "spring.beans.instantiate"
twelve times while the tag beside each one named the actual bean.

- Bean column, present only when at least one row is tagged
- beanType and threadName behind a native <details> (long, and thread only
  matters when it is not main)
- off-main thread also shown as a pill on the row itself
- untagged steps (spring.context.refresh, ...) render exactly as before

Adds the shared .drill style used by the remaining drill-downs.
smellTable took the strings 'dependents' and 'dependencies' as column labels
while the arrays with those exact names sat unused in the same objects β€” the
table said a bean has 4 dependents and never which 4.

Click the bean name to expand them, capped at 30 with a pointer to the JSON
for the rest.
cumulativeMs had zero references. A 369ms bean at the end of a 1111ms chain
means something different from the same 369ms at the start, and the chain
chips showed only ownMs.

Dim running total on each chip, cumulative in the timeline row tooltip, and
the explaining note renders only when the field is present β€” pre-1.1.3
reports keep exactly the chain they had.
orphanBeans, cglibBeans and jdkBeans were read only to colour graph nodes and
produce a count β€” the names never appeared as text anywhere in the report.

Both are now expandable rows in the composition card, capped at 30 names with
a pointer to the JSON. Orphans keep their heuristic caveat, spelled out where
the list is: nothing declares a dependency on them, which is not the same as
unused.

nameList/NAME_LIST_CAP moved up to the shared helpers β€” the overview renders
before the smells tab, so declaring them there was a TDZ error on any report
with at least one orphan.
node --check only proves the template's JS parses. This runs it against a real
wiredoctor-report.json and fails on any runtime error or empty pane β€” it is
what caught the NAME_LIST_CAP TDZ error in the previous commit.

Renders without the vis-network bundle, so the no-library degrade path is
covered every run. Skips cleanly when jsdom is absent, so it never becomes a
build dependency.
The slow-bean table answers "which bean is slowest". It does not answer the
question a developer actually has before deciding to spend an afternoon on it:
is the cost concentrated in a handful of beans, or spread thin across hundreds?

startupPareto() sorts beanTimings descending and plots the cumulative share.
The knee β€” the bean where the running total crosses 80% β€” is marked with a
dashed line and labelled, so the count is readable without counting pixels.
On petclinic that is 33 of 274 beans.

The caption does not present the sum of beanTimings as wall-clock time.
spring.beans.instantiate steps nest, and the analyzer keeps max-per-bean, so a
bean's number includes the beans its constructor triggered and the sum counts
nested work more than once. The share is a valid ranking of where cost
concentrates; the total is not a budget, and a dim line under the curve says so.

Degrades to the empty state below 5 timed beans (a curve through 3 points
claims a distribution that isn't there) β€” verified against a report with
beanTimings: {}.
highFanIn and highFanOut are ten rows each. They name the extremes and say
nothing about the distribution, which is the question an architect actually
asks: is coupling concentrated in three god beans, or is this a flat field of
leaves? One dot per (fan-out, fan-in) position answers it.

Bucketed by position, because 95 petclinic beans sit at fan-out 0 / fan-in 1 and
95 overlapping identical dots would be a lie told with ink. Dot area is how many
beans share the spot; the tooltip names the first six.

Square-root axes rather than the clamped edge band the plan called for. Clamping
the top decile puts a fan-in 25 god bean in the same band as a fan-in 4 bean and
destroys the only reading worth having. Every tick prints its real count, so the
scale is visible instead of silently applied.

The dashed curve is I = Ce/(Ca+Ce) = 0.8 β€” the same threshold the unstable table
uses, so chart and table cannot disagree on screen. Framework beans are dimmed,
not dropped, with one checkbox to hide them; the caption states the heuristic's
blind spot (a camelCase framework bean reads as user-defined).

isFw moves up to the shared helpers: the smells tab renders long before the
graph section, so reading it in place was the NAME_LIST_CAP TDZ error again.
The render check now exercises the toggle, which is the template's only
interactive re-render.
trendHistory carried timestamp, totalStartupMs and slowBeanCount. That is enough
to draw a line and not enough to read it: a run that got 800ms slower because
someone added forty beans looks identical to one that got 800ms slower for no
reason, and only the second is a bug.

beanCount is the number the dependencies section already reports, so the two
cannot disagree. Carried forward from prior entries only when present β€” a
baseline written before 1.1.3 keeps its entries and simply has no count on them,
rather than being back-filled with a number nobody measured.

Prepares the trend chart's explained / unexplained verdict. 259 tests.
The sparkline plotted one line and could not answer the only question worth
asking about it. An app that added forty beans and an app that added none draw
the identical rising curve, and only the second one is a bug.

Bean count now shares the plot on its own labelled axis (dashed, faint), and the
signal is the band between two consecutive runs rather than the points:

  red    regressed past the gate's thresholds with bean growth under 1%
  amber  regressed, and the app grew β€” or one endpoint has no bean count
  green  improved past the thresholds
  plain  inside the thresholds

Thresholds come from gates.config in the report, not from a constant, so the
chart cannot call something a regression that the gate lets pass. The fallback
for a pre-0.7.1 report without that section is WireDoctorProperties' own default
(500ms / 20%) β€” guessing lower would paint bands the gate would have passed.

Red needs a bean count on BOTH endpoints. A pre-1.1.3 entry can only reach
amber, because "nothing was added" is a claim its data cannot support. The bean
line is drawn as segments over consecutive counted runs, so it never joins run 2
to run 5 across a hole as though the runs between had been measured.

The caption says "unexplained by bean count", never "regression detected". A
flat bean count with rising startup does not prove a code regression β€” a bigger
dataset, a slower runner or a cold page cache draw the same line. The band says
where to look.

Verified on six real wiredoctor-test runs with the regression induced on
purpose (AlphaBean's sleep 50 -> 450ms, bean count held at 67): the four band
states came out amber, green, red, green in the order constructed. Entries with
no totalStartupMs are dropped rather than read as zero. 259 tests.
The distribution caption read "N over 100ms β€” the slow-bean threshold". The last
histogram bucket is fixed at 100ms+; the threshold is configurable, so on the
demo app (50ms) the sentence named the wrong number and read as if the tool had
measured against it. Found by re-running the JSON-key-vs-template diff:
slowBeanThreshold was in the report and nowhere in the HTML.

Now the bucket and the threshold are stated separately, the threshold read from
the report β€” "66 beans measured Β· 0 at 100ms or more Β· slow-bean threshold 50ms".
Parent version bumped in all three child poms as well β€” a bump that
misses one child breaks the reactor build in CI.
Report tour gains the coupling quadrant and the Pareto curve, and states
that its 0.7.1 screenshots predate them. The trend guide gets the
verdict-band table, the beanCount field and the two claims the chart
deliberately does not make.
@ddsha441981
ddsha441981 merged commit 8954e02 into main Aug 24, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant