diff --git a/lib/codegen/benchmarkModelGenerator/benchmarkModelGenerator.js b/lib/codegen/benchmarkModelGenerator/benchmarkModelGenerator.js new file mode 100644 index 00000000..701362ea --- /dev/null +++ b/lib/codegen/benchmarkModelGenerator/benchmarkModelGenerator.js @@ -0,0 +1,407 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const { + uniqueNamesGenerator, adjectives, colors, animals +} = require('unique-names-generator'); + +const DEFAULT_CONCERTO_METAMODEL_VERSION = '1.0.0'; +const DEFAULT_MODEL_NAMESPACE = 'big.benchmark.model'; +const DEFAULT_MODEL_VERSION = '1.0.0'; + +/** + * Generate a benchmark model. + * + * @private + * @class + */ +class BenchmarkModelGenerator { + /** + * Formats the given byte size into a human-readable string. + * @param {number} bytes The number of bytes. + * @param {number} [decimals=2] The number of decimal places to include. + * @returns {string} A formatted string representing the byte size. + */ + formatBytes(bytes, decimals = 2) { + if (!+bytes) {return '0 Bytes';} + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; + } + + /** + * Calculates the size of a JSON object in bytes. + * @param {Object} json The JSON object to measure. + * @returns {number} The size of the JSON object in bytes. + */ + jsonSize(json) { + return new Blob( + [JSON.stringify(json)] + ).size; + } + + /** + * Gathers statistics about the number of properties in declarations. + * @param {Array} declarations An array of declaration objects. + * @returns {Object} An object containing statistics about the properties. + */ + gatherPropertiesNStats(declarations) { + const propertiesNInDeclarations = declarations.map( + declaration => declaration.properties.length + ); + + return { + propertiesNInSmallestDeclaration: Math.min(...propertiesNInDeclarations), + propertiesNInLargestDeclaration: Math.max(...propertiesNInDeclarations), + }; + } + + /** + * Generates a unique name based on a seed value. + * @param {string} seed A seed value to generate the name. + * @returns {string} A unique name. + */ + generateName(seed) { + return uniqueNamesGenerator({ + dictionaries: [adjectives, colors, animals], + style: 'capital', + separator: '', + seed, + }); + } + + /** + * Generates a property object. + * @param {Object} params An object containing indices for model, declaration, and property. + * @returns {Object} A property object. + */ + generateProperty({ + modelI, + declarationI, + propertyI, + }) { + return { + $class: 'concerto.metamodel@1.0.0.StringProperty', + name: this.generateName(`${modelI}.${declarationI}.${propertyI}`), + isArray: false, + isOptional: false, + }; + } + + /** + * Generates multiple property objects. + * @param {Object} params An object containing model index, declaration index, and the number of properties. + * @returns {Array} An array of property objects. + */ + generateNProperties({ + modelI, + declarationI, + nProperties, + }) { + let properties = []; + + for (let propertyI = 0; propertyI < nProperties; propertyI++) { + properties = [ + ...properties, + this.generateProperty({ + modelI, + declarationI, + propertyI, + }) + ]; + } + + return properties; + } + + /** + * Generates property objects up to a specified size. + * @param {Object} params An object containing model index, declaration index, and a size budget for properties. + * @returns {Array} An array of property objects within the specified size budget. + */ + generatePropertiesUpToSize({ + modelI, + declarationI, + propertiesSizeBudget, + }) { + let properties = []; + let oversized = false; + let propertyI = 0; + + while (!oversized) { + const propertiesWithNewAddition = [ + ...properties, + this.generateProperty({ + modelI, + declarationI, + propertyI, + }) + ]; + + if (this.jsonSize(propertiesWithNewAddition) <= propertiesSizeBudget) { + propertyI++; + properties = propertiesWithNewAddition; + } else { + oversized = true; + } + } + + return properties; + } + + /** + * Generates a declaration object with a specified number of properties. + * @param {Object} params An object containing model index, declaration index, and the number of properties. + * @returns {Object} A declaration object. + */ + generateDeclarationWithNProperties({ + modelI, + declarationI, + nProperties, + }) { + return { + $class: 'concerto.metamodel@1.0.0.ConceptDeclaration', + name: this.generateName(`${modelI}.${declarationI}`), + isAbstract: false, + properties: this.generateNProperties({ + modelI, + declarationI, + nProperties, + }) + }; + } + + /** + * Generates a declaration object to a specified size. + * @param {Object} params An object containing model index, declaration index, and a size budget for the declaration. + * @returns {Object} A declaration object within the specified size budget. + */ + generateDeclarationToSize({ + modelI, + declarationI, + declarationSizeBudget, + }) { + let declaration = { + $class: 'concerto.metamodel@1.0.0.ConceptDeclaration', + name: this.generateName(`${modelI}.${declarationI}`), + isAbstract: false, + properties: [], + }; + + declaration.properties = this.generatePropertiesUpToSize({ + modelI, + declarationI, + propertiesSizeBudget: declarationSizeBudget - this.jsonSize(declaration), + }); + + return declaration; + } + + /** + * Generates multiple declarations with properties, aiming to fit within a size budget by adding declarations. + * @param {Object} params An object containing model index, a size budget for declarations, and the number of properties per declaration. + * @returns {Array} An array of declaration objects. + */ + generateDeclarationsToSizeGrowByDeclarations({ + modelI, + declarationsSizeBudget, + nProperties, + }) { + let declarations = []; + let oversized = false; + let declarationI = 0; + + while (!oversized) { + const declarationsWithNewAddition = [ + ...declarations, + this.generateDeclarationWithNProperties({ + modelI, + declarationI, + nProperties, + }) + ]; + + if (this.jsonSize(declarationsWithNewAddition) <= declarationsSizeBudget) { + declarationI++; + declarations = declarationsWithNewAddition; + } else { + oversized = true; + } + } + + return declarations; + } + + /** + * Generates multiple declarations with properties, distributing the size budget across properties within declarations. + * @param {Object} params An object containing model index, a size budget for declarations, and the number of declarations. + * @returns {Array} An array of declaration objects. + */ + generateDeclarationsToSizeGrowByProperties({ + modelI, + declarationsSizeBudget, + nDeclarations, + }) { + let declarations = []; + let remainingDeclarationSizeBudget; + let declarationI = 0; + + while (nDeclarations - declarationI > 0) { + remainingDeclarationSizeBudget = ( + declarationsSizeBudget - this.jsonSize(declarations) + ) / (nDeclarations - declarationI); + + const declaration = this.generateDeclarationToSize({ + modelI, + declarationI, + declarationSizeBudget: remainingDeclarationSizeBudget, + }); + declarationI++; + declarations = [ ...declarations, declaration ]; + } + + return declarations; + } + + /** + * Generates declarations according to a specified size budget and growth strategy. + * @param {Object} params An object containing model index, size budget, number of declarations, number of properties, and growth strategy. + * @returns {Array} An array of declaration objects according to the specified growth strategy. + */ + generateDeclarationsToSize({ + modelI, + declarationsSizeBudget, + nDeclarations, + nProperties, + growBy, + }) { + switch(growBy) { + case 'declarations': + return this.generateDeclarationsToSizeGrowByDeclarations({ + modelI, + declarationsSizeBudget, + nProperties, + }); + case 'properties': + return this.generateDeclarationsToSizeGrowByProperties({ + modelI, + declarationsSizeBudget, + nDeclarations, + }); + default: + throw new Error('growBy can be either set to "declarations" or "properties".'); + } + } + + /** + * Generates a specified number of declarations, each with a fixed number of properties. + * @param {Object} params An object containing model index, the number of declarations, and the number of properties per declaration. + * @returns {Array} An array of declaration objects. + */ + generateNDeclarations({ + modelI, + nDeclarations, + nProperties, + }) { + let declarations = []; + + for (let declarationI = 0; declarationI < nDeclarations; declarationI++) { + declarations = [ + ...declarations, + this.generateDeclarationWithNProperties({ + modelI, + declarationI, + nProperties, + }) + ]; + } + + return declarations; + } + + /** + * Generates a Concerto model with specified parameters, optionally generating up to a size limit. + * @param {Object} params An object containing parameters for the model generation, such as metamodel version, namespace, version, indices, and size constraints. + * @returns {Object} An object containing the generated model and metadata about the generation process. + * @public + */ + generateConcertoModel({ + concertoMetamodelVersion = DEFAULT_CONCERTO_METAMODEL_VERSION, + modelNamespace = DEFAULT_MODEL_NAMESPACE, + modelVersion = DEFAULT_MODEL_VERSION, + modelI = 0, + nDeclarations = 1, + nProperties = 1, + generateUpToSize, + growBy = 'declarations' + }) { + const model = { + $class: `concerto.metamodel@${concertoMetamodelVersion}.Model`, + decorators: [], + namespace: `${modelNamespace}@${modelVersion}`, + imports: [], + declarations: [] + }; + + if (generateUpToSize) { + const modelSizeWithoutDeclarations = this.jsonSize(model); + + if (modelSizeWithoutDeclarations < generateUpToSize) { + model.declarations = this.generateDeclarationsToSize({ + modelI, + declarationsSizeBudget: generateUpToSize - modelSizeWithoutDeclarations, + nDeclarations, + nProperties, + growBy, + }); + } + } else { + model.declarations = this.generateNDeclarations({ + modelI, + nDeclarations, + nProperties, + }); + } + + const generatedModelSizeInBytes = this.jsonSize(model); + + this.gatherPropertiesNStats(model.declarations); + const { + propertiesNInSmallestDeclaration, + propertiesNInLargestDeclaration, + } = this.gatherPropertiesNStats(model.declarations); + + return { + model, + metadata: { + requestedModelSizeInBytes: generateUpToSize, + humanReadableRequestedModelSize: this.formatBytes(generateUpToSize, 2), + generatedModelSizeInBytes, + humanReadableGeneratedModelSize: this.formatBytes(generatedModelSizeInBytes, 2), + declarationsN: model.declarations.length, + propertiesNInSmallestDeclaration, + propertiesNInLargestDeclaration, + } + }; + } +} + +module.exports = BenchmarkModelGenerator; diff --git a/lib/codegen/codegen.js b/lib/codegen/codegen.js index 646b372f..0c5320ac 100644 --- a/lib/codegen/codegen.js +++ b/lib/codegen/codegen.js @@ -36,6 +36,7 @@ const JSONSchemaToConcertoVisitor = require( const OpenApiToConcertoVisitor = require('./fromOpenApi/cto/openApiVisitor'); const RustVisitor = require('./fromcto/rust/rustvisitor'); const VocabularyVisitor = require('./fromcto/vocabulary/vocabularyvisitor'); +const BenchmarkModelGenerator = require('./benchmarkModelGenerator/benchmarkModelGenerator'); module.exports = { AbstractPlugin, @@ -57,6 +58,7 @@ module.exports = { OpenApiToConcertoVisitor, RustVisitor, VocabularyVisitor, + BenchmarkModelGenerator, formats: { golang: GoLangVisitor, jsonschema: JSONSchemaVisitor, @@ -73,6 +75,7 @@ module.exports = { openapi: OpenApiVisitor, avro: AvroVisitor, rust: RustVisitor, - vocabulary: VocabularyVisitor + vocabulary: VocabularyVisitor, + benchmarkModelGenerator: BenchmarkModelGenerator, } }; diff --git a/package-lock.json b/package-lock.json index 61a1ad01..4aff933d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,8 @@ "debug": "4.3.1", "get-value": "3.0.1", "json-schema-migrate": "2.0.0", - "pluralize": "8.0.0" + "pluralize": "8.0.0", + "unique-names-generator": "4.7.1" }, "devDependencies": { "@accordproject/concerto-cto": "3.16.4", @@ -8059,6 +8060,14 @@ "node": ">=4" } }, + "node_modules/unique-names-generator": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", + "integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==", + "engines": { + "node": ">=8" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", diff --git a/package.json b/package.json index 43a6f245..8296750a 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,8 @@ "debug": "4.3.1", "get-value": "3.0.1", "json-schema-migrate": "2.0.0", - "pluralize": "8.0.0" + "pluralize": "8.0.0", + "unique-names-generator": "4.7.1" }, "license-check-and-add-config": { "folder": "./lib",