[performance] use our own typed value context types for Value() key checking to improve performance (#4316)

Replaces our gtscontext package context.Context handling with our own typed contexts instead of `context.WithValue()`.

I wrote a quick benchmark consisting of (printlns to stop the compiler optimizing instructions away):
```golang
func BenchmarkContexts(b *testing.B) {
	var receiving *gtsmodel.Account
	var requesting *gtsmodel.Account
	var otherIRIs []*url.URL

	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			ctx := context.Background()

			ctx = gtscontext.SetBarebones(ctx)
			ctx = gtscontext.SetFastFail(ctx)
			ctx = gtscontext.SetDryRun(ctx)
			ctx = gtscontext.SetReceivingAccount(ctx, receiving)
			ctx = gtscontext.SetRequestingAccount(ctx, requesting)
			ctx = gtscontext.SetOtherIRIs(ctx, otherIRIs)

			if !gtscontext.Barebones(ctx) {
				println("oh no!")
			}

			if !gtscontext.IsFastfail(ctx) {
				println("oh no!")
			}

			if !gtscontext.DryRun(ctx) {
				println("oh no!")
			}

			if gtscontext.ReceivingAccount(ctx) != nil {
				println("oh no!")
			}

			if gtscontext.RequestingAccount(ctx) != nil {
				println("oh no!")
			}

			if len(gtscontext.OtherIRIs(ctx)) > 0 {
				println("oh no!")
			}
		}
	})
}
```

Before results:
```shell
kim @ ~/Projects/main/gts.4
--> go test -v -run=none -bench=.* -benchmem ./internal/gtscontext/ -count=5
goos: linux
goarch: amd64
pkg: code.superseriousbusiness.org/gotosocial/internal/gtscontext
cpu: AMD Ryzen 7 7840U w/ Radeon  780M Graphics
BenchmarkContexts
BenchmarkContexts-16            19050348                61.73 ns/op          288 B/op          6 allocs/op
BenchmarkContexts-16            18245772                61.71 ns/op          288 B/op          6 allocs/op
BenchmarkContexts-16            18853680                61.80 ns/op          288 B/op          6 allocs/op
BenchmarkContexts-16            18561621                62.67 ns/op          288 B/op          6 allocs/op
BenchmarkContexts-16            17819241                62.89 ns/op          288 B/op          6 allocs/op
PASS
ok      code.superseriousbusiness.org/gotosocial/internal/gtscontext    6.112s
```

After results:
```shell
kim @ ~/Projects/main/gts.4
--> go test -v -run=none -bench=.* -benchmem ./internal/gtscontext/ -count=5
goos: linux
goarch: amd64
pkg: code.superseriousbusiness.org/gotosocial/internal/gtscontext
cpu: AMD Ryzen 7 7840U w/ Radeon  780M Graphics
BenchmarkContexts
BenchmarkContexts-16            28038618                41.67 ns/op          144 B/op          6 allocs/op
BenchmarkContexts-16            26537552                42.50 ns/op          144 B/op          6 allocs/op
BenchmarkContexts-16            26720542                42.39 ns/op          144 B/op          6 allocs/op
BenchmarkContexts-16            27408031                43.15 ns/op          144 B/op          6 allocs/op
BenchmarkContexts-16            25597026                44.02 ns/op          144 B/op          6 allocs/op
PASS
ok      code.superseriousbusiness.org/gotosocial/internal/gtscontext    5.997s
```

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4316
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-07-09 01:38:33 +02:00 committed by kim
commit c8a4ce9a88
2 changed files with 200 additions and 12 deletions

View file

@ -57,7 +57,16 @@ func DryRun(ctx context.Context) bool {
// SetDryRun sets the "dryrun" context flag and returns this wrapped context.
// See DryRun() for further information on the "dryrun" context flag.
func SetDryRun(ctx context.Context) context.Context {
return context.WithValue(ctx, dryRunKey, struct{}{})
return dryRunContext{ctx}
}
type dryRunContext struct{ context.Context }
func (ctx dryRunContext) Value(key any) any {
if key == dryRunKey {
return struct{}{}
}
return ctx.Context.Value(key)
}
// RequestID returns the request ID associated with context. This value will usually
@ -72,7 +81,19 @@ func RequestID(ctx context.Context) string {
// SetRequestID stores the given request ID value and returns the wrapped
// context. See RequestID() for further information on the request ID value.
func SetRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
return requestIDContext{Context: ctx, requestID: id}
}
type requestIDContext struct {
context.Context
requestID string
}
func (ctx requestIDContext) Value(key any) any {
if key == requestIDKey {
return ctx.requestID
}
return ctx.Context.Value(key)
}
// OutgoingPublicKeyID returns the public key ID (URI) associated with context. This
@ -86,7 +107,19 @@ func OutgoingPublicKeyID(ctx context.Context) string {
// SetOutgoingPublicKeyID stores the given public key ID value and returns the wrapped
// context. See PublicKeyID() for further information on the public key ID value.
func SetOutgoingPublicKeyID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, outgoingPubKeyIDKey, id)
return outgoingPublicKeyIDKeyContext{Context: ctx, pubKeyID: id}
}
type outgoingPublicKeyIDKeyContext struct {
context.Context
pubKeyID string
}
func (ctx outgoingPublicKeyIDKeyContext) Value(key any) any {
if key == outgoingPubKeyIDKey {
return ctx.pubKeyID
}
return ctx.Context.Value(key)
}
// ReceivingAccount returns the local account who owns the resource being
@ -99,7 +132,19 @@ func ReceivingAccount(ctx context.Context) *gtsmodel.Account {
// SetReceivingAccount stores the given receiving account value and returns the wrapped
// context. See ReceivingAccount() for further information on the receiving account value.
func SetReceivingAccount(ctx context.Context, acct *gtsmodel.Account) context.Context {
return context.WithValue(ctx, receivingAccountKey, acct)
return receivingAccountContext{Context: ctx, account: acct}
}
type receivingAccountContext struct {
context.Context
account *gtsmodel.Account
}
func (ctx receivingAccountContext) Value(key any) any {
if key == receivingAccountKey {
return ctx.account
}
return ctx.Context.Value(key)
}
// RequestingAccount returns the remote account interacting with a local
@ -112,7 +157,19 @@ func RequestingAccount(ctx context.Context) *gtsmodel.Account {
// SetRequestingAccount stores the given requesting account value and returns the wrapped
// context. See RequestingAccount() for further information on the requesting account value.
func SetRequestingAccount(ctx context.Context, acct *gtsmodel.Account) context.Context {
return context.WithValue(ctx, requestingAccountKey, acct)
return requestingAccountContext{Context: ctx, account: acct}
}
type requestingAccountContext struct {
context.Context
account *gtsmodel.Account
}
func (ctx requestingAccountContext) Value(key any) any {
if key == requestingAccountKey {
return ctx.account
}
return ctx.Context.Value(key)
}
// OtherIRIs returns other IRIs which are involved in the current ActivityPub request
@ -126,7 +183,19 @@ func OtherIRIs(ctx context.Context) []*url.URL {
// SetOtherIRIs stores the given IRIs slice and returns the wrapped context.
// See OtherIRIs() for further information on the IRIs slice value.
func SetOtherIRIs(ctx context.Context, iris []*url.URL) context.Context {
return context.WithValue(ctx, otherIRIsKey, iris)
return otherIRIsContext{Context: ctx, iris: iris}
}
type otherIRIsContext struct {
context.Context
iris []*url.URL
}
func (ctx otherIRIsContext) Value(key any) any {
if key == otherIRIsKey {
return ctx.iris
}
return ctx.Context.Value(key)
}
// HTTPClientSignFunc returns an httpclient signing function for the current client
@ -139,7 +208,19 @@ func HTTPClientSignFunc(ctx context.Context) func(*http.Request) error {
// SetHTTPClientSignFunc stores the given httpclient signing function and returns the wrapped
// context. See HTTPClientSignFunc() for further information on the signing function value.
func SetHTTPClientSignFunc(ctx context.Context, fn func(*http.Request) error) context.Context {
return context.WithValue(ctx, httpClientSignFnKey, fn)
return httpClientSignFuncContext{Context: ctx, signfn: fn}
}
type httpClientSignFuncContext struct {
context.Context
signfn func(*http.Request) error
}
func (ctx httpClientSignFuncContext) Value(key any) any {
if key == httpClientSignFnKey {
return ctx.signfn
}
return ctx.Context.Value(key)
}
// HTTPSignatureVerifier returns an http signature verifier for the current ActivityPub
@ -152,7 +233,19 @@ func HTTPSignatureVerifier(ctx context.Context) httpsig.VerifierWithOptions {
// SetHTTPSignatureVerifier stores the given http signature verifier and returns the
// wrapped context. See HTTPSignatureVerifier() for further information on the verifier value.
func SetHTTPSignatureVerifier(ctx context.Context, verifier httpsig.VerifierWithOptions) context.Context {
return context.WithValue(ctx, httpSigVerifierKey, verifier)
return httpSignatureVerifierContext{Context: ctx, verifier: verifier}
}
type httpSignatureVerifierContext struct {
context.Context
verifier httpsig.VerifierWithOptions
}
func (ctx httpSignatureVerifierContext) Value(key any) any {
if key == httpSigVerifierKey {
return ctx.verifier
}
return ctx.Context.Value(key)
}
// HTTPSignature returns the http signature string
@ -165,7 +258,19 @@ func HTTPSignature(ctx context.Context) string {
// SetHTTPSignature stores the given http signature string and returns the wrapped
// context. See HTTPSignature() for further information on the verifier value.
func SetHTTPSignature(ctx context.Context, signature string) context.Context {
return context.WithValue(ctx, httpSigKey, signature)
return httpSignatureContext{Context: ctx, signature: signature}
}
type httpSignatureContext struct {
context.Context
signature string
}
func (ctx httpSignatureContext) Value(key any) any {
if key == httpSigKey {
return ctx.signature
}
return ctx.Context.Value(key)
}
// HTTPSignaturePubKeyID returns the public key id of the http signature
@ -178,7 +283,19 @@ func HTTPSignaturePubKeyID(ctx context.Context) *url.URL {
// SetHTTPSignaturePubKeyID stores the given http signature public key id and returns
// the wrapped context. See HTTPSignaturePubKeyID() for further information on the value.
func SetHTTPSignaturePubKeyID(ctx context.Context, pubKeyID *url.URL) context.Context {
return context.WithValue(ctx, httpSigPubKeyIDKey, pubKeyID)
return httpSigPubKeyIDContext{Context: ctx, pubKeyID: pubKeyID}
}
type httpSigPubKeyIDContext struct {
context.Context
pubKeyID *url.URL
}
func (ctx httpSigPubKeyIDContext) Value(key any) any {
if key == httpSigPubKeyIDKey {
return ctx.pubKeyID
}
return ctx.Context.Value(key)
}
// IsFastFail returns whether the "fastfail" context key has been set. This
@ -192,7 +309,16 @@ func IsFastfail(ctx context.Context) bool {
// SetFastFail sets the "fastfail" context flag and returns this wrapped context.
// See IsFastFail() for further information on the "fastfail" context flag.
func SetFastFail(ctx context.Context) context.Context {
return context.WithValue(ctx, fastFailKey, struct{}{})
return fastFailContext{ctx}
}
type fastFailContext struct{ context.Context }
func (ctx fastFailContext) Value(key any) any {
if key == fastFailKey {
return struct{}{}
}
return ctx.Context.Value(key)
}
// Barebones returns whether the "barebones" context key has been set. This
@ -206,5 +332,14 @@ func Barebones(ctx context.Context) bool {
// SetBarebones sets the "barebones" context flag and returns this wrapped context.
// See Barebones() for further information on the "barebones" context flag.
func SetBarebones(ctx context.Context) context.Context {
return context.WithValue(ctx, barebonesKey, struct{}{})
return barebonesContext{ctx}
}
type barebonesContext struct{ context.Context }
func (ctx barebonesContext) Value(key any) any {
if key == barebonesKey {
return struct{}{}
}
return ctx.Context.Value(key)
}

View file

@ -0,0 +1,53 @@
package gtscontext_test
import (
"context"
"net/url"
"testing"
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
func BenchmarkContexts(b *testing.B) {
var receiving *gtsmodel.Account
var requesting *gtsmodel.Account
var otherIRIs []*url.URL
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := context.Background()
ctx = gtscontext.SetBarebones(ctx)
ctx = gtscontext.SetFastFail(ctx)
ctx = gtscontext.SetDryRun(ctx)
ctx = gtscontext.SetReceivingAccount(ctx, receiving)
ctx = gtscontext.SetRequestingAccount(ctx, requesting)
ctx = gtscontext.SetOtherIRIs(ctx, otherIRIs)
if !gtscontext.Barebones(ctx) {
println("oh no!")
}
if !gtscontext.IsFastfail(ctx) {
println("oh no!")
}
if !gtscontext.DryRun(ctx) {
println("oh no!")
}
if gtscontext.ReceivingAccount(ctx) != nil {
println("oh no!")
}
if gtscontext.RequestingAccount(ctx) != nil {
println("oh no!")
}
if len(gtscontext.OtherIRIs(ctx)) > 0 {
println("oh no!")
}
}
})
}