Skip to content

fix(github): serialize keychain access to close BUG-052 data race - #287

Merged
tstapler merged 1 commit into
mainfrom
worktree-agent-a658918adc63c8583
Jul 29, 2026
Merged

fix(github): serialize keychain access to close BUG-052 data race#287
tstapler merged 1 commit into
mainfrom
worktree-agent-a658918adc63c8583

Conversation

@tstapler

Copy link
Copy Markdown
Owner

Summary

Fixes BUG-052: a data race in github/keychain.go between SetKeychainTokenForAccount (write, e.g. AddGitHubAccountWithToken RPC) and ListKeychainAccounts/GetAllKeychainTokens (read, UserPRCache's background refresh loop) — confirmed under go test -race across 4 separate CI runs on PR #285.

Context

github/keychain.go called zalando/go-keyring's Get/Set/Delete directly with no synchronization of its own. The package has two independently-triggerable concurrent callers: RPC handlers and UserPRCache.loop()'s periodic fetch -> resolveAllLogins -> collectAllTokens refresh. Neither the test-only mock backend nor the real OS backends (macOS Keychain, Secret Service over D-Bus) are guaranteed thread-safe by this dependency, so nothing prevented these two paths racing on shared state.

Changes

  • github/keychain.go: added a package-level sync.Mutex (keychainMu) and three private wrapper functions (keyringGet, keyringSet, keyringDelete) that guard every keyring.Get/Set/Delete call in the file. All 11 direct keyring.* call sites across the package's 8 keyring-touching functions now route through the wrappers. The lock lives in these leaf wrappers (not the outer exported functions) specifically to avoid self-deadlock from this package's internal nested calls (e.g. SetKeychainTokenForAccountaddToAccountListListKeychainAccounts).
  • github/keychain_test.go (new): two regression tests exercising the write path concurrently with both read paths from the original race trace, plus the delete path, using goroutines + sync.WaitGroup under -race.
  • docs/bugs/open/BUG-052-*.md → moved to docs/bugs/fixed/ with the full fix writeup and Phase D reflection.

Impact

  • Scope: github package only (keychain access). No RPC/proto/UI changes.
  • Breaking Changes: none — pure internal synchronization, no signature changes.
  • Performance: negligible — keychain access is not a hot path (background poll interval is tens of seconds+, plus occasional RPC calls); a plain Mutex was chosen over RWMutex since read-read concurrency has no measurable benefit at this frequency.
  • Dependencies: none added.

Testing

  • go build ./github/... ./server/services/... — clean
  • go test ./github/... ./server/services/... -race — both packages pass (github 1.06s, server/services 393s)
  • go test ./server/services/... -run TestAddGitHubAccountWithToken_ValidToken_StoresAndReturnsAccount -race -count=5 -v — 5/5 pass, no race warnings (previously flaked under -race)
  • go test ./github/... -run 'TestSetKeychainTokenForAccount_NoRace|TestDeleteKeychainTokenForAccount_NoRace' -race -count=5 -v — 5/5 each, no race warnings
  • Verified the new tests actually reproduce the race: temporarily neutered the mutex in the wrapper functions, reran under -race -count=5 — reproduced the original race plus a fatal error: concurrent map read and map write crash; restored the fix, reran clean
  • golangci-lint run --enable=nilnil,staticcheck,ineffassign,govet ./github/... ./server/services/... — 0 issues
  • Custom project linter (bin/linter ./github/... ./server/services/...) — 0 issues
  • make lint full run — blocked in this dev environment by a pre-existing, unrelated server/web/embed.go web-dist gap (same issue already documented in docs/bugs/fixed/BUG-048-*.md); both lint stages were run directly against the touched packages instead (see above)

Reviewer Notes

  • Focus areas: confirm the leaf-wrapper locking placement (keyringGet/keyringSet/keyringDelete) doesn't miss any keyring.* call site — verified via grep -n "keyring\." github/keychain.go showing only the three wrapper definitions call the real package now.
  • Known limitations: this fixes in-process synchronization only; it does not add cross-process locking for the real OS keyring backends (out of scope for this bug — no evidence of a cross-process race, only in-process).
  • Follow-up tasks: none identified.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

github/keychain.go called the zalando/go-keyring package's Get/Set/Delete
directly with no synchronization, so an AddGitHubAccountWithToken RPC call
(write) and UserPRCache's background refresh loop (read, via
ListKeychainAccounts/GetAllKeychainTokens) could race on the shared keyring
backend, confirmed under go test -race across 4 CI runs on PR #285.

