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

Optimize APT FA transactions - gas charging and transfering #13194

Merged
merged 9 commits into from
May 28, 2024

Conversation

igor-aptos
Copy link
Contributor

@igor-aptos igor-aptos commented May 3, 2024

Description

  • optimize apt PFS operations by specializing them in primary_fungible_store, and shortcircuiting unnecessary checks (owner/frozen/dispatchable/burned)
  • Add feature flags to default to FA for new accounts, and use exclusively FA (i.e. for apt transfers/gas charging). these not yet ready to be enabled on devnet, but we can for some tests.
  • add to genesis to create the mapping (i.e. so it's on on devnet/forge). migrate root account to have FA - as nobody will look at it in the explorer, but useful to test e2e
  • make concurrent supply default

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Performance improvement
  • Refactoring
  • Dependency update
  • Documentation update
  • Tests

Which Components or Systems Does This Change Impact?

  • Validator Node
  • Full Node (API, Indexer, etc.)
  • Move/Aptos Virtual Machine
  • Aptos Framework
  • Aptos CLI/SDK
  • Developer Infrastructure
  • Other (specify)

How Has This Been Tested?

Key Areas to Review

Checklist

  • I have read and followed the CONTRIBUTING doc
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I identified and added all stakeholders and component owners affected by this change as reviewers
  • I tested both happy and unhappy path of the functionality
  • I have made corresponding changes to the documentation

Copy link

trunk-io bot commented May 3, 2024

⏱️ 6h 37m total CI duration on this PR
Job Cumulative Duration Recent Runs
rust-targeted-unit-tests 2h 22m 🟥🟥🟥🟥🟥
rust-move-unit-coverage 1h 44m 🟩🟩🟩🟩🟩
rust-move-tests 1h 8m 🟥🟥🟥🟥🟥
rust-lints 39m 🟩🟥🟩🟩🟩
run-tests-main-branch 25m 🟩🟩🟩🟩🟩 (+1 more)
general-lints 8m 🟩🟩🟩🟩🟩
check-dynamic-deps 6m 🟩🟩🟩🟩🟩
semgrep/ci 2m 🟩🟩🟩🟩🟩
file_change_determinator 58s 🟩🟩🟩🟩🟩
file_change_determinator 47s 🟩🟩🟩🟩🟩
permission-check 17s 🟩🟩🟩🟩🟩
permission-check 16s 🟩🟩🟩🟩🟩
permission-check 15s 🟩🟩🟩🟩🟩
permission-check 14s 🟩🟩🟩🟩🟩

🚨 2 jobs on the last run were significantly faster/slower than expected

Job Duration vs 7d avg Delta
rust-targeted-unit-tests 34m 19m +79%
rust-lints 10m 9m +21%

settingsfeedbackdocs ⋅ learn more about trunk.io

Copy link

codecov bot commented May 3, 2024

Codecov Report

Attention: Patch coverage is 0% with 28 lines in your changes are missing coverage. Please review.

Project coverage is 33.1%. Comparing base (270917f) to head (305937c).
Report is 2 commits behind head on main.

Files Patch % Lines
...cached-packages/src/aptos_framework_sdk_builder.rs 0.0% 26 Missing ⚠️
crates/transaction-emitter-lib/src/emitter/mod.rs 0.0% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             main   #13194     +/-   ##
=========================================
- Coverage    33.1%    33.1%   -0.1%     
=========================================
  Files        1753     1753             
  Lines      337763   337795     +32     
=========================================
- Hits       111995   111964     -31     
- Misses     225768   225831     +63     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -478,6 +478,20 @@ module aptos_framework::coin {
(option::extract(burn_ref_opt), BurnRefReceipt { metadata })
}

// Permanently convert to BurnRef, and take it from the pairing.
// (i.e. future calls to borrow/convert BurnRef will fail)
public fun convert_and_take_paired_burn_ref<CoinType>(
Copy link
Contributor

Choose a reason for hiding this comment

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

cc @lightmark. This might not make sense here before the end of the migration, as taking BurnRef means that the conversion for migration no longer works

Copy link
Contributor Author

Choose a reason for hiding this comment

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

conversion doesn't use BurnRef, but uses fungible_asset::burn_internal

This prevents calling get_paired_burn_ref afterwards. I am not sure if we can clone BurnRef somehow, and return it and keep it in pairing as well.

Also I am not sure if caller can have multiple BurnCapability objects, that each needs to be converted.

@lightmark , thoughts?

For APT, we call this only after features::operations_default_to_fa_apt_store() flag has been set, which will not be set until there are no more APT CoinStores

Copy link
Contributor

Choose a reason for hiding this comment

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

Refs do not allow clone/copy, so that why we use hot potato pattern here. As @igor-aptos said, the conversion does not need this for better perf so you can take Refs out anytime. The issue is this is a one-time call since there is only one copy of each ref but multiple for caps.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lightmark does framework have multiple caps? If so - how are we going to convert them to Refs? do we need to actually clone/copy here with some friend function?

Copy link
Contributor

Choose a reason for hiding this comment

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

We have two mint caps...lemme think about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we cannot generate refs... mint cap should be stored in stake.move and burn is in transaction_fee.move ( transaction_fee is a friend of stake)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok cool, so for this PR we are good (it is burn ref we are touching).
for transforming mint ref, we need to consolidate transaction_fee and stake modules

store_exists_inline(store)
}

fun store_exists_inline(store: address): bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

argh it's missing inline, but intention was for cheaper (and lower) gas costs

Copy link
Contributor

Choose a reason for hiding this comment

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

To save one function call? I'm not sure if it's worth making the code longer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

store_exists_inline is a single operation - so inling leaves the byte representation the same. If you prefer - I could just not use store_exists in this file and do "exists<>" itself throughout.

}
}

