diff --git a/.changeset/legal-roses-dress.md b/.changeset/legal-roses-dress.md new file mode 100644 index 00000000000..11c0ed433e5 --- /dev/null +++ b/.changeset/legal-roses-dress.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Optimize the initialization of EDR Network Connections by only processing the build outputs once. diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts index e4834adbfd4..c18046b8464 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts @@ -135,7 +135,7 @@ interface EdrProviderConfig { chainDescriptors: ChainDescriptorsConfig; networkConfig: RequireField; loggerConfig?: LoggerConfig; - tracingConfig?: TracingConfigWithBuffers; + contractDecoder: ContractDecoder; jsonRpcRequestWrapper?: JsonRpcRequestWrapperFunction; coverageConfig?: CoverageConfig; gasReportConfig?: GasReportConfig; @@ -147,6 +147,12 @@ export class EdrProvider extends BaseProvider { #provider: Provider | undefined; #nextRequestId = 1; + public static async createContractDecoder( + tracingConfig: TracingConfigWithBuffers, + ): Promise { + return ContractDecoder.withContracts(tracingConfig); + } + /** * Creates a new instance of `EdrProvider`. */ @@ -154,7 +160,7 @@ export class EdrProvider extends BaseProvider { chainDescriptors, networkConfig, loggerConfig = { enabled: false }, - tracingConfig = {}, + contractDecoder, jsonRpcRequestWrapper, coverageConfig, gasReportConfig, @@ -174,8 +180,6 @@ export class EdrProvider extends BaseProvider { // We need to catch errors here, as the provider creation can panic unexpectedly, // and we want to make sure such a crash is propagated as a ProviderError. try { - const contractDecoder = ContractDecoder.withContracts(tracingConfig); - const context = await getGlobalEdrContext(); const provider = await context.createProvider( hardhatChainTypeToEdrChainType(networkConfig.chainType), diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts index 06829590205..22f34609441 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts @@ -21,7 +21,7 @@ import type { JsonRpcRequest, JsonRpcResponse, } from "../../../types/providers.js"; -import type { GasReportConfig } from "@nomicfoundation/edr"; +import type { ContractDecoder, GasReportConfig } from "@nomicfoundation/edr"; import { HardhatError, @@ -29,6 +29,7 @@ import { } from "@nomicfoundation/hardhat-errors"; import { exists, readBinaryFile } from "@nomicfoundation/hardhat-utils/fs"; import { deepMerge } from "@nomicfoundation/hardhat-utils/lang"; +import { AsyncMutex } from "@nomicfoundation/hardhat-utils/synchronization"; import { resolveUserConfigToHardhatConfig } from "../../core/hre.js"; import { isSupportedChainType } from "../../edr/chain-type.js"; @@ -57,6 +58,8 @@ export class NetworkManagerImplementation implements NetworkManager { readonly #projectRoot: string; #nextConnectionId = 0; + readonly #contractDecoderMutex = new AsyncMutex(); + #contractDecoder: ContractDecoder | undefined; constructor( defaultNetwork: string, @@ -233,6 +236,40 @@ export class NetworkManagerImplementation implements NetworkManager { }; } + // We load the build infos and their outputs to create a contract + // decoder when the first provider is created. Successive providers will + // reuse the same decoder as a performance optimization. + // + // The trade-off here is that if you create an EDR provider, then + // compile new contracts, and create a new provider, the new contracts + // won't be loaded. + // + // Even without this optimization, we already had the problem of new + // contracts not being visible to existing providers. + // + // In practice, most workflows compile everything before creating + // any network connection. + if (this.#contractDecoder === undefined) { + // We want to ensure that only one contract decoder is created so we + // protect the initialization with a mutex. + await this.#contractDecoderMutex.exclusiveRun(async () => { + // We check again if the decoder is undefined because another async + // execution context could have already initialized it while we were + // waiting for the mutex. + if (this.#contractDecoder === undefined) { + this.#contractDecoder = await EdrProvider.createContractDecoder({ + buildInfos: await this.#getBuildInfosAndOutputsAsBuffers(), + ignoreContracts: false, + }); + } + }); + } + + assertHardhatInvariant( + this.#contractDecoder !== undefined, + "Contract decoder should have been initialized before creating the provider", + ); + return EdrProvider.create({ chainDescriptors: this.#chainDescriptors, // The resolvedNetworkConfig can have its chainType set to `undefined` @@ -249,10 +286,7 @@ export class NetworkManagerImplementation implements NetworkManager { chainType: resolvedChainType as ChainType, }, jsonRpcRequestWrapper, - tracingConfig: { - buildInfos: await this.#getBuildInfosAndOutputsAsBuffers(), - ignoreContracts: false, - }, + contractDecoder: this.#contractDecoder, coverageConfig, gasReportConfig, }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts index 5bfa9ca7c73..46a775bb931 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts @@ -35,6 +35,7 @@ import { resolveEdrNetwork, resolveHttpNetwork, } from "../../../../src/internal/builtin-plugins/network-manager/config-resolution.js"; +import { EdrProvider } from "../../../../src/internal/builtin-plugins/network-manager/edr/edr-provider.js"; import { getCurrentHardfork, getHardforks, @@ -2512,8 +2513,8 @@ describe("NetworkManagerImplementation", () => { for (const hardfork of Object.values(OpHardforkName)) { const validationErrors = await validateNetworkUserConfig({ ...edrConfig({ hardfork }), - /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions - -- Type assertion needed because changing defaultChainType requires module + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Type assertion needed because changing defaultChainType requires module augmentation, which can't be done in test files */ defaultChainType: OPTIMISM_CHAIN_TYPE as any, }); @@ -2556,8 +2557,8 @@ describe("NetworkManagerImplementation", () => { ...edrConfig({ hardfork: L1HardforkName.OSAKA, }), - /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions - -- Type assertion needed because changing defaultChainType requires module + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Type assertion needed because changing defaultChainType requires module augmentation, which can't be done in test files */ defaultChainType: OPTIMISM_CHAIN_TYPE as any, }); @@ -2959,4 +2960,24 @@ describe("NetworkManagerImplementation", () => { }); }); }); + + describe("ContractDecoder caching", () => { + it("should create the ContractDecoder only once across multiple EDR connections", async (t) => { + const spy = t.mock.method(EdrProvider, "createContractDecoder"); + + await networkManager.connect({ + network: "edrNetwork", + }); + + await networkManager.connect({ + network: "edrNetwork", + }); + + assert.equal( + spy.mock.callCount(), + 1, + "createContractDecoder should be called exactly once", + ); + }); + }); });