Add a package-level sync.Mutex plus keyringGet/keyringSet/keyringDelete
wrapper functions and route every keyring.* call in the package through them.
The lock lives in the leaf wrappers (not the outer exported functions) to
avoid self-deadlock from this package's internal nested calls (e.g.
GetKeychainToken -> ListKeychainAccounts, SetKeychainTokenForAccount ->
addToAccountList -> ListKeychainAccounts).

Adds github/keychain_test.go with two concurrent regression tests, verified
to reproduce the original race (and a fatal concurrent-map crash) against
the pre-fix wrappers and pass clean with the mutex restored.

Moves docs/bugs/open/BUG-052-*.md to docs/bugs/fixed/ with the fix writeup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
@github-actions

Copy link
Copy Markdown
Contributor

✅ Registry Validation

Registry Validation
===================

Building backend scanner...
Scanning backend features...
Wrote 117 feature files to /tmp/tmp.cBEa7DG7NN/backend
Wrote 15 feature files to /tmp/tmp.cBEa7DG7NN/backend
Wrote 45 feature files to /tmp/tmp.cBEa7DG7NN/backend
Wrote 7 feature files to /tmp/tmp.cBEa7DG7NN/backend
Wrote 12 feature files to /tmp/tmp.cBEa7DG7NN/backend

=== Backend Registry Diff ===
Committed: 180  Generated: 180  Divergence: 0.0%
⚠️  110 feature(s) missing // +api: marker (markerFound: false)

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 22/180 features have testIds (12.2%)

Divergence > 2% blocks merges. Coverage reporting is advisory only.

@github-actions

Copy link
Copy Markdown
Contributor

Go Benchmarks (Tier 1)

benchmarks/go/tier1-baseline.txt:98: missing iteration count
benchmarks/go/tier1-baseline.txt:197: missing iteration count
tier1-bench.txt:98: missing iteration count
tier1-bench.txt:198: missing iteration count
goos: linux
goarch: amd64
pkg: github.com/tstapler/stapler-squad/session
cpu: AMD EPYC 7763 64-Core Processor                
                                            │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                                            │              sec/op              │   sec/op     vs base               │
CircularBufferWrite_4KB-4                                         82.55n ±  1%   81.36n ± 1%   -1.44% (p=0.000 n=8)
CircularBufferWrite_4KB_Allocs-4                                  83.59n ±  1%   81.38n ± 0%   -2.64% (p=0.000 n=8)
CircularBufferGetRecent_4KB-4                                     677.0n ±  5%   474.1n ± 1%  -29.97% (p=0.001 n=7)
CircularBufferGetAll-4                                            6.244µ ± 13%   3.670µ ± 2%  -41.22% (p=0.000 n=8)
GetTimeSinceLastMeaningfulOutput_HotPath-4                        65.84n ±  0%   65.80n ± 0%        ~ (p=0.124 n=8)
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       32.80n ±  0%   32.78n ± 0%        ~ (p=0.170 n=7)
geomean                                                           199.5n         170.8n       -14.36%

                                            │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt            │
                                            │               B/op               │     B/op      vs base                │
CircularBufferWrite_4KB-4                                         0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferWrite_4KB_Allocs-4                                  0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferGetRecent_4KB-4                                   4.000Ki ± 0%     4.000Ki ± 0%       ~ (p=1.000 n=7) ¹
CircularBufferGetAll-4                                          40.00Ki ± 0%     40.00Ki ± 0%       ~ (p=1.000 n=8) ¹
GetTimeSinceLastMeaningfulOutput_HotPath-4                        0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=7) ¹
geomean                                                                      ²                 +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                            │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                                            │            allocs/op             │ allocs/op   vs base                │
CircularBufferWrite_4KB-4                                         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferWrite_4KB_Allocs-4                                  0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferGetRecent_4KB-4                                     1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=7) ¹
CircularBufferGetAll-4                                            1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=8) ¹
GetTimeSinceLastMeaningfulOutput_HotPath-4                        0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
GetTimeSinceLastMeaningfulOutput_ColdPath-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=7) ¹
geomean                                                                      ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt           │
                              │               B/s                │     B/s       vs base               │
CircularBufferWrite_4KB-4                           46.21Gi ± 1%   46.89Gi ± 1%   +1.47% (p=0.000 n=8)
CircularBufferGetRecent_4KB-4                       5.635Gi ± 5%   8.046Gi ± 1%  +42.79% (p=0.001 n=7)
geomean                                             16.14Gi        19.42Gi       +20.37%

pkg: github.com/tstapler/stapler-squad/session/detection/ratelimit
                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt          │
                              │              sec/op              │   sec/op     vs base              │