inline fun apt_ensure_primary_store_exists(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this inline? A long inline function used multiple times = bloating up the bytecode

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 made not huge private functions inline, to make things faster/cheaper, and was suggested to inline more things on other PRs.
What's the threshold for "function being too long for inline" in your view? here it is like if + 4 function calls, and below one is if + 2 function calls

Copy link
Contributor

@alnoki alnoki May 4, 2024

Choose a reason for hiding this comment

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

bloating up the bytecode

@movekevin why is this a problem? The Aptos Framework does not face the same package size constraints as dApps, so what is the concern with trading off more bytecode size for less execution gas?

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 not but I'd rather not bloat the bytecode if possible. Also people look at the framework for examples

Copy link
Contributor

Choose a reason for hiding this comment

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

bloat the bytecode

@movekevin it it appropriate to consider increased bytecode size "bloat" if the bytecode size is a function of reducing gas size?

What is the problem with people looking at the framework for an example of gas-optimized code, especially for critical execution paths?

You can always add in a func comment explaining that things are inlined for the sake of performance

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#13194 (comment)

and in general - inline is always done for performance, there is no other reasons to inline.

if (!primary_store_exists(owner, metadata)) {
let store_addr = primary_store_address(owner, metadata);
if (fungible_asset::store_exists(store_addr)) {
object::address_to_object<FungibleStore>(store_addr)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: < FungibleStore > is not needed

@@ -132,6 +147,31 @@ module aptos_framework::primary_fungible_store {
}
}

inline fun apt_store_address(account: address): address {
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 here - why is this inline?

}
}

public(friend) fun is_apt_balance_at_least(account: address, amount: u64): bool {
Copy link
Contributor

@movekevin movekevin May 3, 2024

Choose a reason for hiding this comment

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

Doesn't feel like any apt-specific logic should be in PFS as it should remain generic. Can this be in aptos_account?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we are specializing APT PFS to be more efficient, so these are all variants of the same functions defined in this file. (so is easier to look and see)

but I can create aptos_primary_fungible_store.move if you prefer to move them out

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moves to aptos_primary_fungible_store.move.

let me know if apt_primary_fungible_store name (i.e. aptos -> apt) makes more sense

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah apt makes more sense

Comment on lines 50 to 54
// use internal APIs, as they skip:
// - owner, frozen and dispatchable checks
// as APT cannot be frozen or have dispatch, and PFS cannot be transfered
// (PFS could potentially be burned. regular transfer would permanently unburn the store.
// Ignoring the check here has the equivalent of unburning, transfers, and then burning again)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

to reviewers - want to call this out, as this biggest not-purely-optimization change, but a logical change in which invariants are checked.

@igor-aptos
Copy link
Contributor Author

chatted with @movekevin offline - will measure what inline brings, but if it has measurable perf improvement, there are no concerns of using inlining for these short functions.

Copy link
Contributor

@lightmark lightmark left a comment

Choose a reason for hiding this comment

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

Object is really annoying sometimes

}
}

