[core] Add model cache to speed up code generation (#7250)

This commit is contained in:
Sebastien Rosset 2020-09-20 19:15:32 -07:00 committed by GitHub
parent 8cd503f194
commit 4f27939879
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 162 additions and 47 deletions

View File

@ -247,6 +247,9 @@ public class DefaultCodegen implements CodegenConfig {
// make openapi available to all methods // make openapi available to all methods
protected OpenAPI openAPI; protected OpenAPI openAPI;
// A cache to efficiently lookup a Schema instance based on the return value of `toModelName()`.
private Map<String, Schema> modelNameToSchemaCache;
public List<CliOption> cliOptions() { public List<CliOption> cliOptions() {
return cliOptions; return cliOptions;
} }
@ -450,6 +453,23 @@ public class DefaultCodegen implements CodegenConfig {
return objs; return objs;
} }
/**
* Return a map from model name to Schema for efficient lookup.
*
* @return map from model name to Schema.
*/
protected Map<String, Schema> getModelNameToSchemaCache() {
if (modelNameToSchemaCache == null) {
// Create a cache to efficiently lookup schema based on model name.
Map<String, Schema> m = new HashMap<String, Schema>();
ModelUtils.getSchemas(openAPI).forEach((key, schema) -> {
m.put(toModelName(key), schema);
});
modelNameToSchemaCache = Collections.unmodifiableMap(m);
}
return modelNameToSchemaCache;
}
/** /**
* Index all CodegenModels by model name. * Index all CodegenModels by model name.
* *
@ -1293,14 +1313,13 @@ public class DefaultCodegen implements CodegenConfig {
* @param name the variable name * @param name the variable name
* @return the sanitized variable name * @return the sanitized variable name
*/ */
public String toVarName(String name) { public String toVarName(final String name) {
if (reservedWords.contains(name)) { if (reservedWords.contains(name)) {
return escapeReservedWord(name); return escapeReservedWord(name);
} else if (((CharSequence) name).chars().anyMatch(character -> specialCharReplacements.keySet().contains("" + ((char) character)))) { } else if (((CharSequence) name).chars().anyMatch(character -> specialCharReplacements.keySet().contains("" + ((char) character)))) {
return escape(name, specialCharReplacements, null, null); return escape(name, specialCharReplacements, null, null);
} else {
return name;
} }
return name;
} }
/** /**
@ -1318,6 +1337,7 @@ public class DefaultCodegen implements CodegenConfig {
return escape(name, specialCharReplacements, null, null); return escape(name, specialCharReplacements, null, null);
} }
return name; return name;
} }
/** /**
@ -2195,7 +2215,8 @@ public class DefaultCodegen implements CodegenConfig {
} }
/** /**
* Output the proper model name (capitalized). * Converts the OpenAPI schema name to a model name suitable for the current code generator.
* May be overriden for each programming language.
* In case the name belongs to the TypeSystem it won't be renamed. * In case the name belongs to the TypeSystem it won't be renamed.
* *
* @param name the name of the model * @param name the name of the model
@ -2205,8 +2226,33 @@ public class DefaultCodegen implements CodegenConfig {
return camelize(modelNamePrefix + "_" + name + "_" + modelNameSuffix); return camelize(modelNamePrefix + "_" + name + "_" + modelNameSuffix);
} }
private static class NamedSchema {
private NamedSchema(String name, Schema s) {
this.name = name;
this.schema = s;
}
private String name;
private Schema schema;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NamedSchema that = (NamedSchema) o;
return Objects.equals(name, that.name) &&
Objects.equals(schema, that.schema);
}
@Override
public int hashCode() {
return Objects.hash(name, schema);
}
}
Map<NamedSchema, CodegenProperty> schemaCodegenPropertyCache = new HashMap<NamedSchema, CodegenProperty>();
/** /**
* Convert OAS Model object to Codegen Model object * Convert OAS Model object to Codegen Model object.
* *
* @param name the name of the model * @param name the name of the model
* @param schema OAS Model object * @param schema OAS Model object
@ -2565,7 +2611,6 @@ public class DefaultCodegen implements CodegenConfig {
postProcessModelProperty(m, prop); postProcessModelProperty(m, prop);
} }
} }
return m; return m;
} }
@ -2984,7 +3029,13 @@ public class DefaultCodegen implements CodegenConfig {
} }
/** /**
* Convert OAS Property object to Codegen Property object * Convert OAS Property object to Codegen Property object.
*
* The return value is cached. An internal cache is looked up to determine
* if the CodegenProperty return value has already been instantiated for
* the (String name, Schema p) arguments.
* Any subsequent processing of the CodegenModel return value must be idempotent
* for a given (String name, Schema schema).
* *
* @param name name of the property * @param name name of the property
* @param p OAS property schema * @param p OAS property schema
@ -2996,7 +3047,12 @@ public class DefaultCodegen implements CodegenConfig {
return null; return null;
} }
LOGGER.debug("debugging fromProperty for " + name + " : " + p); LOGGER.debug("debugging fromProperty for " + name + " : " + p);
NamedSchema ns = new NamedSchema(name, p);
CodegenProperty cpc = schemaCodegenPropertyCache.get(ns);
if (cpc != null) {
LOGGER.debug("Cached fromProperty for " + name + " : " + p.getName());
return cpc;
}
// unalias schema // unalias schema
p = unaliasSchema(p, importMapping); p = unaliasSchema(p, importMapping);
@ -3296,6 +3352,7 @@ public class DefaultCodegen implements CodegenConfig {
} }
LOGGER.debug("debugging from property return: " + property); LOGGER.debug("debugging from property return: " + property);
schemaCodegenPropertyCache.put(ns, property);
return property; return property;
} }
@ -4762,7 +4819,6 @@ public class DefaultCodegen implements CodegenConfig {
final String key = entry.getKey(); final String key = entry.getKey();
final Schema prop = entry.getValue(); final Schema prop = entry.getValue();
if (prop == null) { if (prop == null) {
LOGGER.warn("Please report the issue. There shouldn't be null property for " + key); LOGGER.warn("Please report the issue. There shouldn't be null property for " + key);
} else { } else {
@ -6382,6 +6438,9 @@ public class DefaultCodegen implements CodegenConfig {
.featureSet(builder.build()).build(); .featureSet(builder.build()).build();
} }
/**
* An map entry for cached sanitized names.
*/
private static class SanitizeNameOptions { private static class SanitizeNameOptions {
public SanitizeNameOptions(String name, String removeCharRegEx, List<String> exceptions) { public SanitizeNameOptions(String name, String removeCharRegEx, List<String> exceptions) {
this.name = name; this.name = name;
@ -6389,7 +6448,7 @@ public class DefaultCodegen implements CodegenConfig {
if (exceptions != null) { if (exceptions != null) {
this.exceptions = Collections.unmodifiableList(exceptions); this.exceptions = Collections.unmodifiableList(exceptions);
} else { } else {
this.exceptions = Collections.unmodifiableList(new ArrayList<>()); this.exceptions = Collections.emptyList();
} }
} }

View File

@ -493,19 +493,16 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
} }
String varDataType = var.mostInnerItems != null ? var.mostInnerItems.dataType : var.dataType; String varDataType = var.mostInnerItems != null ? var.mostInnerItems.dataType : var.dataType;
Optional<Schema> referencedSchema = ModelUtils.getSchemas(openAPI).entrySet().stream() Schema referencedSchema = getModelNameToSchemaCache().get(varDataType);
.filter(entry -> Objects.equals(varDataType, toModelName(entry.getKey()))) String dataType = (referencedSchema != null) ? getTypeDeclaration(referencedSchema) : varDataType;
.map(Map.Entry::getValue)
.findFirst();
String dataType = (referencedSchema.isPresent()) ? getTypeDeclaration(referencedSchema.get()) : varDataType;
// put "enumVars" map into `allowableValues", including `name` and `value` // put "enumVars" map into `allowableValues", including `name` and `value`
List<Map<String, Object>> enumVars = buildEnumVars(values, dataType); List<Map<String, Object>> enumVars = buildEnumVars(values, dataType);
// if "x-enum-varnames" or "x-enum-descriptions" defined, update varnames // if "x-enum-varnames" or "x-enum-descriptions" defined, update varnames
Map<String, Object> extensions = var.mostInnerItems != null ? var.mostInnerItems.getVendorExtensions() : var.getVendorExtensions(); Map<String, Object> extensions = var.mostInnerItems != null ? var.mostInnerItems.getVendorExtensions() : var.getVendorExtensions();
if (referencedSchema.isPresent()) { if (referencedSchema != null) {
extensions = referencedSchema.get().getExtensions(); extensions = referencedSchema.getExtensions();
} }
updateEnumVarsWithExtensions(enumVars, extensions); updateEnumVarsWithExtensions(enumVars, extensions);
allowableValues.put("enumVars", enumVars); allowableValues.put("enumVars", enumVars);

View File

@ -1054,7 +1054,7 @@ public class ModelUtils {
/** /**
* Get the actual schema from aliases. If the provided schema is not an alias, the schema itself will be returned. * Get the actual schema from aliases. If the provided schema is not an alias, the schema itself will be returned.
* *
* @param openAPI specification being checked * @param openAPI OpenAPI document containing the schemas.
* @param schema schema (alias or direct reference) * @param schema schema (alias or direct reference)
* @param importMappings mappings of external types to be omitted by unaliasing * @param importMappings mappings of external types to be omitted by unaliasing
* @return actual schema * @return actual schema

View File

@ -7,10 +7,13 @@ import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.openapitools.codegen.config.GlobalSettings; import org.openapitools.codegen.config.GlobalSettings;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.HashMap; import java.util.HashMap;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -18,11 +21,11 @@ import java.util.regex.Pattern;
public class StringUtils { public class StringUtils {
/** /**
* Set the cache size (entry count) of the sanitizedNameCache, camelizedWordsCache and underscoreWords. * Set the cache size (entry count) of the sanitizedNameCache, camelizedWordsCache and underscoreWordsCache.
*/ */
public static final String NAME_CACHE_SIZE_PROPERTY = "org.openapitools.codegen.utils.namecache.cachesize"; public static final String NAME_CACHE_SIZE_PROPERTY = "org.openapitools.codegen.utils.namecache.cachesize";
/** /**
* Set the cache expiry (in seconds) of the sanitizedNameCache, camelizedWordsCache and underscoreWords. * Set the cache expiry (in seconds) of the sanitizedNameCache, camelizedWordsCache and underscoreWordsCache.
*/ */
public static final String NAME_CACHE_EXPIRY_PROPERTY = "org.openapitools.codegen.utils.namecache.expireafter.seconds"; public static final String NAME_CACHE_EXPIRY_PROPERTY = "org.openapitools.codegen.utils.namecache.expireafter.seconds";
@ -31,7 +34,11 @@ public class StringUtils {
private static Cache<Pair<String, Boolean>, String> camelizedWordsCache; private static Cache<Pair<String, Boolean>, String> camelizedWordsCache;
// A cache of underscored words, used to optimize the performance of the underscore() method. // A cache of underscored words, used to optimize the performance of the underscore() method.
private static Cache<String, String> underscoreWords; private static Cache<String, String> underscoreWordsCache;
// A cache of escaped words, used to optimize the performance of the escape() method.
private static Cache<EscapedNameOptions, String> escapedWordsCache;
static { static {
int cacheSize = Integer.parseInt(GlobalSettings.getProperty(NAME_CACHE_SIZE_PROPERTY, "200")); int cacheSize = Integer.parseInt(GlobalSettings.getProperty(NAME_CACHE_SIZE_PROPERTY, "200"));
int cacheExpiry = Integer.parseInt(GlobalSettings.getProperty(NAME_CACHE_EXPIRY_PROPERTY, "5")); int cacheExpiry = Integer.parseInt(GlobalSettings.getProperty(NAME_CACHE_EXPIRY_PROPERTY, "5"));
@ -41,13 +48,24 @@ public class StringUtils {
.ticker(Ticker.systemTicker()) .ticker(Ticker.systemTicker())
.build(); .build();
underscoreWords = Caffeine.newBuilder() escapedWordsCache = Caffeine.newBuilder()
.maximumSize(cacheSize)
.expireAfterAccess(cacheExpiry, TimeUnit.SECONDS)
.ticker(Ticker.systemTicker())
.build();
underscoreWordsCache = Caffeine.newBuilder()
.maximumSize(cacheSize) .maximumSize(cacheSize)
.expireAfterAccess(cacheExpiry, TimeUnit.SECONDS) .expireAfterAccess(cacheExpiry, TimeUnit.SECONDS)
.ticker(Ticker.systemTicker()) .ticker(Ticker.systemTicker())
.build(); .build();
} }
private static Pattern capitalLetterPattern = Pattern.compile("([A-Z]+)([A-Z][a-z])");
private static Pattern lowercasePattern = Pattern.compile("([a-z\\d])([A-Z])");
private static Pattern pkgSeparatorPattern = Pattern.compile("\\.");
private static Pattern dollarPattern = Pattern.compile("\\$");
/** /**
* Underscore the given word. * Underscore the given word.
* Copied from Twitter elephant bird * Copied from Twitter elephant bird
@ -57,18 +75,16 @@ public class StringUtils {
* @return The underscored version of the word * @return The underscored version of the word
*/ */
public static String underscore(final String word) { public static String underscore(final String word) {
return underscoreWords.get(word, wordToUnderscore -> { return underscoreWordsCache.get(word, wordToUnderscore -> {
String result; String result;
String firstPattern = "([A-Z]+)([A-Z][a-z])";
String secondPattern = "([a-z\\d])([A-Z])";
String replacementPattern = "$1_$2"; String replacementPattern = "$1_$2";
// Replace package separator with slash. // Replace package separator with slash.
result = wordToUnderscore.replaceAll("\\.", "/"); result = pkgSeparatorPattern.matcher(wordToUnderscore).replaceAll("/");
// Replace $ with two underscores for inner classes. // Replace $ with two underscores for inner classes.
result = result.replaceAll("\\$", "__"); result = dollarPattern.matcher(result).replaceAll("__");
// Replace capital letter with _ plus lowercase letter. // Replace capital letter with _ plus lowercase letter.
result = result.replaceAll(firstPattern, replacementPattern); result = capitalLetterPattern.matcher(result).replaceAll(replacementPattern);
result = result.replaceAll(secondPattern, replacementPattern); result = lowercasePattern.matcher(result).replaceAll(replacementPattern);
result = result.replace('-', '_'); result = result.replace('-', '_');
// replace space with underscore // replace space with underscore
result = result.replace(' ', '_'); result = result.replace(' ', '_');
@ -103,6 +119,8 @@ public class StringUtils {
private static Pattern camelizeUppercasePattern = Pattern.compile("(\\.?)(\\w)([^\\.]*)$"); private static Pattern camelizeUppercasePattern = Pattern.compile("(\\.?)(\\w)([^\\.]*)$");
private static Pattern camelizeUnderscorePattern = Pattern.compile("(_)(.)"); private static Pattern camelizeUnderscorePattern = Pattern.compile("(_)(.)");
private static Pattern camelizeHyphenPattern = Pattern.compile("(-)(.)"); private static Pattern camelizeHyphenPattern = Pattern.compile("(-)(.)");
private static Pattern camelizeDollarPattern = Pattern.compile("\\$");
private static Pattern camelizeSimpleUnderscorePattern = Pattern.compile("_");
/** /**
* Camelize name (parameter, property, method, etc) * Camelize name (parameter, property, method, etc)
@ -144,7 +162,7 @@ public class StringUtils {
m = camelizeUppercasePattern.matcher(word); m = camelizeUppercasePattern.matcher(word);
if (m.find()) { if (m.find()) {
String rep = m.group(1) + m.group(2).toUpperCase(Locale.ROOT) + m.group(3); String rep = m.group(1) + m.group(2).toUpperCase(Locale.ROOT) + m.group(3);
rep = rep.replaceAll("\\$", "\\\\\\$"); rep = camelizeDollarPattern.matcher(rep).replaceAll("\\\\\\$");
word = m.replaceAll(rep); word = m.replaceAll(rep);
} }
@ -154,7 +172,7 @@ public class StringUtils {
String original = m.group(2); String original = m.group(2);
String upperCase = original.toUpperCase(Locale.ROOT); String upperCase = original.toUpperCase(Locale.ROOT);
if (original.equals(upperCase)) { if (original.equals(upperCase)) {
word = word.replaceFirst("_", ""); word = camelizeSimpleUnderscorePattern.matcher(word).replaceFirst("");
} else { } else {
word = m.replaceFirst(upperCase); word = m.replaceFirst(upperCase);
} }
@ -180,11 +198,49 @@ public class StringUtils {
} }
// remove all underscore // remove all underscore
word = word.replaceAll("_", ""); word = camelizeSimpleUnderscorePattern.matcher(word).replaceAll("");
return word; return word;
}); });
} }
private static class EscapedNameOptions {
public EscapedNameOptions(String name, Set<String> specialChars, List<String> charactersToAllow, String appendToReplacement) {
this.name = name;
this.appendToReplacement = appendToReplacement;
if (specialChars != null) {
this.specialChars = Collections.unmodifiableSet(specialChars);
} else {
this.specialChars = Collections.emptySet();
}
if (charactersToAllow != null) {
this.charactersToAllow = Collections.unmodifiableList(charactersToAllow);
} else {
this.charactersToAllow = Collections.emptyList();
}
}
private String name;
private String appendToReplacement;
private Set<String> specialChars;
private List<String> charactersToAllow;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EscapedNameOptions that = (EscapedNameOptions) o;
return Objects.equals(name, that.name) &&
Objects.equals(appendToReplacement, that.appendToReplacement) &&
Objects.equals(specialChars, that.specialChars) &&
Objects.equals(charactersToAllow, that.charactersToAllow);
}
@Override
public int hashCode() {
return Objects.hash(name, appendToReplacement, specialChars, charactersToAllow);
}
}
/** /**
* Return the name with escaped characters. * Return the name with escaped characters.
* *
@ -196,20 +252,23 @@ public class StringUtils {
* <p> * <p>
* throws Runtime exception as word is not escaped properly. * throws Runtime exception as word is not escaped properly.
*/ */
public static String escape(String name, Map<String, String> replacementMap, public static String escape(final String name, final Map<String, String> replacementMap,
List<String> charactersToAllow, String appendToReplacement) { final List<String> charactersToAllow, final String appendToReplacement) {
String result = name.chars().mapToObj(c -> { EscapedNameOptions ns = new EscapedNameOptions(name, replacementMap.keySet(), charactersToAllow, appendToReplacement);
String character = "" + (char) c; return escapedWordsCache.get(ns, wordToEscape -> {
if (charactersToAllow != null && charactersToAllow.contains(character)) { String result = name.chars().mapToObj(c -> {
return character; String character = "" + (char) c;
} else if (replacementMap.containsKey(character)) { if (charactersToAllow != null && charactersToAllow.contains(character)) {
return replacementMap.get(character) + (appendToReplacement != null ? appendToReplacement: ""); return character;
} else { } else if (replacementMap.containsKey(character)) {
return character; return replacementMap.get(character) + (appendToReplacement != null ? appendToReplacement: "");
} } else {
}).reduce( (c1, c2) -> "" + c1 + c2).orElse(null); return character;
}
if (result != null) return result; }).reduce( (c1, c2) -> "" + c1 + c2).orElse(null);
throw new RuntimeException("Word '" + name + "' could not be escaped.");
if (result != null) return result;
throw new RuntimeException("Word '" + name + "' could not be escaped.");
});
} }
} }