Perf optimisations#98
Conversation
| let mapArrayOfResults = (results: array<result<'a, 'b>>): result<array<'a>, 'b> => { | ||
| results->Belt.Array.reduce(Ok([]), (accum, nextItem) => { | ||
| accum->Belt.Result.flatMap(currentOkItems => { | ||
| nextItem->Belt.Result.map(item => Belt.Array.concat(currentOkItems, [item])) | ||
| }) | ||
| }) | ||
| let rec loop = (index: int, output: array<'a>): result<array<'a>, 'b> => { | ||
| if index >= Array.length(results) { | ||
| Ok(output) | ||
| } else { | ||
| switch results->Js.Array2.unsafe_get(index) { | ||
| | Ok(value) => { | ||
| output[index] = value | ||
| loop(index + 1, output) | ||
| } | ||
| | Error(_) as err => err->(magic: result<'a, 'b> => result<array<'a>, 'b>) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| loop(0, Belt.Array.makeUninitializedUnsafe(results->Js.Array2.length)) | ||
| } |
There was a problem hiding this comment.
@JonoPrest Noticed the code, and it literally looks the same as a redux selector from the messaging app I was talking you about 😁
Here's the benchmark results for running the function with 10000 Ok items:
| if entityIdsMap->EntityIdsMap.hasId(~entityName=EntityMod.name, ~entityId) { | ||
| inMemTable->InMemoryTable.Entity.get(entityId) | ||
| } else { | ||
| // NOTE: this will still return the value if it exists in the in-memory store (despite the loader not being run). | ||
| switch inMemTable->InMemoryTable.Entity.get(entityId) { | ||
| | Some(entity) => Some(entity) | ||
| | None => | ||
| let entities = await asyncGetter(entityId) | ||
|
|
||
| let optEntity = entities->Belt.Array.get(0) | ||
| inMemTable->InMemoryTable.Entity.initValue(~key=entityId, ~entity=optEntity) | ||
|
|
||
| optEntity | ||
| } | ||
| switch inMemTable->InMemoryTable.Entity.get(entityId) { | ||
| | Some(entity) => Some(entity) | ||
| | None => | ||
| let entities = await asyncGetter(entityId) | ||
|
|
||
| let optEntity = entities->Belt.Array.get(0) | ||
| inMemTable->InMemoryTable.Entity.initValue(~key=entityId, ~entity=optEntity) | ||
|
|
||
| optEntity |
There was a problem hiding this comment.
This is the only place where it's used, which doesn't look relevant after we migrated to async handlers.
| output[index] = value | ||
| loop(index + 1, output) | ||
| } | ||
| | Error(_) as err => err->(magic: result<'a, 'b> => result<array<'a>, 'b>) |
There was a problem hiding this comment.
Why not simply do this? Are you trying to reduce overhead with the magic?
| | Error(_) as err => err->(magic: result<'a, 'b> => result<array<'a>, 'b>) | |
| | Error(e) => Error(e) |
There was a problem hiding this comment.
It's a tiny optimisation to avoid recreating the {TAG: "Error", _0: 'a} object
| logger: Pino.t, | ||
| chain: ChainMap.Chain.t, | ||
| addedDynamicContractRegistrations: array<TablesStatic.DynamicContractRegistry.t>, | ||
| entityIdsMap: EntityIdsMap.t, |
There was a problem hiding this comment.
@DZakh this is still used/useful so I would say don't remove it but there may be a nicer way to achieve this that doesn't involve another map?
| if entityIdsMap->EntityIdsMap.hasId(~entityName=EntityMod.name, ~entityId) { | ||
| inMemTable->InMemoryTable.Entity.get(entityId) | ||
| } else { | ||
| // NOTE: this will still return the value if it exists in the in-memory store (despite the loader not being run). | ||
| switch inMemTable->InMemoryTable.Entity.get(entityId) { | ||
| | Some(entity) => Some(entity) | ||
| | None => | ||
| let entities = await asyncGetter(entityId) |
There was a problem hiding this comment.
We can't remove this because it protects against round trips to the db when a value is confirmed doesn't exist.
I think we can probably optimize by first looking in the in mem table and only checking if it was previously looked up before the async call. Not sure how much that would help though.
JonoPrest
left a comment
There was a problem hiding this comment.
Love the util optimization! But we shouldn't remove the other code without another solution 👍🏼
| // It returns option<option<'entity>> where the first option means | ||
| // that the entity is not set to the in memory store, | ||
| // and the second option means that the entity doesn't esist/deleted. | ||
| // It's needed to prevent an additional round trips to the database for deleted entities. | ||
| let get = (inMemTable: t<'entity>, key: Types.id) => | ||
| inMemTable.table | ||
| ->get(key) | ||
| ->Option.flatMap(rowToEntity) | ||
| ->Option.map(rowToEntity) |
There was a problem hiding this comment.
This should also improve these lines in LoaderLayer:
//filter out ids that don't already exist in the in memory store
let idsNotInMemory =
entityLoadIds->Array.keep(id => inMemTable->InMemoryTable.Entity.get(id)->Option.isNone)
//load in values that don't exist in the inMemoryStore
let res = await idsNotInMemory->batchLoadIds
| | Some(entity) => resolve(Some(entity)) | ||
| | None => resolve(None) | ||
| | Some(maybeEntity) => resolve(maybeEntity) | ||
| | None => resolve(None) // Shouldn't happen |
There was a problem hiding this comment.
Why should this not happen? (Could also use your flatten Util here)
There was a problem hiding this comment.
Since we should load all entities a few lines above
There was a problem hiding this comment.
Yes but lets say, "idsNotInMemory" is [1, 2, 3] and we batch load those ids. "res" could be [{id: 1, ..}, {id: 3, ..}] for example skipping values that didn't exist in the db.
Then when you iterate over that they don't get initialized in the in memory store.
There was a problem hiding this comment.
So I guess for this assumption to be correct you need to initialize all values that weren't returned from the DB as NotSet or something
There was a problem hiding this comment.
Hm, sounds like something to improve in the future.
Co-authored-by: Jono Prest <65739024+JonoPrest@users.noreply.github.com>
|
Hey @DZakh, I like the improvements and moving to static but looks like we still need to solve for the regression of adding round trips to the DB when they have already been requested in loaders. Maybe as a suggestion instead of "idsNotInMemory" being an array. Make it a set, and on the response remove ids from the set as you initialize values. Then with the remaining set items initialize them as NotSet? |
No description provided.