From 4f8b734ff162f00d5fe58b64137b581e9808999e Mon Sep 17 00:00:00 2001 From: Mikolaj Grzaslewicz Date: Thu, 18 Jan 2024 11:59:50 +0100 Subject: [PATCH] JPERF-1454 Move rainbow to API, with a better name TDD: refactor --- CHANGELOG.md | 12 +- .../api/drilldown/ActionMetricExplainer.kt | 39 +++ .../report/api/drilldown/DurationDrilldown.kt | 170 ++++++++++ .../tools/report/drilldown/TimeTrain.kt | 40 +++ .../drilldown/ActionMetricExplainerTest.kt | 136 ++++++++ .../tools/report/chart/RainbowTest.kt | 305 ------------------ ...ction-metrics-with-elements-and-server.jpt | 0 7 files changed, 395 insertions(+), 307 deletions(-) create mode 100644 src/main/kotlin/com/atlassian/performance/tools/report/api/drilldown/ActionMetricExplainer.kt create mode 100644 src/main/kotlin/com/atlassian/performance/tools/report/api/drilldown/DurationDrilldown.kt create mode 100644 src/main/kotlin/com/atlassian/performance/tools/report/drilldown/TimeTrain.kt create mode 100755 src/test/kotlin/com/atlassian/performance/tools/report/api/drilldown/ActionMetricExplainerTest.kt delete mode 100755 src/test/kotlin/com/atlassian/performance/tools/report/chart/RainbowTest.kt rename src/test/resources/com/atlassian/performance/tools/report/{chart => api/drilldown}/action-metrics-with-elements-and-server.jpt (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 401d9386..a300c9a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,10 +24,18 @@ Adding a requirement of a major version of a dependency is breaking a contract. Dropping a requirement of a major version of a dependency is a new contract. ## [Unreleased] -[Unreleased]: https://github.com/atlassian/report/compare/release-4.2.0...master +[Unreleased]: https://github.com/atlassian/report/compare/release-4.3.0...master ### Added -- Add `MultiJfrFilter` to process input once and create multiple outputs. Aids [JPERF-1409]. +- Add `ActionMetricExplainer` and `DurationDrilldown` for explaining `ActionMetric.duration`. Unblock [JPERF-1454]. + +[JPERF-1454]: https://ecosystem.atlassian.net/browse/JPERF-1454 + +## [4.3.0] - 2023-12-13 +[4.3.0]: https://github.com/atlassian/report/compare/release-4.2.0...release-4.3.0 + +### Added +- Add `MultiJfrFilter` to process input once and create multiple outputs. Aid [JPERF-1409]. ## [4.2.0] - 2023-12-11 [4.2.0]: https://github.com/atlassian/report/compare/release-4.1.0...release-4.2.0 diff --git a/src/main/kotlin/com/atlassian/performance/tools/report/api/drilldown/ActionMetricExplainer.kt b/src/main/kotlin/com/atlassian/performance/tools/report/api/drilldown/ActionMetricExplainer.kt new file mode 100644 index 00000000..d5ff076b --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/report/api/drilldown/ActionMetricExplainer.kt @@ -0,0 +1,39 @@ +package com.atlassian.performance.tools.report.api.drilldown + +import com.atlassian.performance.tools.jiraactions.api.ActionMetric +import com.atlassian.performance.tools.report.drilldown.TimeTrain +import java.time.Instant + +object ActionMetricExplainer { + fun explainDuration(metric: ActionMetric): DurationDrilldown { + val drilldown = metric.drilldown!! + val nav = drilldown.navigations.single() + val timeOrigin = drilldown.timeOrigin!! + return with(nav.resource) { + val train = TimeTrain(metric.start, metric.end, timeOrigin) + val lastResource = drilldown.resources + .map { timeOrigin + it.responseEnd } + .filter { it < metric.end } + .max() ?: Instant.MIN + + DurationDrilldown.Builder(metric.duration) + .preNav(train.jumpOff(redirectStart)) + .redirect(train.jumpOff(redirectEnd)) + .preWorker(train.jumpOff(workerStart)) + .serviceWorkerInit(train.jumpOff(fetchStart)) + .fetchAndCache(train.jumpOff(domainLookupStart)) + .dns(train.jumpOff(domainLookupEnd)) + .preConnect(train.jumpOff(connectStart)) + .tcp(train.jumpOff(connectEnd)) + .preRequest(train.jumpOff(requestStart)) + .request(train.jumpOff(responseStart)) + .response(train.jumpOff(responseEnd)) + .domProcessing(train.jumpOff(nav.domComplete)) + .preLoad(train.jumpOff(nav.loadEventStart)) + .load(train.jumpOff(nav.loadEventEnd)) + .lateResources(train.jumpOff(lastResource)) + .excessProcessing(train.jumpOff(metric.end)) + .build() + } + } +} diff --git a/src/main/kotlin/com/atlassian/performance/tools/report/api/drilldown/DurationDrilldown.kt b/src/main/kotlin/com/atlassian/performance/tools/report/api/drilldown/DurationDrilldown.kt new file mode 100644 index 00000000..f174f1f9 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/report/api/drilldown/DurationDrilldown.kt @@ -0,0 +1,170 @@ +package com.atlassian.performance.tools.report.api.drilldown + +import com.atlassian.performance.tools.jiraactions.api.ActionMetric +import java.time.Duration + +/** + * Represents [ActionMetric.duration] split into linear segments based on [ActionMetric.drilldown]. + */ +class DurationDrilldown private constructor( + /** + * Everything that happened before current navigation started, including, but not limited to: + * - delay between measurement start and browser starting to load the page + * - cross-origin redirects + * - previous navigations (as within one [ActionMetric] there can be multiple navigations) + */ + val preNav: Duration, + val redirect: Duration, + val preWorker: Duration, + val serviceWorkerInit: Duration, + val fetchAndCache: Duration, + val dns: Duration, + val preConnect: Duration, + val tcp: Duration, + val preRequest: Duration, + val request: Duration, + val response: Duration, + val domProcessing: Duration, + val preLoad: Duration, + val load: Duration, + /** + * Resources (XHR, JS, images) still downloading after [load]. + */ + val lateResources: Duration, + /** + * Everything that happened after [lateResources]. That is no-network activity, including, but not limited to: + * - JavaScript execution + * - rendering + */ + val excessProcessing: Duration, + /** + * @return [ActionMetric.duration] + */ + val total: Duration +) { + + /** + * All the segments should add up to [total] + * @return difference between the sum of segments and the [total], should be zero + */ + val unexplained: Duration = total + .minus(preNav) + .minus(redirect) + .minus(preWorker) + .minus(serviceWorkerInit) + .minus(fetchAndCache) + .minus(dns) + .minus(preConnect) + .minus(tcp) + .minus(preRequest) + .minus(request) + .minus(response) + .minus(domProcessing) + .minus(lateResources) + .minus(excessProcessing) + .minus(preLoad) + .minus(load) + + init { + assert(preNav.isNegative.not()) { "preNav duration cannot be negative" } + assert(redirect.isNegative.not()) { "redirect duration cannot be negative" } + assert(preWorker.isNegative.not()) { "preWorker duration cannot be negative" } + assert(serviceWorkerInit.isNegative.not()) { "serviceWorkerInit duration cannot be negative" } + assert(fetchAndCache.isNegative.not()) { "fetchAndCache duration cannot be negative" } + assert(dns.isNegative.not()) { "dns duration cannot be negative" } + assert(preConnect.isNegative.not()) { "preConnect duration cannot be negative" } + assert(tcp.isNegative.not()) { "tcp duration cannot be negative" } + assert(preRequest.isNegative.not()) { "preRequest duration cannot be negative" } + assert(request.isNegative.not()) { "request duration cannot be negative" } + assert(response.isNegative.not()) { "response duration cannot be negative" } + assert(domProcessing.isNegative.not()) { "processing duration cannot be negative" } + assert(preLoad.isNegative.not()) { "preLoad duration cannot be negative" } + assert(load.isNegative.not()) { "load duration cannot be negative" } + assert(lateResources.isNegative.not()) { "lateResources cannot be negative" } + assert(excessProcessing.isNegative.not()) { "excessProcessing cannot be negative" } + assert(total.isNegative.not()) { "total duration cannot be negative" } + } + + override fun toString(): String { + return "DurationDrilldown(" + + "preNav=$preNav, " + + "redirect=$redirect, " + + "preWorker=$preWorker, " + + "serviceWorkerInit=$serviceWorkerInit, " + + "fetchAndCache=$fetchAndCache, " + + "dns=$dns, " + + "preConnect=$preConnect, " + + "tcp=$tcp, " + + "preRequest=$preRequest, " + + "request=$request, " + + "response=$response, " + + "domProcessing=$domProcessing, " + + "preLoad=$preLoad, " + + "load=$load, " + + "lateResources=$lateResources, " + + "excessProcessing=$excessProcessing, " + + "total=$total, " + + "unexplained=$unexplained" + + ")" + } + + class Builder( + private var total: Duration + ) { + private var preNav: Duration = Duration.ZERO + private var redirect: Duration = Duration.ZERO + private var preWorker: Duration = Duration.ZERO + private var serviceWorkerInit: Duration = Duration.ZERO + private var fetchAndCache: Duration = Duration.ZERO + private var dns: Duration = Duration.ZERO + private var preConnect: Duration = Duration.ZERO + private var tcp: Duration = Duration.ZERO + private var preRequest: Duration = Duration.ZERO + private var request: Duration = Duration.ZERO + private var response: Duration = Duration.ZERO + private var domProcessing: Duration = Duration.ZERO + private var preLoad: Duration = Duration.ZERO + private var load: Duration = Duration.ZERO + private var lateResources: Duration = Duration.ZERO + private var excessProcessing: Duration = Duration.ZERO + + fun preNav(preNav: Duration) = apply { this.preNav = preNav } + fun redirect(redirect: Duration) = apply { this.redirect = redirect } + fun preWorker(preWorker: Duration) = apply { this.preWorker = preWorker } + fun serviceWorkerInit(serviceWorkerInit: Duration) = apply { this.serviceWorkerInit = serviceWorkerInit } + fun fetchAndCache(fetchAndCache: Duration) = apply { this.fetchAndCache = fetchAndCache } + fun dns(dns: Duration) = apply { this.dns = dns } + fun preConnect(preConnect: Duration) = apply { this.preConnect = preConnect } + fun tcp(tcp: Duration) = apply { this.tcp = tcp } + fun preRequest(preRequest: Duration) = apply { this.preRequest = preRequest } + fun request(request: Duration) = apply { this.request = request } + fun response(response: Duration) = apply { this.response = response } + fun domProcessing(processing: Duration) = apply { this.domProcessing = processing } + fun preLoad(preLoad: Duration) = apply { this.preLoad = preLoad } + fun load(load: Duration) = apply { this.load = load } + fun lateResources(lateResources: Duration) = apply { this.lateResources = lateResources } + fun excessProcessing(excessProcessing: Duration) = apply { this.excessProcessing = excessProcessing } + fun total(total: Duration) = apply { this.total = total } + + fun build() = DurationDrilldown( + preNav = preNav, + redirect = redirect, + preWorker = preWorker, + serviceWorkerInit = serviceWorkerInit, + fetchAndCache = fetchAndCache, + dns = dns, + preConnect = preConnect, + tcp = tcp, + preRequest = preRequest, + request = request, + response = response, + domProcessing = domProcessing, + preLoad = preLoad, + load = load, + lateResources = lateResources, + excessProcessing = excessProcessing, + total = total + ) + } +} + diff --git a/src/main/kotlin/com/atlassian/performance/tools/report/drilldown/TimeTrain.kt b/src/main/kotlin/com/atlassian/performance/tools/report/drilldown/TimeTrain.kt new file mode 100644 index 00000000..d9c6fa50 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/report/drilldown/TimeTrain.kt @@ -0,0 +1,40 @@ +package com.atlassian.performance.tools.report.drilldown + +import com.atlassian.performance.tools.jiraactions.api.w3c.PerformanceNavigationTiming +import com.atlassian.performance.tools.jiraactions.api.w3c.PerformanceResourceTiming +import java.time.Duration +import java.time.Instant + +internal class TimeTrain( + firstStation: Instant, + private val lastStation: Instant, + private val timeOrigin: Instant +) { + + private var prevStation: Instant = firstStation + + /** + * Jump off at the next station + * + * @return how much time elapsed since the last station, a linear time segment + */ + fun jumpOff(nextStation: Instant): Duration { + /** + * Some stations are optional, e.g. [PerformanceResourceTiming.workerStart] + * or might not have happened yet, e.g. before [PerformanceNavigationTiming.loadEventStart] + * Some stations are parallel and can come in different order in runtime, + * e.g. last [PerformanceResourceTiming] might come before or after [PerformanceNavigationTiming.loadEventEnd]. + */ + if (nextStation < prevStation) { + return Duration.ZERO + } + val jumpOffStation = minOf(nextStation, lastStation) + val segment = Duration.between(prevStation, jumpOffStation) + prevStation = jumpOffStation + return segment + } + + fun jumpOff(nextStation: Duration): Duration { + return jumpOff(timeOrigin + nextStation) + } +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/report/api/drilldown/ActionMetricExplainerTest.kt b/src/test/kotlin/com/atlassian/performance/tools/report/api/drilldown/ActionMetricExplainerTest.kt new file mode 100755 index 00000000..4022a6eb --- /dev/null +++ b/src/test/kotlin/com/atlassian/performance/tools/report/api/drilldown/ActionMetricExplainerTest.kt @@ -0,0 +1,136 @@ +package com.atlassian.performance.tools.report.api.drilldown + +import com.atlassian.performance.tools.jiraactions.api.ActionMetric +import com.atlassian.performance.tools.jiraactions.api.parser.ActionMetricsParser +import com.atlassian.performance.tools.report.api.drilldown.ActionMetricExplainer.explainDuration +import com.atlassian.performance.tools.report.chart.waterfall.WaterfallChart +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.SoftAssertions.assertSoftly +import org.junit.Test +import java.io.File.createTempFile +import java.time.Duration.ZERO +import java.time.Duration.ofMillis +import kotlin.streams.toList + +class ActionMetricExplainerTest { + + private val metricsStream = javaClass.getResourceAsStream("action-metrics-with-elements-and-server.jpt")!! + private val metrics = ActionMetricsParser().stream(metricsStream).toList() + + @Test + fun shouldExplainRedirects() { + // given + val interestingMetric = metrics.first { + val drilldown = it.drilldown!! + val nav = drilldown.navigations.firstOrNull() ?: return@first false + drilldown.elements.isNotEmpty() + && nav.redirectCount > 0 + && nav.loadEventEnd != nav.domComplete + } + + // when + val interestingDrilldown = explainDuration(interestingMetric) + + // then + plotWaterfall(interestingMetric) + assertSoftly { + with(interestingDrilldown) { + it.assertThat(preNav).isEqualTo(ofMillis(573).plusNanos(940951)) // huge, right? + it.assertThat(redirect).isEqualTo(ofMillis(11)) + it.assertThat(serviceWorkerInit).isEqualTo(ZERO) + it.assertThat(fetchAndCache).isEqualTo(ZERO) + it.assertThat(dns).isEqualTo(ZERO) + it.assertThat(tcp).isEqualTo(ZERO) + it.assertThat(request).isEqualTo(ofMillis(28)) + it.assertThat(response).isEqualTo(ofMillis(57)) + it.assertThat(domProcessing).isEqualTo(ofMillis(158)) + it.assertThat(load).isEqualTo(ofMillis(1)) + it.assertThat(lateResources).isEqualTo(ofMillis(233)) + it.assertThat(excessProcessing).isEqualTo(ofMillis(49).plusNanos(323049)) + it.assertThat(total).isEqualTo(ofMillis(1111).plusNanos(264000)) + it.assertThat(unexplained).isEqualTo(ZERO) + } + } + } + + /** + * Sometimes UX is measured long after loading the page in the browser, + * e.g. interact with buttons on an already-loaded page. + */ + @Test + fun shouldExplainSinglePageApp() { + // given + val spaMetric = metrics.first { it.start > it.drilldown!!.timeOrigin } + + // when + val drilldown = explainDuration(spaMetric) + + // then + plotWaterfall(spaMetric) + assertSoftly { + with(drilldown) { + it.assertThat(preNav).isEqualTo(ZERO) + it.assertThat(redirect).isEqualTo(ZERO) + it.assertThat(serviceWorkerInit).isEqualTo(ZERO) + it.assertThat(fetchAndCache).isEqualTo(ZERO) + it.assertThat(dns).isEqualTo(ZERO) + it.assertThat(tcp).isEqualTo(ZERO) + it.assertThat(request).isEqualTo(ZERO) + it.assertThat(response).isEqualTo(ZERO) + it.assertThat(domProcessing).isEqualTo(ZERO) + it.assertThat(load).isEqualTo(ZERO) + it.assertThat(lateResources).isEqualTo(ofMillis(367).plusNanos(743097)) + it.assertThat(excessProcessing).isEqualTo(ofMillis(60).plusNanos(79903)) + it.assertThat(total).isEqualTo(ofMillis(427).plusNanos(823000)) + it.assertThat(unexplained).isEqualTo(ZERO) + } + } + } + + /** + * Browser can display the element we need in the middle of many phases, including DCL event or load event. + */ + @Test + fun shouldExplainMidNav() { + // given + val midNavMetric = metrics.first { metric -> + val drilldown = metric.drilldown!! + val timeOrigin = drilldown.timeOrigin!! + val nav = drilldown.navigations.singleOrNull()?.resource?.entry ?: return@first false + val navStart = timeOrigin + nav.startTime + val navEnd = navStart + nav.duration + metric.end > navStart && metric.end < navEnd + } + + // when + val drilldown = explainDuration(midNavMetric) + + // then + plotWaterfall(midNavMetric) + assertSoftly { + with(drilldown) { + it.assertThat(domProcessing).isEqualTo(ofMillis(205).plusNanos(809903)) + it.assertThat(lateResources).isEqualTo(ZERO) + it.assertThat(excessProcessing).isEqualTo(ZERO) + it.assertThat(total).isEqualTo(ofMillis(307).plusNanos(334000)) + it.assertThat(unexplained).isEqualTo(ZERO) + } + } + } + + @Test + fun shouldExplainDuration() { + // when + val drilldowns = metrics.map { it to explainDuration(it) } + + // then + val unexplained = drilldowns.filter { (_, drilldown) -> drilldown.unexplained != ZERO } + assertThat(unexplained).isEmpty() + } + + private fun plotWaterfall(metric: ActionMetric) { + WaterfallChart().plot(metric, createTempFile("waterfall-${metric.label}-", ".html")) + } + + +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/report/chart/RainbowTest.kt b/src/test/kotlin/com/atlassian/performance/tools/report/chart/RainbowTest.kt deleted file mode 100755 index 25c04fc9..00000000 --- a/src/test/kotlin/com/atlassian/performance/tools/report/chart/RainbowTest.kt +++ /dev/null @@ -1,305 +0,0 @@ -package com.atlassian.performance.tools.report.chart - -import com.atlassian.performance.tools.jiraactions.api.ActionMetric -import com.atlassian.performance.tools.jiraactions.api.parser.ActionMetricsParser -import com.atlassian.performance.tools.jiraactions.api.w3c.PerformanceNavigationTiming -import com.atlassian.performance.tools.jiraactions.api.w3c.PerformanceResourceTiming -import com.atlassian.performance.tools.report.chart.waterfall.WaterfallChart -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.SoftAssertions.assertSoftly -import org.junit.Test -import java.io.File.createTempFile -import java.time.Duration -import java.time.Duration.ZERO -import java.time.Duration.ofMillis -import java.time.Instant -import kotlin.streams.toList - -class RainbowTest { - - private val metricsStream = javaClass.getResourceAsStream("action-metrics-with-elements-and-server.jpt")!! - private val metrics = ActionMetricsParser().stream(metricsStream).toList() - - @Test - fun shouldCalculateRainbowWithNavigation() { - // given - val interestingMetric = metrics.first { - val drilldown = it.drilldown!! - val nav = drilldown.navigations.firstOrNull() ?: return@first false - drilldown.elements.isNotEmpty() - && nav.redirectCount > 0 - && nav.loadEventEnd != nav.domComplete - } - - // when - val interestingRainbow = inferRainbow(interestingMetric) - - // then - plotWaterfall(interestingMetric) - assertSoftly { - with(interestingRainbow) { - it.assertThat(preNav).isEqualTo(ofMillis(573).plusNanos(940951)) // huge, right? - it.assertThat(redirect).isEqualTo(ofMillis(11)) - it.assertThat(serviceWorkerInit).isEqualTo(ZERO) - it.assertThat(fetchAndCache).isEqualTo(ZERO) - it.assertThat(dns).isEqualTo(ZERO) - it.assertThat(tcp).isEqualTo(ZERO) - it.assertThat(request).isEqualTo(ofMillis(28)) - it.assertThat(response).isEqualTo(ofMillis(57)) - it.assertThat(processing).isEqualTo(ofMillis(158)) - it.assertThat(load).isEqualTo(ofMillis(1)) - it.assertThat(excessResource).isEqualTo(ofMillis(233)) - it.assertThat(excessJavascript).isEqualTo(ofMillis(49).plusNanos(323049)) - it.assertThat(total).isEqualTo(ofMillis(1111).plusNanos(264000)) - it.assertThat(unexplained).isEqualTo(ZERO) - } - } - } - - /** - * Sometimes UX is measured long after loading the page in the browser, - * e.g. interact with buttons on an already-loaded page. - */ - @Test - fun shouldCalculateRainbowForSinglePageApp() { - // given - val spaMetric = metrics.first { it.start > it.drilldown!!.timeOrigin } - - // when - val rainbow = inferRainbow(spaMetric) - - // then - plotWaterfall(spaMetric) - assertSoftly { - with(rainbow) { - it.assertThat(preNav).isEqualTo(ZERO) - it.assertThat(redirect).isEqualTo(ZERO) - it.assertThat(serviceWorkerInit).isEqualTo(ZERO) - it.assertThat(fetchAndCache).isEqualTo(ZERO) - it.assertThat(dns).isEqualTo(ZERO) - it.assertThat(tcp).isEqualTo(ZERO) - it.assertThat(request).isEqualTo(ZERO) - it.assertThat(response).isEqualTo(ZERO) - it.assertThat(processing).isEqualTo(ZERO) - it.assertThat(load).isEqualTo(ZERO) - it.assertThat(excessResource).isEqualTo(ofMillis(367).plusNanos(743097)) - it.assertThat(excessJavascript).isEqualTo(ofMillis(60).plusNanos(79903)) - it.assertThat(total).isEqualTo(ofMillis(427).plusNanos(823000)) - it.assertThat(unexplained).isEqualTo(ZERO) - } - } - } - - /** - * Browser can display the element we need in the middle of many phases, including DCL event or load event. - */ - @Test - fun shouldExplainMidNav() { - // given - val midNavMetric = metrics.first { metric -> - val drilldown = metric.drilldown!! - val timeOrigin = drilldown.timeOrigin!! - val nav = drilldown.navigations.singleOrNull()?.resource?.entry ?: return@first false - val navStart = timeOrigin + nav.startTime - val navEnd = navStart + nav.duration - metric.end > navStart && metric.end < navEnd - } - - // when - val rainbow = inferRainbow(midNavMetric) - - // then - plotWaterfall(midNavMetric) - assertSoftly { - with(rainbow) { - it.assertThat(processing).isEqualTo(ofMillis(205).plusNanos(809903)) - it.assertThat(excessResource).isEqualTo(ZERO) - it.assertThat(excessJavascript).isEqualTo(ZERO) - it.assertThat(total).isEqualTo(ofMillis(307).plusNanos(334000)) - it.assertThat(unexplained).isEqualTo(ZERO) - } - } - } - - @Test - fun shouldExplainEverything() { - // when - val rainbows = metrics.map { it to inferRainbow(it) } - - // then - val unexplained = rainbows.filter { (_, rainbow) -> rainbow.unexplained != ZERO } - assertThat(unexplained).isEmpty() - } - - private fun plotWaterfall(metric: ActionMetric) { - WaterfallChart().plot(metric, createTempFile("waterfall-${metric.label}-", ".html")) - } - - private fun inferRainbow(metric: ActionMetric): Rainbow { - val drilldown = metric.drilldown!! - val nav = drilldown.navigations.single() - val timeOrigin = drilldown.timeOrigin!! - return with(nav.resource) { - val train = TimeTrain(metric.start, timeOrigin, metric.end) - val preNav = train.jumpOff(redirectStart) - val redirect = train.jumpOff(redirectEnd) - val preWorker = train.jumpOff(workerStart) - val serviceWorkerInit = train.jumpOff(fetchStart) - val fetchAndCache = train.jumpOff(domainLookupStart) - val dns = train.jumpOff(domainLookupEnd) - val preConnect = train.jumpOff(connectStart) - val tcp = train.jumpOff(connectEnd) - val preRequest = train.jumpOff(requestStart) - val request = train.jumpOff(responseStart) - val response = train.jumpOff(responseEnd) - val processing = train.jumpOff(nav.domComplete) - val preLoad = train.jumpOff(nav.loadEventStart) - val load = train.jumpOff(nav.loadEventEnd) - val lastResource = drilldown.resources - .map { timeOrigin + it.responseEnd } - .filter { it < metric.end } - .max() ?: Instant.MIN - val excessResource = train.jumpOff(lastResource) - val excessJavascript = train.jumpOff(metric.end) - Rainbow( - preNav = preNav, - redirect = redirect, - preWorker = preWorker, - serviceWorkerInit = serviceWorkerInit, - fetchAndCache = fetchAndCache, - dns = dns, - preConnect = preConnect, - tcp = tcp, - preRequest = preRequest, - request = request, - response = response, - processing = processing, - preLoad = preLoad, - load = load, - excessResource = excessResource, - excessJavascript = excessJavascript, - total = metric.duration - ) - } - } - - class TimeTrain( - firstStation: Instant, - private val timeOrigin: Instant, - private val lastStation: Instant - ) { - - private var prevStation: Instant = firstStation - - /** - * Jump off at the next station - * - * @return how much time elapsed since the last station, a linear time segment - */ - fun jumpOff(nextStation: Instant): Duration { - /** - * Some stations are optional, e.g. [PerformanceResourceTiming.workerStart] - * or might not have happened yet, e.g. before [PerformanceNavigationTiming.loadEventStart] - * Some stations are parallel and can come in different order in runtime, - * e.g. last [PerformanceResourceTiming] might come before or after [PerformanceNavigationTiming.loadEventEnd]. - */ - if (nextStation < prevStation) { - return ZERO - } - val jumpOffStation = minOf(nextStation, lastStation) - val segment = Duration.between(prevStation, jumpOffStation) - prevStation = jumpOffStation - return segment - } - - fun jumpOff(nextStation: Duration): Duration { - return jumpOff(timeOrigin + nextStation) - } - } - - /** - * TODO find a better name - * it's temporarily a rainbow, because the old visualisation of similar data looked like one - */ - class Rainbow( - val preNav: Duration, - val redirect: Duration, - val preWorker: Duration, - val serviceWorkerInit: Duration, - val fetchAndCache: Duration, - val dns: Duration, - val preConnect: Duration, - val tcp: Duration, - val preRequest: Duration, - val request: Duration, - val response: Duration, - val processing: Duration, - val preLoad: Duration, - val load: Duration, - val excessResource: Duration, - val excessJavascript: Duration, - val total: Duration - ) { - - val unexplained: Duration = total - .minus(preNav) - .minus(redirect) - .minus(preWorker) - .minus(serviceWorkerInit) - .minus(fetchAndCache) - .minus(dns) - .minus(preConnect) - .minus(tcp) - .minus(preRequest) - .minus(request) - .minus(response) - .minus(processing) - .minus(excessResource) - .minus(excessJavascript) - .minus(preLoad) - .minus(load) - - init { - assert(preNav.isNegative.not()) { "preNav duration cannot be negative" } - assert(redirect.isNegative.not()) { "redirect duration cannot be negative" } - assert(preWorker.isNegative.not()) { "preWorker duration cannot be negative" } - assert(serviceWorkerInit.isNegative.not()) { "serviceWorkerInit duration cannot be negative" } - assert(fetchAndCache.isNegative.not()) { "fetchAndCache duration cannot be negative" } - assert(dns.isNegative.not()) { "dns duration cannot be negative" } - assert(preConnect.isNegative.not()) { "preConnect duration cannot be negative" } - assert(tcp.isNegative.not()) { "tcp duration cannot be negative" } - assert(preRequest.isNegative.not()) { "preRequest duration cannot be negative" } - assert(request.isNegative.not()) { "request duration cannot be negative" } - assert(response.isNegative.not()) { "response duration cannot be negative" } - assert(processing.isNegative.not()) { "processing duration cannot be negative" } - assert(preLoad.isNegative.not()) { "preLoad duration cannot be negative" } - assert(load.isNegative.not()) { "load duration cannot be negative" } - assert(excessResource.isNegative.not()) { "excessResource cannot be negative" } - assert(excessJavascript.isNegative.not()) { "excessJavascript cannot be negative" } - assert(total.isNegative.not()) { "total duration cannot be negative" } - } - - override fun toString(): String { - return "Rainbow(" + - "preNav=$preNav, " + - "redirect=$redirect, " + - "preWorker=$preWorker, " + - "serviceWorkerInit=$serviceWorkerInit, " + - "fetchAndCache=$fetchAndCache, " + - "dns=$dns, " + - "preConnect=$preConnect, " + - "tcp=$tcp, " + - "preRequest=$preRequest, " + - "request=$request, " + - "response=$response, " + - "processing=$processing, " + - "preLoad=$preLoad, " + - "load=$load, " + - "excessResource=$excessResource, " + - "excessJavascript=$excessJavascript, " + - "total=$total, " + - "unexplained=$unexplained" + - ")" - } - } -} diff --git a/src/test/resources/com/atlassian/performance/tools/report/chart/action-metrics-with-elements-and-server.jpt b/src/test/resources/com/atlassian/performance/tools/report/api/drilldown/action-metrics-with-elements-and-server.jpt similarity index 100% rename from src/test/resources/com/atlassian/performance/tools/report/chart/action-metrics-with-elements-and-server.jpt rename to src/test/resources/com/atlassian/performance/tools/report/api/drilldown/action-metrics-with-elements-and-server.jpt