Skip to content

Perf optimisations#98

Merged
DZakh merged 8 commits into
mainfrom
dz/perf-optimisations
Aug 1, 2024
Merged

Perf optimisations#98
DZakh merged 8 commits into
mainfrom
dz/perf-optimisations

Conversation

@DZakh

@DZakh DZakh commented Jul 31, 2024

Copy link
Copy Markdown
Member

No description provided.

Comment on lines 71 to 87
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))
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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:

image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nice!

Comment on lines -158 to +136
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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>)

@JonoPrest JonoPrest Jul 31, 2024

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not simply do this? Are you trying to reduce overhead with the magic?

Suggested change
| Error(_) as err => err->(magic: result<'a, 'b> => result<array<'a>, 'b>)
| Error(e) => Error(e)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Comment on lines -158 to -165
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 JonoPrest left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the util optimization! But we shouldn't remove the other code without another solution 👍🏼

Comment on lines +196 to +203
// 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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@DZakh DZakh requested a review from JonoPrest July 31, 2024 08:37
Comment thread codegenerator/cli/templates/static/codegen/src/InMemoryTable.res Outdated
| Some(entity) => resolve(Some(entity))
| None => resolve(None)
| Some(maybeEntity) => resolve(maybeEntity)
| None => resolve(None) // Shouldn't happen

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should this not happen? (Could also use your flatten Util here)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we should load all entities a few lines above

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, sounds like something to improve in the future.

DZakh and others added 2 commits July 31, 2024 13:00
Co-authored-by: Jono Prest <65739024+JonoPrest@users.noreply.github.com>
@DZakh DZakh requested a review from JonoPrest July 31, 2024 09:27
@JonoPrest

Copy link
Copy Markdown
Collaborator

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?

@JonoPrest JonoPrest left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome stuff @DZakh looks great 👍🏼

@DZakh DZakh merged commit 27566cc into main Aug 1, 2024
@DZakh DZakh deleted the dz/perf-optimisations branch August 1, 2024 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants