Skip to content
168 linesCodeBlameRaw
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 # Signed in with an access token that has "Use the website as you" on:
20 # G1T_TOKEN_FILE names the file holding it. The script never prints it.
21 $env:G1T_TOKEN_FILE = "$HOME\.config\g1t\website-token"
22 powershell -File scripts/perf/measure.ps1 -Runs 7
23
24.EXAMPLE
25 # As a browser sees it: React Router streams to browsers and renders the
26 # whole page first for anything it takes for a crawler, which curl's own
27 # user agent is. Compare the two to see what streaming saves.
28 powershell -File scripts/perf/measure.ps1 -BrowserUA
29
30.NOTES
31 Uses curl.exe (Windows' own). Git Bash's curl fails here with exit 43.
32#>
33param(
34 [string]$Base = "https://g1t.sh",
35 [string]$Repo = "flagon-io/g1t",
36 [int]$Pull = 1,
37 [int]$Issue = 2,
38 [int]$Runs = 5,
39 [string[]]$Paths = @(),
40 [string]$Out = "",
41 # Send a desktop Chrome user agent: streamed pages, as people get them.
42 # Without it the requests look like a crawler's and wait for the whole page.
43 [switch]$BrowserUA
44)
45
46$ErrorActionPreference = "Stop"
47
48# Pages people open most, and the data request a click inside the app makes
49# for the same page (React Router's `.data`, with only the page's own
50# loader, as an in-app navigation sends it).
51if ($Paths.Count -eq 0) {
52 $Paths = @(
53 "/",
54 "/explore",
55 "/$Repo",
56 "/$Repo/pulls",
57 "/$Repo/issues",
58 "/$Repo/pull/$Pull",
59 "/$Repo/issues/$Issue",
60 "/$Repo/pull/$Pull.data?_routes=routes%2Frepo%2Fpull",
61 "/$Repo/pulls.data?_routes=routes%2Frepo%2Fpulls",
62 "/pricing"
63 )
64}
65
66$signedIn = [bool]$env:G1T_SESSION
67$cookieArgs = @()
68if ($signedIn) { $cookieArgs = @("-H", "Cookie: g1t_session=$($env:G1T_SESSION)") }
69# Signed in with an access token that may use the website: G1T_TOKEN_FILE
70# names the file holding it. curl reads the header from a temporary file,
71# so the token is never on a command line or printed. It wins over
72# G1T_SESSION, as on the site.
73$tokenHeaderFile = $null
74if ($env:G1T_TOKEN_FILE) {
75 if (-not (Test-Path -LiteralPath $env:G1T_TOKEN_FILE -PathType Leaf)) { throw "G1T_TOKEN_FILE does not name a file." }
76 $tokenHeaderFile = [System.IO.Path]::GetTempFileName()
77 $header = "Authorization: Bearer " + (Get-Content -Raw -LiteralPath $env:G1T_TOKEN_FILE).Trim()
78 [System.IO.File]::WriteAllText($tokenHeaderFile, $header)
79 Remove-Variable header
80 $cookieArgs = @("-H", "@$tokenHeaderFile")
81 $signedIn = $true
82}
83$agentArgs = @()
84if ($BrowserUA) {
85 $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")
86}
87
88function Measure-Once([string]$url) {
89 $headerFile = [System.IO.Path]::GetTempFileName()
90 try {
91 $timing = & curl.exe -s -o NUL -D $headerFile -H "cache-control: no-cache" @cookieArgs @agentArgs `
92 -w "%{http_code} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}" $url
93 $parts = $timing.Trim().Split(" ")
94 $headers = Get-Content $headerFile
95 $serverTiming = ($headers | Where-Object { $_ -match "^server-timing:" } | ForEach-Object { ($_ -split ":", 2)[1].Trim() }) -join ", "
96 $cookie = [bool]($headers | Where-Object { $_ -match "^set-cookie:\s*g1t_d1=" })
97 $placement = ($headers | Where-Object { $_ -match "^cf-placement:" } | ForEach-Object { ($_ -split ":", 2)[1].Trim() }) -join ""
98 $ray = ($headers | Where-Object { $_ -match "^cf-ray:" } | ForEach-Object { (($_ -split ":", 2)[1].Trim() -split "-")[-1] }) -join ""
99 [pscustomobject]@{
100 Status = [int]$parts[0]
101 # From the TLS handshake's end to the first byte: the server's share.
102 ServerMs = [math]::Round(([double]$parts[3] - [double]$parts[2]) * 1000)
103 TtfbMs = [math]::Round([double]$parts[3] * 1000)
104 TotalMs = [math]::Round([double]$parts[4] * 1000)
105 Placement = $placement
106 Edge = $ray
107 Timing = $serverTiming
108 # A read-only page should never set g1t_d1: it would then skip the
109 # public cache and send the person's next 30 s of reads to the primary.
110 SetsD1 = [bool]$cookie
111 }
112 } finally {
113 Remove-Item $headerFile -ErrorAction SilentlyContinue
114 }
115}
116
117function Percentile([double[]]$values, [double]$p) {
118 $sorted = $values | Sort-Object
119 $index = [math]::Min($sorted.Count - 1, [math]::Floor($p * $sorted.Count))
120 return $sorted[$index]
121}
122
123# The Server-Timing entries worth a column: total, and the slowest three.
124function Summarize-Timing([string]$header) {
125 if (-not $header) { return "" }
126 $entries = $header -split ",\s*(?=[A-Za-z0-9_.-]+;)" | ForEach-Object {
127 $name = ($_ -split ";")[0]
128 $dur = if ($_ -match "dur=([0-9.]+)") { [double]$Matches[1] } else { $null }
129 [pscustomobject]@{ Name = $name; Dur = $dur }
130 }
131 $total = $entries | Where-Object { $_.Name -eq "total" } | Select-Object -First 1
132 $rest = $entries | Where-Object { $_.Name -ne "total" -and $_.Dur -ne $null } | Sort-Object Dur -Descending | Select-Object -First 4
133 $shown = @()
134 if ($total) { $shown += "total=$($total.Dur)" }
135 $shown += $rest | ForEach-Object { "$($_.Name)=$($_.Dur)" }
136 return ($shown -join " ")
137}
138
139Write-Host "Measuring $Base, $Runs runs each, $(if ($tokenHeaderFile) { 'signed in with a token' } elseif ($signedIn) { 'signed in' } else { 'signed out' }), $(if ($BrowserUA) { 'as a browser (streamed)' } else { 'as curl (a crawler: whole page first)' })."
140try {
141$rows = foreach ($path in $Paths) {
142 $url = "$Base$path"
143 $null = Measure-Once $url # warm the connection and the isolate
144 $samples = 1..$Runs | ForEach-Object { Measure-Once $url }
145 $server = $samples | ForEach-Object { [double]$_.ServerMs }
146 $last = $samples[-1]
147 [pscustomobject]@{
148 Path = $path
149 Status = $last.Status
150 "p50 ms" = Percentile $server 0.5
151 "p90 ms" = Percentile $server 0.9
152 "TTFB p50" = Percentile ($samples | ForEach-Object { [double]$_.TtfbMs }) 0.5
153 Placement = $last.Placement
154 Edge = $last.Edge
155 "Sets g1t_d1" = $last.SetsD1
156 "Server-Timing (last run)" = Summarize-Timing $last.Timing
157 }
158}
159} finally {
160 if ($tokenHeaderFile) { Remove-Item -LiteralPath $tokenHeaderFile -ErrorAction SilentlyContinue }
161}
162
163$rows | Format-Table -AutoSize -Wrap
164if ($Out) {
165 $rows | Export-Csv -NoTypeInformation -Path $Out
166 Write-Host "Saved to $Out"
167}
168Write-Host "p50/p90 ms: from the end of the TLS handshake to the first byte (the server's share). TTFB includes connecting."