Skip to content

Commit b75f193

Browse files
authored
Reuse single shared allocation for ABI data (#970)
Currently whenever you create a new instance of a contract the entire `Abi` gets cloned (which can be really huge) plus a bunch of signatures get computed (a lot of hashing) and cached. This is really costly considering that the `Abi` of a contract never changes so it would be an easy win to share the same allocation of those immutable pieces of data (`Abi` and computed hashes) across all instances of the same contract. This would make the creation of subsequent instances of the same contract mostly free. This can already be achieved with ugly workarounds discussed and implemented [here](cowprotocol/services#2628) but if this issue gets resolved in the library itself manual allocation optimization will not be needed and the performance improvement for any code that creates a lot of contract instances would be huge. The actual change is overall pretty small. I created a new type `Interface` that contains all the immutable data derived from the `Abi`. Then I gave it custom `(De)Serialize` implementations which mostly just do the same thing it did before (defer to `Abi`'s implementation). The `Deserialize` implementation of course also computes the important hash and stores it's values together with the `Abi`. Because the typesafe rust binding code generated by `ethcontract-rs` already stores the raw contract in a static allocation (which now contains the relevant immutable state `Arc`ed) we now get cheap contract instantiations. Thanks @nlordell for the idea of fixing the performance issue directly in the library. 👍 The code also has a few unrelated changes to make the `nightly` CI build pass. ### Breaking changes * `Artifact::insert` can now run into annoying borrow checker issues (that can be worked around, though) * `Contract:abi` is now accessible with `Contract::interface::abi` * `Contract::interface::abi` is harder to mutate than `Contract::abi` due to the introduction of the `Arc` Overall I don't know how many people actually rely on this crate and whether or not these breaking changes would be a big deal for them. But even if this should not get merged into the main release of the crate I think this patch is still useful for anybody who needs to optimize performance across the board. ### Test Plan existing tests ran cow protocol backend using the patched library and didn't see any obvious issues
1 parent db4a20f commit b75f193

File tree

21 files changed

+219
-133
lines changed

21 files changed

+219
-133
lines changed

ethcontract-common/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ethcontract-common"
3-
version = "0.25.5"
3+
version = "0.25.6"
44
authors = ["Gnosis developers <[email protected]>"]
55
edition = "2021"
66
license = "MIT OR Apache-2.0"
@@ -14,7 +14,7 @@ Common types for ethcontract-rs runtime and proc macro.
1414
[dependencies]
1515
ethabi = "18.0"
1616
hex = "0.4"
17-
serde = "1.0"
17+
serde= { version = "1.0", features = ["rc"] }
1818
serde_derive = "1.0"
1919
serde_json = "1.0"
2020
thiserror = "1.0"

ethcontract-common/src/artifact.rs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
//! artifact models. It also provides tools to load artifacts from different
99
//! sources, and parse them using different formats.
1010
11-
use crate::contract::{Documentation, Network};
11+
use crate::contract::{Documentation, Interface, Network};
1212
use crate::{Abi, Bytecode, Contract};
1313
use std::collections::hash_map::Entry;
1414
use std::collections::HashMap;
1515
use std::ops::Deref;
16+
use std::sync::Arc;
1617

1718
pub mod hardhat;
1819
pub mod truffle;
@@ -151,7 +152,7 @@ pub struct ContractMut<'a>(&'a mut Contract);
151152
impl<'a> ContractMut<'a> {
152153
/// Returns mutable reference to contract's abi.
153154
pub fn abi_mut(&mut self) -> &mut Abi {
154-
&mut self.0.abi
155+
&mut Arc::make_mut(&mut self.0.interface).abi
155156
}
156157

157158
/// Returns mutable reference to contract's bytecode.
@@ -188,6 +189,18 @@ impl Deref for ContractMut<'_> {
188189
}
189190
}
190191

