Skip to content

Commit

Permalink
JPERF-1454 Move rainbow to API, with a better name
Browse files Browse the repository at this point in the history
TDD: refactor
  • Loading branch information
mgrzaslewicz authored and dagguh committed Jan 18, 2024
1 parent bb5f851 commit 4f8b734
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 307 deletions.
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}

Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 4f8b734

Please sign in to comment.