-
Notifications
You must be signed in to change notification settings - Fork 17.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
log/slog: one allocation in TextHandler with ReplaceAttr #61774
Comments
CC @jba |
Arguing against my proposed solution, I can see two issues:
I'm tempted to close this issue... But maybe something can be done for Level in particular. I wonder if the custom ReplaceAttr function can replace the attr.Any with a pre-allocated string and avoid the allocation that way. I'll try that and report the results. |
Yes! Adding this to the ReplaceAttr function removes the allocation. Global map: var levelNames = map[slog.Level]slog.Value{
slog.LevelDebug: slog.StringValue("DEBUG"),
slog.LevelInfo: slog.StringValue("INFO"), // default
slog.LevelError: slog.StringValue("ERROR"),
slog.LevelWarn: slog.StringValue("WARN"),
} Inside the function: if a.Value.Kind() == slog.KindAny {
level, ok := a.Value.Any().(slog.Level)
if ok {
a.Value = levelNames[level]
}
} |
I think this is more of an obscure gotcha with a subtle workaround rather than something that needs to be fixed. Maybe it's worth a small mention in the documentation that any use of |
@vikstrous thanks for traveling down this rabbit hole! I think adding anything to the doc about this will just make people think too much about allocation. It almost never matters. But as you say, we have this issue to point people to if they notice. One problem with your solution: a |
perhaps |
@vikstrous @jba I found that this snippet in ReplaceAttr avoids allocations just as well as the map version, but is more contained and tries to avoid false positives on arbitrary integers: func(groups []string, a slog.Attr) slog.Attr {
if groups == nil && a.Key == slog.LevelKey && a.Value.Kind() == slog.KindAny {
if level, ok := a.Value.Any().(slog.Level); ok {
a.Value = slog.StringValue(level.String())
}
}
return a
} If I apply the following change directly to diff --git src/log/slog/handler.go src/log/slog/handler.go
index 03d631c0ac..72a39ec9a6 100644
--- src/log/slog/handler.go
+++ src/log/slog/handler.go
@@ -290,7 +290,7 @@ func (h *commonHandler) handle(r Record) error {
state.appendKey(key)
state.appendString(val.String())
} else {
- state.appendAttr(Any(key, val))
+ state.appendAttr(String(key, val.String()))
}
// source
if h.opts.AddSource { some of the package benchmarks report reduced allocations:
All tests pass, with the exception of go/src/log/slog/example_custom_levels_test.go Lines 46 to 47 in 26b5783
|
Thanks, @artyom! I pasted your example ReplaceAttr function into my test and it passes. I don't think the change to the standard library should be made because it would make it harder to work with levels in ReplaceAttr implementations, but the snippet you shared can be used by anyone that cares about getting to 0 allocations with ReplaceAttr. I think there's nothing more to be said on this topic, so I'll close the issue :) |
Disclaimer: I was messing around with slog mostly for my own amusement and none of this comes from an actual need.
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
yes, with go1.21rc4
What operating system and processor architecture are you using (
go env
)?not relevant
What did you do?
When using TextHandler with slog and setting ReplaceAttr, there's one allocation per log rather than zero.
Note that with JSONHandler, there are 4 allocations reported by this test and I haven't tracked them down.
What did you expect to see?
Test passing
What did you see instead?
allocs without ReplaceAttr 0 != allocs with ReplaceAttr 1
More context
The difference is that without ReplaceAttr, Level is directly converted to string and with it, it's kept as the Level type so that ReplaceAttr can act on the Level type.
When it comes time to print Level's value, its implementation of MarshalText returns a byte slice, but the conversion from string to byte slice causes an allocation. This is basically a limitation of the TextMarshaler interface. Not that everything we are seeing here applies to any marshaling of slog.Any wrapped values.
I don't think there's a more efficient way to implement MarshalText for Level. I considered two bad alternatives:
Proposed solution
Prioritizing
Stringer
overTextMarshaler
would solve this issue. We can put this:right here above the TextMarshaler check.
Right now
String()
is effectively what's called ifTextMarshaler
is not implemented, but it's called indirectly throughfmt.Sprintf("%+v", v.Any())
, which causes an allocation. By putting the stringer option first and doing it explicitly, we give all "Any" values an opportunity to marshal themselves efficiently as strings.A similar trick can be done in appendJSONMarshal for the json case to remove all 4 allocations. I think it wouldn't be completely crazy for slog to prefer to format values as strings regardless of the encoding of the logs. It can be argued that the encoding of the logs shouldn't affect the content of the logs and the preference for using Stringer would make the two implementations more consistent with each other.
The text was updated successfully, but these errors were encountered: