Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Fix slow balances #6471

Merged
merged 40 commits into from
Sep 10, 2017
Merged

Fix slow balances #6471

merged 40 commits into from
Sep 10, 2017

Conversation

ngotchac
Copy link
Contributor

@ngotchac ngotchac commented Sep 5, 2017

Better fetching of the token balances.

  1. The UI now makes one call for about 64 balances updates, thanks to this https://github.com/paritytech/contracts/pull/76
  2. It caches the tokens info it got from the registry (only the META data can be changed, ie. the images)
  3. A bunch of fixes on how the balances are retrieved.

@ngotchac ngotchac added A0-pleasereview 🤓 Pull request needs code review. M7-ui labels Sep 5, 2017
@tomusdrw tomusdrw added M4-core ⛓ Core client code / Rust. A3-inprogress ⏳ Pull request is in progress. No review needed at this stage. and removed A0-pleasereview 🤓 Pull request needs code review. labels Sep 5, 2017
@tomusdrw tomusdrw added A0-pleasereview 🤓 Pull request needs code review. and removed A3-inprogress ⏳ Pull request is in progress. No review needed at this stage. labels Sep 5, 2017
const { data, to } = call.params[0];

if (!contracts[to]) {
contracts[to] = [];
Copy link
Collaborator

Choose a reason for hiding this comment

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

contacts[to] = contracts[to] || [];


const progress = Math.round(calls.length / 20);

calls.forEach((call, index) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I would prefer reduce to altering the promise

get methods () {
const methods = this.logs.reduce((methods, log) => {
if (!methods[log.method]) {
methods[log.method] = [];
Copy link
Collaborator

Choose a reason for hiding this comment

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

methods[log.method] = methods[log.method] || [];

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how this would be better?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Single line instead of 3 = less visual clutter and it's also a common pattern in JS

}

get methods () {
const methods = this.logs.reduce((methods, log) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can be just returned directly.

@@ -87,6 +87,7 @@ export default class TransferStore {
this.newError = newError;
this.tokens = tokens;

console.log(gasLimit);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should be removed.

return { to: tokenReg.address, data: metaCalldata };
});

const calls = requests.map((req) => api.eth.call(req));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Fetching all images and all info looks a bit heavy.

I think we should further optimize it and only fetch the info/images of tokens that are actually displayed (but in a separate PR)

Copy link
Contributor Author

@ngotchac ngotchac Sep 7, 2017

Choose a reason for hiding this comment

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

That's actually what's implemented ! :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, can't see where do we have it.
we take allTokens from state and then we divide them between the ones that are fully fetched and the ones that need partially fetched. And we get the images for the latter ones.
What I miss is where we only take tokens that user has in her account (i.e are actually displayed).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So it's in /src/redux/providers/balancesActions.js > fetchTokensBalances : the fetchTokens method is called only for the tokens we don't have meta-data/name yet.

loadTokensBasics in tokensActions is the method that is called on load, and that fetches only the tokens addresses. When the token cache is already filled, it actually only updates the images that might have changed since the last load.


// Updates for the ETH balances
const ethUpdates = accountAddresses
.map((accountAddress) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

map/reduce here seems overcomplicated, tokenIds will always have either 0 or 1 element if I'm not mistaken:

const ethUpdates = accountAddresses
   // only accounts fetching eth 
  .filter(address => updates[address].find(tokenId => ETH_TOKEN.id))
  .reduce((nextUpdates, address) => {
     nextUpdates[address] = [ETH_TOKEN.id];
   }, {});

// Updates for Tokens balances
const tokenUpdates = Object.keys(updates)
.map((accountAddress) => {
return updates[accountAddress].filter((tokenId) => tokenId !== ETH_TOKEN.id);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This could be simplified as well:

const tokenUpdates = Object.keys(updates).reduce((nextUpdates, address) => {
   const tokens = updates[address].filter(tokenId => tokenId !== ETH_TOKEN.id);
   if (tokens.length) {
      nextUpdates[address] = tokens;
   }
   return nextUpdates;
}, {});


const ethPromise = fetchEthBalances(api, Object.keys(ethUpdates))
.then((_ethBalances) => {
ethBalances = _ethBalances;
Copy link
Collaborator

Choose a reason for hiding this comment

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

const ethPromise = fetchEthBalances(api, ...);
const tokenPromise = Object.keys(tokenUpdates).reduce((promise, address) => {
  return promise.then(balances => {
     return fetchTokensBalances(api, updateTokens, ...).then(newBalances => {
       balances[address] = newBalances[address];
       return balances;
     });
  });
}, Promise.resolve({}));

return Promise.all([ethPromise, tokenPromise]).then(([ethBalances, tokensBalances]) => {
 ...
});

Copy link
Collaborator

Choose a reason for hiding this comment

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

Now that I wrote the code I think there is a bit of redundancy in calling fetchTokensBalances:

Why can't we do a single query for all tokens seen in any account and all accounts at once?
Something like:

const updateTokens = uniq(Object.values(tokenUpdates).reduce((all, ids) => all.concat(ids), []));
const accounts = Object.keys(tokenUpdates);
const tokenPromise = fetchTokensBalances(api, updateTokens, accounts);

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So it's because not all accounts need to update the same tokens.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, but I'm wondering if a single call would not be more performant for users that more than one account and cause less convoluted code - I don't like that we use some scope variables to pass asynchronous results, it's really hard to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I agree that it's hard to read, and the whole thing could be simplified if we didn't really care about having a fine-tuned fetching. But I guess the scope of the PR was to be able to make just the right number of calls.


return Promise.all(promises)
.then((balancesArray) => {
return balancesArray.reduce((balances, balance, index) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the future it might actually be better to display balances as they come instead of waiting for all of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, right now it's first the ETH balances, then tokens. But with Redux, the more we dispatch a new state, the more it re-renders a lot of Components...

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is that? It should only re-render the components that use the updated state, no? Maybe we should focus on improving performance of re-renders?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I spoke/wrote too fast. It shouldn't re-render components, but will run the dispatcher methods and compare the results for every rendered components, so it's anyway better to do it once than multiple times.

@gavofyork
Copy link
Contributor

is this still polling on the JS side? what about using filters?

@tomusdrw
Copy link
Collaborator

tomusdrw commented Sep 6, 2017

  1. It uses filter for transfer even on any non-zero address token in the registry.
  2. When the event is received we check if to or from is one of our accounts
  3. If it is we refresh a balance of this particular token for affected accounts.

Another filter is set up to monitor the token registry and update the other filters if there are any changes there.

The main issue was with initial fetch that now is working much better:

  1. The token ids are cached (so we don't need to query the token registry 600 times just to get the addresses)
  2. Initial balances are fetched in chunks for all accounts - for each chunk of 64 tokens we produce eth_call that is able to fetch balances for all accounts. (Fetching all tokens at once takes too long and causes noticable lag).
  3. The rest of polling methods is now using parity_pubsub, so we don't need to periodicaly check for:
  • best block
  • pending transactions (signer)
  • nodeHealth
  • peers
  • syncStatus

That's from the top of my head, @ngotchac correct me if I'm wrong on anything.

@ngotchac
Copy link
Contributor Author

ngotchac commented Sep 7, 2017

Grumbles should be addressed!

Copy link
Collaborator

@tomusdrw tomusdrw left a comment

Choose a reason for hiding this comment

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

Some additional questions, but LGTM.

And the UX difference is yuge.

.filter((t) => tokenAddresses.includes(t.address));
Object.keys(updates).forEach((who) => {
// Keep non-empty token addresses
updates[who] = uniq(updates[who]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see. Fair enough.

return { to: tokenReg.address, data: metaCalldata };
});

const calls = requests.map((req) => api.eth.call(req));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, can't see where do we have it.
we take allTokens from state and then we divide them between the ones that are fully fetched and the ones that need partially fetched. And we get the images for the latter ones.
What I miss is where we only take tokens that user has in her account (i.e are actually displayed).


const ethPromise = fetchEthBalances(api, Object.keys(ethUpdates))
.then((_ethBalances) => {
ethBalances = _ethBalances;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, but I'm wondering if a single call would not be more performant for users that more than one account and cause less convoluted code - I don't like that we use some scope variables to pass asynchronous results, it's really hard to read.


return Promise.all(promises)
.then((balancesArray) => {
return balancesArray.reduce((balances, balance, index) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is that? It should only re-render the components that use the updated state, no? Maybe we should focus on improving performance of re-renders?

@tomusdrw tomusdrw added A6-mustntgrumble 💦 Pull request has areas for improvement. The author need not address them before merging. and removed A0-pleasereview 🤓 Pull request needs code review. labels Sep 8, 2017
@gavofyork gavofyork merged commit f1a0503 into master Sep 10, 2017
@gavofyork gavofyork deleted the ng-update-token-updates branch September 10, 2017 16:03
tomusdrw pushed a commit that referenced this pull request Sep 11, 2017
* Update token updates

* Update token info fetching

* Update logger

* Minor fixes to updates and notifications for balances

* Use Pubsub

* Fix timeout.

* Use pubsub for status.

* Fix signer subscription.

* Process tokens in chunks.

* Fix tokens loaded by chunks

* Linting

* Dispatch tokens asap

* Fix chunks processing.

* Better filter options

* Parallel log fetching.

* Fix signer polling.

* Fix initial block query.

* Token balances updates : the right(er) way

* Better tokens info fetching

* Fixes in token data fetching

* Only fetch what's needed (tokens)

* Fix linting issues

* Revert "Transaction permissioning (#6441)"

This reverts commit eed0e8b.

* Revert "Revert "Transaction permissioning (#6441)""

This reverts commit 8f96415.

* Update wasm-tests.

* Fixing balances fetching

* Fix requests tracking in UI

* Fix request watching

* Update the Logger

* PR Grumbles Fixes

* PR Grumbles fixes

* Linting...
gavofyork pushed a commit that referenced this pull request Sep 11, 2017
* Fix slow balances (#6471)

* Update token updates

* Update token info fetching

* Update logger

* Minor fixes to updates and notifications for balances

* Use Pubsub

* Fix timeout.

* Use pubsub for status.

* Fix signer subscription.

* Process tokens in chunks.

* Fix tokens loaded by chunks

* Linting

* Dispatch tokens asap

* Fix chunks processing.

* Better filter options

* Parallel log fetching.

* Fix signer polling.

* Fix initial block query.

* Token balances updates : the right(er) way

* Better tokens info fetching

* Fixes in token data fetching

* Only fetch what's needed (tokens)

* Fix linting issues

* Revert "Transaction permissioning (#6441)"

This reverts commit eed0e8b.

* Revert "Revert "Transaction permissioning (#6441)""

This reverts commit 8f96415.

* Update wasm-tests.

* Fixing balances fetching

* Fix requests tracking in UI

* Fix request watching

* Update the Logger

* PR Grumbles Fixes

* PR Grumbles fixes

* Linting...

* eth_call returns output of contract creations (#6420)

* eth_call returns output of contract creations

* Fix parameters order.

* Save outputs for light client as well.

* Don't accept transactions above block gas limit.

* Expose health status over RPC (#6274)

* Node-health to a separate crate.

* Initialize node_health outside of dapps.

* Expose health over RPC.

* Bring back 412 and fix JS.

* Add health to workspace and tests.

* Fix compilation without default features.

* Fix borked merge.

* Revert to generics to avoid virtual calls.

* Fix node-health tests.

* Add missing trailing comma.

* Fixing/removing failing JS tests.

* do not activate genesis epoch in immediate transition validator contract (#6349)

* Fix memory tracing.

* Add test to cover that.

* ensure balances of constructor accounts are kept

* test balance of spec-constructed account is kept
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A6-mustntgrumble 💦 Pull request has areas for improvement. The author need not address them before merging. M4-core ⛓ Core client code / Rust.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants