cosmos-sdk/types/events.go
Emmanuel T Odeke abe3e0c6da
perf: all: remove unnecessary allocations from strings.Builder.WriteString(fmt.Sprintf(...)) (#13230)
This change removes a code pattern that I noticed while on a late night
audit of cosmovisor in which
strings.Builder.WriteString(fmt.Sprintf(...))
calls were being made, yet that's counterproductive to using fmt.Fprintf
which will check whether the writer implements .WriteString and then
avoids the need to firstly build a string using fmt.Sprintf.

The performance wins from this change transcend all dimensions as
exhibited below:

```shell
$ benchstat before.txt after.txt
name            old time/op    new time/op    delta
DetailString-8    5.48µs ±23%    4.40µs ±11%  -19.79%  (p=0.000 n=20+17)

name            old alloc/op   new alloc/op   delta
DetailString-8    2.63kB ± 0%    2.11kB ± 0%  -19.76%  (p=0.000 n=20+20)

name            old allocs/op  new allocs/op  delta
DetailString-8      63.0 ± 0%      50.0 ± 0%  -20.63%  (p=0.000 n=20+20)
```

Fixes #13229
2022-09-09 21:57:46 +00:00

334 lines
8.0 KiB
Go

package types
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"github.com/cosmos/gogoproto/jsonpb"
proto "github.com/cosmos/gogoproto/proto"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/codec"
)
// ----------------------------------------------------------------------------
// Event Manager
// ----------------------------------------------------------------------------
// EventManager implements a simple wrapper around a slice of Event objects that
// can be emitted from.
type EventManager struct {
events Events
}
func NewEventManager() *EventManager {
return &EventManager{EmptyEvents()}
}
func (em *EventManager) Events() Events { return em.events }
// EmitEvent stores a single Event object.
// Deprecated: Use EmitTypedEvent
func (em *EventManager) EmitEvent(event Event) {
em.events = em.events.AppendEvent(event)
}
// EmitEvents stores a series of Event objects.
// Deprecated: Use EmitTypedEvents
func (em *EventManager) EmitEvents(events Events) {
em.events = em.events.AppendEvents(events)
}
// ABCIEvents returns all stored Event objects as abci.Event objects.
func (em EventManager) ABCIEvents() []abci.Event {
return em.events.ToABCIEvents()
}
// EmitTypedEvent takes typed event and emits converting it into Event
func (em *EventManager) EmitTypedEvent(tev proto.Message) error {
event, err := TypedEventToEvent(tev)
if err != nil {
return err
}
em.EmitEvent(event)
return nil
}
// EmitTypedEvents takes series of typed events and emit
func (em *EventManager) EmitTypedEvents(tevs ...proto.Message) error {
events := make(Events, len(tevs))
for i, tev := range tevs {
res, err := TypedEventToEvent(tev)
if err != nil {
return err
}
events[i] = res
}
em.EmitEvents(events)
return nil
}
// TypedEventToEvent takes typed event and converts to Event object
func TypedEventToEvent(tev proto.Message) (Event, error) {
evtType := proto.MessageName(tev)
evtJSON, err := codec.ProtoMarshalJSON(tev, nil)
if err != nil {
return Event{}, err
}
var attrMap map[string]json.RawMessage
err = json.Unmarshal(evtJSON, &attrMap)
if err != nil {
return Event{}, err
}
// sort the keys to ensure the order is always the same
keys := maps.Keys(attrMap)
slices.Sort(keys)
attrs := make([]abci.EventAttribute, 0, len(attrMap))
for _, k := range keys {
v := attrMap[k]
attrs = append(attrs, abci.EventAttribute{
Key: k,
Value: string(v),
})
}
return Event{
Type: evtType,
Attributes: attrs,
}, nil
}
// ParseTypedEvent converts abci.Event back to typed event
func ParseTypedEvent(event abci.Event) (proto.Message, error) {
concreteGoType := proto.MessageType(event.Type)
if concreteGoType == nil {
return nil, fmt.Errorf("failed to retrieve the message of type %q", event.Type)
}
var value reflect.Value
if concreteGoType.Kind() == reflect.Ptr {
value = reflect.New(concreteGoType.Elem())
} else {
value = reflect.Zero(concreteGoType)
}
protoMsg, ok := value.Interface().(proto.Message)
if !ok {
return nil, fmt.Errorf("%q does not implement proto.Message", event.Type)
}
attrMap := make(map[string]json.RawMessage)
for _, attr := range event.Attributes {
attrMap[attr.Key] = json.RawMessage(attr.Value)
}
attrBytes, err := json.Marshal(attrMap)
if err != nil {
return nil, err
}
err = jsonpb.Unmarshal(strings.NewReader(string(attrBytes)), protoMsg)
if err != nil {
return nil, err
}
return protoMsg, nil
}
// ----------------------------------------------------------------------------
// Events
// ----------------------------------------------------------------------------
type (
// Event is a type alias for an ABCI Event
Event abci.Event
// Events defines a slice of Event objects
Events []Event
)
// NewEvent creates a new Event object with a given type and slice of one or more
// attributes.
func NewEvent(ty string, attrs ...Attribute) Event {
e := Event{Type: ty}
for _, attr := range attrs {
e.Attributes = append(e.Attributes, attr.ToKVPair())
}
return e
}
// NewAttribute returns a new key/value Attribute object.
func NewAttribute(k, v string) Attribute {
return Attribute{k, v}
}
// EmptyEvents returns an empty slice of events.
func EmptyEvents() Events {
return make(Events, 0)
}
func (a Attribute) String() string {
return fmt.Sprintf("%s: %s", a.Key, a.Value)
}
// ToKVPair converts an Attribute object into a Tendermint key/value pair.
func (a Attribute) ToKVPair() abci.EventAttribute {
return abci.EventAttribute{Key: a.Key, Value: a.Value}
}
// AppendAttributes adds one or more attributes to an Event.
func (e Event) AppendAttributes(attrs ...Attribute) Event {
for _, attr := range attrs {
e.Attributes = append(e.Attributes, attr.ToKVPair())
}
return e
}
// AppendEvent adds an Event to a slice of events.
func (e Events) AppendEvent(event Event) Events {
return append(e, event)
}
// AppendEvents adds a slice of Event objects to an exist slice of Event objects.
func (e Events) AppendEvents(events Events) Events {
return append(e, events...)
}
// ToABCIEvents converts a slice of Event objects to a slice of abci.Event
// objects.
func (e Events) ToABCIEvents() []abci.Event {
res := make([]abci.Event, len(e))
for i, ev := range e {
res[i] = abci.Event{Type: ev.Type, Attributes: ev.Attributes}
}
return res
}
// Common event types and attribute keys
const (
EventTypeTx = "tx"
AttributeKeyAccountSequence = "acc_seq"
AttributeKeySignature = "signature"
AttributeKeyFee = "fee"
AttributeKeyFeePayer = "fee_payer"
EventTypeMessage = "message"
AttributeKeyAction = "action"
AttributeKeyModule = "module"
AttributeKeySender = "sender"
AttributeKeyAmount = "amount"
)
type (
// StringAttributes defines a slice of StringEvents objects.
StringEvents []StringEvent
)
func (se StringEvents) String() string {
var sb strings.Builder
for _, e := range se {
fmt.Fprintf(&sb, "\t\t- %s\n", e.Type)
for _, attr := range e.Attributes {
fmt.Fprintf(&sb, "\t\t\t- %s\n", attr)
}
}
return strings.TrimRight(sb.String(), "\n")
}
// Flatten returns a flattened version of StringEvents by grouping all attributes
// per unique event type.
func (se StringEvents) Flatten() StringEvents {
flatEvents := make(map[string][]Attribute)
for _, e := range se {
flatEvents[e.Type] = append(flatEvents[e.Type], e.Attributes...)
}
keys := make([]string, 0, len(flatEvents))
res := make(StringEvents, 0, len(flatEvents)) // appeneded to keys, same length of what is allocated to keys
for ty := range flatEvents {
keys = append(keys, ty)
}
sort.Strings(keys)
for _, ty := range keys {
res = append(res, StringEvent{Type: ty, Attributes: flatEvents[ty]})
}
return res
}
// StringifyEvent converts an Event object to a StringEvent object.
func StringifyEvent(e abci.Event) StringEvent {
res := StringEvent{Type: e.Type}
for _, attr := range e.Attributes {
res.Attributes = append(
res.Attributes,
Attribute{Key: attr.Key, Value: attr.Value},
)
}
return res
}
// StringifyEvents converts a slice of Event objects into a slice of StringEvent
// objects.
func StringifyEvents(events []abci.Event) StringEvents {
res := make(StringEvents, 0, len(events))
for _, e := range events {
res = append(res, StringifyEvent(e))
}
return res.Flatten()
}
// MarkEventsToIndex returns the set of ABCI events, where each event's attribute
// has it's index value marked based on the provided set of events to index.
func MarkEventsToIndex(events []abci.Event, indexSet map[string]struct{}) []abci.Event {
indexAll := len(indexSet) == 0
updatedEvents := make([]abci.Event, len(events))
for i, e := range events {
updatedEvent := abci.Event{
Type: e.Type,
Attributes: make([]abci.EventAttribute, len(e.Attributes)),
}
for j, attr := range e.Attributes {
_, index := indexSet[fmt.Sprintf("%s.%s", e.Type, attr.Key)]
updatedAttr := abci.EventAttribute{
Key: attr.Key,
Value: attr.Value,
Index: index || indexAll,
}
updatedEvent.Attributes[j] = updatedAttr
}
updatedEvents[i] = updatedEvent
}
return updatedEvents
}