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 node startup speed and memory allocation #6952

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -89,7 +89,7 @@ public Condition awaitSignerSetChange(final Node node) {

private int cliqueBlockPeriod(final BesuNode node) {
final String config = node.getGenesisConfigProvider().create(emptyList()).get();
final GenesisConfigFile genesisConfigFile = GenesisConfigFile.fromConfig(config);
final GenesisConfigFile genesisConfigFile = GenesisConfigFile.fromConfigWithoutAccounts(config);
final CliqueConfigOptions cliqueConfigOptions =
genesisConfigFile.getConfigOptions().getCliqueConfigOptions();
return cliqueConfigOptions.getBlockPeriodSeconds();
Expand Down
49 changes: 40 additions & 9 deletions besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
Expand Up @@ -91,6 +91,7 @@
import org.hyperledger.besu.config.CheckpointConfigOptions;
import org.hyperledger.besu.config.GenesisConfigFile;
import org.hyperledger.besu.config.GenesisConfigOptions;
import org.hyperledger.besu.config.JsonUtil;
import org.hyperledger.besu.config.MergeConfigOptions;
import org.hyperledger.besu.consensus.qbft.pki.PkiBlockCreationConfiguration;
import org.hyperledger.besu.consensus.qbft.pki.PkiBlockCreationConfigurationProvider;
Expand All @@ -117,6 +118,7 @@
import org.hyperledger.besu.ethereum.api.jsonrpc.websocket.WebSocketConfiguration;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.chain.VariablesStorage;
import org.hyperledger.besu.ethereum.core.MiningParameters;
import org.hyperledger.besu.ethereum.core.MiningParametersMetrics;
import org.hyperledger.besu.ethereum.core.PrivacyParameters;
Expand Down Expand Up @@ -1598,7 +1600,8 @@ private void validateChainDataPruningParams() {
private GenesisConfigOptions readGenesisConfigOptions() {

try {
final GenesisConfigFile genesisConfigFile = GenesisConfigFile.fromConfig(genesisConfig());
final GenesisConfigFile genesisConfigFile =
GenesisConfigFile.fromConfigWithoutAccounts(genesisConfig());
genesisConfigOptions = genesisConfigFile.getConfigOptions(genesisConfigOverrides);
} catch (final Exception e) {
throw new ParameterException(
Expand Down Expand Up @@ -2352,18 +2355,46 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) {
}

private GenesisConfigFile getGenesisConfigFile() {
return GenesisConfigFile.fromConfig(genesisConfig());
}

private String genesisConfig() {
return GenesisConfigFile.fromConfigWithoutAccounts(genesisConfig());
}

private final Supplier<String> genesisConfigSupplier = Suppliers.memoize(this::loadGenesisConfig);

private String loadGenesisConfig() {
if (genesisStateHashCacheEnabled) {
// If the genesis state hash is present in the database, we can use the genesis file without
pluginCommonConfiguration.init(
dataDir(),
dataDir().resolve(DATABASE_PATH),
getDataStorageConfiguration(),
getMiningParameters());
final KeyValueStorageProvider storageProvider = keyValueStorageProvider(keyValueStorageName);
if (storageProvider != null) {
boolean isGenesisStateHashPresent;
try {
// A null pointer exception may be thrown here if the database is not initialized.
VariablesStorage variablesStorage = storageProvider.createVariablesStorage();
Optional<Hash> genesisStateHash = variablesStorage.getGenesisStateHash();
isGenesisStateHashPresent = genesisStateHash.isPresent();
} catch (Exception ignored) {
isGenesisStateHashPresent = false;
}
if (isGenesisStateHashPresent) {
return JsonUtil.getJsonFromFileWithout(genesisFile, "alloc");
}
}
}
try {
return Resources.toString(genesisFile.toURI().toURL(), UTF_8);
} catch (final IOException e) {
throw new ParameterException(
this.commandLine, String.format("Unable to load genesis URL %s.", genesisFile), e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private String genesisConfig() {
return genesisConfigSupplier.get();
}

private static String genesisConfig(final NetworkName networkName) {
try (final InputStream genesisFileInputStream =
EthNetworkConfig.class.getResourceAsStream(networkName.getGenesisFile())) {
Expand Down Expand Up @@ -2607,7 +2638,7 @@ protected GenesisConfigOptions getActualGenesisConfigOptions() {
return Optional.ofNullable(genesisConfigOptions)
.orElseGet(
() ->
GenesisConfigFile.fromConfig(
GenesisConfigFile.fromConfigWithoutAccounts(
genesisConfig(Optional.ofNullable(network).orElse(MAINNET)))
.getConfigOptions(genesisConfigOverrides));
}
Expand Down
Expand Up @@ -138,7 +138,7 @@ public String toString() {
public static EthNetworkConfig getNetworkConfig(final NetworkName networkName) {
final String genesisContent = jsonConfig(networkName.getGenesisFile());
final GenesisConfigOptions genesisConfigOptions =
GenesisConfigFile.fromConfig(genesisContent).getConfigOptions();
GenesisConfigFile.fromConfigWithoutAccounts(genesisContent).getConfigOptions();
final Optional<List<String>> rawBootNodes =
genesisConfigOptions.getDiscoveryOptions().getBootNodes();
final List<EnodeURL> bootNodes =
Expand Down
Expand Up @@ -283,7 +283,9 @@ private void parseConfig() throws IOException {

/** Sets the selected signature algorithm instance in SignatureAlgorithmFactory. */
private void processEcCurve() {
GenesisConfigOptions options = GenesisConfigFile.fromConfig(genesisConfig).getConfigOptions();
GenesisConfigOptions options =
GenesisConfigFile.fromConfigWithoutAccounts(String.valueOf(genesisConfig))
.getConfigOptions();
Optional<String> ecCurve = options.getEcCurve();

if (ecCurve.isEmpty()) {
Expand Down
Expand Up @@ -332,6 +332,25 @@ public BesuControllerBuilder fromEthNetworkConfig(
.networkId(ethNetworkConfig.getNetworkId());
}

/**
* From eth network config without alloc besu controller builder.
*
* @param ethNetworkConfig the eth network config
* @param genesisConfigOverrides the genesis config overrides
* @param syncMode The sync mode
* @return the besu controller builder
*/
public BesuControllerBuilder fromEthNetworkConfigWithoutAccounts(
final EthNetworkConfig ethNetworkConfig,
final Map<String, String> genesisConfigOverrides,
final SyncMode syncMode) {
return fromGenesisConfig(
GenesisConfigFile.fromConfigWithoutAccounts(ethNetworkConfig.getGenesisConfig()),
genesisConfigOverrides,
syncMode)
.networkId(ethNetworkConfig.getNetworkId());
}

/**
* From genesis config besu controller builder.
*
Expand Down Expand Up @@ -390,8 +409,9 @@ BesuControllerBuilder fromGenesisConfig(
return new TransitionBesuControllerBuilder(builder, new MergeBesuControllerBuilder())
.genesisConfigFile(genesisConfig);
}

} else return builder.genesisConfigFile(genesisConfig);
} else {
return builder.genesisConfigFile(genesisConfig);
}
}

private BesuControllerBuilder createConsensusScheduleBesuControllerBuilder(
Expand Down
Expand Up @@ -106,6 +106,16 @@ public static GenesisConfigFile fromConfig(final String jsonString) {
return fromConfig(JsonUtil.objectNodeFromString(jsonString, false));
}

/**
* From config without account genesis config file.
*
* @param jsonString the json string
* @return the genesis config file
*/
public static GenesisConfigFile fromConfigWithoutAccounts(final String jsonString) {
return fromConfig(JsonUtil.objectNodeFromStringWithout(jsonString, false, "alloc"));
}

/**
* From config genesis config file.
*
Expand Down
135 changes: 135 additions & 0 deletions config/src/main/java/org/hyperledger/besu/config/JsonUtil.java
Expand Up @@ -16,15 +16,19 @@

import org.hyperledger.besu.util.number.PositiveNumber;

import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
Expand Down Expand Up @@ -321,6 +325,47 @@ public static ObjectNode objectNodeFromString(
}
}

/**
* Object node from string without some field.
*
* @param jsonData the json data
* @param allowComments true to allow comments
* @param withoutField the without field
* @return the object node
*/
public static ObjectNode objectNodeFromStringWithout(
final String jsonData, final boolean allowComments, final String withoutField) {
final ObjectMapper objectMapper = new ObjectMapper();
JsonFactory jsonFactory =
JsonFactory.builder()
.configure(JsonFactory.Feature.INTERN_FIELD_NAMES, false)
.configure(JsonFactory.Feature.CANONICALIZE_FIELD_NAMES, false)
.build();
jsonFactory.configure(JsonParser.Feature.ALLOW_COMMENTS, allowComments);

ObjectNode root = objectMapper.createObjectNode();

try (JsonParser jp = jsonFactory.createParser(jsonData)) {
if (jp.nextToken() != JsonToken.START_OBJECT) {
throw new RuntimeException("Expected data to start with an Object");
}

while (jp.nextToken() != JsonToken.END_OBJECT) {
String fieldName = jp.getCurrentName();
if (withoutField.equals(fieldName)) {
jp.nextToken();
jp.skipChildren();
} else {
jp.nextToken();
root.set(fieldName, objectMapper.readTree(jp));
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return root;
}

/**
* Gets json.
*
Expand Down Expand Up @@ -466,4 +511,94 @@ private static boolean validateInt(final JsonNode node) {
}
return true;
}

/**
* Get the JSON representation of a genesis file without a specific field.
*
* @param genesisFile The genesis file to read.
* @param excludedFieldName The field to exclude from the JSON representation.
* @return The JSON representation of the genesis file without the excluded field.
*/
public static String getJsonFromFileWithout(
final File genesisFile, final String excludedFieldName) {
StringBuilder jsonBuilder = new StringBuilder();
JsonFactory jsonFactory =
JsonFactory.builder()
.configure(JsonFactory.Feature.INTERN_FIELD_NAMES, false)
.configure(JsonFactory.Feature.CANONICALIZE_FIELD_NAMES, false)
.build();
try (JsonParser parser = jsonFactory.createParser(genesisFile)) {
JsonToken token;
while ((token = parser.nextToken()) != null) {
if (token == JsonToken.START_OBJECT) {
jsonBuilder.append(handleObject(parser, excludedFieldName));
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return jsonBuilder.toString();
}
Comment on lines +522 to +541
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you checked if Jackson natively support excluding fields without having to implement the parsing methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I have checked it, and this is the fastest method with the least memory usage. I will check it again in a moment.


private static String handleObject(final JsonParser parser, final String excludedFieldName)
throws IOException {
StringBuilder objectBuilder = new StringBuilder();
objectBuilder.append("{");
String fieldName;
boolean isFirstField = true;
while (parser.nextToken() != JsonToken.END_OBJECT) {
fieldName = parser.getCurrentName();
if (fieldName != null && fieldName.equals(excludedFieldName)) {
parser.skipChildren(); // Skip this field
continue;
}
if (!isFirstField) objectBuilder.append(", ");
parser.nextToken(); // move to value
objectBuilder
.append("\"")
.append(fieldName)
.append("\":")
.append(handleValue(parser, excludedFieldName));
isFirstField = false;
}
objectBuilder.append("}");
return objectBuilder.toString();
}

private static String handleValue(final JsonParser parser, final String excludedFieldName)
throws IOException {
JsonToken token = parser.getCurrentToken();
switch (token) {
case START_OBJECT:
return handleObject(parser, excludedFieldName);
case START_ARRAY:
return handleArray(parser, excludedFieldName);
case VALUE_STRING:
return "\"" + parser.getText() + "\"";
case VALUE_NUMBER_INT:
case VALUE_NUMBER_FLOAT:
return parser.getNumberValue().toString();
case VALUE_TRUE:
case VALUE_FALSE:
return parser.getBooleanValue() ? "true" : "false";
case VALUE_NULL:
return "null";
default:
throw new IllegalStateException("Unrecognized token: " + token);
}
}

private static String handleArray(final JsonParser parser, final String excludedFieldName)
throws IOException {
StringBuilder arrayBuilder = new StringBuilder();
arrayBuilder.append("[");
boolean isFirstElement = true;
while (parser.nextToken() != JsonToken.END_ARRAY) {
if (!isFirstElement) arrayBuilder.append(", ");
arrayBuilder.append(handleValue(parser, excludedFieldName));
isFirstElement = false;
}
arrayBuilder.append("]");
return arrayBuilder.toString();
}
}
23 changes: 23 additions & 0 deletions config/src/test/java/org/hyperledger/besu/config/JsonUtilTest.java
Expand Up @@ -16,7 +16,9 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -781,4 +783,25 @@ public void hasKey_nonEmptyMatchingKey() {

assertThat(JsonUtil.hasKey(rootNode, "target")).isTrue();
}

@Test
void objectNodeFromStringWithoutAllocField() {
String genesisSting =
"{\"coinbase\":\"0x0000000000000000000000000000000000000000\",\"alloc\":{\"000d836201318ec6899a67540690382780743280\":{\"balance\":\"0xad78ebc5ac6200000\"}}}";

ObjectNode jsonNodes = JsonUtil.objectNodeFromStringWithout(genesisSting, false, "alloc");

assertThat(jsonNodes.get("coinbase").toString()).isNotEmpty();
assertThat(jsonNodes.get("alloc")).isNull();
}

@Test
void getJsonFromFileWithoutAllocField() {
String genesisStingWithoutAllocField =
"{\"config\":{\"chainId\":1337, \"londonBlock\":0, \"phillyBlock\":5, \"parisBlock\":10, \"contractSizeLimit\":2147483647, \"ethash\":{\"fixeddifficulty\":100}}, \"nonce\":\"0x42\", \"timestamp\":\"0x0\", \"extraData\":\"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa\", \"gasLimit\":\"0x1fffffffffffff\", \"difficulty\":\"0x10000\", \"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\", \"coinbase\":\"0x0000000000000000000000000000000000000000\"}";
File genesisFile = new File("src/test/resources/preMerge.json");

String genesisStringFromFile = JsonUtil.getJsonFromFileWithout(genesisFile, "alloc");
assertEquals(genesisStingWithoutAllocField, genesisStringFromFile);
}
}
Expand Up @@ -44,7 +44,7 @@ public class BlockchainImporter {
public BlockchainImporter(final URL blocksUrl, final String genesisJson) throws Exception {
protocolSchedule =
MainnetProtocolSchedule.fromConfig(
GenesisConfigFile.fromConfig(genesisJson).getConfigOptions(),
GenesisConfigFile.fromConfigWithoutAccounts(genesisJson).getConfigOptions(),
MiningParameters.newDefault(),
new BadBlockManager());
final BlockHeaderFunctions blockHeaderFunctions =
Expand Down