StripANSI_PlainText-4                                6.897n ± 0%   6.870n ± 0%  -0.39% (p=0.012 n=8)
StripANSI_WithEscapes-4                              765.1n ± 1%   744.2n ± 0%  -2.74% (p=0.000 n=8)
ProcessOutput_InactiveState-4                        6.350n ± 0%   6.343n ± 1%       ~ (p=0.425 n=8)
geomean                                              32.24n        31.89n       -1.08%

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │               B/op               │    B/op     vs base                │
StripANSI_PlainText-4                               0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
StripANSI_WithEscapes-4                             136.0 ± 0%     136.0 ± 0%       ~ (p=1.000 n=8) ¹
ProcessOutput_InactiveState-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                        ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │            allocs/op             │ allocs/op   vs base                │
StripANSI_PlainText-4                               0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
StripANSI_WithEscapes-4                             5.000 ± 0%     5.000 ± 0%       ~ (p=1.000 n=8) ¹
ProcessOutput_InactiveState-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                        ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/queue
                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │              sec/op              │    sec/op     vs base              │
ReviewQueue_ConcurrentReads-4                        90.67n ± 5%   82.52n ± 11%  -8.99% (p=0.000 n=8)
ReviewQueue_Add-4                                    525.8n ± 1%   498.9n ±  1%  -5.10% (p=0.000 n=8)
geomean                                              218.3n        202.9n        -7.07%

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │               B/op               │    B/op     vs base                │
ReviewQueue_ConcurrentReads-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
ReviewQueue_Add-4                                   640.0 ± 0%     640.0 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                        ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                              │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                              │            allocs/op             │ allocs/op   vs base                │
ReviewQueue_ConcurrentReads-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
ReviewQueue_Add-4                                   4.000 ± 0%     4.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                        ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/scrollback
                                      │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                                      │              sec/op              │   sec/op     vs base               │
CircularBuffer_ConcurrentReadWrite-4                        4.404µ ±  2%   3.737µ ± 2%  -15.16% (p=0.000 n=8)
CircularBuffer_BurstAppend-4                                107.5µ ±  0%   103.8µ ± 2%   -3.48% (p=0.007 n=8)
CircularBuffer_GetLastN_LargeBuffer-4                       23.45µ ±  1%   19.67µ ± 2%  -16.13% (p=0.000 n=8)
CircularBuffer_GetRange_Sequential-4                        16.01µ ± 12%   10.69µ ± 1%  -33.22% (p=0.000 n=8)
CircularBufferAppend-4                                      104.5n ±  1%   100.6n ± 3%   -3.78% (p=0.000 n=8)
CircularBufferGetLastN-4                                    2.910µ ±  2%   2.213µ ± 1%  -23.96% (p=0.000 n=8)
CircularBufferConcurrentAppend-4                            132.6n ±  1%   124.3n ± 1%   -6.26% (p=0.000 n=8)
geomean                                                     3.554µ         3.013µ       -15.23%

                                      │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt            │
                                      │               B/op               │     B/op      vs base                │
CircularBuffer_ConcurrentReadWrite-4                        6.062Ki ± 0%   6.062Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_BurstAppend-4                                62.50Ki ± 0%   62.50Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_GetLastN_LargeBuffer-4                       56.00Ki ± 0%   56.00Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_GetRange_Sequential-4                        28.00Ki ± 0%   28.00Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferAppend-4                                        24.00 ± 0%     24.00 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferGetLastN-4                                    6.000Ki ± 0%   6.000Ki ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferConcurrentAppend-4                              32.00 ± 0%     32.00 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                     3.077Ki        3.077Ki       +0.00%
¹ all samples are equal

                                      │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt           │
                                      │            allocs/op             │  allocs/op   vs base                │
CircularBuffer_ConcurrentReadWrite-4                          2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_BurstAppend-4                                 1.000k ± 0%   1.000k ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_GetLastN_LargeBuffer-4                         1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBuffer_GetRange_Sequential-4                          1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferAppend-4                                        1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferGetLastN-4                                      1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
CircularBufferConcurrentAppend-4                              1.000 ± 0%    1.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                       2.962         2.962       +0.00%
¹ all samples are equal

                             │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                             │               B/s                │     B/s       vs base              │
CircularBuffer_BurstAppend-4                       567.6Mi ± 1%   588.1Mi ± 4%  +3.60% (p=0.007 n=8)

pkg: github.com/tstapler/stapler-squad/session/tmux
                             │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt          │
                             │              sec/op              │   sec/op     vs base              │
