Skip to content

Commit 2199839

Browse files
authored
perf: don't request unnecessary output (#231)
We're currently requesting all `evm.bytecode` (and `evm.deployedBytecode`) output by default. This includes `evm.bytecode.generatedSources`, introduced in solc 0.8.0, which is a list of potentially huge Yul AST objects that is completely unused in Foundry. Only request the `Compact*` bytecode fields in the defaults. This cuts down a clean `forge build`: - from 2:20m to 1:30m (-36%) on [superform-xyz/superform-core](https://github.com/superform-xyz/superform-core) - from 1:39m to 1:29m (-10%) on [sablier-labs/v2-core](https://github.com/sablier-labs/v2-core) - from 54.5s to 45.7s (-20%) on [sablier-labs/v2-core](https://github.com/sablier-labs/v2-core) (FOUNDRY_PROFILE=lite which is `optimizer = false`) It may have a larger impact when compiling with >=0.8.28, because the cache storing this data was removed in that version (argotorg/solidity@3c5e46b), or when optimizations are disabled as more IR will be generated and output to JSON. I verified that these inputs are accepted by solidity 0.4.14, 0.6.3, 0.8.28, and vyper 0.3.10, 0.4.0. All of these outputs are supported by all of them except for vyper which needs to be normalized. Drive-by: buffer stdin when writing to the solc subprocess.
1 parent 256918a commit 2199839

File tree

14 files changed

+132
-101
lines changed

14 files changed

+132
-101
lines changed

crates/artifacts/solc/src/output_selection.rs

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,6 @@ pub type FileOutputSelection = BTreeMap<String, Vec<String>>;
5151
/// Note that using a using `evm`, `evm.bytecode`, `ewasm`, etc. will select
5252
/// every target part of that output. Additionally, `*` can be used as a
5353
/// wildcard to request everything.
54-
///
55-
/// The default output selection is
56-
///
57-
/// ```json
58-
/// {
59-
/// "*": {
60-
/// "*": [
61-
/// "abi",
62-
/// "evm.bytecode",
63-
/// "evm.deployedBytecode",
64-
/// "evm.methodIdentifiers"
65-
/// ],
66-
/// "": [
67-
/// "ast"
68-
/// ]
69-
/// }
70-
/// }
71-
/// ```
7254
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
7355
#[serde(transparent)]
7456
pub struct OutputSelection(pub BTreeMap<String, FileOutputSelection>);
@@ -88,31 +70,18 @@ impl OutputSelection {
8870
.into()
8971
}
9072

91-
/// Default output selection for compiler output:
92-
///
93-
/// `{ "*": { "*": [ "*" ], "": [
94-
/// "abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers"] } }`
95-
///
96-
/// Which enables it for all files and all their contracts ("*" wildcard)
73+
/// Default output selection.
9774
pub fn default_output_selection() -> Self {
9875
BTreeMap::from([("*".to_string(), Self::default_file_output_selection())]).into()
9976
}
10077

101-
/// Default output selection for a single file:
102-
///
103-
/// `{ "*": [ "*" ], "": [
104-
/// "abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers"] }`
78+
/// Default file output selection.
10579
///
106-
/// Which enables it for all the contracts in the file ("*" wildcard)
80+
/// Uses [`ContractOutputSelection::basic`].
10781
pub fn default_file_output_selection() -> FileOutputSelection {
10882
BTreeMap::from([(
10983
"*".to_string(),
110-
vec![
111-
"abi".to_string(),
112-
"evm.bytecode".to_string(),
113-
"evm.deployedBytecode".to_string(),
114-
"evm.methodIdentifiers".to_string(),
115-
],
84+
ContractOutputSelection::basic().iter().map(ToString::to_string).collect(),
11685
)])
11786
}
11887

@@ -146,7 +115,7 @@ impl OutputSelection {
146115
}
147116

