From 78f6ec773081bbfbe626439af460bc868aa7bcb5 Mon Sep 17 00:00:00 2001 From: David <3dgiordano@gmail.com> Date: Tue, 25 Aug 2026 13:39:49 -0300 Subject: [PATCH] Initial commit --- README.md | 135 ++++ docs/waterfall-viewer-grouped-dark.png | Bin 0 -> 61411 bytes docs/waterfall-viewer-response.png | Bin 0 -> 56016 bytes docs/waterfall-viewer-timing.png | Bin 0 -> 52256 bytes docs/waterfall-viewer.md | 425 +++++++++++++ docs/waterfall-viewer.png | Bin 0 -> 39959 bytes .../visualizers/waterfall/WaterfallHost.java | 38 ++ .../visualizers/waterfall/WaterfallPanel.java | 360 +++++++++++ .../waterfall/WaterfallStatusBar.java | 59 ++ .../visualizers/waterfall/WaterfallTable.java | 595 ++++++++++++++++++ .../waterfall/WaterfallToolBar.java | 365 +++++++++++ .../waterfall/WaterfallViewerGui.java | 123 ++++ .../waterfall/WaterfallWindow.java | 156 +++++ .../visualizers/waterfall/WrapLayout.java | 122 ++++ .../waterfall/details/CookiesTab.java | 209 ++++++ .../waterfall/details/DetailTab.java | 103 +++ .../waterfall/details/HeadersTab.java | 188 ++++++ .../waterfall/details/HtmlDoc.java | 101 +++ .../waterfall/details/HttpHeaders.java | 85 +++ .../details/ResponseRendererTab.java | 271 ++++++++ .../waterfall/details/SampleDetailsPanel.java | 158 +++++ .../waterfall/details/TimingTab.java | 272 ++++++++ .../waterfall/model/FilterCriteria.java | 228 +++++++ .../waterfall/model/GroupMode.java | 54 ++ .../waterfall/model/PhaseBreakdown.java | 160 +++++ .../waterfall/model/ProtocolDetector.java | 67 ++ .../waterfall/model/SampleRecord.java | 297 +++++++++ .../visualizers/waterfall/model/TimeAxis.java | 217 +++++++ .../waterfall/model/WaterfallColumn.java | 118 ++++ .../waterfall/model/WaterfallFormat.java | 113 ++++ .../waterfall/model/WaterfallRow.java | 144 +++++ .../waterfall/model/WaterfallStore.java | 180 ++++++ .../waterfall/model/WaterfallTableModel.java | 498 +++++++++++++++ .../waterfall/render/PhaseLegend.java | 68 ++ .../waterfall/render/TimeTicks.java | 64 ++ .../render/TimelineHeaderRenderer.java | 120 ++++ .../render/WaterfallBarRenderer.java | 238 +++++++ .../render/WaterfallCellRenderer.java | 216 +++++++ .../waterfall/render/WaterfallColors.java | 212 +++++++ .../render/WaterfallHeaderRenderer.java | 55 ++ .../waterfall/FilterCriteriaTest.java | 152 +++++ .../waterfall/HttpHeadersTest.java | 81 +++ .../waterfall/PhaseBreakdownTest.java | 109 ++++ .../waterfall/ProtocolDetectorTest.java | 51 ++ .../waterfall/ResponseRendererTabTest.java | 165 +++++ .../waterfall/SampleRecordTest.java | 87 +++ .../waterfall/SampleResultBuilder.java | 148 +++++ .../visualizers/waterfall/TimeAxisTest.java | 139 ++++ .../waterfall/WaterfallBarRendererTest.java | 152 +++++ .../waterfall/WaterfallFormatTest.java | 65 ++ .../waterfall/WaterfallJtlLoadingTest.java | 172 +++++ .../waterfall/WaterfallScreenshotHarness.java | 465 ++++++++++++++ .../waterfall/WaterfallStoreTest.java | 138 ++++ .../waterfall/WaterfallTableModelTest.java | 296 +++++++++ .../waterfall/WaterfallViewerGuiTest.java | 80 +++ 55 files changed, 9114 insertions(+) create mode 100644 docs/waterfall-viewer-grouped-dark.png create mode 100644 docs/waterfall-viewer-response.png create mode 100644 docs/waterfall-viewer-timing.png create mode 100644 docs/waterfall-viewer.md create mode 100644 docs/waterfall-viewer.png create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallHost.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallPanel.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallStatusBar.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallTable.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallToolBar.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallViewerGui.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallWindow.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WrapLayout.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/CookiesTab.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/DetailTab.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HeadersTab.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HtmlDoc.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HttpHeaders.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/ResponseRendererTab.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/SampleDetailsPanel.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/TimingTab.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/FilterCriteria.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/GroupMode.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/PhaseBreakdown.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/ProtocolDetector.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/SampleRecord.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/TimeAxis.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/WaterfallColumn.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/WaterfallFormat.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/WaterfallRow.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/WaterfallStore.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/model/WaterfallTableModel.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/render/PhaseLegend.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/render/TimeTicks.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/render/TimelineHeaderRenderer.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/render/WaterfallBarRenderer.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/render/WaterfallCellRenderer.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/render/WaterfallColors.java create mode 100644 src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/render/WaterfallHeaderRenderer.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/FilterCriteriaTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/HttpHeadersTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/PhaseBreakdownTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/ProtocolDetectorTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/ResponseRendererTabTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/SampleRecordTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/SampleResultBuilder.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/TimeAxisTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallBarRendererTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallFormatTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallJtlLoadingTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallScreenshotHarness.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallStoreTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallTableModelTest.java create mode 100644 src/test/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallViewerGuiTest.java diff --git a/README.md b/README.md index 43295131..15613843 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,11 @@ This plugin provides a `bzm - HTTP Sampler` (multi-protocol: **HTTP/1.1**, **HTT - [Multiplexing, HTTP/2, and overlapping sampler execution](#readme-multiplexing) - [**HTTP Async Controller**](#readme-http-async-controller) - [Controller panel](#readme-http-async-panel) +- [**Waterfall Viewer**](#readme-waterfall-viewer) + - [Reading the bars](#readme-waterfall-bars) + - [The details panel](#readme-waterfall-details) + - [Live runs and .jtl files](#readme-waterfall-sources) + - [The expanded window](#readme-waterfall-expanded) - [**JMeter property reference**](#readme-jmeter-property-reference) - [**Logging and low-level debugging**](#readme-logging) - [Keeping Jetty's own logging out of it](#readme-logging-jetty-verbosity) @@ -389,6 +394,131 @@ In **View Results Tree**, rows may follow **completion order**, not test-plan or > **When a Thread Group runs for a duration**, the requests this controller had already sent when the time ran out are still collected and reported, the same way JMeter lets a running request finish before it stops a thread. Nothing new is dispatched once the time is up, so no load reaches the system under test past the end of the run; the thread ends as soon as the responses it was already waiting for arrive. Tune or disable that grace with **`blazemeter.http.schedulerHoldMillis`**. A **Stop** is immediate and is never held off. + +# Waterfall Viewer + +Add it with **Add → Listener → bzm - Waterfall Viewer**. It draws a run the way a browser's Network +panel draws a page load: a table of samples on the left, a time-aligned waterfall of coloured phase +bars on the right, and a DevTools-style details panel for whichever request you click. + +It is the listener to reach for when the question is *why was this slow* rather than *what did this +return* — and it keeps what **View Results Tree** gives you for the second question too. + +Works with **HTTP/1.0, HTTP/1.1, HTTP/2 and HTTP/3**, and with any other sampler as well: a +`SampleResult` needs only a **start time and an elapsed time** to get a bar. JDBC, JMS, gRPC, +GraphQL and JSR223 samples all appear. Connect time, latency, protocol, headers and body are +enrichment — present when the sampler reports them, and explicitly reported as missing when not. + +📖 **[Full guide: docs/waterfall-viewer.md](docs/waterfall-viewer.md)** + +![Waterfall Viewer](docs/waterfall-viewer.png) + + +### Reading the bars + +Each bar spans the sample's real wall-clock extent on a shared time axis, split into the phases in +the order they happen: + +| Colour | Phase | Source | +|---|---|---| +| ⬜ Grey | **Queueing / Idle** | `idleTime` — wall-clock time the sample did not measure, such as the pauses between a transaction controller's children | +| 🟧 Orange | **Connect** | `connectTime`. Absent on a reused connection. On HTTPS this includes the TLS handshake | +| 🟩 Green | **Waiting (TTFB)** | `latency − connectTime` — the server thinking | +| 🟦 Blue | **Content Download** | `elapsed − latency` — reading the body off the wire | +| ▪ Slate | **No phase data** | The whole elapsed time as one segment, for a sample that reported neither connect time nor latency | + +Columns are **Name, Method, Status** (coloured by status class)**, Protocol, Type, Size, Time, Start** +(relative)**, Thread** and the waterfall itself; hover a bar for a tooltip carrying the full phase +breakdown. + +> [!NOTE] +> These are exactly the phases JMeter measures, and no others. There is **no separate SSL/TLS +> phase**: `SampleResult.connectTime` is the whole connection setup with the handshake inside it, +> there is no API to ask for the handshake alone, and no JTL column carries it. A browser can draw a +> purple TLS band because it instrumented its own socket; JMeter does not report that number, so the +> viewer does not draw a band for it. The Timing tab labels the connect row *Connect (TLS handshake +> included)* on a secure request. + + +### The details panel + +Click a row and four tabs describe that sample. The split between them is a division of labour with +JMeter: **Response** is JMeter's own machinery, and the other three are what a waterfall needs that +View Results Tree has no equivalent for. Only the visible tab does any work. + +| Tab | What it holds | +|---|---| +| **Response** | View Results Tree's right-hand side, hosted here: the same **Render:** selector over every renderer this JMeter has, and the renderer's own *Sampler result / Request / Response data* tabs | +| **Headers** | A **General** section (URL, method, status, protocol, sizes), then request and response headers together, with one filter box that narrows all three and per-block copy buttons | +| **Timing** | Every reported phase as a stacked bar plus a table of durations and shares, absolute start and finish, and **Copy timing** | +| **Cookies** | Request cookies (from the Cookie Manager field or the `Cookie` header), and response cookies split into Name, Value, Domain, Path, Expires, Max-Age, HttpOnly, Secure, SameSite | + +> [!IMPORTANT] +> **Nothing in the Response tab looks at a content type itself.** How to present a response body is +> a problem JMeter already solved, dynamically and extensibly, through its `ResultRenderer` +> interface — HTML, JSON, XML, images, the extractor testers, `RenderAsDocument` running the bytes +> through Apache Tika so a PDF or a Word response comes out as text, and whatever renderer a +> third-party plugin dropped into `lib/ext`. Renderers are discovered exactly as View Results Tree +> discovers them and ordered by the same `view.results.tree.renderers_order` property, so this tab +> behaves identically and gains whatever a future JMeter release adds. Reimplementing that MIME +> handling here would have produced a second, worse copy that drifts at every JMeter release. + + +### Live runs and .jtl files + +**Live:** rows appear as samples complete. Samples are queued on the sampler threads and drained +into the table four times a second in bounded batches — a listener must never make a test thread wait +on the UI, or it distorts the numbers it is reporting. While you have not zoomed, the axis follows +the growing run; once you zoom, the view stays put. + +**From a file:** use the listener's own **Write results to file / Read from file** panel to browse to +a `.jtl`. Both CSV and XML are read, including files from a non-GUI run, and parsing happens on a +background thread so a large file does not freeze JMeter. + +To get a full waterfall out of `jmeter -n -l results.jtl`, the run has to save the columns the +viewer draws with — `connect_time`, `latency`, `idle_time`, and for the protocol and the details +panel an XML file with the header and data columns on. Two caveats worth knowing: + +- **`sampleresult.timestamp.start` must be `true`** (JMeter's own default in `jmeter.properties`, but + the built-in fallback is `false`). Whether a JTL's `timeStamp` is the *start* or the *end* of a + sample is decided by this property in the JMeter **reading** the file. Get it wrong and every bar + is offset by its own duration — internally consistent, and quietly wrong. +- **A CSV `.jtl` carries no URL and no protocol.** JMeter's CSV reader writes the URL column and then + skips it on the way back in, and the protocol lives in the response status line, which CSV has no + column for. Use `output_format=xml` when you need either. + +Full property list and the reasoning behind each limitation: +**[docs/waterfall-viewer.md](docs/waterfall-viewer.md)**. + + +### The expanded window + +A waterfall is only as useful as it is wide — a bar three pixels across cannot say which phase +dominated it — and JMeter's listener panel is a fraction of a screen with the test plan tree beside +it. So the view has two modes: + +- **Embedded (mini):** inside the listener panel, for a glance while a test runs. +- **Expanded:** **Expand to window** opens it in a **maximised window of its own**, which is where + the analysis is meant to happen. **F11** toggles fullscreen, **Escape** leaves it, and **Return to + panel** (or closing the window) puts it back. + +Expanding **moves** the view rather than cloning it: same samples, same filters, same zoom, same +selection either way, so switching never loses what you were reading. The listener panel shows a +note while the window is open, so it is never unclear where the data went. + +Filters cover label or URL (substring or regex), status class, protocol, minimum elapsed time and +errors-only; rows can be grouped into collapsible headings by **Thread Group** or **Label**; the +timeline zooms with **Ctrl + wheel** over the bars, pans with **Shift + wheel**, and zooms to a +dragged range on the ruler. + +> [!NOTE] +> Each row keeps a reference to its `SampleResult` so the details panel can show bodies, which makes +> retention the viewer's memory ceiling. The oldest samples are evicted past +> **`blazemeter.waterfall.maxSamples`** (default **50 000**) and the status line reports how many +> were dropped, so a partial timeline never looks complete. For a very long run, write a `.jtl` +> during the test and open that afterwards. + + # JMeter property reference @@ -441,6 +571,11 @@ The diagnostic switches are **not** in this table: `blazemeter.http.lowLevelLog` | **blazemeter.http.controller.generateParentSample** | If you group all requests into a parent sample | false | | **blazemeter.http.controller.limitMaxParallel** | Limit max number of parallel executions | false | | **blazemeter.http.controller.maxConcurrentAsyncInController** | Maximum parallel requests (integer ≥ 1) | 100 | +| **blazemeter.waterfall.maxSamples** | Samples **bzm - Waterfall Viewer** retains before evicting the oldest. Each retained row holds its `SampleResult`, response body included, so this is the viewer's memory ceiling. **`-1`** for no limit | 50000 | + +The Waterfall Viewer adds only that one property: its Response tab is JMeter's own renderer stack and +so obeys JMeter's own `view.results.tree.*` settings — `renderers_order`, `max_size`, +`max_line_size` and `soft_wrap_line_size`. diff --git a/docs/waterfall-viewer-grouped-dark.png b/docs/waterfall-viewer-grouped-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..170ba41df095453ef6b321b9b06205179b7a780e GIT binary patch literal 61411 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 PJpgdGaq$n?mhD?YI0)fz^o{K4gKnTJh(7iYJ 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 literal 0 HcmV?d00001 diff --git a/docs/waterfall-viewer-timing.png b/docs/waterfall-viewer-timing.png new file mode 100644 index 0000000000000000000000000000000000000000..891d645475a4e92cc46d7f9038e2324e49b6b52c GIT binary patch literal 52256 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/waterfall-viewer.md b/docs/waterfall-viewer.md new file mode 100644 index 00000000..7dcf81dd --- /dev/null +++ b/docs/waterfall-viewer.md @@ -0,0 +1,425 @@ +# Waterfall Viewer + +A JMeter listener that draws a run the way a browser's Network panel draws a page load: a table of +samples on the left, a time-aligned waterfall of coloured phase bars on the right, and a DevTools +style details panel for whichever request you click. + +It is meant as a replacement for **View Results Tree** when the question is *why was this slow* +rather than *what did this return*, and it keeps everything View Results Tree gives you for the +second question too. + +- [What it shows](#what-it-shows) +- [Installation](#installation) +- [Using it on a live run](#using-it-on-a-live-run) +- [Loading a .jtl file](#loading-a-jtl-file) +- [The expanded window](#the-expanded-window) +- [Reading the bars](#reading-the-bars) +- [The details panel](#the-details-panel) +- [Filtering, grouping, zooming](#filtering-grouping-zooming) +- [Performance and memory](#performance-and-memory) +- [Properties](#properties) +- [What each data source can and cannot tell you](#what-each-data-source-can-and-cannot-tell-you) +- [Reviewing the UI: the screenshot harness](#reviewing-the-ui-the-screenshot-harness) + +![The Waterfall Viewer showing a page load](waterfall-viewer.png) + +--- + +## What it shows + +| Column | Meaning | +|---|---| +| **Name** | The sample label. Sub-samples (embedded resources, redirect hops, transaction children) are indented under their parent. | +| **Method** | `GET`, `POST`, … Blank for a sampler with no HTTP method. | +| **Status** | Response code, coloured: green for 2xx, amber for 3xx, red for 4xx/5xx, red for a non-numeric code on a failed sample. | +| **Protocol** | `HTTP/1.0`, `HTTP/1.1`, `HTTP/2`, `HTTP/3`, read from the response status line. | +| **Type** | Response content subtype (`json`, `html`, `png`, …). Hidden by default. | +| **Size** | Bytes received, headers included. | +| **Time** | Elapsed time. | +| **Start** | Start time relative to the first sample on screen. | +| **Thread** | Thread name. Hidden by default. | +| **Waterfall** | The bars, with the time ruler above them. | + +Right-click is not needed for the column set: use the **Columns** button in the toolbar. + +Any `SampleResult` with a **start time and an elapsed time** gets a bar — HTTP, JDBC, JMS, gRPC, +GraphQL, a JSR223 sampler, anything. Connect time, latency, protocol, headers and body are +*enrichment*: when a sampler reports them the bar is split into phases and the details panel fills +up; when it does not, you still get a correctly placed bar and the panel says what is missing. + +--- + +## Installation + +The viewer ships inside the plugin jar, so if the plugin is installed you already have it. + +**With Plugins Manager** — install or update *BlazeMeter HTTP Plugin* the usual way +(see the [main README](../README.md#readme-install-plugins-manager)). + +**Manually** — drop `jmeter-bzm-http2-.jar` into `/lib/ext` and restart +JMeter. + +**Building it yourself:** + +```bash +mvn clean package +``` + +The jar lands in `target/`. Copy it to `/lib/ext` and restart JMeter. + +**Verify:** the listener appears as **bzm - Waterfall Viewer** under +**Add → Listener** on any Thread Group or Test Plan node. If it is missing, the plugin jar is not on +JMeter's `lib/ext` path — check `jmeter.log` for a class-loading error. + +--- + +## Using it on a live run + +1. Right-click a Thread Group (or the Test Plan) → **Add → Listener → bzm - Waterfall Viewer**. +2. Start the test. + +Rows appear as samples complete, and the timeline grows with the run. While you have not zoomed, +the axis follows that growth so the whole run stays on screen; once you zoom or pan, the view stays +where you put it so an arriving sample cannot yank the region you are inspecting. + +**Clear** (the broom, or Ctrl+E) empties the viewer and resets the filters. + +A few things worth knowing for a live run: + +- Samples are handed over on the sampler threads and only *queued* there. The table is updated four + times a second on the UI thread, in bounded batches. A listener that made a test thread wait would + distort the very numbers it is reporting. +- The **Sub-samples** checkbox controls whether embedded resources, redirect hops and transaction + children get rows of their own. It applies to samples arriving from then on — turn it off before + the run, not during it. +- Leave the viewer's window open or closed as you like; data collection does not depend on it being + the visible tree node. + +--- + +## Loading a .jtl file + +The viewer uses JMeter's own result file panel, so it reads both flavours of `.jtl` — CSV and XML — +including files produced by a non-GUI run: + +```bash +jmeter -n -t plan.jmx -l results.jtl +``` + +1. Add **bzm - Waterfall Viewer** to the plan (it does not need to be the plan that produced the + file). +2. In **Write results to file / Read from file**, **Browse** to the `.jtl`. + +The file is parsed on a background thread, so a large result file does not freeze JMeter while it +loads; rows stream in as they are read. + +**To get a full waterfall out of a non-GUI run**, the run has to save the columns the viewer draws +with. In `user.properties`: + +```properties +# Timings: connect and latency are what split a bar into phases +jmeter.save.saveservice.connect_time=true +jmeter.save.saveservice.latency=true +jmeter.save.saveservice.idle_time=true + +# timeStamp is the START of the sample, not the end (this is JMeter's own default in +# jmeter.properties, but the built-in fallback is false — see the note below) +sampleresult.timestamp.start=true + +# XML output, if you want protocol, headers and bodies in the details panel +jmeter.save.saveservice.output_format=xml +jmeter.save.saveservice.response_headers=true +jmeter.save.saveservice.requestHeaders=true +jmeter.save.saveservice.response_data=true +jmeter.save.saveservice.samplerData=true +jmeter.save.saveservice.url=true +``` + +> **`sampleresult.timestamp.start` matters more here than anywhere else.** Whether a JTL's +> `timeStamp` column is the *start* or the *end* of a sample is decided by this property in the +> JMeter that **reads** the file, not the one that wrote it. If the two disagree, every bar is +> offset by its own duration — the picture stays internally consistent and is quietly wrong. +> JMeter's shipped `jmeter.properties` sets it to `true`; keep it that way on both ends. + +--- + +## The expanded window + +A waterfall is only as useful as it is wide: a bar three pixels across cannot tell you which phase +dominated it. JMeter's listener panel is a fraction of a screen with the test plan tree beside it, so +the viewer can move out of it. + +- **Expand to window** (toolbar) opens the waterfall in a **maximised window of its own**. +- **F11** in that window toggles **fullscreen**; **Escape** leaves fullscreen. +- **Return to panel**, or just closing the window, puts it back. + +Expanding *moves* the view rather than cloning it: same samples, same filters, same zoom level, same +selection, either way. The listener panel shows a note while the window is open so it is never +unclear where the data went. + +--- + +## Reading the bars + +Each bar spans the sample's real wall-clock extent on a shared time axis, split into phases in the +order they happen: + +| Colour | Phase | Where it comes from | +|---|---|---| +| ⬜ Grey | **Queueing / Idle** | `idleTime` — wall-clock time the sample did not measure, such as the pauses between a transaction controller's children. Zero for a plain request. | +| 🟧 Orange | **Connect** | `connectTime`. Absent on a reused connection, which is the normal case after the first request to a host. On an HTTPS request this figure includes the TLS handshake, and the Timing tab labels it as such. | +| 🟩 Green | **Waiting (TTFB)** | `latency − connectTime` — the server thinking. | +| 🟦 Blue | **Content Download** | `elapsed − latency` — reading the body off the wire. | +| ▪ Slate | **No phase data** | The whole elapsed time as one segment, for a sample that reported neither a connect time nor a latency. | + +These are exactly the phases JMeter measures, and no others. In particular **there is no separate +SSL/TLS phase**: `SampleResult.connectTime` covers the whole connection setup with the handshake +inside it, and neither JTL format has a column for the handshake alone. A browser can show a purple +TLS band because it instrumented its own socket; JMeter does not report that number, so the viewer +does not draw a band for it. + +The slate bar is the one with no browser equivalent, and it is deliberate too: a sampler that never +measured a connect time gets an honest single bar rather than a fabricated split. + +The elapsed time is written next to each bar when there is room. **Hover** a bar for a tooltip with +the label, URL, status, protocol, size, relative start and the full phase breakdown. + +A bar too short to fill a pixel is widened to a marker, because a 3 ms request in a five-minute run +would otherwise be invisible and read as *no sample here*. + +--- + +## The details panel + +Click any row. The panel below the table describes that sample across four tabs, and the split +between them is a deliberate division of labour with JMeter. + +**Response** is JMeter's own machinery, unmodified. **Headers**, **Timing** and **Cookies** are the +things a waterfall needs that View Results Tree has no equivalent for. + +Only the visible tab does any work, so clicking down a list of megabyte responses does not decode +four copies of each body. Every tab that has nothing to show says *why* instead of going blank. + +### Response + +This tab **is** View Results Tree's right-hand side, hosted inside the waterfall. + +At the top is the same **Render:** selector, listing every renderer this JMeter has, in the order +set by the standard `view.results.tree.renderers_order` property, defaulting to the same +`RenderAsText`. Below it are the renderer's own tabs — *Sampler result*, *Request*, *Response data* +— with JMeter's request views, its response metadata table and its search box. + +That means the presentation of a response body is decided the way JMeter decides it: dynamically, +by content type, through the `ResultRenderer` interface. + +| Renderer | What it handles | +|---|---| +| **Text** | Plain text, and the default. | +| **HTML** / **HTML Source Formatted** / **HTML (download resources)** | Markup, rendered or as formatted source. | +| **JSON** | Pretty-printed and navigable. | +| **XML** | As a collapsible DOM tree. | +| **Document** | Anything Apache Tika can read — PDF, Word, Excel, OpenDocument — extracted to text. | +| **Regexp Tester**, **CSS/JQuery Tester**, **XPath Tester**, **XPath2 Tester**, **JSON Path Tester**, **JMESPath Tester** | Try an extractor expression against this very response. | +| **Browser** | A real browser view, when JavaFX is present. | +| *anything else* | A third-party renderer dropped into `lib/ext` shows up here on its own. | + +Images are handled the same way JMeter handles them: a sample whose data type is not text goes down +JMeter's image path rather than its text path. + +**Nothing in this tab looks at a content type itself.** Reimplementing JMeter's MIME handling would +have meant a second, worse copy of it — one that would not know about Tika-backed document +extraction, would not pick up a renderer a plugin installed, and would drift from View Results Tree +at every JMeter release. The response-viewing behaviour here is JMeter's, and it improves when +JMeter's does. + +> The renderer list is discovered by scanning the classpath, which is slow, so it happens once per +> JMeter session and only the first time a response is actually shown — never while JMeter is +> building its Add menu. + +### Headers + +The DevTools-style header view, in three sections in the order the questions get asked: + +- **General** — URL, method, status code and message, protocol, label, thread, elapsed, response + size broken into headers and body, request size, success. +- **Request Headers** +- **Response Headers** + +A **Filter headers** box narrows all three at once — the way to find one header in a request that +carries thirty. **Copy request headers** and **Copy response headers** put the raw blocks on the +clipboard. + +JMeter shows the same headers, but split across its *Request* and *Response data* tabs and in +separate tables. This tab exists for the case where you want them together and searchable in one +place; the Response tab remains the authority on the exact bytes. + +### Timing + +Every phase the sample reported, as a stacked bar and as a table of durations and shares of the +span, plus the absolute start and finish times and a **Copy timing** button. + +This is the tab with no View Results Tree counterpart, and the reason the listener exists. It is +also where the limits of the data are stated rather than papered over: + +- A sample with no connect time and no latency gets one row saying its elapsed time cannot be split. +- On an HTTPS request the connect row is labelled **Connect (TLS handshake included)**, because that + is what JMeter's `connectTime` is. +- Idle time is explained where it appears. + +### Cookies + +**Request cookies** — from JMeter's own cookie field when the Cookie Manager filled it in, and from +the `Cookie` request header otherwise, because a request built with an explicit Header Manager has +one and not the other. + +**Response cookies** — one row per `Set-Cookie`, split into Name, Value, Domain, Path, Expires, +Max-Age, HttpOnly, Secure and SameSite. That breakdown is where the answers to *why did my session +not stick* live: a wrong path, a domain that does not match, an expiry in the past. +--- + +## Filtering, grouping, zooming + +**Filters** (first toolbar row), all combined with AND: + +| Control | Effect | +|---|---| +| **Filter** | Substring match on label or URL. Tick **regex** to treat it as a regular expression; the field turns red while an expression does not compile, and filters nothing rather than everything. | +| **Status** | A menu of status classes (`2xx`…`5xx`, plus *Other / non-numeric*). Nothing ticked means everything passes. | +| **Protocol** | Built from the protocols actually seen. Everything seen stays available to filter on, so narrowing to HTTP/2 does not remove HTTP/1.1 from the list. | +| **Errors only** | Hide successful samples. | +| **Min time (ms)** | Hide anything faster — the fastest way to isolate the slow tail. | +| **Clear filters** | Back to pass-through. | + +**View options** (second row): + +| Control | Effect | +|---|---| +| **Group by** | *No grouping*, *Thread Group* or *Label*. Group headings are collapsible and carry the group's sample count, failure count, total bytes and overall span as a hollow envelope bar. Grouping by thread group answers *what was this virtual user doing*; grouping by label answers *how did this request behave across the run*. | +| **Big rows** | Taller rows with thicker bars. | +| **Sub-samples** | Whether embedded resources, redirect hops and transaction children get their own rows. | +| **Details** | Show or hide the details panel. | +| **Columns** | Column visibility. | +| **Timeline** | `−` `+` zoom, **Fit**, `<` `>` pan. | + +**Timeline gestures:** + +| Gesture | Effect | +|---|---| +| **Ctrl + mouse wheel** over the bars | Zoom about the pointer — the instant under the cursor stays under the cursor. | +| **Shift + mouse wheel** over the bars | Pan. | +| **Middle-drag**, or **Shift + drag**, over the bars | Pan. | +| **Drag across the ruler** | Zoom to the dragged range. | +| **Double-click the ruler** | Fit the whole timeline. | +| **Ctrl +** / **Ctrl −** / **Ctrl 0** | Zoom in / out / fit. | + +The plain mouse wheel and plain clicks are left alone: a waterfall that hijacked the wheel would be +useless for its main job, which is scrolling a long list of requests. + +**Sorting:** click a column header — first click ascending, second descending, third back to arrival +order. With grouping on, sorting applies within each group; groups stay in timeline order. + +**Copy:** Ctrl+C copies the selected row's visible columns as tab-separated text with headings. + +--- + +## Performance and memory + +Row virtualisation is not an option here, it is the design. The bars are the table's **last column**, +not a separate component synchronised with it, so JTable's own virtualisation covers them: only the +rows on screen are ever painted, whether the model holds fifty samples or fifty thousand. Renderers +are shared instances that allocate nothing per row. + +Two more things keep a long run affordable: + +- **Incremental updates.** While nothing is grouped and no column is sorted — the state the viewer + is in during a run — arriving samples are appended and only the new rows are announced. Any other + state rebuilds the row list, which only happens when you change something. +- **A retention bound.** Each row keeps a reference to its `SampleResult` so the details panel can + show headers and bodies, and that reference is the memory ceiling of the whole viewer. The oldest + samples are evicted past **`blazemeter.waterfall.maxSamples`** (default **50 000**), and the status + line says how many were dropped so a partial timeline never looks complete. + +If you need every sample of a very long run, raise the bound and give the JVM the heap for it — or +write a `.jtl` during the run and open that afterwards, which costs no memory during the test at +all. + +--- + +## Properties + +Set these in `user.properties` and restart JMeter. + +| Property | Description | Default | +|---|---|---:| +| **blazemeter.waterfall.maxSamples** | Samples retained before the oldest are evicted. `-1` for no limit — every sample and its response body then stays in memory for the life of the viewer. | 50000 | + +The viewer adds only that one, because the Response tab is JMeter's own renderer stack and obeys +JMeter's own settings. The ones worth knowing: + +| Property | Effect on the Response tab | +|---|---| +| **view.results.tree.renderers_order** | Order of the renderers in the **Render:** selector, and therefore which appears first. | +| **view.results.tree.max_size** | Response bytes a renderer will show before truncating. | +| **view.results.tree.max_line_size** / **soft_wrap_line_size** | Line-length handling in the text views. | + +--- + +## What each data source can and cannot tell you + +A waterfall is only as good as the timings behind it, and not every field exists everywhere. The +short version: + +**A live run** gives you everything the sampler reports. `bzm - HTTP Sampler` and JMeter's own HTTP +Request both report connect time and latency, so bars are fully split; the protocol comes from the +response status line, and headers and bodies are in memory. + +**A CSV `.jtl`** gives you timings and status, but not the URL or the protocol: + +- The **URL column is not restored by JMeter's CSV reader** — it is written and then skipped on the + way back in. Nothing the viewer can do; the Name column still shows the label. +- The **protocol** lives in the response status line, which the CSV format has no column for. + +**An XML `.jtl`** can carry response headers, request headers and bodies, so a file saved with +`output_format=xml` and the data columns enabled gives you the protocol column and a fully populated +details panel. + +**No data source anywhere gives a separate TLS handshake time.** `SampleResult.connectTime` is the +whole connection setup with the handshake inside it, there is no API to ask for the handshake alone, +and neither JTL format has a column for it. So the viewer has no TLS phase at all: the connect bar is +one orange segment, and the Timing tab labels it *Connect (TLS handshake included)* on an HTTPS +request. Showing a purple band that was always empty, or an `SSL: 0 ms` row, would have been worse +than not showing one. + +--- + +## Reviewing the UI: the screenshot harness + +`WaterfallScreenshotHarness` (in the test sources) renders the viewer to PNG files. It is not a test +and the build does not run it; it exists because the defects a waterfall actually suffers from are +not the kind a pixel assertion catches. A toolbar that pushes its own controls off the right edge, a +Size column too narrow for `500.29 kB`, a bar flush against the edge of its column, a failed row +whose colour leaks into every row below it — all of those were found by generating images and looking +at them, and all of them pass every unit test. + +```bash +mvn test-compile +java -Djmeter.home="$JMETER_HOME" \ + -cp "target/test-classes:target/classes:$JMETER_HOME/lib/*:$JMETER_HOME/lib/ext/*" \ + com.blazemeter.jmeter.http2.visualizers.waterfall.WaterfallScreenshotHarness /tmp/shots +``` + +Add `dark` as a second argument to render with Darklaf, the look and feel JMeter ships for its dark +theme. Pointing `-Djmeter.home` at a real installation is what makes the Response tab show the +genuine renderer set, since JMeter's own component jars live under `lib/ext`. + +The thirteen scenarios cover a browser-like page load with embedded resources, the narrow embedded +view, each details tab, grouping by thread group, errors-only filtering, big rows, a zoomed timeline, +samples with no phase data, an empty viewer, a long run, and a width narrow enough to force the +toolbar to wrap. Layout needs a display — Swing will not lay out a detached component tree — but the +frames are packed and never shown, so nothing appears on screen. + +![The Timing tab](waterfall-viewer-timing.png) + +![The Response tab, hosting JMeter's own renderers](waterfall-viewer-response.png) + +![Grouped by thread group, under JMeter's dark theme](waterfall-viewer-grouped-dark.png) diff --git a/docs/waterfall-viewer.png b/docs/waterfall-viewer.png new file mode 100644 index 0000000000000000000000000000000000000000..ee9aa1c2a60d919885ca1c4b4fcb61fdb622224e GIT binary patch literal 39959 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 Nbwl%d_EocA{tt`2-_`&C literal 0 HcmV?d00001 diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallHost.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallHost.java new file mode 100644 index 00000000..1e145950 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallHost.java @@ -0,0 +1,38 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall; + +/** + * The actions the toolbar needs from whatever is hosting the waterfall. + * + *

An interface rather than a direct reference so the toolbar has no idea whether it is sitting + * inside JMeter's listener panel or inside the detached window - the two differ in what "expand" + * means, and in nothing else. + */ +public interface WaterfallHost { + + /** + * Shows or hides the details panel. + * + * @param visible {@code true} to show it + */ + void setDetailsVisible(boolean visible); + + /** + * Whether the details panel is showing. + * + * @return {@code true} when visible + */ + boolean isDetailsVisible(); + + /** Moves the waterfall into the detached window, or back into the listener panel. */ + void toggleExpandedWindow(); + + /** + * Whether the waterfall is currently in the detached window. + * + * @return {@code true} when expanded + */ + boolean isExpanded(); + + /** Discards every sample. */ + void clearResults(); +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallPanel.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallPanel.java new file mode 100644 index 00000000..d48a5384 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallPanel.java @@ -0,0 +1,360 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall; + +import com.blazemeter.jmeter.http2.visualizers.waterfall.details.SampleDetailsPanel; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.SampleRecord; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallRow; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallStore; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallTableModel; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import org.apache.jmeter.samplers.SampleResult; + +/** + * The waterfall view: toolbar, table, and the details panel below it. + * + *

This is the whole viewer, and it is deliberately one component that can live in two places. + * Expanding does not build a second view - it moves this panel out of JMeter's listener slot and + * into a window of its own, leaving a note behind. So the samples, the filters, the zoom level and + * the selection are the same objects either way, and switching between embedded and expanded never + * loses the state someone was in the middle of reading. + * + *

Samples arrive on sampler threads and are only ever queued there. A timer on the event + * dispatch thread drains the queue in bounded batches, which is what stops a fast test - or a large + * {@code .jtl} being read - from turning into thousands of individual table events. + */ +public final class WaterfallPanel extends JPanel implements WaterfallHost { + + private static final long serialVersionUID = 1L; + + /** Often enough to feel live, rarely enough that a fast test does not repaint continuously. */ + private static final int REFRESH_INTERVAL_MS = 250; + + /** Records created per drain, bounding how long one tick occupies the event dispatch thread. */ + private static final int MAX_RECORDS_PER_TICK = 5000; + + private static final int DETAILS_DIVIDER_LOCATION = 320; + private static final int MIN_TABLE_HEIGHT = 120; + private static final int MIN_DETAILS_HEIGHT = 140; + + private final WaterfallStore store = new WaterfallStore(); + private final WaterfallTableModel model = new WaterfallTableModel(store); + private final WaterfallTable table = new WaterfallTable(model); + private final SampleDetailsPanel details = new SampleDetailsPanel(); + private final JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + private final WaterfallStatusBar statusBar = new WaterfallStatusBar(); + private final WaterfallToolBar toolBar; + private final Container embeddedSlot; + private final Timer refreshTimer; + private final int dividerSize; + private transient SampleRecord selectedRecord; + private WaterfallWindow window; + private boolean restoringSelection; + private volatile boolean draining; + + /** + * Builds the view. + * + * @param embeddedSlot the container this panel lives in while not expanded, so it knows where to + * return to + */ + public WaterfallPanel(Container embeddedSlot) { + super(new BorderLayout()); + this.embeddedSlot = embeddedSlot; + this.toolBar = new WaterfallToolBar(model, table, this); + JScrollPane tableScroll = new JScrollPane(table); + tableScroll.setMinimumSize(new Dimension(0, MIN_TABLE_HEIGHT)); + details.setMinimumSize(new Dimension(0, MIN_DETAILS_HEIGHT)); + details.setCloseAction(() -> { + setDetailsVisible(false); + toolBar.setDetailsSelected(false); + }); + splitPane.setTopComponent(tableScroll); + splitPane.setBottomComponent(details); + splitPane.setResizeWeight(0.65); + splitPane.setDividerLocation(DETAILS_DIVIDER_LOCATION); + this.dividerSize = splitPane.getDividerSize(); + add(toolBar, BorderLayout.NORTH); + add(splitPane, BorderLayout.CENTER); + add(statusBar, BorderLayout.SOUTH); + table.getSelectionModel().addListSelectionListener(event -> { + if (!event.getValueIsAdjusting()) { + selectionChanged(); + } + }); + model.addTableModelListener(event -> restoreSelection()); + refreshTimer = new Timer(REFRESH_INTERVAL_MS, event -> pump()); + refreshTimer.setRepeats(true); + } + + /** + * Queues a sample. Called from sampler threads, and from the JTL reader. + * + *

Does no more than enqueue, and wakes the drain timer if it had gone idle. JMeter builds one + * instance of every listener GUI at startup just to read its menu label and then discards it, so + * a timer started in the constructor would run for the rest of the session on an object nobody + * can see; starting on the first sample means the throwaway instance never starts one. + * + * @param result the sample + */ + public void addResult(SampleResult result) { + store.offer(result); + if (!draining) { + SwingUtilities.invokeLater(this::startDraining); + } + } + + /** Starts the drain timer if it is not already running. Runs on the event dispatch thread. */ + private void startDraining() { + if (!draining) { + draining = true; + refreshTimer.start(); + } + } + + /** + * Drains queued samples into the table, then goes idle again when the queue is empty. + * + *

Idling matters because a listener sits in a test plan for the whole JMeter session: a timer + * left ticking forever would wake the event dispatch thread four times a second for a test that + * finished an hour ago. + */ + private void pump() { + boolean drained = store.drainPending(MAX_RECORDS_PER_TICK) > 0; + boolean changed = model.refresh(); + if (drained || changed) { + table.repaintTimeline(); + updateStatus(); + } + if (store.hasPending()) { + return; + } + // Clearing the flag before stopping closes the race with addResult: a sample arriving in the + // gap either sees the flag already down and schedules a restart, or lands in the queue that + // the re-check below notices. + draining = false; + refreshTimer.stop(); + if (store.hasPending()) { + startDraining(); + } + } + + private void updateStatus() { + statusBar.update(model.getVisibleSampleCount(), store.getRecords().size(), + store.getDroppedCount()); + } + + private void selectionChanged() { + if (restoringSelection) { + return; + } + WaterfallRow row = model.getRow(table.getSelectedRow()); + selectedRecord = row == null ? null : row.getRecord(); + details.setRecord(selectedRecord); + } + + /** + * Re-selects the previously selected sample after the row list was rebuilt. + * + *

Filtering, grouping and sorting all replace the row list, and a table event of that kind + * drops the selection. Without this, changing a filter would close the details panel on the very + * request being investigated. + */ + private void restoreSelection() { + if (selectedRecord == null || restoringSelection) { + return; + } + int index = model.indexOf(selectedRecord); + if (index < 0 || index == table.getSelectedRow()) { + return; + } + restoringSelection = true; + try { + table.setRowSelectionInterval(index, index); + } finally { + restoringSelection = false; + } + } + + @Override + public void setDetailsVisible(boolean visible) { + if (visible == isDetailsVisible()) { + return; + } + if (visible) { + splitPane.setBottomComponent(details); + splitPane.setDividerSize(dividerSize); + splitPane.setDividerLocation(DETAILS_DIVIDER_LOCATION); + } else { + splitPane.setBottomComponent(null); + // A split pane with nothing below it still draws its divider and its drag handles, which + // reads as a collapsed panel waiting to be dragged open rather than one that is switched off. + splitPane.setDividerSize(0); + } + revalidate(); + repaint(); + } + + @Override + public boolean isDetailsVisible() { + return splitPane.getBottomComponent() == details; + } + + @Override + public void toggleExpandedWindow() { + if (window != null) { + returnToPanel(); + } else { + expandToWindow(); + } + } + + @Override + public boolean isExpanded() { + return window != null; + } + + /** + * Moves this panel into a window of its own, leaving a note in the listener slot. + * + *

Moving rather than copying is what keeps one set of samples and one selection. The note + * matters too: an empty listener panel with the data apparently gone is alarming, and JMeter + * gives no other clue that a window belonging to this listener is open somewhere. + */ + private void expandToWindow() { + embeddedSlot.remove(this); + embeddedSlot.add(buildExpandedNotice(), BorderLayout.CENTER); + embeddedSlot.revalidate(); + embeddedSlot.repaint(); + window = new WaterfallWindow(this, this::returnToPanel); + window.open(); + } + + /** Moves this panel back into the listener slot and closes the window. */ + private void returnToPanel() { + if (window == null) { + return; + } + WaterfallWindow closing = window; + window = null; + closing.close(); + embeddedSlot.removeAll(); + embeddedSlot.add(this, BorderLayout.CENTER); + embeddedSlot.revalidate(); + embeddedSlot.repaint(); + toolBar.updateExpandButton(); + } + + private static JPanel buildExpandedNotice() { + JPanel notice = new JPanel(new BorderLayout()); + JLabel label = new JLabel("The Waterfall Viewer is open in its own window." + + " Close that window, or use its Return to panel button, to bring it back here.", + SwingConstants.CENTER); + label.setBorder(BorderFactory.createEmptyBorder(20, 12, 20, 12)); + label.setEnabled(false); + notice.add(label, BorderLayout.CENTER); + return notice; + } + + @Override + public void clearResults() { + store.clear(); + model.clear(); + selectedRecord = null; + details.setRecord(null); + table.fitTimeline(); + updateStatus(); + } + + /** + * Discards every sample and every filter, for the listener's Clear action. + * + *

Filters are reset too: JMeter's Clear is understood as "start again", and leaving a filter + * in place would make a fresh run look empty for reasons nothing on screen explains. + */ + public void reset() { + clearResults(); + toolBar.clearFilters(); + } + + /** + * Stops the refresh timer, for when the listener is discarded. + * + *

Swing timers are held by a shared queue, so one left running keeps this panel - and every + * sample it holds - reachable for the rest of the JMeter session. + */ + public void dispose() { + refreshTimer.stop(); + if (window != null) { + returnToPanel(); + } + } + + /** + * The toolbar, so the window can keep its expand button in step. + * + * @return the toolbar + */ + WaterfallToolBar getToolBar() { + return toolBar; + } + + /** + * The table model. Package-private for tests and for the screenshot harness, which need to set up + * a filter or a grouping without going through the toolbar's widgets. + * + * @return the model + */ + WaterfallTableModel getModel() { + return model; + } + + /** + * The table. Package-private for the same reason as {@link #getModel()}. + * + * @return the table + */ + WaterfallTable getTable() { + return table; + } + + /** + * Drains queued samples immediately instead of waiting for the timer. + * + *

For tests and the screenshot harness: a scenario that has just pushed samples in needs them + * on screen now, not in 250 ms. + */ + void drainNow() { + store.drainPending(Integer.MAX_VALUE); + model.refresh(); + updateStatus(); + } + + /** + * Selects a row, as a click would. + * + * @param rowIndex the row to select + */ + void selectRow(int rowIndex) { + if (rowIndex >= 0 && rowIndex < table.getRowCount()) { + table.setRowSelectionInterval(rowIndex, rowIndex); + } + } + + /** + * Brings one details tab to the front. + * + * @param title the tab title + */ + void selectDetailsTab(String title) { + details.selectTab(title); + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallStatusBar.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallStatusBar.java new file mode 100644 index 00000000..9e791164 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallStatusBar.java @@ -0,0 +1,59 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall; + +import com.blazemeter.jmeter.http2.visualizers.waterfall.render.PhaseLegend; +import com.blazemeter.jmeter.http2.visualizers.waterfall.render.WaterfallColors; +import java.awt.BorderLayout; +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; + +/** + * The strip along the bottom: how many samples are on show, and the colour key for the bars. + * + *

These two used to sit at the end of the toolbar, where they were the first things to be pushed + * off the edge as soon as the viewer was embedded in JMeter's listener panel - and they are exactly + * the two things that must not disappear. The legend is what makes the bars readable at all, and + * the counter is the only place the viewer can admit that it dropped older samples. A status bar of + * their own is always the full width of the panel, so neither can be squeezed out. + */ +public final class WaterfallStatusBar extends JPanel { + + private static final long serialVersionUID = 1L; + + private final JLabel countLabel = new JLabel(); + + /** Builds an empty status bar. */ + public WaterfallStatusBar() { + super(new BorderLayout(12, 0)); + setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, WaterfallColors.grid()), + BorderFactory.createEmptyBorder(3, 6, 2, 6))); + add(countLabel, BorderLayout.WEST); + add(new PhaseLegend(), BorderLayout.EAST); + update(0, 0, 0); + } + + /** + * Updates the sample counter. + * + * @param visible how many sample rows are on screen + * @param total how many samples are retained + * @param dropped how many were evicted to stay within the retention bound + */ + public void update(int visible, int total, int dropped) { + StringBuilder text = new StringBuilder(); + if (visible == total) { + text.append(total).append(total == 1 ? " sample" : " samples"); + } else { + text.append(visible).append(" of ").append(total).append(" samples"); + } + if (dropped > 0) { + text.append(" (").append(dropped).append(" older samples dropped)"); + } + countLabel.setText(text.toString()); + countLabel.setToolTipText(dropped > 0 + ? "The viewer keeps a bounded number of samples so a long run cannot exhaust memory." + + " Raise blazemeter.waterfall.maxSamples to keep more." + : null); + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallTable.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallTable.java new file mode 100644 index 00000000..22cd8d62 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallTable.java @@ -0,0 +1,595 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall; + +import com.blazemeter.jmeter.http2.visualizers.waterfall.details.HtmlDoc; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.PhaseBreakdown; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.SampleRecord; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.TimeAxis; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallColumn; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallFormat; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallRow; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallTableModel; +import com.blazemeter.jmeter.http2.visualizers.waterfall.render.TimelineHeaderRenderer; +import com.blazemeter.jmeter.http2.visualizers.waterfall.render.WaterfallBarRenderer; +import com.blazemeter.jmeter.http2.visualizers.waterfall.render.WaterfallCellRenderer; +import com.blazemeter.jmeter.http2.visualizers.waterfall.render.WaterfallColors; +import com.blazemeter.jmeter.http2.visualizers.waterfall.render.WaterfallHeaderRenderer; +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import javax.swing.AbstractAction; +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.TableModelEvent; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +/** + * The waterfall table: the sample columns on the left, the bars on the right, one row per sample. + * + *

Making the bars the table's last column, rather than a separate component synchronised with + * the table, is the decision the rest of this class rests on. It means the rows cannot drift out of + * alignment, the selection and the scroll position are shared by construction, and JTable's own + * virtualisation covers the bars as well as the text - only the rows on screen are ever painted, + * whether the model holds fifty samples or fifty thousand. + * + *

What that leaves this class to do is the interaction a plain table has no notion of: zooming + * and panning the time axis, dragging a range on the ruler, tooltips that describe a bar's phases, + * folding group headings, and copying a selection out as text. + */ +public final class WaterfallTable extends JTable { + + private static final long serialVersionUID = 1L; + + private static final int COMPACT_ROW_HEIGHT_PADDING = 4; + private static final int BIG_ROW_HEIGHT_PADDING = 16; + private static final double ZOOM_IN_FACTOR = 0.8; + private static final double ZOOM_OUT_FACTOR = 1.25; + private static final double PAN_STEP = 0.15; + + /** A ruler drag shorter than this is a click, not a range selection. */ + private static final int MIN_DRAG_PIXELS = 4; + + /** Rows of text height down from the top at which the empty-table hint sits. */ + private static final int EMPTY_HINT_LINES = 3; + + /** Least distance the empty-table hint keeps from an edge. */ + private static final int EMPTY_HINT_INSET = 12; + + /** Width of the disclosure triangle at the start of a group heading's Name cell. */ + private static final int DISCLOSURE_HIT_WIDTH = 22; + + private final WaterfallTableModel waterfallModel; + private final TimeAxis axis; + private int dragStartX = -1; + private int panAnchorX = -1; + + /** + * Builds the table. + * + * @param model the model to show + */ + public WaterfallTable(WaterfallTableModel model) { + super(model); + this.waterfallModel = model; + this.axis = model.getAxis(); + setAutoResizeMode(AUTO_RESIZE_LAST_COLUMN); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + setShowGrid(false); + setIntercellSpacing(new Dimension(0, 0)); + setFillsViewportHeight(true); + setBigRows(false); + configureColumns(); + installHeaderInteraction(); + installTimelineInteraction(); + installCopyAction(); + } + + /** + * Installs the renderers and widths. Re-run whenever the model's column set changes, because + * JTable throws its {@code TableColumn}s away and rebuilds them from scratch on a structure + * change. + * + *

Returns early when the fields are not populated yet. {@code JTable(TableModel)} installs the + * model from inside its own constructor, which fires a structure change and reaches + * {@link #tableChanged} before {@code super(...)} has returned and before any field declared here + * has been assigned. The constructor calls this again once it can. + */ + private void configureColumns() { + if (waterfallModel == null) { + return; + } + TableColumnModel columns = getColumnModel(); + for (int i = 0; i < columns.getColumnCount() && i < waterfallModel.getColumnCount(); i++) { + WaterfallColumn column = waterfallModel.getColumn(i); + TableColumn tableColumn = columns.getColumn(i); + tableColumn.setPreferredWidth(column.getPreferredWidth()); + tableColumn.setMinWidth(column.getMinimumWidth()); + if (column == WaterfallColumn.WATERFALL) { + tableColumn.setCellRenderer(new WaterfallBarRenderer(axis)); + tableColumn.setHeaderRenderer(new TimelineHeaderRenderer(axis)); + } else { + tableColumn.setCellRenderer(new WaterfallCellRenderer(column, axis)); + tableColumn.setHeaderRenderer(new WaterfallHeaderRenderer( + getTableHeader().getDefaultRenderer(), waterfallModel)); + } + } + } + + @Override + public void tableChanged(TableModelEvent event) { + super.tableChanged(event); + // A header event means the columns were rebuilt, taking the renderers with them. + if (event == null || event.getFirstRow() == TableModelEvent.HEADER_ROW) { + configureColumns(); + } + } + + /** + * Header behaviour: a click on a text column sorts, a drag across the ruler zooms to the dragged + * range, and a double click on the ruler returns to the full extent. + */ + private void installHeaderInteraction() { + JTableHeader header = getTableHeader(); + header.setReorderingAllowed(false); + header.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + dragStartX = isOverTimeline(event.getPoint()) ? event.getX() : -1; + } + + @Override + public void mouseReleased(MouseEvent event) { + if (dragStartX >= 0 && Math.abs(event.getX() - dragStartX) >= MIN_DRAG_PIXELS) { + zoomToDraggedRange(dragStartX, event.getX()); + } + dragStartX = -1; + } + + @Override + public void mouseClicked(MouseEvent event) { + if (isOverTimeline(event.getPoint())) { + if (event.getClickCount() >= 2) { + axis.fit(); + repaintTimeline(); + } + return; + } + int index = header.columnAtPoint(event.getPoint()); + if (index >= 0) { + waterfallModel.cycleSort(waterfallModel.getColumn(index)); + } + } + }); + } + + /** + * Zooms so the dragged pixel range fills the waterfall column. + * + * @param fromX the pixel the drag started at, in header coordinates + * @param toX the pixel the drag ended at + */ + private void zoomToDraggedRange(int fromX, int toX) { + Rectangle bounds = timelineBounds(); + if (bounds == null) { + return; + } + long from = axis.timeAt(fromX - bounds.x, bounds.width); + long to = axis.timeAt(toX - bounds.x, bounds.width); + axis.setView(from, to); + repaintTimeline(); + } + + /** + * Body behaviour over the waterfall column: control and the wheel zooms about the pointer, shift + * and the wheel pans, and a middle-button drag pans. + * + *

Plain wheel scrolling and plain clicks are left alone: a waterfall that hijacked the wheel + * would be unusable for its main job, which is scrolling a long list of requests. + */ + private void installTimelineInteraction() { + addMouseWheelListener(event -> { + if (!isOverTimeline(event.getPoint())) { + return; + } + if (event.isControlDown()) { + zoomAt(event.getPoint(), event.getWheelRotation() < 0); + event.consume(); + } else if (event.isShiftDown()) { + axis.pan(event.getWheelRotation() * PAN_STEP); + repaintTimeline(); + event.consume(); + } + }); + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + panAnchorX = isPanGesture(event) && isOverTimeline(event.getPoint()) ? event.getX() : -1; + } + + @Override + public void mouseReleased(MouseEvent event) { + panAnchorX = -1; + } + + @Override + public void mouseClicked(MouseEvent event) { + handleRowClick(event); + } + }); + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent event) { + if (panAnchorX < 0) { + return; + } + Rectangle bounds = timelineBounds(); + if (bounds != null && bounds.width > 0) { + axis.pan((panAnchorX - event.getX()) / (double) bounds.width); + panAnchorX = event.getX(); + repaintTimeline(); + } + } + }); + } + + private static boolean isPanGesture(MouseEvent event) { + return event.getButton() == MouseEvent.BUTTON2 + || (event.getButton() == MouseEvent.BUTTON1 && event.isShiftDown()); + } + + /** + * Folds a group heading: a single click on its disclosure triangle, or a double click anywhere on + * the row. + * + *

Not on a single click anywhere in the Name column, which would make a group impossible to + * select without collapsing it. + * + * @param event the click + */ + private void handleRowClick(MouseEvent event) { + if (waterfallModel == null) { + return; + } + WaterfallRow row = waterfallModel.getRow(rowAtPoint(event.getPoint())); + if (row == null || !row.isGroup()) { + return; + } + if (event.getClickCount() >= 2 || onDisclosureTriangle(event)) { + waterfallModel.toggleGroup(row.getGroupName()); + } + } + + /** + * Whether a click landed on the leading icon of the Name column. + * + * @param event the click + * @return {@code true} when it is within the disclosure area + */ + private boolean onDisclosureTriangle(MouseEvent event) { + int columnIndex = columnAtPoint(event.getPoint()); + if (columnIndex < 0 || waterfallModel.getColumn(columnIndex) != WaterfallColumn.NAME) { + return false; + } + int columnStart = 0; + for (int i = 0; i < columnIndex; i++) { + columnStart += getColumnModel().getColumn(i).getWidth(); + } + return event.getX() - columnStart <= DISCLOSURE_HIT_WIDTH; + } + + /** Copies the selected row as tab-separated text, so it can go straight into a report. */ + private void installCopyAction() { + KeyStroke copy = KeyStroke.getKeyStroke(KeyEvent.VK_C, menuShortcutMask()); + getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(copy, "waterfallCopy"); + getActionMap().put("waterfallCopy", new AbstractAction() { + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed(ActionEvent event) { + copySelectionToClipboard(); + } + }); + } + + /** + * The platform's copy modifier - command on macOS, control elsewhere. + * + *

Falls back to control when there is no display. That path never runs in JMeter's GUI, but + * asking the toolkit for it throws when headless, and a constructor that cannot complete outside + * a GUI cannot be covered by a test either. + * + * @return the modifier mask for the copy shortcut + */ + private static int menuShortcutMask() { + if (GraphicsEnvironment.isHeadless()) { + return InputEvent.CTRL_DOWN_MASK; + } + return Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + } + + /** Puts the selected row's visible columns on the clipboard, headings included. */ + public void copySelectionToClipboard() { + WaterfallRow row = waterfallModel.getRow(getSelectedRow()); + if (row == null) { + return; + } + StringBuilder headings = new StringBuilder(); + StringBuilder values = new StringBuilder(); + for (int i = 0; i < waterfallModel.getColumnCount(); i++) { + WaterfallColumn column = waterfallModel.getColumn(i); + if (column == WaterfallColumn.WATERFALL) { + continue; + } + if (headings.length() > 0) { + headings.append('\t'); + values.append('\t'); + } + headings.append(column.getTitle()); + values.append(WaterfallCellRenderer.textFor(row, column, axis)); + } + Toolkit.getDefaultToolkit().getSystemClipboard().setContents( + new StringSelection(headings + System.lineSeparator() + values), null); + } + + /** + * Zooms one step about a point. + * + * @param point the pointer position in this table's coordinates + * @param in {@code true} to zoom in + */ + private void zoomAt(Point point, boolean in) { + Rectangle bounds = timelineBounds(); + if (bounds == null || bounds.width <= 0) { + return; + } + double anchor = (point.x - bounds.x) / (double) bounds.width; + axis.zoom(in ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR, anchor); + repaintTimeline(); + } + + /** + * Zooms one step about the middle of the visible range, for the toolbar buttons. + * + * @param in {@code true} to zoom in + */ + public void zoomStep(boolean in) { + axis.zoom(in ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR, 0.5); + repaintTimeline(); + } + + /** + * Pans one step, for the toolbar buttons. + * + * @param forward {@code true} to move later in time + */ + public void panStep(boolean forward) { + axis.pan(forward ? PAN_STEP : -PAN_STEP); + repaintTimeline(); + } + + /** Returns the axis to the full extent of the timeline. */ + public void fitTimeline() { + axis.fit(); + repaintTimeline(); + } + + /** Repaints the bars and the ruler together, since they share the axis. */ + public void repaintTimeline() { + if (getTableHeader() != null) { + getTableHeader().repaint(); + } + repaint(); + } + + /** + * Switches between compact and roomy rows. + * + * @param big {@code true} for taller rows, which leaves space for a thicker bar and is easier to + * follow across a wide window + */ + public void setBigRows(boolean big) { + int base = getFont() != null ? getFontMetrics(getFont()).getHeight() : 16; + setRowHeight(base + (big ? BIG_ROW_HEIGHT_PADDING : COMPACT_ROW_HEIGHT_PADDING)); + } + + /** + * Whether a point is inside the waterfall column. + * + * @param point a point in this table's - or its header's - coordinates + * @return {@code true} when it falls in the bars + */ + private boolean isOverTimeline(Point point) { + Rectangle bounds = timelineBounds(); + return bounds != null && point.x >= bounds.x && point.x <= bounds.x + bounds.width; + } + + /** + * The horizontal extent of the waterfall column. + * + * @return the column's x offset and width, or {@code null} when the column is hidden + */ + private Rectangle timelineBounds() { + if (waterfallModel == null) { + return null; + } + TableColumnModel columns = getColumnModel(); + int x = 0; + for (int i = 0; i < columns.getColumnCount() && i < waterfallModel.getColumnCount(); i++) { + int width = columns.getColumn(i).getWidth(); + if (waterfallModel.getColumn(i) == WaterfallColumn.WATERFALL) { + return new Rectangle(x, 0, width, getHeight()); + } + x += width; + } + return null; + } + + /** + * A tooltip describing the row under the pointer: the phase breakdown over the bars, and the + * label and URL elsewhere. + * + * @param event the pointer position + * @return the tooltip markup, or {@code null} when there is nothing to say + */ + @Override + public String getToolTipText(MouseEvent event) { + if (waterfallModel == null) { + return null; + } + WaterfallRow row = waterfallModel.getRow(rowAtPoint(event.getPoint())); + if (row == null) { + return null; + } + if (row.isGroup()) { + return HtmlDoc.tooltip("" + HtmlDoc.escape(row.getGroupName()) + "
" + + row.getSampleCount() + " samples, " + row.getErrorCount() + " failed
" + + "span " + WaterfallFormat.duration(row.getEndTime() - row.getStartTime()) + ", " + + WaterfallFormat.size(row.getBytes())); + } + return HtmlDoc.tooltip(describe(row.getRecord())); + } + + /** + * Builds the rich tooltip for one sample. + * + * @param record the sample + * @return the tooltip body markup + */ + private String describe(SampleRecord record) { + StringBuilder text = new StringBuilder(); + text.append("").append(HtmlDoc.escape(record.getLabel())).append(""); + if (!record.getUrl().isEmpty()) { + text.append("
").append(HtmlDoc.escape(record.getUrl())).append(""); + } + text.append("
"); + if (!record.getMethod().isEmpty()) { + text.append(HtmlDoc.escape(record.getMethod())).append(' '); + } + text.append(HtmlDoc.escape(record.getResponseCode())); + if (!record.getProtocol().isEmpty()) { + text.append(" over ").append(HtmlDoc.escape(record.getProtocol())); + } + text.append("
").append(WaterfallFormat.size(record.getBytes())).append(" in ") + .append(WaterfallFormat.duration(record.getElapsed())); + text.append("
starts at +") + .append(WaterfallFormat.duration(record.getStartTime() - axis.getBoundsStart())); + appendPhases(text, record.getPhases()); + if (!record.isSuccess()) { + text.append("

Failed: ") + .append(HtmlDoc.escape(record.getResult().getResponseMessage())); + } + return text.toString(); + } + + private void appendPhases(StringBuilder text, PhaseBreakdown phases) { + text.append("

"); + appendPhase(text, "Queueing / Idle", phases.getIdleMs()); + appendPhase(text, "Connect", phases.getConnectMs()); + appendPhase(text, "Waiting (TTFB)", phases.getTtfbMs()); + appendPhase(text, "Download", phases.getDownloadMs()); + if (phases.getUnbrokenMs() > 0) { + text.append("No phase breakdown reported by this sampler"); + } + } + + private static void appendPhase(StringBuilder text, String name, long millis) { + if (millis > 0) { + text.append(name).append(": ").append(WaterfallFormat.duration(millis)).append("
"); + } + } + + /** + * Paints a hint over the empty table. + * + *

A listener that has just been added shows an empty grid, and an empty grid is + * indistinguishable from a broken one. Saying which of the two it is - waiting for a run, or + * filtered down to nothing - is the difference between a user starting their test and a user + * checking whether the plugin installed correctly. + * + * @param graphics the target + */ + @Override + protected void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + if (waterfallModel == null || waterfallModel.getRowCount() > 0) { + return; + } + String hint = waterfallModel.getFilter().isActive() + ? "No sample matches the current filters." + : "No samples yet. Start the test, or load a .jtl with the file panel above."; + Graphics2D g = (Graphics2D) graphics.create(); + try { + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setFont(getFont()); + g.setColor(WaterfallColors.mutedForeground()); + FontMetrics metrics = g.getFontMetrics(); + g.drawString(hint, Math.max(EMPTY_HINT_INSET, + (getWidth() - metrics.stringWidth(hint)) / 2), + Math.min(getHeight() - EMPTY_HINT_INSET, metrics.getHeight() * EMPTY_HINT_LINES)); + } finally { + g.dispose(); + } + } + + /** + * Keeps the ruler in step with a column resize. Widening the window changes how many pixels a + * millisecond gets, so the labels have to be redrawn even though no data changed. + * + * @param event the resize + */ + @Override + public void columnMarginChanged(ChangeEvent event) { + super.columnMarginChanged(event); + repaintTimeline(); + } + + /** + * The keyboard shortcuts the toolbar duplicates, so the timeline can be driven without the + * mouse. + * + * @param stroke the key pressed + * @param event the originating event + * @param condition the focus condition + * @param pressed whether the key went down + * @return whether the stroke was handled + */ + @Override + protected boolean processKeyBinding(KeyStroke stroke, KeyEvent event, int condition, + boolean pressed) { + if (pressed && (event.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0) { + switch (event.getKeyCode()) { + case KeyEvent.VK_PLUS: + case KeyEvent.VK_EQUALS: + zoomStep(true); + return true; + case KeyEvent.VK_MINUS: + zoomStep(false); + return true; + case KeyEvent.VK_0: + fitTimeline(); + return true; + default: + break; + } + } + return super.processKeyBinding(stroke, event, condition, pressed); + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallToolBar.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallToolBar.java new file mode 100644 index 00000000..3f3f52ba --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallToolBar.java @@ -0,0 +1,365 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall; + +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.FilterCriteria; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.GroupMode; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallColumn; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallTableModel; +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSpinner; +import javax.swing.JTextField; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +/** + * The controls above the waterfall: the filters on one line, the view options on the next. + * + *

Two lines rather than one because the embedded view inside JMeter's listener panel is narrow, + * and a single line of controls would either wrap unpredictably or scroll out of reach. The split + * also matches how the controls get used - filters while looking for something, view options while + * looking at it. + */ +public final class WaterfallToolBar extends JPanel { + + private static final long serialVersionUID = 1L; + + private static final int FILTER_COLUMNS = 18; + private static final int MAX_MIN_TIME_MS = 600_000; + private static final int MIN_TIME_STEP_MS = 50; + + /** Pixels between two logical groups of controls. */ + private static final int GROUP_SEPARATION = 14; + + /** Status classes offered in the status filter, {@code 0} standing for a non-numeric code. */ + private static final int[] STATUS_CLASSES = {2, 3, 4, 5, 0}; + + private final WaterfallTableModel model; + private final WaterfallTable table; + private final WaterfallHost host; + private final JTextField filterField = new JTextField(FILTER_COLUMNS); + private final JCheckBox regexCheckBox = new JCheckBox("regex"); + private final JCheckBox errorsOnlyCheckBox = new JCheckBox("Errors only"); + private final JSpinner minTimeSpinner = + new JSpinner(new SpinnerNumberModel(0, 0, MAX_MIN_TIME_MS, MIN_TIME_STEP_MS)); + private final JComboBox groupCombo = new JComboBox<>(GroupMode.values()); + private final JCheckBox bigRowsCheckBox = new JCheckBox("Big rows"); + private final JCheckBox subSamplesCheckBox = new JCheckBox("Sub-samples", true); + private final JCheckBox detailsCheckBox = new JCheckBox("Details", true); + private final JButton expandButton = new JButton(); + private final Color filterFieldForeground; + + /** + * Builds the toolbar. + * + * @param model the model whose filter, grouping and columns these controls drive + * @param table the table whose timeline and row height these controls drive + * @param host the panel or window hosting the waterfall + */ + public WaterfallToolBar(WaterfallTableModel model, WaterfallTable table, WaterfallHost host) { + this.model = model; + this.table = table; + this.host = host; + this.filterFieldForeground = filterField.getForeground(); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + setBorder(BorderFactory.createEmptyBorder(2, 2, 4, 2)); + add(buildFilterRow()); + add(Box.createVerticalStrut(3)); + add(buildViewRow()); + updateExpandButton(); + // A width change alters how many rows the wrapping layout needs, and a layout manager is never + // asked to re-measure on its own; without this the toolbar keeps the height it was first given + // and the wrapped controls are laid out below its bottom edge, where nothing paints them. + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent event) { + revalidate(); + } + }); + } + + private JPanel buildFilterRow() { + JPanel row = new JPanel(new WrapLayout(4, 2)); + row.add(new JLabel("Filter:")); + filterField.setToolTipText("Keep rows whose label or URL matches this text"); + filterField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent event) { + applyTextFilter(); + } + + @Override + public void removeUpdate(DocumentEvent event) { + applyTextFilter(); + } + + @Override + public void changedUpdate(DocumentEvent event) { + applyTextFilter(); + } + }); + row.add(filterField); + regexCheckBox.setToolTipText("Treat the filter as a regular expression"); + regexCheckBox.addActionListener(event -> applyTextFilter()); + row.add(regexCheckBox); + row.add(separator()); + row.add(statusMenuButton()); + row.add(protocolMenuButton()); + row.add(separator()); + errorsOnlyCheckBox.addActionListener(event -> { + model.getFilter().setErrorsOnly(errorsOnlyCheckBox.isSelected()); + refilter(); + }); + row.add(errorsOnlyCheckBox); + row.add(separator()); + row.add(new JLabel("Min time (ms):")); + minTimeSpinner.setToolTipText("Hide samples faster than this"); + minTimeSpinner.addChangeListener(event -> { + model.getFilter().setMinTimeMs(((Number) minTimeSpinner.getValue()).longValue()); + refilter(); + }); + row.add(minTimeSpinner); + row.add(separator()); + JButton clear = new JButton("Clear filters"); + clear.addActionListener(event -> clearFilters()); + row.add(clear); + return row; + } + + private JPanel buildViewRow() { + JPanel row = new JPanel(new WrapLayout(4, 2)); + row.add(new JLabel("Group by:")); + groupCombo.addActionListener(event -> + model.setGroupMode((GroupMode) groupCombo.getSelectedItem())); + row.add(groupCombo); + row.add(separator()); + bigRowsCheckBox.setToolTipText("Taller rows with thicker bars"); + bigRowsCheckBox.addActionListener(event -> table.setBigRows(bigRowsCheckBox.isSelected())); + row.add(bigRowsCheckBox); + subSamplesCheckBox.setToolTipText("Show embedded resources, redirect hops and transaction" + + " children as rows of their own. Applies to samples arriving from now on."); + subSamplesCheckBox.addActionListener(event -> + model.getStore().setIncludeSubSamples(subSamplesCheckBox.isSelected())); + row.add(subSamplesCheckBox); + detailsCheckBox.addActionListener( + event -> host.setDetailsVisible(detailsCheckBox.isSelected())); + row.add(detailsCheckBox); + row.add(separator()); + row.add(new JLabel("Timeline:")); + row.add(timelineButton("-", "Zoom out (Ctrl and the mouse wheel over the bars)", + () -> table.zoomStep(false))); + row.add(timelineButton("+", "Zoom in (Ctrl and the mouse wheel over the bars)", + () -> table.zoomStep(true))); + row.add(timelineButton("Fit", "Show the whole timeline (double click the ruler)", + table::fitTimeline)); + row.add(timelineButton("<", "Pan earlier (Shift and the mouse wheel over the bars)", + () -> table.panStep(false))); + row.add(timelineButton(">", "Pan later (Shift and the mouse wheel over the bars)", + () -> table.panStep(true))); + row.add(separator()); + row.add(columnsMenuButton()); + expandButton.addActionListener(event -> { + host.toggleExpandedWindow(); + updateExpandButton(); + }); + row.add(expandButton); + return row; + } + + /** + * A fixed gap between two logical groups of controls. + * + *

The wrapping layout gives every child the same small gap, which ran "Details" straight into + * "Timeline:" and made each row read as one undifferentiated strip of widgets. + * + * @return a rigid spacer + */ + private static Component separator() { + return Box.createHorizontalStrut(GROUP_SEPARATION); + } + + private JButton timelineButton(String text, String tooltip, Runnable action) { + JButton button = new JButton(text); + button.setToolTipText(tooltip); + button.addActionListener(event -> action.run()); + return button; + } + + /** + * The status filter, as a menu of checkboxes. + * + *

Nothing ticked means everything passes, which is the state a fresh viewer starts in; ticking + * a class narrows to it. That is the opposite of starting with all five ticked, and it keeps + * "show me the failures" one click away. + * + * @return the button that opens the menu + */ + private JButton statusMenuButton() { + JPopupMenu menu = new JPopupMenu(); + for (int statusClass : STATUS_CLASSES) { + JCheckBoxMenuItem item = new JCheckBoxMenuItem(labelForStatusClass(statusClass)); + item.addActionListener(event -> { + model.getFilter().setStatusClassAccepted(statusClass, item.isSelected()); + refilter(); + }); + menu.add(item); + } + return menuButton("Status", "Keep only the ticked status classes", menu); + } + + private static String labelForStatusClass(int statusClass) { + return statusClass == 0 ? "Other / non-numeric" : statusClass + "xx"; + } + + /** + * The protocol filter. Rebuilt each time it opens, because which protocols exist is only known + * once samples have arrived. + * + * @return the button that opens the menu + */ + private JButton protocolMenuButton() { + JPopupMenu menu = new JPopupMenu(); + JButton button = menuButton("Protocol", "Keep only the ticked protocols", menu); + button.addActionListener(event -> { + menu.removeAll(); + FilterCriteria filter = model.getFilter(); + if (model.getKnownProtocols().isEmpty()) { + JCheckBoxMenuItem none = new JCheckBoxMenuItem("(no samples yet)"); + none.setEnabled(false); + menu.add(none); + } + for (String protocol : model.getKnownProtocols()) { + String label = protocol.isEmpty() ? "(not reported)" : protocol; + JCheckBoxMenuItem item = new JCheckBoxMenuItem(label, + filter.getProtocols().contains(protocol)); + item.addActionListener(inner -> { + filter.setProtocolAccepted(protocol, item.isSelected()); + refilter(); + }); + menu.add(item); + } + }); + return button; + } + + /** + * The column chooser. + * + * @return the button that opens the menu + */ + private JButton columnsMenuButton() { + JPopupMenu menu = new JPopupMenu(); + for (WaterfallColumn column : WaterfallColumn.values()) { + JCheckBoxMenuItem item = new JCheckBoxMenuItem(column.getTitle(), + model.isColumnVisible(column)); + item.addActionListener(event -> model.setColumnVisible(column, item.isSelected())); + menu.add(item); + } + return menuButton("Columns", "Choose which columns are shown", menu); + } + + private JButton menuButton(String text, String tooltip, JPopupMenu menu) { + JButton button = new JButton(text); + button.setToolTipText(tooltip); + button.addActionListener(event -> menu.show(button, 0, button.getHeight())); + return button; + } + + private void applyTextFilter() { + FilterCriteria filter = model.getFilter(); + filter.setRegex(regexCheckBox.isSelected()); + filter.setText(filterField.getText()); + // A half-typed regular expression is not an error to shout about, but the field should not look + // like it is filtering when it cannot. + filterField.setForeground(filter.isRegexValid() ? filterFieldForeground : Color.RED); + refilter(); + } + + private void refilter() { + model.filterChanged(); + table.repaintTimeline(); + } + + /** Returns every filter to its pass-through state. */ + public void clearFilters() { + model.getFilter().reset(); + filterField.setText(""); + regexCheckBox.setSelected(false); + errorsOnlyCheckBox.setSelected(false); + minTimeSpinner.setValue(0); + filterField.setForeground(filterFieldForeground); + refilter(); + } + + /** Re-reads the host's expanded state, after the window was opened or closed elsewhere. */ + public void updateExpandButton() { + expandButton.setText(host.isExpanded() ? "Return to panel" : "Expand to window"); + expandButton.setToolTipText(host.isExpanded() + ? "Put the waterfall back into the JMeter listener panel" + : "Open the waterfall in a maximisable window of its own"); + } + + /** + * Reflects the details panel's visibility, after it was toggled from the panel's own Hide button. + * + * @param visible whether the details panel is showing + */ + public void setDetailsSelected(boolean visible) { + detailsCheckBox.setSelected(visible); + } + + /** + * Applies a grouping through the widget, so the selector shows what is actually in force. + * + *

Package-private for the screenshot harness: setting the model's mode directly left the + * selector reading "No grouping" over a plainly grouped table, which is exactly the kind of lie a + * screenshot must not tell. + * + * @param mode the grouping to apply + */ + void applyGroupMode(GroupMode mode) { + groupCombo.setSelectedItem(mode); + } + + /** + * Applies the errors-only filter through the widget. + * + * @param errorsOnly whether to hide successful samples + */ + void applyErrorsOnly(boolean errorsOnly) { + errorsOnlyCheckBox.setSelected(errorsOnly); + model.getFilter().setErrorsOnly(errorsOnly); + refilter(); + } + + /** + * Applies the big-rows option through the widget. + * + * @param big whether rows are taller + */ + void applyBigRows(boolean big) { + bigRowsCheckBox.setSelected(big); + table.setBigRows(big); + } + + /** + * The component that should get the focus when the viewer opens. + * + * @return the filter field + */ + public Component getInitialFocusComponent() { + return filterField; + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallViewerGui.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallViewerGui.java new file mode 100644 index 00000000..42338340 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallViewerGui.java @@ -0,0 +1,123 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall; + +import com.blazemeter.jmeter.commons.BlazemeterLabsLogo; +import java.awt.BorderLayout; +import javax.swing.JPanel; +import javax.swing.SwingWorker; +import javax.swing.event.ChangeEvent; +import org.apache.jmeter.reporters.ResultCollector; +import org.apache.jmeter.samplers.SampleResult; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jmeter.visualizers.gui.AbstractVisualizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The Waterfall Viewer listener: a network waterfall for a JMeter test, live or from a + * {@code .jtl}. + * + *

Extending {@link AbstractVisualizer} is what makes reading a {@code .jtl} free rather than + * something to implement: the base class contributes the result-file panel, and JMeter's own + * {@link ResultCollector} parses both the CSV and the XML flavours and feeds the rows in through + * {@link #add(SampleResult)} - the same entry point a live run uses. So a run recorded in non-GUI + * mode opens in exactly the same view as one being watched. + * + *

Two things this class does differently from a plain visualizer, both to do with not blocking: + * {@link #add(SampleResult)} does nothing but queue, because it runs on sampler threads and must + * never make a test wait on the UI; and loading a file happens on a worker thread rather than on + * the event dispatch thread, because the base class's own implementation parses the whole file + * inline and a large result file would freeze JMeter until it finished. + */ +public class WaterfallViewerGui extends AbstractVisualizer { + + private static final long serialVersionUID = 1L; + + private static final Logger LOG = LoggerFactory.getLogger(WaterfallViewerGui.class); + + private static final String PLUGIN_REPOSITORY_URL = + "https://github.com/Blazemeter/jmeter-http2-plugin"; + + /** Holds the waterfall while it is embedded, and the "open elsewhere" note while it is not. */ + private final JPanel viewerSlot = new JPanel(new BorderLayout()); + + private final WaterfallPanel waterfallPanel = new WaterfallPanel(viewerSlot); + + /** Builds the listener GUI. */ + public WaterfallViewerGui() { + viewerSlot.add(waterfallPanel, BorderLayout.CENTER); + setLayout(new BorderLayout(0, 5)); + setBorder(makeBorder()); + add(makeTitlePanel(), BorderLayout.NORTH); + add(viewerSlot, BorderLayout.CENTER); + add(new BlazemeterLabsLogo(PLUGIN_REPOSITORY_URL), BorderLayout.PAGE_END); + } + + @Override + public String getStaticLabel() { + return "bzm - Waterfall Viewer"; + } + + @Override + public String getLabelResource() { + return null; + } + + /** + * Receives one sample. Called on sampler threads during a run, and on the loader thread while a + * result file is being read. + * + *

Only queues: no Swing call, no allocation beyond the queue node, no lock a sampler thread + * could contend on. Sub-samples are expanded later, when the queue is drained. + * + * @param sample the finished sample + */ + @Override + public void add(SampleResult sample) { + waterfallPanel.addResult(sample); + } + + /** + * Loads the result file named in the file panel. + * + *

Overrides the base class, which parses the file on the event dispatch thread; a result file + * from a real run is large enough that doing so locks up JMeter for the duration. Reading it on a + * worker thread is safe here precisely because {@link #add(SampleResult)} only queues. + * + * @param event the file panel's change event + */ + @Override + public void stateChanged(ChangeEvent event) { + collector = (ResultCollector) createTestElement(); + ResultCollector loader = collector; + waterfallPanel.reset(); + new SwingWorker() { + @Override + protected Void doInBackground() { + loader.loadExistingFile(); + return null; + } + + @Override + protected void done() { + LOG.debug("Finished loading results from {}", loader.getFilename()); + } + }.execute(); + } + + /** + * Discards every sample, for JMeter's Clear action and for the clear that precedes a run. + * + *

Guarded with {@code runSafe} because a clear can be triggered from the thread starting the + * test rather than from the event dispatch thread, and everything it touches is Swing state. + */ + @Override + public void clearData() { + JMeterUtils.runSafe(false, waterfallPanel::reset); + } + + @Override + public void clearGui() { + super.clearGui(); + JMeterUtils.runSafe(false, waterfallPanel::reset); + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallWindow.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallWindow.java new file mode 100644 index 00000000..ed3e42ab --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WaterfallWindow.java @@ -0,0 +1,156 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall; + +import java.awt.BorderLayout; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.AbstractAction; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.KeyStroke; +import org.apache.jmeter.gui.GuiPackage; + +/** + * The detached window: the same waterfall panel, given the whole screen. + * + *

This is meant to be where the analysis happens. A waterfall is a picture whose usefulness is + * proportional to its width - a bar three pixels wide says nothing about which phase dominated it - + * and JMeter's listener panel is a fraction of a screen with a test plan tree beside it. So the + * window opens maximised, and F11 takes it fullscreen for a projector or a second monitor. + * + *

Closing it never destroys anything: the panel is handed back to the listener slot it came + * from, with its samples, filters and zoom level intact. + */ +final class WaterfallWindow { + + private static final String TITLE = "Waterfall Viewer"; + private static final int FALLBACK_WIDTH = 1400; + private static final int FALLBACK_HEIGHT = 900; + + private final JFrame frame = new JFrame(TITLE); + private final WaterfallPanel panel; + private boolean fullScreen; + + /** + * Builds the window around a panel. + * + * @param panel the panel to host + * @param onClose what to run when the window is closed, which is what puts the panel back + */ + WaterfallWindow(WaterfallPanel panel, Runnable onClose) { + this.panel = panel; + JPanel content = new JPanel(new BorderLayout()); + content.add(panel, BorderLayout.CENTER); + frame.setContentPane(content); + // The window must not dispose itself: closing it has to hand the panel back first. + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + onClose.run(); + } + }); + adoptJmeterIcons(); + installShortcuts(content); + } + + /** + * Reuses JMeter's own window icons, so the expanded viewer looks like part of the application + * rather than a stray dialog in the taskbar. + */ + private void adoptJmeterIcons() { + GuiPackage guiPackage = GuiPackage.getInstance(); + if (guiPackage == null || guiPackage.getMainFrame() == null) { + return; + } + if (!guiPackage.getMainFrame().getIconImages().isEmpty()) { + frame.setIconImages(guiPackage.getMainFrame().getIconImages()); + } + } + + /** + * F11 toggles fullscreen and Escape leaves it, matching what every other viewer with a fullscreen + * mode does. + * + * @param content the frame's content pane, which owns the key bindings + */ + private void installShortcuts(JComponent content) { + content.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_F11, 0), "waterfallFullScreen"); + content.getActionMap().put("waterfallFullScreen", new AbstractAction() { + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed(ActionEvent event) { + toggleFullScreen(); + } + }); + content.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "waterfallExitFullScreen"); + content.getActionMap().put("waterfallExitFullScreen", new AbstractAction() { + private static final long serialVersionUID = 1L; + + @Override + public void actionPerformed(ActionEvent event) { + if (fullScreen) { + toggleFullScreen(); + } + } + }); + } + + /** Shows the window maximised. */ + void open() { + frame.setSize(FALLBACK_WIDTH, FALLBACK_HEIGHT); + frame.setLocationRelativeTo(null); + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + frame.setVisible(true); + panel.getToolBar().updateExpandButton(); + frame.toFront(); + } + + /** Takes the panel out and disposes of the window. */ + void close() { + leaveFullScreen(); + frame.getContentPane().removeAll(); + frame.dispose(); + } + + /** + * Switches between fullscreen and maximised. + * + *

Exclusive fullscreen is a request the platform can refuse - a window manager may decline, or + * there may be no device to give. Maximised is the fallback, which is close enough that the user + * still gets the screen space they asked for. + */ + private void toggleFullScreen() { + if (fullScreen) { + leaveFullScreen(); + return; + } + GraphicsDevice device = GraphicsEnvironment.getLocalGraphicsEnvironment() + .getDefaultScreenDevice(); + if (device.getFullScreenWindow() != null) { + return; + } + device.setFullScreenWindow(frame); + fullScreen = device.getFullScreenWindow() == frame; + if (!fullScreen) { + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + } + } + + private void leaveFullScreen() { + if (!fullScreen) { + return; + } + fullScreen = false; + GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice() + .setFullScreenWindow(null); + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WrapLayout.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WrapLayout.java new file mode 100644 index 00000000..d8ec2d4c --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/WrapLayout.java @@ -0,0 +1,122 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; + +/** + * A {@link FlowLayout} that reports the height it will actually need once its rows wrap. + * + *

Plain {@code FlowLayout} does wrap its children, but it reports a preferred size as if they + * all fitted on one line. Inside anything that honours the preferred height - a {@code BoxLayout} + * column, a {@code BorderLayout} north region - that means the wrapped rows are laid out below the + * container's own bottom edge and simply never appear. + * + *

Which is exactly what the toolbar needs to avoid: the viewer is embedded in JMeter's listener + * panel, which is a fraction of a screen wide, and controls silently vanishing off the right-hand + * side is worse than a toolbar that grows a second line. + */ +public final class WrapLayout extends FlowLayout { + + private static final long serialVersionUID = 1L; + + /** + * Creates a left-aligned wrapping layout. + * + * @param horizontalGap gap between components on a row + * @param verticalGap gap between rows + */ + public WrapLayout(int horizontalGap, int verticalGap) { + super(FlowLayout.LEFT, horizontalGap, verticalGap); + } + + @Override + public Dimension preferredLayoutSize(Container target) { + return layoutSize(target, true); + } + + @Override + public Dimension minimumLayoutSize(Container target) { + Dimension minimum = layoutSize(target, false); + // FlowLayout consults the minimum size when it decides whether to wrap, so a minimum equal to + // the preferred size would make the container refuse to shrink and never wrap at all. + minimum.width -= getHgap() + 1; + return minimum; + } + + /** + * Measures the container by walking its children and breaking rows at the available width. + * + * @param target the container being measured + * @param preferred whether to use each child's preferred size rather than its minimum + * @return the size the container needs + */ + private Dimension layoutSize(Container target, boolean preferred) { + synchronized (target.getTreeLock()) { + int targetWidth = availableWidth(target); + Insets insets = target.getInsets(); + int horizontalInsets = insets.left + insets.right + getHgap() * 2; + int maxWidth = targetWidth - horizontalInsets; + Dimension total = new Dimension(0, 0); + int rowWidth = 0; + int rowHeight = 0; + for (int i = 0; i < target.getComponentCount(); i++) { + Component component = target.getComponent(i); + if (!component.isVisible()) { + continue; + } + Dimension size = preferred ? component.getPreferredSize() : component.getMinimumSize(); + if (rowWidth + size.width > maxWidth && rowWidth > 0) { + addRow(total, rowWidth, rowHeight); + rowWidth = 0; + rowHeight = 0; + } + if (rowWidth > 0) { + rowWidth += getHgap(); + } + rowWidth += size.width; + rowHeight = Math.max(rowHeight, size.height); + } + addRow(total, rowWidth, rowHeight); + total.width += horizontalInsets; + total.height += insets.top + insets.bottom + getVgap() * 2; + return total; + } + } + + /** + * The width to wrap against. + * + *

The awkward part of a wrapping layout: the preferred height depends on the width, but the + * width is only assigned after the parent has asked for the preferred height. A container being + * measured for the first time therefore reports a width of zero, and measuring against zero would + * put every child on a row of its own. + * + *

So the search walks up the ancestry to the first container that does have a width. By the + * time a toolbar is measured, the panel and the window above it have been sized, and their width + * is the width this row is about to be given - which makes the first measurement the right one + * rather than something a second layout pass has to correct. + * + * @param target the container being measured + * @return the width available for a row + */ + private static int availableWidth(Container target) { + for (Container candidate = target; candidate != null; candidate = candidate.getParent()) { + if (candidate.getWidth() > 0) { + Insets insets = candidate.getInsets(); + return candidate.getWidth() - insets.left - insets.right; + } + } + return Integer.MAX_VALUE; + } + + private void addRow(Dimension total, int rowWidth, int rowHeight) { + total.width = Math.max(total.width, rowWidth); + if (total.height > 0) { + total.height += getVgap(); + } + total.height += rowHeight; + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/CookiesTab.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/CookiesTab.java new file mode 100644 index 00000000..90b73919 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/CookiesTab.java @@ -0,0 +1,209 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall.details; + +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.SampleRecord; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Font; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.table.DefaultTableModel; +import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult; +import org.apache.jmeter.samplers.SampleResult; + +/** + * The Cookies tab: what the request sent and what the response set. + * + *

Two sources have to be reconciled. The request side comes from JMeter's own cookie field when + * the Cookie Manager filled it in, and from the {@code Cookie} request header otherwise - a request + * built with an explicit header manager has one and not the other. The response side is parsed out + * of the {@code Set-Cookie} headers, attribute by attribute, because that is where the answers to + * "why did my session not stick" live: a wrong path, a domain that does not match, an expiry in the + * past. + */ +public final class CookiesTab extends DetailTab { + + private static final long serialVersionUID = 1L; + + private static final String[] REQUEST_COLUMNS = {"Name", "Value"}; + private static final String[] RESPONSE_COLUMNS = {"Name", "Value", "Domain", "Path", "Expires", + "Max-Age", "HttpOnly", "Secure", "SameSite"}; + + /** Creates the tab. */ + public CookiesTab() { + setContent(emptyState("Select a request to see its cookies.")); + } + + @Override + public String getTitle() { + return "Cookies"; + } + + @Override + protected void render(SampleRecord record) { + if (record == null) { + setContent(emptyState("Select a request to see its cookies.")); + return; + } + SampleResult result = record.getResult(); + List requestCookies = requestCookiesOf(result); + List responseCookies = responseCookiesOf(result); + if (requestCookies.isEmpty() && responseCookies.isEmpty()) { + setContent(emptyState("This sample neither sent nor received any cookie.")); + return; + } + Box content = Box.createVerticalBox(); + content.setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10)); + content.add(section("Request Cookies", REQUEST_COLUMNS, requestCookies, + "No cookie was sent with this request.")); + content.add(Box.createVerticalStrut(10)); + content.add(section("Response Cookies", RESPONSE_COLUMNS, responseCookies, + "This response set no cookie.")); + setContent(new JScrollPane(content)); + } + + /** + * The cookies the request carried. + * + * @param result the sample + * @return name and value pairs + */ + private static List requestCookiesOf(SampleResult result) { + String raw = result instanceof HTTPSampleResult + ? ((HTTPSampleResult) result).getCookies() + : null; + if (raw == null || raw.isEmpty()) { + List headers = HttpHeaders.valuesOf(result.getRequestHeaders(), false, "Cookie"); + raw = headers.isEmpty() ? "" : String.join("; ", headers); + } + List cookies = new ArrayList<>(); + for (String pair : raw.split(";")) { + String trimmed = pair.trim(); + if (trimmed.isEmpty()) { + continue; + } + int equals = trimmed.indexOf('='); + cookies.add(equals < 0 ? new String[] {trimmed, ""} + : new String[] {trimmed.substring(0, equals).trim(), + trimmed.substring(equals + 1).trim()}); + } + return cookies; + } + + /** + * The cookies the response set, one row per {@code Set-Cookie} header. + * + * @param result the sample + * @return a row per cookie, in the order of {@link #RESPONSE_COLUMNS} + */ + private static List responseCookiesOf(SampleResult result) { + List cookies = new ArrayList<>(); + for (String header : HttpHeaders.valuesOf(result.getResponseHeaders(), true, "Set-Cookie")) { + cookies.add(parseSetCookie(header)); + } + return cookies; + } + + /** + * Splits one {@code Set-Cookie} value into its name, value and attributes. + * + * @param header the header value + * @return a row in the order of {@link #RESPONSE_COLUMNS} + */ + private static String[] parseSetCookie(String header) { + String[] row = new String[RESPONSE_COLUMNS.length]; + for (int i = 0; i < row.length; i++) { + row[i] = ""; + } + String[] parts = header.split(";"); + if (parts.length > 0) { + int equals = parts[0].indexOf('='); + row[0] = equals < 0 ? parts[0].trim() : parts[0].substring(0, equals).trim(); + row[1] = equals < 0 ? "" : parts[0].substring(equals + 1).trim(); + } + for (int i = 1; i < parts.length; i++) { + applyAttribute(row, parts[i].trim()); + } + return row; + } + + private static void applyAttribute(String[] row, String attribute) { + int equals = attribute.indexOf('='); + String name = (equals < 0 ? attribute : attribute.substring(0, equals)) + .trim().toLowerCase(Locale.ROOT); + String value = equals < 0 ? "yes" : attribute.substring(equals + 1).trim(); + switch (name) { + case "domain": + row[2] = value; + break; + case "path": + row[3] = value; + break; + case "expires": + row[4] = value; + break; + case "max-age": + row[5] = value; + break; + case "httponly": + row[6] = "yes"; + break; + case "secure": + row[7] = "yes"; + break; + case "samesite": + row[8] = value; + break; + default: + break; + } + } + + /** + * A titled table, or the title plus a note when there is nothing to put in it. + * + * @param title the heading + * @param columns the column names + * @param rows the rows + * @param ifAbsent what to say when there are none + * @return the section + */ + private static JPanel section(String title, String[] columns, List rows, + String ifAbsent) { + JPanel panel = new JPanel(new BorderLayout(0, 4)); + JLabel heading = new JLabel(title); + heading.setFont(heading.getFont().deriveFont(Font.BOLD)); + panel.add(heading, BorderLayout.NORTH); + if (rows.isEmpty()) { + JLabel note = new JLabel(ifAbsent); + note.setEnabled(false); + panel.add(note, BorderLayout.CENTER); + return panel; + } + DefaultTableModel model = new DefaultTableModel(columns, 0) { + private static final long serialVersionUID = 1L; + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + for (String[] row : rows) { + model.addRow(row); + } + JTable table = new JTable(model); + table.setAutoCreateRowSorter(true); + table.setFillsViewportHeight(false); + JScrollPane scrollPane = new JScrollPane(table); + scrollPane.setPreferredSize(new Dimension(0, + table.getRowHeight() * (Math.min(rows.size(), 8) + 1) + 4)); + panel.add(scrollPane, BorderLayout.CENTER); + return panel; + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/DetailTab.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/DetailTab.java new file mode 100644 index 00000000..681560e2 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/DetailTab.java @@ -0,0 +1,103 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall.details; + +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.SampleRecord; +import java.awt.BorderLayout; +import java.awt.Component; +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; + +/** + * Base class for the tabs of the details panel. + * + *

Carries the one behaviour they all need: a tab is told which sample is selected, but only + * builds its contents when it is actually the visible tab. Selecting a row in a waterfall of large + * responses would otherwise decode the body six times over - once per tab - for five tabs nobody + * was looking at. + */ +public abstract class DetailTab extends JPanel { + + private static final long serialVersionUID = 1L; + + private SampleRecord record; + private boolean stale = true; + + /** Creates a tab with a border layout. */ + protected DetailTab() { + super(new BorderLayout()); + } + + /** + * The tab's title. + * + * @return the text shown on the tab + */ + public abstract String getTitle(); + + /** + * Builds the tab's contents for a sample. + * + * @param sample the selected sample, or {@code null} when the selection was cleared + */ + protected abstract void render(SampleRecord sample); + + /** + * Notes which sample is selected, without doing any work yet. + * + * @param sample the selected sample, or {@code null} + */ + public final void setRecord(SampleRecord sample) { + if (this.record != sample) { + this.record = sample; + this.stale = true; + } + } + + /** Builds the contents if the selection changed since the last time this tab was shown. */ + public final void refreshIfStale() { + if (stale) { + stale = false; + render(record); + } + } + + /** + * The selected sample. + * + * @return the sample, or {@code null} + */ + protected final SampleRecord getRecord() { + return record; + } + + /** + * A centred message, for a tab that has nothing to show. + * + *

Always a specific sentence rather than a blank panel: "this sample has no response body" and + * "this tab is broken" look identical when both are empty. + * + * @param message what is missing and why + * @return the placeholder + */ + protected static JPanel emptyState(String message) { + JPanel panel = new JPanel(new BorderLayout()); + JLabel label = new JLabel(message, SwingConstants.CENTER); + label.setBorder(BorderFactory.createEmptyBorder(20, 12, 20, 12)); + label.setEnabled(false); + panel.add(label, BorderLayout.CENTER); + return panel; + } + + /** + * Replaces the tab's contents. + * + * @param content the new contents + */ + protected final void setContent(Component content) { + removeAll(); + add(content, BorderLayout.CENTER); + revalidate(); + repaint(); + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HeadersTab.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HeadersTab.java new file mode 100644 index 00000000..5f4002a6 --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HeadersTab.java @@ -0,0 +1,188 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall.details; + +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.SampleRecord; +import com.blazemeter.jmeter.http2.visualizers.waterfall.model.WaterfallFormat; +import java.awt.BorderLayout; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.util.List; +import java.util.Locale; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JEditorPane; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.apache.jmeter.samplers.SampleResult; + +/** + * The Headers tab: a General section, then the request headers, then the response headers. + * + *

Follows the layout browsers settled on because the ordering answers questions in the order + * they get asked - what was requested, what was sent, what came back. The filter box narrows all + * three sections at once, which is how you find one header across a request that carries thirty. + */ +public final class HeadersTab extends DetailTab { + + private static final long serialVersionUID = 1L; + + private static final int FILTER_COLUMNS = 20; + + private final JEditorPane view = new JEditorPane(); + private final JTextField filterField = new JTextField(FILTER_COLUMNS); + + /** Creates the tab. */ + public HeadersTab() { + view.setEditable(false); + view.setContentType("text/html"); + // A JEditorPane will happily fetch whatever a link points at when it is clicked. This one only + // ever shows text, so no hyperlink listener is installed and nothing can be followed. + view.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); + add(buildToolBar(), BorderLayout.NORTH); + add(new JScrollPane(view), BorderLayout.CENTER); + filterField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent event) { + render(getRecord()); + } + + @Override + public void removeUpdate(DocumentEvent event) { + render(getRecord()); + } + + @Override + public void changedUpdate(DocumentEvent event) { + render(getRecord()); + } + }); + } + + private JPanel buildToolBar() { + JPanel bar = new JPanel(new BorderLayout(6, 0)); + bar.setBorder(BorderFactory.createEmptyBorder(2, 2, 4, 2)); + JPanel left = new JPanel(new BorderLayout(4, 0)); + left.add(new JLabel("Filter headers:"), BorderLayout.WEST); + filterField.setToolTipText("Show only headers whose name or value contains this text"); + left.add(filterField, BorderLayout.CENTER); + bar.add(left, BorderLayout.WEST); + Box right = Box.createHorizontalBox(); + right.add(copyButton("Copy request headers", true)); + right.add(Box.createHorizontalStrut(6)); + right.add(copyButton("Copy response headers", false)); + bar.add(right, BorderLayout.EAST); + return bar; + } + + private JButton copyButton(String text, boolean request) { + JButton button = new JButton(text); + button.addActionListener(event -> { + SampleRecord record = getRecord(); + if (record == null) { + return; + } + String block = request ? record.getResult().getRequestHeaders() + : record.getResult().getResponseHeaders(); + Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new StringSelection(block == null ? "" : block), null); + }); + return button; + } + + @Override + public String getTitle() { + return "Headers"; + } + + @Override + protected void render(SampleRecord record) { + if (record == null) { + view.setText(HtmlDoc.wrap("Select a request to see its headers.")); + return; + } + String needle = filterField.getText().trim().toLowerCase(Locale.ROOT); + StringBuilder body = new StringBuilder(); + appendGeneral(body, record); + SampleResult result = record.getResult(); + appendSection(body, "Request Headers", HttpHeaders.parse(result.getRequestHeaders(), false), + needle, "This sample recorded no request headers."); + appendSection(body, "Response Headers", HttpHeaders.parse(result.getResponseHeaders(), true), + needle, "This sample recorded no response headers."); + view.setText(HtmlDoc.wrap(body.toString())); + view.setCaretPosition(0); + } + + /** + * Writes the General section, whose fields come from the sample itself rather than from a header. + * + * @param body the document being built + * @param record the selected sample + */ + private void appendGeneral(StringBuilder body, SampleRecord record) { + SampleResult result = record.getResult(); + body.append("

General

"); + appendRow(body, "Request URL", record.getUrl()); + appendRow(body, "Request Method", record.getMethod()); + appendRow(body, "Status Code", + record.getResponseCode() + " " + orEmpty(result.getResponseMessage())); + appendRow(body, "Protocol", record.getProtocol()); + appendRow(body, "Sample Label", record.getLabel()); + appendRow(body, "Thread", record.getThreadName()); + appendRow(body, "Elapsed", WaterfallFormat.duration(record.getElapsed())); + appendRow(body, "Response Size", WaterfallFormat.size(record.getBytes()) + + " (headers " + WaterfallFormat.size(result.getHeadersSize()) + + ", body " + WaterfallFormat.size(result.getBodySizeAsLong()) + ")"); + appendRow(body, "Request Size", WaterfallFormat.size(record.getSentBytes())); + appendRow(body, "Successful", String.valueOf(record.isSuccess())); + body.append("
"); + } + + /** + * Writes one header section, or a note when it is empty or entirely filtered away. + * + * @param body the document being built + * @param title the section heading + * @param headers the headers in the section + * @param needle the lower-cased filter text, empty when not filtering + * @param ifAbsent what to say when the section has no headers at all + */ + private void appendSection(StringBuilder body, String title, List headers, + String needle, String ifAbsent) { + body.append("

").append(HtmlDoc.escape(title)).append("

"); + if (headers.isEmpty()) { + body.append("").append(HtmlDoc.escape(ifAbsent)).append(""); + return; + } + int shown = 0; + body.append(""); + for (String[] header : headers) { + if (!matches(header, needle)) { + continue; + } + appendRow(body, header[0], header[1]); + shown++; + } + body.append("
"); + if (shown == 0) { + body.append("No header here matches the filter."); + } + } + + private static boolean matches(String[] header, String needle) { + return needle.isEmpty() || header[0].toLowerCase(Locale.ROOT).contains(needle) + || header[1].toLowerCase(Locale.ROOT).contains(needle); + } + + private static void appendRow(StringBuilder body, String name, String value) { + body.append("").append(HtmlDoc.escape(name)) + .append("").append(HtmlDoc.escape(value)).append(""); + } + + private static String orEmpty(String value) { + return value == null ? "" : value; + } +} diff --git a/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HtmlDoc.java b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HtmlDoc.java new file mode 100644 index 00000000..4b2b1a4c --- /dev/null +++ b/src/main/java/com/blazemeter/jmeter/http2/visualizers/waterfall/details/HtmlDoc.java @@ -0,0 +1,101 @@ +package com.blazemeter.jmeter.http2.visualizers.waterfall.details; + +import com.blazemeter.jmeter.http2.visualizers.waterfall.render.WaterfallColors; +import java.awt.Color; + +/** + * Builds the small HTML documents the Headers tab renders in a {@code JEditorPane}. + * + *

HTML is a good fit for a header list specifically: the content is tiny, it wants a name in + * bold next to a value that wraps, and Swing's renderer gives that for free with text that stays + * selectable and copyable. It is a bad fit for a response body, which is why the body tabs use a + * plain text area instead. + * + *

Everything that comes from a sample goes through {@link #escape(String)}. Header values are + * whatever the server under test chose to send; a value containing markup would otherwise be + * interpreted as part of the document and could rewrite the panel around it. + */ +public final class HtmlDoc { + + private HtmlDoc() { + } + + /** + * Escapes text for inclusion in a document. + * + * @param text the raw text, {@code null} treated as empty + * @return the escaped text + */ + public static String escape(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + StringBuilder out = new StringBuilder(text.length() + 16); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + switch (c) { + case '&': + out.append("&"); + break; + case '<': + out.append("<"); + break; + case '>': + out.append(">"); + break; + case '"': + out.append("""); + break; + default: + out.append(c); + break; + } + } + return out.toString(); + } + + /** + * Wraps body markup in a document styled to match the current look and feel. + * + *

Declares colours and spacing but deliberately no font. The panes that render this set + * {@code JEditorPane.HONOR_DISPLAY_PROPERTIES}, so the component's own font applies - which is + * JMeter's font, at whatever size the look and feel and the display scaling settled on. A + * hard-coded {@code font-size} here would override that and come out wrong on a high-density + * screen. + * + * @param body the body markup, already escaped where it came from a sample + * @return a complete document + */ + public static String wrap(String body) { + String foreground = hex(WaterfallColors.foreground()); + String muted = hex(WaterfallColors.mutedForeground()); + String background = hex(WaterfallColors.background()); + return "" + body + ""; + } + + /** + * Wraps markup for a tooltip. + * + *

Separate from {@link #wrap(String)} and deliberately plainer: a tooltip is painted by a + * light-weight HTML view that does not carry a stylesheet, so a document with a {@code