latticexyz / mud

MUD is a framework for building autonomous worlds
https://mud.dev
MIT License
753 stars 187 forks source link

Consider registering individual hook methods (instead of all-or-nothing) #1159

Closed holic closed 1 year ago

holic commented 1 year ago

frolic — Today at 1:40 PM thinking about store hooks and we should prob move to either 1) separate registration array per hook method (eg set record) or 2) the registration includes a bitmap of all the hook methods used registering a hook right now means every operation goes through that hook, even if it’s a no op?

alvarius — Today at 1:42 PM we could say we can expect a store hook to implement all functions because data can be modified via each of them, it's similar to how an off-chain indexer has to support all store events and can't just ignore one

frolic — Today at 1:43 PM I’m just thinking this is the worst approach for gas in the hot code path

alvarius — Today at 1:44 PM we could do a bitmap if it doesn't add much gas (which might be reasonable since we can store it in the same storage slot) rn i can't think of a use case of only implementing one store hook function but not another though

frolic — Today at 1:45 PM yeah, there’s a trade off of my two proposals: iterate through all hooks but skip some (per store op) vs more registration gas cost (one time)

alvarius — Today at 1:45 PM do you have something in mind?

frolic — Today at 1:45 PM maybe I’m thinking more about system hooks where you may only want before or after could see potentially doing a before and after for store events too but can’t think of a specific use case right now one thought, prob not a great example: a hook that makes an array field append only

alvarius — Today at 1:48 PM we currently do that for field but not for record, because for handling a field update you might want to just read the entire record from storage before and after the update instead of manually reconstructing the new record with the field update applied in the hook could allow this for setRecord too but the usecase is less clear

frolic — Today at 1:50 PM this feels like one of those “I can’t think of a good example but maybe someone will think of one” and we get some potential gas benefits

curious if there’s a strong case for store hooks implementing all methods

alvarius — Today at 1:51 PM curious if there’s a strong case for store hooks implementing all methods just that if you want to react to data changes you probably want to react to all possible ways the data can change i see the point for system hooks though a use case for afterSetRecord might be if you want to actually change the storage value of what has been set

frolic — Today at 2:01 PM I think my main point is: first order effect is better gas (in the hot code path), second order effect is more granularity

alvarius — Today at 2:05 PM yeah i understand the argument for one array in terms of gas would be: if we expect that every hook implements all functions, one array would be cheaper if a table is modified twice in a transaction with different methods (setRecord, setField, deleteRecord, ...) because the storage is hot for every access after the first one but i see advantages for system hooks, and the potential disadvantages for store hooks are small enough to justify making it consistent for both imo

frolic — Today at 2:17 PM this makes sense, at the very least we could couple a “which hooks” bitmap with the address into the same slot

qbzzt commented 1 year ago

@alvrs : a use case for afterSetRecord might be if you want to actually change the storage value of what has been set.

Ori: I agree. I don't like the situation where we can fix some changes, but only revert out of others.

alvrs commented 1 year ago

Action items here:

alvrs commented 1 year ago

Approach A: a single Hooks table with flags for enabling/disabling hooks Approach B: individual lists for each type of hook (i.e. the hooks table changes its schema from { hooks: address[] } to { beforeSetRecord: address[], afterSetRecord: [], ... })

Tradeoffs between approach A and B

Note the cost estimates are disregarding the cost to read the dynamic data length because it's the same for both approaches.

Example calculations

Summary

In general, for each hook that cares about both the before and after event approach A is about 1800 gas cheaper than approach B (due to the missing SLOAD, minus the bitmap overhead), while for each hook that only cares about one of before or after approach B is about 300 gas cheaper than approach A (due to the missing bitmap overhead)

If there is a second storage operation on the same table in the same transaction, approach A is around 1700 gas cheaper than approach B (due to a warm SLOAD instead of a cold SLOAD, minus the bitmap overhead).

While I do think in most cases hooks would only care about either before or after and I estimate it to be rather uncommon to have multiple storage operations on the same table in a single transaction, the savings for approach A in those scenarios are much more significant (~6x), and 300 gas overhead is also not that significant for operations that likely cost 20k+ (because they're writing storage), so I would go with approach A.