148117
/// Returns true if this output selection is a subset of the other output selection.
149-
/// TODO: correctly process wildcard keys to reduce false negatives
118+
// TODO: correctly process wildcard keys to reduce false negatives
150119
pub fn is_subset_of(&self, other: &Self) -> bool {
151120
self.0.iter().all(|(file, selection)| {
152121
other.0.get(file).is_some_and(|other_selection| {
@@ -229,16 +198,24 @@ pub enum ContractOutputSelection {
229198

230199
impl ContractOutputSelection {
231200
/// Returns the basic set of contract level settings that should be included in the `Contract`
232-
/// that solc emits:
233-
/// - "abi"
234-
/// - "evm.bytecode"
235-
/// - "evm.deployedBytecode"
236-
/// - "evm.methodIdentifiers"
201+
/// that solc emits.
202+
///
203+
/// These correspond to the fields in `CompactBytecode`, `CompactDeployedBytecode`, ABI, and
204+
/// method identfiers.
237205
pub fn basic() -> Vec<Self> {
206+
// We don't include all the `bytecode` fields because `generatedSources` is a massive JSON
207+
// object and is not used by Foundry.
238208
vec![
239209
Self::Abi,
240-
BytecodeOutputSelection::All.into(),
241-
DeployedBytecodeOutputSelection::All.into(),
210+
// The fields in `CompactBytecode`.
211+
BytecodeOutputSelection::Object.into(),
212+
BytecodeOutputSelection::SourceMap.into(),
213+
BytecodeOutputSelection::LinkReferences.into(),
214+
// The fields in `CompactDeployedBytecode`.
215+
DeployedBytecodeOutputSelection::Object.into(),
216+
DeployedBytecodeOutputSelection::SourceMap.into(),
217+
DeployedBytecodeOutputSelection::LinkReferences.into(),
218+
DeployedBytecodeOutputSelection::ImmutableReferences.into(),
242219
EvmOutputSelection::MethodIdentifiers.into(),
243220
]
244221
}

crates/artifacts/vyper/src/input.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub struct VyperInput {
1717
}
1818

1919
impl VyperInput {
20-
pub fn new(sources: Sources, mut settings: VyperSettings) -> Self {
20+
pub fn new(sources: Sources, mut settings: VyperSettings, version: &Version) -> Self {
2121
let mut new_sources = Sources::new();
2222
let mut interfaces = Sources::new();
2323

@@ -31,7 +31,7 @@ impl VyperInput {
3131
}
3232
}
3333

34-
settings.sanitize_output_selection();
34+
settings.sanitize_output_selection(version);
3535
Self { language: "Vyper".to_string(), sources: new_sources, interfaces, settings }
3636
}
3737

crates/artifacts/vyper/src/settings.rs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ use std::{
88
path::{Path, PathBuf},
99
};
1010

11-
pub const VYPER_SEARCH_PATHS: Version = Version::new(0, 4, 0);
11+
pub const VYPER_SEARCH_PATHS: Version = VYPER_0_4;
1212
pub const VYPER_BERLIN: Version = Version::new(0, 3, 0);
1313
pub const VYPER_PARIS: Version = Version::new(0, 3, 7);
1414
pub const VYPER_SHANGHAI: Version = Version::new(0, 3, 8);
1515
pub const VYPER_CANCUN: Version = Version::new(0, 3, 8);
1616

17+
const VYPER_0_4: Version = Version::new(0, 4, 0);
18+
1719
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1820
#[serde(rename_all = "lowercase")]
1921
pub enum VyperOptimizationMode {
@@ -65,15 +67,42 @@ impl VyperSettings {
6567
});
6668
}
6769

68-
/// During caching we prune output selection for some of the sources, however, Vyper will reject
69-
/// [] as an output selection, so we are adding "abi" as a default output selection which is
70-
/// cheap to be produced.
71-
pub fn sanitize_output_selection(&mut self) {
70+
/// Sanitize the output selection.
71+
#[allow(clippy::collapsible_if)]
72+
pub fn sanitize_output_selection(&mut self, version: &Version) {
7273
self.output_selection.0.values_mut().for_each(|selection| {
7374
selection.values_mut().for_each(|selection| {
75+
// During caching we prune output selection for some of the sources, however, Vyper
76+
// will reject `[]` as an output selection, so we are adding "abi" as a default
77+
// output selection which is cheap to be produced.
7478
if selection.is_empty() {
7579
selection.push("abi".to_string())
7680
}
81+
82+
// Unsupported selections.
83+
#[rustfmt::skip]
84+
selection.retain(|selection| {
85+
if *version <= VYPER_0_4 {
86+
if matches!(
87+
selection.as_str(),
88+
| "evm.bytecode.sourceMap" | "evm.deployedBytecode.sourceMap"
89+
) {
90+
return false;
91+
}
92+
}
93+
94+
if matches!(
95+
selection.as_str(),
96+
| "evm.bytecode.sourceMap" | "evm.deployedBytecode.sourceMap"
97+
// https://github.com/vyperlang/vyper/issues/4389
98+
| "evm.bytecode.linkReferences" | "evm.deployedBytecode.linkReferences"
99+
| "evm.deployedBytecode.immutableReferences"
100+
) {
101+
return false;
102+
}
103+
104+
true
105+
});
77106
})
78107
});
79108
}
@@ -84,7 +113,7 @@ impl VyperSettings {
84113
self.search_paths = None;
85114
}
86115

87-
self.sanitize_output_selection();
116+
self.sanitize_output_selection(version);
88117
self.normalize_evm_version(version);
89118
}
90119

crates/compilers/src/artifact_output/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ pub trait ArtifactOutput {
868868

869869
let mut files = contracts.keys().collect::<Vec<_>>();
870870
// Iterate starting with top-most files to ensure that they get the shortest paths.
871-
files.sort_by(|file1, file2| {
871+
files.sort_by(|&file1, &file2| {
872872
(file1.components().count(), file1).cmp(&(file2.components().count(), file2))
873873
});
874874
for file in files {

crates/compilers/src/compilers/solc/compiler.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use semver::{Version, VersionReq};
99
use serde::{de::DeserializeOwned, Deserialize, Serialize};
1010
use std::{
1111
collections::BTreeSet,
12+
io::{self, Write},
1213
path::{Path, PathBuf},
1314
process::{Command, Output, Stdio},
1415
str::FromStr,
@@ -428,8 +429,11 @@ impl Solc {
428429
let mut child = cmd.spawn().map_err(self.map_io_err())?;
429430
debug!("spawned");
430431

431-
let stdin = child.stdin.as_mut().unwrap();
432-
serde_json::to_writer(stdin, input)?;
432+
{
433+
let mut stdin = io::BufWriter::new(child.stdin.take().unwrap());
434+
serde_json::to_writer(&mut stdin, input)?;
435+
stdin.flush().map_err(self.map_io_err())?;
436+
}
433437
debug!("wrote JSON input to stdin");
434438

435439
let output = child.wait_with_output().map_err(self.map_io_err())?;

crates/compilers/src/compilers/vyper/input.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ impl CompilerInput for VyperVersionedInput {
2626
_language: Self::Language,
2727
version: Version,
2828
) -> Self {
29-
Self { input: VyperInput::new(sources, settings), version }
29+
Self { input: VyperInput::new(sources, settings, &version), version }
3030
}
3131

3232
fn compiler_name(&self) -> Cow<'static, str> {

crates/compilers/src/compilers/vyper/mod.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use foundry_compilers_core::error::{Result, SolcError};
77
use semver::Version;
88
use serde::{de::DeserializeOwned, Serialize};
99
use std::{
10+
io::{self, Write},
1011
path::{Path, PathBuf},
1112
process::{Command, Stdio},
1213
str::FromStr,
@@ -79,8 +80,11 @@ impl Vyper {
7980

8081
/// Convenience function for compiling all sources under the given path
8182
pub fn compile_source(&self, path: &Path) -> Result<VyperOutput> {
82-
let input =
83-
VyperInput::new(Source::read_all_from(path, VYPER_EXTENSIONS)?, Default::default());
83+
let input = VyperInput::new(
84+
Source::read_all_from(path, VYPER_EXTENSIONS)?,
85+
Default::default(),
86+
&self.version,
87+
);
8488
self.compile(&input)
8589
}
8690

@@ -114,7 +118,7 @@ impl Vyper {
114118
/// let vyper = Vyper::new("vyper")?;
115119
/// let path = Path::new("path/to/sources");
116120
/// let sources = Source::read_all_from(path, &["vy", "vyi"])?;
117-
/// let input = VyperInput::new(sources, VyperSettings::default());
121+
/// let input = VyperInput::new(sources, VyperSettings::default(), &vyper.version);
118122
/// let output = vyper.compile(&input)?;
119123
/// # Ok::<_, Box<dyn std::error::Error>>(())
120124
/// ```
@@ -149,8 +153,11 @@ impl Vyper {
149153
let mut child = cmd.spawn().map_err(self.map_io_err())?;
150154
debug!("spawned");
151155

152-
let stdin = child.stdin.as_mut().unwrap();
153-
serde_json::to_writer(stdin, input)?;
156+
{
157+
let mut stdin = io::BufWriter::new(child.stdin.take().unwrap());
158+
serde_json::to_writer(&mut stdin, input)?;
159+
stdin.flush().map_err(self.map_io_err())?;
160+
}
154161
debug!("wrote JSON input to stdin");
155162

156163
let output = child.wait_with_output().map_err(self.map_io_err())?;

crates/compilers/tests/project.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,23 +47,43 @@ pub static VYPER: LazyLock<Vyper> = LazyLock::new(|| {
4747
#[cfg(target_family = "unix")]
4848
use std::{fs::Permissions, os::unix::fs::PermissionsExt};
4949

50+
if let Ok(vyper) = Vyper::new("vyper") {
51+
return vyper;
52+
}
53+
5054
take_solc_installer_lock!(_lock);
5155
let path = std::env::temp_dir().join("vyper");
5256

5357
if path.exists() {
5458
return Vyper::new(&path).unwrap();
5559
}
5660

57-
let url = match platform() {
58-
Platform::MacOsAarch64 => "https://github.com/vyperlang/vyper/releases/download/v0.3.10/vyper.0.3.10+commit.91361694.darwin",
59-
Platform::LinuxAmd64 => "https://github.com/vyperlang/vyper/releases/download/v0.3.10/vyper.0.3.10+commit.91361694.linux",
60-
Platform::WindowsAmd64 => "https://github.com/vyperlang/vyper/releases/download/v0.3.10/vyper.0.3.10+commit.91361694.windows.exe",
61-
_ => panic!("unsupported")
62-
};
63-
64-
let res = reqwest::Client::builder().build().unwrap().get(url).send().await.unwrap();
61+
let base = "https://github.com/vyperlang/vyper/releases/download/v0.4.0/vyper.0.4.0+commit.e9db8d9f";
62+
let url = format!(
63+
"{base}.{}",
64+
match platform() {
65+
Platform::MacOsAarch64 => "darwin",
66+
Platform::LinuxAmd64 => "linux",
67+
Platform::WindowsAmd64 => "windows.exe",
68+
platform => panic!("unsupported platform: {platform:?}"),
69+
}
70+
);
6571

66-
assert!(res.status().is_success());
72+
let mut retry = 3;
73+
let mut res = None;
74+
while retry > 0 {
75+
match reqwest::get(&url).await.unwrap().error_for_status() {
76+
Ok(res2) => {
77+
res = Some(res2);
78+
break;
79+
}
80+
Err(e) => {
81+
eprintln!("{e}");
82+
retry -= 1;
83+
}
84+
}
85+
}
86+
let res = res.expect("failed to get vyper binary");
6787

6888
let bytes = res.bytes().await.unwrap();
6989

@@ -3889,17 +3909,20 @@ fn can_compile_vyper_with_cache() {
38893909
.unwrap();
38903910

38913911
let compiled = project.compile().unwrap();
3912+
compiled.assert_success();
38923913
assert!(compiled.find_first("Counter").is_some());
38933914
compiled.assert_success();
38943915

38953916
// cache is used when nothing to compile
38963917
let compiled = project.compile().unwrap();
3918+
compiled.assert_success();
38973919
assert!(compiled.find_first("Counter").is_some());
38983920
assert!(compiled.is_unchanged());
38993921

39003922
// deleted artifacts cause recompile even with cache
39013923
std::fs::remove_dir_all(project.artifacts_path()).unwrap();
39023924
let compiled = project.compile().unwrap();
3925+
compiled.assert_success();
39033926
assert!(compiled.find_first("Counter").is_some());
39043927
assert!(!compiled.is_unchanged());
39053928
}
@@ -3975,9 +3998,9 @@ fn test_can_compile_multi() {
39753998
.unwrap();
39763999

39774000
let compiled = project.compile().unwrap();
4001+
compiled.assert_success();
39784002
assert!(compiled.find(&root.join("src/Counter.sol"), "Counter").is_some());
39794003
assert!(compiled.find(&root.join("src/Counter.vy"), "Counter").is_some());
3980-
compiled.assert_success();
39814004
}
39824005

39834006
// This is a reproduction of https://github.com/foundry-rs/compilers/issues/47

test-data/multi-sample/src/interfaces/ICounter.vy

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# pragma version ^0.4.0
2+
3+
@external
4+
@view
5+
def number() -> uint256: ...
6+
7+
@external
8+
def set_number(new_number: uint256): ...
9+
10+
@external
11+
def increment() -> uint256: ...

0 commit comments

Comments
 (0)