192+
impl Drop for ContractMut<'_> {
193+
fn drop(&mut self) {
194+
// The ABI might have gotten mutated while this guard was alive.
195+
// Since we compute pre-compute and cache a few values based on the ABI
196+
// as a performance optimization we need to recompute those cached values
197+
// with the new ABI once the user is done updating the mutable contract.
198+
let abi = self.0.interface.abi.clone();
199+
let interface = Interface::from(abi);
200+
*Arc::make_mut(&mut self.0.interface) = interface;
201+
}
202+
}
203+
191204
#[cfg(test)]
192205
mod test {
193206
use super::*;
@@ -204,26 +217,32 @@ mod test {
204217

205218
assert_eq!(artifact.len(), 0);
206219

207-
let insert_res = artifact.insert(make_contract("C1"));
220+
{
221+
let insert_res = artifact.insert(make_contract("C1"));
208222

209-
assert_eq!(insert_res.inserted_contract.name, "C1");
210-
assert!(insert_res.old_contract.is_none());
223+
assert_eq!(insert_res.inserted_contract.name, "C1");
224+
assert!(insert_res.old_contract.is_none());
225+
}
211226

212227
assert_eq!(artifact.len(), 1);
213228
assert!(artifact.contains("C1"));
214229

215-
let insert_res = artifact.insert(make_contract("C2"));
230+
{
231+
let insert_res = artifact.insert(make_contract("C2"));
216232

217-
assert_eq!(insert_res.inserted_contract.name, "C2");
218-
assert!(insert_res.old_contract.is_none());
233+
assert_eq!(insert_res.inserted_contract.name, "C2");
234+
assert!(insert_res.old_contract.is_none());
235+
}
219236

220237
assert_eq!(artifact.len(), 2);
221238
assert!(artifact.contains("C2"));
222239

223-
let insert_res = artifact.insert(make_contract("C1"));
240+
{
241+
let insert_res = artifact.insert(make_contract("C1"));
224242

225-
assert_eq!(insert_res.inserted_contract.name, "C1");
226-
assert!(insert_res.old_contract.is_some());
243+
assert_eq!(insert_res.inserted_contract.name, "C1");
244+
assert!(insert_res.old_contract.is_some());
245+
}
227246

228247
assert_eq!(artifact.len(), 2);
229248
}

ethcontract-common/src/artifact/hardhat.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -426,15 +426,18 @@ impl HardHatLoader {
426426
address: Address,
427427
transaction_hash: Option<TransactionHash>,
428428
) -> Result<(), ArtifactError> {
429-
let mut contract = match artifact.get_mut(&contract.name) {
430-
Some(existing_contract) => {
431-
if existing_contract.abi != contract.abi {
432-
return Err(ArtifactError::AbiMismatch(contract.name));
433-
}
434-
435-
existing_contract
429+
let contract_guard = artifact.get_mut(&contract.name);
430+
let mut contract = if let Some(existing_contract) = contract_guard {
431+
if existing_contract.interface != contract.interface {
432+
return Err(ArtifactError::AbiMismatch(contract.name));
436433
}
437-
None => artifact.insert(contract).inserted_contract,
434+
435+
existing_contract
436+
} else {
437+
// `Drop` of the contract guard can update the underlying contract which can lead
438+
// to borrowing issues. To work around those we manually drop the guard here.
439+
drop(contract_guard);
440+
artifact.insert(contract).inserted_contract
438441
};
439442

440443
let deployment_information = transaction_hash.map(DeploymentInformation::TransactionHash);

ethcontract-common/src/artifact/truffle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ impl TruffleLoader {
141141
let mut contract: Contract = loader(source)?;
142142

143143
if let Some(name) = &self.name {
144-
contract.name = name.clone();
144+
contract.name.clone_from(name);
145145
}
146146

147147
Ok(contract)

ethcontract-common/src/contract.rs

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
//! Module for reading and examining data produced by truffle.
22
3+
use crate::abiext::FunctionExt;
4+
use crate::hash::H32;
35
use crate::Abi;
46
use crate::{bytecode::Bytecode, DeploymentInformation};
7+
use ethabi::ethereum_types::H256;
8+
use serde::Deserializer;
9+
use serde::Serializer;
510
use serde::{Deserialize, Serialize};
6-
use std::collections::HashMap;
11+
use std::collections::{BTreeMap, HashMap};
12+
use std::hash::Hash;
13+
use std::sync::Arc;
714
use web3::types::Address;
815

916
/// Represents a contract data.
@@ -13,8 +20,9 @@ pub struct Contract {
1320
/// The contract name. Unnamed contracts have an empty string as their name.
1421
#[serde(rename = "contractName")]
1522
pub name: String,
16-
/// The contract ABI
17-
pub abi: Abi,
23+
/// The contract interface.
24+
#[serde(rename = "abi")]
25+
pub interface: Arc<Interface>,
1826
/// The contract deployment bytecode.
1927
pub bytecode: Bytecode,
2028
/// The contract's expected deployed bytecode.
@@ -28,6 +36,71 @@ pub struct Contract {
2836
pub userdoc: Documentation,
2937
}
3038

39+
/// Struct representing publicly accessible interface of a smart contract.
40+
#[derive(Clone, Debug, Default, PartialEq)]
41+
pub struct Interface {
42+
/// The contract ABI
43+
pub abi: Abi,
44+
/// A mapping from method signature to a name-index pair for accessing
45+
/// functions in the contract ABI. This is used to avoid allocation when
46+
/// searching for matching functions by signature.
47+
pub methods: HashMap<H32, (String, usize)>,
48+
/// A mapping from event signature to a name-index pair for resolving
49+
/// events in the contract ABI.
50+
pub events: HashMap<H256, (String, usize)>,
51+
}
52+
53+
impl<'de> Deserialize<'de> for Interface {
54+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55+
where
56+
D: Deserializer<'de>,
57+
{
58+
let abi = Abi::deserialize(deserializer)?;
59+
Ok(abi.into())
60+
}
61+
}
62+
63+
impl Serialize for Interface {
64+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65+
where
66+
S: Serializer,
67+
{
68+
self.abi.serialize(serializer)
69+
}
70+
}
71+
72+
impl From<Abi> for Interface {
73+
fn from(abi: Abi) -> Self {
74+
Self {
75+
methods: create_mapping(&abi.functions, |function| function.selector()),
76+
events: create_mapping(&abi.events, |event| event.signature()),
77+
abi,
78+
}
79+
}
80+
}
81+
82+
/// Utility function for creating a mapping between a unique signature and a
83+
/// name-index pair for accessing contract ABI items.
84+
fn create_mapping<T, S, F>(
85+
elements: &BTreeMap<String, Vec<T>>,
86+
signature: F,
87+
) -> HashMap<S, (String, usize)>
88+
where
89+
S: Hash + Eq + Ord,
90+
F: Fn(&T) -> S,
91+
{
92+
let signature = &signature;
93+
elements
94+
.iter()
95+
.flat_map(|(name, sub_elements)| {
96+
sub_elements
97+
.iter()
98+
.enumerate()
99+
.map(move |(index, element)| (signature(element), (name.to_owned(), index)))
100+
})
101+
.collect()
102+
}
103+
31104
impl Contract {
32105
/// Creates an empty contract instance.
33106
pub fn empty() -> Self {
@@ -38,7 +111,7 @@ impl Contract {
38111
pub fn with_name(name: impl Into<String>) -> Self {
39112
Contract {
40113
name: name.into(),
41-
abi: Default::default(),
114+
interface: Default::default(),
42115
bytecode: Default::default(),
43116
deployed_bytecode: Default::default(),
44117
networks: HashMap::new(),

ethcontract-derive/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ethcontract-derive"
3-
version = "0.25.5"
3+
version = "0.25.6"
44
authors = ["Gnosis developers <[email protected]>"]
55
edition = "2021"
66
license = "MIT OR Apache-2.0"
@@ -20,8 +20,8 @@ proc-macro = true
2020

2121
[dependencies]
2222
anyhow = "1.0"
23-
ethcontract-common = { version = "0.25.5", path = "../ethcontract-common" }
24-
ethcontract-generate = { version = "0.25.5", path = "../ethcontract-generate", default-features = false }
23+
ethcontract-common = { version = "0.25.6", path = "../ethcontract-common" }
24+
ethcontract-generate = { version = "0.25.6", path = "../ethcontract-generate", default-features = false }
2525
proc-macro2 = "1.0"
2626
quote = "1.0"
2727
syn = "2.0"

ethcontract-generate/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ethcontract-generate"
3-
version = "0.25.5"
3+
version = "0.25.6"
44
authors = ["Gnosis developers <[email protected]>"]
55
edition = "2021"
66
license = "MIT OR Apache-2.0"
@@ -18,7 +18,7 @@ http = ["curl"]
1818
[dependencies]
1919
anyhow = "1.0"
2020
curl = { version = "0.4", optional = true }
21-
ethcontract-common = { version = "0.25.5", path = "../ethcontract-common" }
21+
ethcontract-common = { version = "0.25.6", path = "../ethcontract-common" }
2222
Inflector = "0.11"
2323
proc-macro2 = "1.0"
2424
quote = "1.0"

ethcontract-generate/src/generate/common.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub(crate) fn expand(cx: &Context) -> TokenStream {
5252
lazy_static! {
5353
pub static ref CONTRACT: Contract = {
5454
#[allow(unused_mut)]
55-
let mut contract = TruffleLoader::new()
55+
let mut contract: Contract = TruffleLoader::new()
5656
.load_contract_from_str(#contract_json)
5757
.expect("valid contract JSON");
5858
#( #deployments )*
@@ -146,8 +146,8 @@ pub(crate) fn expand(cx: &Context) -> TokenStream {
146146

147147
let transport = DynTransport::new(web3.transport().clone());
148148
let web3 = Web3::new(transport);
149-
let abi = Self::raw_contract().abi.clone();
150-
let instance = Instance::with_deployment_info(web3, abi, address, deployment_information);
149+
let interface = Self::raw_contract().interface.clone();
150+
let instance = Instance::with_deployment_info(web3, interface, address, deployment_information);
151151

152152
Contract::from_raw(instance)
153153
}

ethcontract-generate/src/generate/deployment.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ fn expand_deploy(cx: &Context) -> Result<TokenStream> {
8080
// can't seem to get truffle to output it
8181
let doc = util::expand_doc("Generated by `ethcontract`");
8282

83-
let (input, arg) = match cx.contract.abi.constructor() {
83+
let (input, arg) = match cx.contract.interface.abi.constructor() {
8484
Some(constructor) => (
8585
methods::expand_inputs(&constructor.inputs)?,
8686
methods::expand_inputs_call_arg(&constructor.inputs),
@@ -190,7 +190,7 @@ fn expand_deploy(cx: &Context) -> Result<TokenStream> {
190190
}
191191

192192
fn abi(_: &Self::Context) -> &self::ethcontract::common::Abi {
193-
&Self::raw_contract().abi
193+
&Self::raw_contract().interface.abi
194194
}
195195

196196
fn from_deployment(

0 commit comments

Comments
 (0)