public entry fun transfer(
Copy link
Contributor

Choose a reason for hiding this comment

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

My hunch is we should make all the functions in this module public(friend) for internal use only.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

how should you transfer APT efficiently without wanting to create full account?

only aptos_account::transfer and primary_fungible_store::transfer are options, first one creates account, second one is much less performant.

coin::register<AptosCoin>(&signer);
let account_signer = account::create_account(auth_key);
if (features::new_accounts_default_to_fa_apt_store_enabled()) {
apt_primary_fungible_store::ensure_primary_store_exists(signer::address_of(&account_signer));
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

your coin.move functions, to keep same invariants, fail if target doesn't have neither coin store nor PFS

so this is required, if people use coin module to transfer APT

basically after I create an account, I should be able to receive APT

@@ -478,6 +478,20 @@ module aptos_framework::coin {
(option::extract(burn_ref_opt), BurnRefReceipt { metadata })
}

// Permanently convert to BurnRef, and take it from the pairing.
// (i.e. future calls to borrow/convert BurnRef will fail)
public fun convert_and_take_paired_burn_ref<CoinType>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Refs do not allow clone/copy, so that why we use hot potato pattern here. As @igor-aptos said, the conversion does not need this for better perf so you can take Refs out anytime. The issue is this is a one-time call since there is only one copy of each ref but multiple for caps.

&borrow_global<AptosCoinCapabilities>(@aptos_framework).burn_cap,
);
public(friend) fun burn_fee(account: address, fee: u64) acquires AptosFABurnCapabilities, AptosCoinCapabilities {
if (exists<AptosFABurnCapabilities>(@aptos_framework)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

after migration is done, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AptosFABurnCapabilities can be created only after migration is done.

if AptosFABurnCapabilities exists, other one doesn't, and so this is the only way to charge gas from that point

Comment on lines 218 to 220
let (burn_ref, burn_receipt) = coin::get_paired_burn_ref(burn_cap);
apt_primary_fungible_store::burn_from(&burn_ref, account, fee);
coin::return_paired_burn_ref(burn_ref, burn_receipt);
Copy link
Contributor

Choose a reason for hiding this comment

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

perf concering?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this will be slow, but we should convert to AptosFABurnCapabilities after nobody has CoinStore, and then this path will not be executed

@igor-aptos igor-aptos force-pushed the igor/optimize_fa_apt branch 2 times, most recently from ae06727 to b2a904d Compare May 28, 2024 06:54
@igor-aptos igor-aptos enabled auto-merge (squash) May 28, 2024 15:19

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Copy link
Contributor

✅ Forge suite compat success on 3ffe0986b5fe4acb76544ae7ae85d73b91a6a411 ==> 305937c24f6fc3f5cde35045da6715b0d7616dbb

Compatibility test results for 3ffe0986b5fe4acb76544ae7ae85d73b91a6a411 ==> 305937c24f6fc3f5cde35045da6715b0d7616dbb (PR)
1. Check liveness of validators at old version: 3ffe0986b5fe4acb76544ae7ae85d73b91a6a411
compatibility::simple-validator-upgrade::liveness-check : committed: 6532.268900886151 txn/s, latency: 4687.347972434672 ms, (p50: 4800 ms, p90: 5300 ms, p99: 7800 ms), latency samples: 251040
2. Upgrading first Validator to new version: 305937c24f6fc3f5cde35045da6715b0d7616dbb
compatibility::simple-validator-upgrade::single-validator-upgrade : committed: 3325.7361729596823 txn/s, latency: 9338.678352873892 ms, (p50: 9500 ms, p90: 13900 ms, p99: 14500 ms), latency samples: 139880
3. Upgrading rest of first batch to new version: 305937c24f6fc3f5cde35045da6715b0d7616dbb
compatibility::simple-validator-upgrade::half-validator-upgrade : committed: 3348.3818270570355 txn/s, latency: 9228.036518380775 ms, (p50: 9300 ms, p90: 13900 ms, p99: 14400 ms), latency samples: 139820
4. upgrading second batch to new version: 305937c24f6fc3f5cde35045da6715b0d7616dbb
compatibility::simple-validator-upgrade::rest-validator-upgrade : committed: 6359.512078262484 txn/s, latency: 5132.229266226063 ms, (p50: 4800 ms, p90: 8400 ms, p99: 9600 ms), latency samples: 236040
5. check swarm health
Compatibility test for 3ffe0986b5fe4acb76544ae7ae85d73b91a6a411 ==> 305937c24f6fc3f5cde35045da6715b0d7616dbb passed
Test Ok

Copy link
Contributor

✅ Forge suite realistic_env_max_load success on 305937c24f6fc3f5cde35045da6715b0d7616dbb

two traffics test: inner traffic : committed: 8195.109241869624 txn/s, latency: 4784.082090091514 ms, (p50: 4800 ms, p90: 6000 ms, p99: 10200 ms), latency samples: 3542620
two traffics test : committed: 100.05585950337796 txn/s, latency: 1816.5713483146067 ms, (p50: 1800 ms, p90: 2000 ms, p99: 2400 ms), latency samples: 1780
Latency breakdown for phase 0: ["QsBatchToPos: max: 0.218, avg: 0.205", "QsPosToProposal: max: 0.258, avg: 0.240", "ConsensusProposalToOrdered: max: 0.447, avg: 0.411", "ConsensusOrderedToCommit: max: 0.370, avg: 0.356", "ConsensusProposalToCommit: max: 0.779, avg: 0.767"]
Max round gap was 1 [limit 4] at version 1604050. Max no progress secs was 4.871368 [limit 15] at version 1604050.
Test Ok

Copy link
Contributor

✅ Forge suite framework_upgrade success on 3ffe0986b5fe4acb76544ae7ae85d73b91a6a411 ==> 305937c24f6fc3f5cde35045da6715b0d7616dbb

Compatibility test results for 3ffe0986b5fe4acb76544ae7ae85d73b91a6a411 ==> 305937c24f6fc3f5cde35045da6715b0d7616dbb (PR)
Upgrade the nodes to version: 305937c24f6fc3f5cde35045da6715b0d7616dbb
framework_upgrade::framework-upgrade::full-framework-upgrade : committed: 1235.6989206824774 txn/s, submitted: 1237.2945474016763 txn/s, failed submission: 1.5956267191989193 txn/s, expired: 1.5956267191989193 txn/s, latency: 2669.8135491606713 ms, (p50: 2100 ms, p90: 4500 ms, p99: 9600 ms), latency samples: 108420
framework_upgrade::framework-upgrade::full-framework-upgrade : committed: 1170.799721686851 txn/s, submitted: 1172.2281068698767 txn/s, failed submission: 1.428385183025845 txn/s, expired: 1.428385183025845 txn/s, latency: 2739.6702826352175 ms, (p50: 2100 ms, p90: 4800 ms, p99: 9500 ms), latency samples: 98360
5. check swarm health
Compatibility test for 3ffe0986b5fe4acb76544ae7ae85d73b91a6a411 ==> 305937c24f6fc3f5cde35045da6715b0d7616dbb passed
Upgrade the remaining nodes to version: 305937c24f6fc3f5cde35045da6715b0d7616dbb
framework_upgrade::framework-upgrade::full-framework-upgrade : committed: 1266.493918226074 txn/s, submitted: 1269.0468654682377 txn/s, failed submission: 2.552947242163609 txn/s, expired: 2.552947242163609 txn/s, latency: 2629.498772219168 ms, (p50: 2100 ms, p90: 4500 ms, p99: 10400 ms), latency samples: 109140
Test Ok

@igor-aptos igor-aptos merged commit d972e92 into main May 28, 2024
53 of 54 checks passed
@igor-aptos igor-aptos deleted the igor/optimize_fa_apt branch May 28, 2024 18:03
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.

None yet

6 participants