g1t/scripts/perf/measure.ps1

144 lines5,840 bytesCodeBlame
1<#
2.SYNOPSIS
3 Times g1t.sh pages from this machine: time to first byte, where the Worker
4 ran (cf-placement) and where the time went (Server-Timing). Run it before
5 and after a change and compare. docs/PERFORMANCE.md explains the columns.
6
7.EXAMPLE
8 # Signed out
9 powershell -File scripts/perf/measure.ps1
10
11.EXAMPLE
12 # Signed in: copy the value of your g1t_session cookie from the browser
13 # (DevTools > Application > Cookies > https://g1t.sh) into G1T_SESSION.
14 # The script never prints it.
15 $env:G1T_SESSION = "<64 hex characters>"
16 powershell -File scripts/perf/measure.ps1 -Runs 7 -Out before.csv
17
18.EXAMPLE
19 # As a browser sees it: React Router streams to browsers and renders the
20 # whole page first for anything it takes for a crawler, which curl's own
21 # user agent is. Compare the two to see what streaming saves.
22 powershell -File scripts/perf/measure.ps1 -BrowserUA
23
24.NOTES
25 Uses curl.exe (Windows' own). Git Bash's curl fails here with exit 43.
26#>
27param(
28 [string]$Base = "https://g1t.sh",
29 [string]$Repo = "flagon-io/g1t",
30 [int]$Pull = 1,
31 [int]$Issue = 2,
32 [int]$Runs = 5,
33 [string[]]$Paths = @(),
34 [string]$Out = "",
35 # Send a desktop Chrome user agent: streamed pages, as people get them.
36 # Without it the requests look like a crawler's and wait for the whole page.
37 [switch]$BrowserUA
38)
39
40$ErrorActionPreference = "Stop"
41
42# Pages people open most, and the data request a click inside the app makes
43# for the same page (React Router's `.data`, with only the page's own
44# loader, as an in-app navigation sends it).
45if ($Paths.Count -eq 0) {
46 $Paths = @(
47 "/",
48 "/explore",
49 "/$Repo",
50 "/$Repo/pulls",
51 "/$Repo/issues",
52 "/$Repo/pull/$Pull",
53 "/$Repo/issues/$Issue",
54 "/$Repo/pull/$Pull.data?_routes=routes%2Frepo%2Fpull",
55 "/$Repo/pulls.data?_routes=routes%2Frepo%2Fpulls",
56 "/pricing"
57 )
58}
59
60$signedIn = [bool]$env:G1T_SESSION
61$cookieArgs = @()
62if ($signedIn) { $cookieArgs = @("-H", "Cookie: g1t_session=$($env:G1T_SESSION)") }
63$agentArgs = @()
64if ($BrowserUA) {
65 $agentArgs = @("-A", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36")
66}
67
68function Measure-Once([string]$url) {
69 $headerFile = [System.IO.Path]::GetTempFileName()
70 try {
71 $timing = & curl.exe -s -o NUL -D $headerFile -H "cache-control: no-cache" @cookieArgs @agentArgs `
72 -w "%{http_code} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}" $url
73 $parts = $timing.Trim().Split(" ")
74 $headers = Get-Content $headerFile
75 $serverTiming = ($headers | Where-Object { $_ -match "^server-timing:" } | ForEach-Object { ($_ -split ":", 2)[1].Trim() }) -join ", "
76 $cookie = [bool]($headers | Where-Object { $_ -match "^set-cookie:\s*g1t_d1=" })
77 $placement = ($headers | Where-Object { $_ -match "^cf-placement:" } | ForEach-Object { ($_ -split ":", 2)[1].Trim() }) -join ""
78 $ray = ($headers | Where-Object { $_ -match "^cf-ray:" } | ForEach-Object { (($_ -split ":", 2)[1].Trim() -split "-")[-1] }) -join ""
79 [pscustomobject]@{
80 Status = [int]$parts[0]
81 # From the TLS handshake's end to the first byte: the server's share.
82 ServerMs = [math]::Round(([double]$parts[3] - [double]$parts[2]) * 1000)
83 TtfbMs = [math]::Round([double]$parts[3] * 1000)
84 TotalMs = [math]::Round([double]$parts[4] * 1000)
85 Placement = $placement
86 Edge = $ray
87 Timing = $serverTiming
88 # A read-only page should never set g1t_d1: it would then skip the
89 # public cache and send the person's next 30 s of reads to the primary.
90 SetsD1 = [bool]$cookie
91 }
92 } finally {
93 Remove-Item $headerFile -ErrorAction SilentlyContinue
94 }
95}
96
97function Percentile([double[]]$values, [double]$p) {
98 $sorted = $values | Sort-Object
99 $index = [math]::Min($sorted.Count - 1, [math]::Floor($p * $sorted.Count))
100 return $sorted[$index]
101}
102
103# The Server-Timing entries worth a column: total, and the slowest three.
104function Summarize-Timing([string]$header) {
105 if (-not $header) { return "" }
106 $entries = $header -split ",\s*(?=[A-Za-z0-9_.-]+;)" | ForEach-Object {
107 $name = ($_ -split ";")[0]
108 $dur = if ($_ -match "dur=([0-9.]+)") { [double]$Matches[1] } else { $null }
109 [pscustomobject]@{ Name = $name; Dur = $dur }
110 }
111 $total = $entries | Where-Object { $_.Name -eq "total" } | Select-Object -First 1
112 $rest = $entries | Where-Object { $_.Name -ne "total" -and $_.Dur -ne $null } | Sort-Object Dur -Descending | Select-Object -First 4
113 $shown = @()
114 if ($total) { $shown += "total=$($total.Dur)" }
115 $shown += $rest | ForEach-Object { "$($_.Name)=$($_.Dur)" }
116 return ($shown -join " ")
117}
118
119Write-Host "Measuring $Base, $Runs runs each, $(if ($signedIn) { 'signed in' } else { 'signed out' }), $(if ($BrowserUA) { 'as a browser (streamed)' } else { 'as curl (a crawler: whole page first)' })."
120$rows = foreach ($path in $Paths) {
121 $url = "$Base$path"
122 $null = Measure-Once $url # warm the connection and the isolate
123 $samples = 1..$Runs | ForEach-Object { Measure-Once $url }
124 $server = $samples | ForEach-Object { [double]$_.ServerMs }
125 $last = $samples[-1]
126 [pscustomobject]@{
127 Path = $path
128 Status = $last.Status
129 "p50 ms" = Percentile $server 0.5
130 "p90 ms" = Percentile $server 0.9
131 "TTFB p50" = Percentile ($samples | ForEach-Object { [double]$_.TtfbMs }) 0.5
132 Placement = $last.Placement
133 Edge = $last.Edge
134 "Sets g1t_d1" = $last.SetsD1
135 "Server-Timing (last run)" = Summarize-Timing $last.Timing
136 }
137}
138
139$rows | Format-Table -AutoSize -Wrap
140if ($Out) {
141 $rows | Export-Csv -NoTypeInformation -Path $Out
142 Write-Host "Saved to $Out"
143}
144Write-Host "p50/p90 ms: from the end of the TLS handshake to the first byte (the server's share). TTFB includes connecting."