Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step 6: kernel integration (process fvm transactions in bulk) #795

Merged
merged 5 commits into from
Jan 29, 2019

Conversation

aionick
Copy link
Contributor

@aionick aionick commented Jan 28, 2019

The Fast VM now processes transactions in bulk.

The commit that introduces the bulk processing logic is 382c96a

The remaining commits relate to a bug fix. Essentially, the contract details were not being deeply copied when one repository flushed to another, which allows for a critical object reference to leak across sibling repositories. A deep copy is now being performed, and some tests added.

The option to flush a deep copy as opposed to the regular flush is separate for now because it comes with a performance penalty, of course, and because it is only required when a sibling that wants to hold on to its state flushes. This happens only at one particular point, in the FastVirtualMachine class.

See here for the FVM changes.

- That is, the AionBlockchainImpl class is now sending its transactions
  in bulk into the executor.
- AionRepositoryCache now has a flushCopiesTo() method that allows for
  'sufficiently deep' (this term is defined in the method documentation)
  copies of the state of the repository to be flushed to its target.
- This method is essential for when a repository will flush to a parent
  that will create further children before the current repository will
  fully flush its changes all the way to the database.
- The AionRepositoryCache.flushCopiesTo() method that was introduced in
  the previous commit is now tested here.
- The critical flushing point in the FastVirtualMachine now uses the
  flushCopiesTo() method to ensure that copies of the state are flushed
  to the parent instead of the original references (in certain cases).
- The bridge contract was not re-aligning its storage bytes to be
  16 or 32 bytes long, which is now its responsibility. This is just
  a simple change to make that happen.
@aionick aionick added this to the 0.3.3 milestone Jan 28, 2019
@aionick aionick changed the title Step 7: kernel integration (process fvm transactions in bulk) Step 6: kernel integration (process fvm transactions in bulk) Jan 28, 2019
Copy link
Contributor

@aion-kelvin aion-kelvin left a comment

Choose a reason for hiding this comment

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

I don't see any big problems that should block this from shipping ... but there's a few design decisions that I think should re-visited after we get AVM initially integrated in a functioning way.

@@ -344,4 +345,68 @@ public IContractDetails getSnapshotTo(byte[] hash) {

return details;
}

/**
* Returns a sufficiently deep copy of this contract details object.
Copy link
Contributor

Choose a reason for hiding this comment

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

Hey, so we already talked about this in-person, but just writing it here to keep a record:

I think this copy() implementation is ok for now to just get things working. Some not so nice stuff about it:

  • 'sufficiently deep copy' is a bit nebulous (hard to understand intent)
  • the work of copying primitives, calling clones/copy-constructors of classes is repetitive and error-prone, especially when/if the classes inside change over time
  • each cloneable class needs to have its own clone method, with its own implementation

Another way to deep-copy would be to use Java's Serializable interface to serialize an object to bytes, then create a new object, its clone, from those bytes. Benefits if we apply this to all cloneable classes:

  • instead of using the 'sufficiently deep' cloning definition, it is clear what gets serialized, because that's dictated by Java's serialize mechanism (unless overridden), and that will be consistent across all classes we need to clone/serialize
  • don't need to manually write clone code for all cloneable things (less likely to make "copied reference instead of cloned" mistakes)
  • (maybe) faster (according to a source from 11 years ago)
  • seems like other people do this too, as the Apache Commons library has a clone() utility method specifically for this purpose: https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/SerializationUtils.html#clone-T-

It is probably too much work to change as part of this PR, but I think it's worthwhile to consider for how we want to do cloning in our code for the future.

// determine which accounts should get stored
HashMap<Address, AccountState> cleanedCacheAccounts = new HashMap<>();
for (Map.Entry<Address, AccountState> entry : cachedAccounts.entrySet()) {
AccountState account = entry.getValue().copy();
Copy link
Contributor

Choose a reason for hiding this comment

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

can entry.getValue return null?

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'd prefer to make as few assumptions as possible, so good catch, I'll fix this

HashMap<Address, AccountState> cleanedCacheAccounts = new HashMap<>();
for (Map.Entry<Address, AccountState> entry : cachedAccounts.entrySet()) {
AccountState account = entry.getValue().copy();
if (account != null && account.isDirty() && account.isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

account won't be null unless entry.getValue().copy() returns null, which would be weird

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'll fix that up to test out the null on entry.getValue()

}
// determine which contracts should get stored
for (Map.Entry<Address, IContractDetails> entry : cachedDetails.entrySet()) {
IContractDetails ctd = entry.getValue().copy();
Copy link
Contributor

Choose a reason for hiding this comment

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

same question as above, but this time about getValue/null and ctd != null

* This "work" is specific to the {@link AionBlockchainImpl#applyBlock(IAionBlock)} method.
*/
private static PostExecutionWork getPostExecutionWorkForApplyBlock() {
return (topRepository,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could also do this to avoid constantly creating objects for the lambda every time this is called:

  • Make an instance of PostExecutionWork
  • Your function goes into PostExecutinWork instead of in that lambda
  • This method returns that instance all the time

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's true, though a stateless lambda is detected in the invokedynamic bootstrap phase and the exact same instance is inlined each time, it won't get created anew. It's actually faster or equivalent to go with a stateless lambda than an inner/anonymous class


secureTrieCopy.setPrevRoot(previousRootCopy);
secureTrieCopy.setRoot(rootCopy);
secureTrieCopy.setCache(super.getCache().copy());
Copy link
Contributor

@aion-kelvin aion-kelvin Jan 28, 2019

Choose a reason for hiding this comment

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

seems kind of odd to clone a cache (i'd expect to leave it empty and let it get filled up naturally).. since this code already works, I don't have a problem with it. it might be worth looking into later if/when we try to optimize performance (it wouldn't be very productive to clone to cache if the user of the cloned cache just immediately accesses only values not in the cache and everything gets evicted shortly after the cache has been cloned)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's a good point, I wasn't too sure about this one, but wanted to err on the side of caution since I didn't understand all the implications of the trie, but this decision should probably be confirmed at some time

Copy link
Collaborator

@AionJayT AionJayT left a comment

Choose a reason for hiding this comment

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

LGTM.
However, after the integration is done. Need to do some performance analysis between VM and DB.
I doubt the integration actually make the DB performance slower cause a lot of flush and copy happening.

return (r, c, s, t, b) -> {
return 0L;
Copy link
Contributor

Choose a reason for hiding this comment

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

it's weird that the 0L was changes here, but not below


ExecutionBatch batch = new ExecutionBatch(block, block.getTransactionsList());
BulkExecutor executor =
new BulkExecutor(
Copy link
Contributor

Choose a reason for hiding this comment

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

incorrect formatting

@aionick aionick changed the base branch from incremental-pr5 to master-pre-merge January 29, 2019 16:25
@AlexandraRoatis AlexandraRoatis merged commit 9261224 into master-pre-merge Jan 29, 2019
@AlexandraRoatis AlexandraRoatis deleted the bulk-executor-pr branch February 11, 2019 19:04
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.

4 participants