StripANSICodes_PlainText-4                          6.887n ± 0%   6.918n ± 1%       ~ (p=0.645 n=8)
StripANSICodes_WithEscapes-4                        700.5n ± 0%   687.2n ± 0%  -1.90% (p=0.000 n=8)
IsBanner_PlainText-4                                478.6n ± 0%   477.7n ± 1%       ~ (p=0.574 n=8)
geomean                                             132.2n        131.4n       -0.55%

                             │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                             │               B/op               │    B/op     vs base                │
StripANSICodes_PlainText-4                         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
StripANSICodes_WithEscapes-4                       56.00 ± 0%     56.00 ± 0%       ~ (p=1.000 n=8) ¹
IsBanner_PlainText-4                               0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                       ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                             │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                             │            allocs/op             │ allocs/op   vs base                │
StripANSICodes_PlainText-4                         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
StripANSICodes_WithEscapes-4                       4.000 ± 0%     4.000 ± 0%       ~ (p=1.000 n=8) ¹
IsBanner_PlainText-4                               0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                       ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/tokens
                                   │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                                   │              sec/op              │   sec/op     vs base               │
TokenParser_ProcessUserEntry-4                            5.777m ± 1%   5.172m ± 1%  -10.47% (p=0.000 n=8)
DetectCommandsInText/NoSlash-4                            7.509n ± 0%   7.490n ± 0%   -0.25% (p=0.000 n=8)
DetectCommandsInText/WithCommand-4                        1.704µ ± 0%   1.650µ ± 0%   -3.17% (p=0.000 n=8)
geomean                                                   4.196µ        3.998µ        -4.73%

                                   │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt            │
                                   │               B/op               │     B/op      vs base                │
TokenParser_ProcessUserEntry-4                         11.02Mi ± 0%     11.02Mi ± 0%       ~ (p=0.186 n=8)
DetectCommandsInText/NoSlash-4                           0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
DetectCommandsInText/WithCommand-4                       433.0 ± 0%       433.0 ± 0%       ~ (p=1.000 n=8)
geomean                                                             ²                 -0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                   │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                                   │            allocs/op             │ allocs/op   vs base                │
TokenParser_ProcessUserEntry-4                           34.00 ± 0%     34.00 ± 0%       ~ (p=1.000 n=8) ¹
DetectCommandsInText/NoSlash-4                           0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
DetectCommandsInText/WithCommand-4                       6.000 ± 0%     6.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                             ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/tstapler/stapler-squad/session/unfinished
                               │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                               │              sec/op              │   sec/op     vs base               │
DiffShortstat/GitVCSReader-4                          3.484m ± 7%   3.089m ± 1%  -11.34% (p=0.000 n=8)
DiffShortstat/GoGitVCSReader-4                        76.80n ± 1%   76.59n ± 0%        ~ (p=0.779 n=8)
DiffShortstatCached-4                                 76.14n ± 1%   75.79n ± 4%        ~ (p=0.314 n=8)
geomean                                               2.731µ        2.617µ        -4.17%

                               │ benchmarks/go/tier1-baseline.txt │           tier1-bench.txt            │
                               │               B/op               │     B/op      vs base                │
DiffShortstat/GitVCSReader-4                       62.58Ki ± 0%     62.57Ki ± 0%       ~ (p=0.487 n=8)
DiffShortstat/GoGitVCSReader-4                       0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
DiffShortstatCached-4                                0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                         ²                 -0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                               │ benchmarks/go/tier1-baseline.txt │          tier1-bench.txt           │
                               │            allocs/op             │ allocs/op   vs base                │
DiffShortstat/GitVCSReader-4                         360.0 ± 0%     360.0 ± 0%       ~ (p=1.000 n=8)
DiffShortstat/GoGitVCSReader-4                       0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
DiffShortstatCached-4                                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=8) ¹
geomean                                                         ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

@github-actions

Copy link
Copy Markdown
Contributor

📊 Feature E2E Coverage

Feature coverage report unavailable

Run make e2e-report locally to view the full Allure report.

@github-actions

Copy link
Copy Markdown
Contributor

E2E RPC Latency

list-sessions-ttfb-mean: 5ms (▲ slower +54.1%; baseline: 3ms)
list-sessions-total-mean: 12ms (▲ slower +43.8%; baseline: 8ms)

@tstapler
tstapler marked this pull request as ready for review July 29, 2026 16:28
@tstapler
tstapler merged commit db66eb3 into main Jul 29, 2026
22 checks passed
@tstapler
tstapler deleted the worktree-agent-a658918adc63c8583 branch July 29, 2026 16:28
@github-actions

Copy link
Copy Markdown
Contributor

Frontend Terminal Throughput

terminal-throughput-mean: 16 KB/s ▲ +32.3% (baseline: 12 KB/s)
terminal-throughput-p50: 16 KB/s ▲ +3.3% (baseline: 15 KB/s)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant