Skip to content

Commit

Permalink
JPERF-1435: Reduce JS overhead
Browse files Browse the repository at this point in the history
For every resource (navigation, XHR, JS, CSS, image), it would send a
new HTTP request to parse headers.

The resource URI was not taken into account in the synthetic request,
so it could not infer original HTTP headers. Especially since the new
request can have different response headers than the original requests.
And even if they had the same, then it means we assume sticky session.
So we might read node id once just as well.

Supposedly, the synthetic request was being read from a cache,
but apparently it's still too much overhead.

We'll have to read the node id from `Server-Timing` header,
which is automatically captured by the browser
and it's the same way we obtain the response thread id.
  • Loading branch information
dagguh committed Nov 29, 2023
1 parent 8d7f42e commit 290c38b
Show file tree
Hide file tree
Showing 3 changed files with 15 additions and 56 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Dropping a requirement of a major version of a dependency is a new contract.
- formalize the dependency on `Server-Timing`s convention (e.g. `response-thread-plugin` public behavioral API)
- encapsulate a specific `ActionMetric.drilldown` analysis (e.g. navigations are not the only backend interactions)

### Fixed
- Reduce overhead of `JavascriptW3cPerformanceTimeline`.

## [3.25.0] - 2023-11-09
[3.25.0]: https://github.com/atlassian/jira-actions/compare/release-3.24.0...release-3.25.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,26 @@ import org.openqa.selenium.JavascriptExecutor

internal fun getJsNavigationsPerformance(jsExecutor: JavascriptExecutor): List<PerformanceNavigationTiming> {
val jsNavigations = jsExecutor.executeScript("return window.performance.getEntriesByType(\"navigation\");")
return parseNavigations(jsNavigations, jsExecutor)
return parseNavigations(jsNavigations)
}

private fun parseNavigations(
jsNavigations: Any?,
jsExecutor: JavascriptExecutor
jsNavigations: Any?
): List<PerformanceNavigationTiming> {
if (jsNavigations !is List<*>) {
throw Exception("Unexpected non-list JavaScript value: $jsNavigations")
}
return jsNavigations.map { parsePerformanceNavigationTiming(it, jsExecutor) }
return jsNavigations.map { parsePerformanceNavigationTiming(it) }
}

private fun parsePerformanceNavigationTiming(
map: Any?,
jsExecutor: JavascriptExecutor
map: Any?
): PerformanceNavigationTiming {
if (map !is Map<*, *>) {
throw Exception("Unexpected non-map JavaScript value: $map")
}
return PerformanceNavigationTiming(
resource = parsePerformanceResourceTiming(map, jsExecutor),
resource = parsePerformanceResourceTiming(map),
unloadEventStart = parseTimestamp(map["unloadEventStart"]),
unloadEventEnd = parseTimestamp(map["unloadEventEnd"]),
domInteractive = parseTimestamp(map["domInteractive"]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,23 @@ 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
import java.time.Duration

internal fun getJsResourcesPerformance(jsExecutor: JavascriptExecutor): List<PerformanceResourceTiming> {
val jsResources = jsExecutor.executeScript("return window.performance.getEntriesByType(\"resource\");")
return parseResources(jsResources, jsExecutor)
return parseResources(jsResources)
}

private fun parseResources(
jsResources: Any,
jsExecutor: JavascriptExecutor
jsResources: Any
): List<PerformanceResourceTiming> {
if (jsResources !is List<*>) {
throw Exception("Unexpected non-list JavaScript value: $jsResources")
}
return jsResources.map { parsePerformanceResourceTiming(it, jsExecutor) }
return jsResources.map { parsePerformanceResourceTiming(it) }
}

internal fun parsePerformanceResourceTiming(
map: Any?,
jsExecutor: JavascriptExecutor
map: Any?
): PerformanceResourceTiming {
if (map !is Map<*, *>) {
throw Exception("Unexpected non-map JavaScript value: $map")
Expand All @@ -47,7 +44,7 @@ internal fun parsePerformanceResourceTiming(
transferSize = map["transferSize"] as Long,
encodedBodySize = map["encodedBodySize"] as Long,
decodedBodySize = map["decodedBodySize"] as Long,
serverTiming = map["serverTiming"]?.let { parseServerTimings(it, jsExecutor) }
serverTiming = map["serverTiming"]?.let { parseServerTimings(it) }
)
}

Expand All @@ -66,29 +63,12 @@ private fun parsePerformanceEntry(
}

private fun parseServerTimings(
jsServerTimings: Any,
jsExecutor: JavascriptExecutor
jsServerTimings: Any
): List<PerformanceServerTiming> {
if (jsServerTimings !is List<*>) {
throw Exception("Unexpected non-list JavaScript value: $jsServerTimings")
}
val result = jsServerTimings.map { parseServerTiming(it) }.toMutableList()
val responseHeaders = getResponseHeaders(jsExecutor)
val nodeId = responseHeaders["x-anodeid"]
addAttribute(result, "nodeId", nodeId)
return result
}

private fun addAttribute(result: MutableList<PerformanceServerTiming>, attributeName: String, attributeValue: String?) {
if (attributeValue != null) {
result.add(
PerformanceServerTiming(
name = attributeName,
duration = Duration.ZERO,
description = attributeValue
)
)
}
return jsServerTimings.map { parseServerTiming(it) }.toMutableList()
}

private fun parseServerTiming(
Expand All @@ -103,25 +83,3 @@ private fun parseServerTiming(
description = map["description"] as String
)
}

private fun getResponseHeaders(jsExecutor: JavascriptExecutor): Map<String, String?> {
val jsResult = jsExecutor.executeScript(
"""
var r = new XMLHttpRequest();
r.open('HEAD', window.location, false);
r.send(null);
var headersString = r.getAllResponseHeaders();
const arr = headersString.trim().split(/[\r\n]+/);
const headerMap = {};
arr.forEach((line) => {
const parts = line.split(": ");
const header = parts.shift();
const value = parts.join(": ");
headerMap[header] = value;
});
return headerMap;
""".trimIndent()
)
@Suppress("UNCHECKED_CAST")
return (jsResult as Map<String, String>).mapKeys { it.key.toLowerCase() }
}

0 comments on commit 290c38b

Please sign in to comment.