diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d9501f..405d83f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ Dropping a requirement of a major version of a dependency is a new contract. ## [Unreleased] [Unreleased]: https://github.com/atlassian/jira-actions/compare/release-3.23.1...master +### Added +- Add `PerformanceServerTiming`. Aid with [JPERF-1408]. + +[JPERF-1408]: https://ecosystem.atlassian.net/browse/JPERF-1408 + ## [3.23.1] - 2023-10-05 [3.23.1]: https://github.com/atlassian/jira-actions/compare/release-3.23.0...release-3.23.1 diff --git a/src/main/kotlin/com/atlassian/performance/tools/jiraactions/api/w3c/PerformanceResourceTiming.kt b/src/main/kotlin/com/atlassian/performance/tools/jiraactions/api/w3c/PerformanceResourceTiming.kt index 20a20244..19b9f89a 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/jiraactions/api/w3c/PerformanceResourceTiming.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/jiraactions/api/w3c/PerformanceResourceTiming.kt @@ -23,5 +23,12 @@ class PerformanceResourceTiming internal constructor( val responseEnd: Duration, val transferSize: Long, val encodedBodySize: Long, - val decodedBodySize: Long + val decodedBodySize: Long, + /** + * Represents the [serverTiming attribute](https://www.w3.org/TR/2023/WD-server-timing-20230411/#servertiming-attribute) + * + * @return Null if blocked, e.g. by same-origin policy. + * Empty if not blocked, but server didn't send any `Server-Timing` headers. + */ + val serverTiming: List? ) diff --git a/src/main/kotlin/com/atlassian/performance/tools/jiraactions/api/w3c/PerformanceServerTiming.kt b/src/main/kotlin/com/atlassian/performance/tools/jiraactions/api/w3c/PerformanceServerTiming.kt new file mode 100644 index 00000000..c146d633 --- /dev/null +++ b/src/main/kotlin/com/atlassian/performance/tools/jiraactions/api/w3c/PerformanceServerTiming.kt @@ -0,0 +1,12 @@ +package com.atlassian.performance.tools.jiraactions.api.w3c + +import java.time.Duration + +/** + * Represents the [PerformanceServerTiming](https://www.w3.org/TR/2023/WD-server-timing-20230411/#the-performanceservertiming-interface). + */ +class PerformanceServerTiming internal constructor( + val name: String, + val duration: Duration, + val description: String +) diff --git a/src/main/kotlin/com/atlassian/performance/tools/jiraactions/w3c/VerboseJsonFormat.kt b/src/main/kotlin/com/atlassian/performance/tools/jiraactions/w3c/VerboseJsonFormat.kt index aaceebaa..047780ca 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/jiraactions/w3c/VerboseJsonFormat.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/jiraactions/w3c/VerboseJsonFormat.kt @@ -79,6 +79,11 @@ internal class VerboseJsonFormat { .add("transferSize", transferSize) .add("encodedBodySize", encodedBodySize) .add("decodedBodySize", decodedBodySize) + .apply { + if (serverTiming != null) { + add("serverTiming", serverTiming.map { serializeServerTiming(it) }.toJsonArray()) + } + } .build() } @@ -103,8 +108,10 @@ internal class VerboseJsonFormat { responseEnd = getDuration("responseEnd"), transferSize = getJsonNumber("transferSize").longValueExact(), encodedBodySize = getJsonNumber("encodedBodySize").longValueExact(), - decodedBodySize = getJsonNumber("decodedBodySize").longValueExact() - + decodedBodySize = getJsonNumber("decodedBodySize").longValueExact(), + serverTiming = getJsonArray("serverTiming") + ?.map { it.asJsonObject() } + ?.map { deserializeServerTiming(it) } ) } @@ -144,6 +151,26 @@ internal class VerboseJsonFormat { ) } + private fun serializeServerTiming( + serverTiming: PerformanceServerTiming + ): JsonObject = serverTiming.run { + Json.createObjectBuilder() + .add("name", serverTiming.name) + .add("duration", serverTiming.duration.toString()) + .add("description", serverTiming.description) + .build() + } + + private fun deserializeServerTiming( + json: JsonObject + ): PerformanceServerTiming = json.run { + PerformanceServerTiming( + name = json.getString("name"), + duration = json.getDuration("duration"), + description = json.getString("description") + ) + } + private fun List.toJsonArray(): JsonArray { val builder = Json.createArrayBuilder() forEach { builder.add(it) } diff --git a/src/main/kotlin/com/atlassian/performance/tools/jiraactions/w3c/harvesters/JsResources.kt b/src/main/kotlin/com/atlassian/performance/tools/jiraactions/w3c/harvesters/JsResources.kt index 3c2b6262..100b0c3c 100644 --- a/src/main/kotlin/com/atlassian/performance/tools/jiraactions/w3c/harvesters/JsResources.kt +++ b/src/main/kotlin/com/atlassian/performance/tools/jiraactions/w3c/harvesters/JsResources.kt @@ -2,6 +2,7 @@ package com.atlassian.performance.tools.jiraactions.w3c.harvesters import com.atlassian.performance.tools.jiraactions.api.w3c.PerformanceEntry import com.atlassian.performance.tools.jiraactions.api.w3c.PerformanceResourceTiming +import com.atlassian.performance.tools.jiraactions.api.w3c.PerformanceServerTiming import org.openqa.selenium.JavascriptExecutor internal fun getJsResourcesPerformance(javascript: JavascriptExecutor): List { @@ -42,7 +43,8 @@ internal fun parsePerformanceResourceTiming( responseEnd = parseTimestamp(map["responseEnd"]), transferSize = map["transferSize"] as Long, encodedBodySize = map["encodedBodySize"] as Long, - decodedBodySize = map["decodedBodySize"] as Long + decodedBodySize = map["decodedBodySize"] as Long, + serverTiming = map["serverTiming"]?.let { parseServerTimings(it) } ) } @@ -59,3 +61,25 @@ private fun parsePerformanceEntry( duration = parseTimestamp(map["duration"]) ) } + +private fun parseServerTimings( + jsServerTimings: Any +): List { + if (jsServerTimings !is List<*>) { + throw Exception("Unexpected non-list JavaScript value: $jsServerTimings") + } + return jsServerTimings.map { parseServerTiming(it) } +} + +private fun parseServerTiming( + map: Any? +): PerformanceServerTiming { + if (map !is Map<*, *>) { + throw Exception("Unexpected non-map JavaScript value: $map") + } + return PerformanceServerTiming( + name = map["name"] as String, + duration = parseTimestamp(map["duration"]), + description = map["description"] as String + ) +} diff --git a/src/test/kotlin/com/atlassian/performance/tools/jiraactions/api/measure/output/AppendableActionMetricOutputTest.kt b/src/test/kotlin/com/atlassian/performance/tools/jiraactions/api/measure/output/AppendableActionMetricOutputTest.kt index 08521ffe..75b07b89 100644 --- a/src/test/kotlin/com/atlassian/performance/tools/jiraactions/api/measure/output/AppendableActionMetricOutputTest.kt +++ b/src/test/kotlin/com/atlassian/performance/tools/jiraactions/api/measure/output/AppendableActionMetricOutputTest.kt @@ -123,7 +123,11 @@ class AppendableActionMetricOutputTest { responseEnd = ofMillis(391), transferSize = 12956, encodedBodySize = 11818, - decodedBodySize = 59535 + decodedBodySize = 59535, + serverTiming = listOf( + PerformanceServerTiming("userCache", ofMillis(13), "miss"), + PerformanceServerTiming("sqlTotal", ofMillis(40), "") + ) ), unloadEventStart = ofMillis(210), unloadEventEnd = ofMillis(210), @@ -161,7 +165,8 @@ class AppendableActionMetricOutputTest { responseEnd = ofMillis(982), transferSize = 3524, encodedBodySize = 3032, - decodedBodySize = 24340 + decodedBodySize = 24340, + serverTiming = null ) ), elements = listOf( diff --git a/src/test/resources/action-metrics.jpt b/src/test/resources/action-metrics.jpt index 2150ee02..d98dcbfe 100644 --- a/src/test/resources/action-metrics.jpt +++ b/src/test/resources/action-metrics.jpt @@ -4,4 +4,4 @@ {"label":"Create Issue","result":"OK","duration":"PT2M26.786S","start":"2017-12-12T10:38:36.275Z","virtualUser":"0e5ead7c-dc9c-4f48-854d-5200a1a71058"} {"label":"View Board","result":"OK","duration":"PT26.786S","start":"2017-12-12T10:38:36.277Z","virtualUser":"0e5ead7c-dc9c-4f48-854d-5200a1a71058","observation":{"issues":5}} {"label":"View Board","result":"OK","duration":"PT26.786S","start":"2017-12-12T10:38:36.277Z","virtualUser":"0e5ead7c-dc9c-4f48-854d-5200a1a71058","observation":{"issues":6}} -{"label":"Log In","result":"OK","duration":"PT3.86S","start":"2018-12-18T16:10:23.088Z","virtualUser":"0e5ead7c-dc9c-4f48-854d-5200a1a71058","drilldown":{"navigations":[{"resource":{"entry":{"name":"http://3.120.138.107:8080/","entryType":"navigation","startTime":"PT0S","duration":"PT1.74S"},"initiatorType":"navigation","nextHopProtocol":"http/1.1","workerStart":"PT0S","redirectStart":"PT0.016S","redirectEnd":"PT0.126S","fetchStart":"PT0.126S","domainLookupStart":"PT0.126S","domainLookupEnd":"PT0.126S","connectStart":"PT0.126S","connectEnd":"PT0.126S","secureConnectionStart":"PT0S","requestStart":"PT0.126S","responseStart":"PT0.208S","responseEnd":"PT0.391S","transferSize":12956,"encodedBodySize":11818,"decodedBodySize":59535},"unloadEventStart":"PT0.21S","unloadEventEnd":"PT0.21S","domInteractive":"PT0.805S","domContentLoadedEventStart":"PT0.805S","domContentLoadedEventEnd":"PT0.83S","domComplete":"PT1.73S","loadEventStart":"PT1.73S","loadEventEnd":"PT1.74S","type":"NAVIGATE","redirectCount":1}],"resources":[{"entry":{"name":"http://3.120.138.107:8080/rest/gadget/1.0/issueTable/jql?num=10&tableContext=jira.table.cols.dashboard&addDefault=true&enableSorting=true&paging=true&showActions=true&jql=assignee+%3D+currentUser()+AND+resolution+%3D+unresolved+ORDER+BY+priority+DESC%2C+created+ASC&sortBy=&startIndex=0&_=1545149426038","entryType":"resource","startTime":"PT0.903S","duration":"PT0.078S"},"initiatorType":"xmlhttprequest","nextHopProtocol":"http/1.1","workerStart":"PT0S","redirectStart":"PT0S","redirectEnd":"PT0S","fetchStart":"PT0.903S","domainLookupStart":"PT0.903S","domainLookupEnd":"PT0.903S","connectStart":"PT0.903S","connectEnd":"PT0.903S","secureConnectionStart":"PT0S","requestStart":"PT0.904S","responseStart":"PT0.981S","responseEnd":"PT0.982S","transferSize":3524,"encodedBodySize":3032,"decodedBodySize":24340}],"elements":[{"renderTime":"PT1.147S","loadTime":"PT0S","identifier":"app-header","naturalWidth":0,"naturalHeight":0,"id":"home_link","url":""}]}} +{"label":"Log In","result":"OK","duration":"PT3.86S","start":"2018-12-18T16:10:23.088Z","virtualUser":"0e5ead7c-dc9c-4f48-854d-5200a1a71058","drilldown":{"navigations":[{"resource":{"entry":{"name":"http://3.120.138.107:8080/","entryType":"navigation","startTime":"PT0S","duration":"PT1.74S"},"initiatorType":"navigation","nextHopProtocol":"http/1.1","workerStart":"PT0S","redirectStart":"PT0.016S","redirectEnd":"PT0.126S","fetchStart":"PT0.126S","domainLookupStart":"PT0.126S","domainLookupEnd":"PT0.126S","connectStart":"PT0.126S","connectEnd":"PT0.126S","secureConnectionStart":"PT0S","requestStart":"PT0.126S","responseStart":"PT0.208S","responseEnd":"PT0.391S","transferSize":12956,"encodedBodySize":11818,"decodedBodySize":59535,"serverTiming":[{"name":"userCache","duration":"PT0.013S","description":"miss"},{"name":"sqlTotal","duration":"PT0.04S","description":""}]},"unloadEventStart":"PT0.21S","unloadEventEnd":"PT0.21S","domInteractive":"PT0.805S","domContentLoadedEventStart":"PT0.805S","domContentLoadedEventEnd":"PT0.83S","domComplete":"PT1.73S","loadEventStart":"PT1.73S","loadEventEnd":"PT1.74S","type":"NAVIGATE","redirectCount":1}],"resources":[{"entry":{"name":"http://3.120.138.107:8080/rest/gadget/1.0/issueTable/jql?num=10&tableContext=jira.table.cols.dashboard&addDefault=true&enableSorting=true&paging=true&showActions=true&jql=assignee+%3D+currentUser()+AND+resolution+%3D+unresolved+ORDER+BY+priority+DESC%2C+created+ASC&sortBy=&startIndex=0&_=1545149426038","entryType":"resource","startTime":"PT0.903S","duration":"PT0.078S"},"initiatorType":"xmlhttprequest","nextHopProtocol":"http/1.1","workerStart":"PT0S","redirectStart":"PT0S","redirectEnd":"PT0S","fetchStart":"PT0.903S","domainLookupStart":"PT0.903S","domainLookupEnd":"PT0.903S","connectStart":"PT0.903S","connectEnd":"PT0.903S","secureConnectionStart":"PT0S","requestStart":"PT0.904S","responseStart":"PT0.981S","responseEnd":"PT0.982S","transferSize":3524,"encodedBodySize":3032,"decodedBodySize":24340}],"elements":[{"renderTime":"PT1.147S","loadTime":"PT0S","identifier":"app-header","naturalWidth":0,"naturalHeight":0,"id":"home_link","url":""}]}}