From 2446a58f868f2275244a3d6ec00dfcd5851d9993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:52:40 +0200 Subject: [PATCH 1/5] Improvements of lookup of objects metadata speeding up preview --- .../CodeGeneration/EventsCodeGenerator.cpp | 32 +++++++++++++++---- .../CodeGeneration/EventsCodeGenerator.h | 30 +++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp index 147a1b67e3c2..77d7bc4d46ea 100644 --- a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp +++ b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp @@ -305,6 +305,26 @@ gd::String EventsCodeGenerator::GenerateMutatorCall( rhs + ")"; } +const gd::String& EventsCodeGenerator::GetCachedTypeOfObject( + const gd::String& objectName) const { + auto it = cachedObjectTypes.find(objectName); + if (it != cachedObjectTypes.end()) return it->second; + + return cachedObjectTypes[objectName] = + GetObjectsContainersList().GetTypeOfObject(objectName); +} + +const gd::ObjectMetadata& EventsCodeGenerator::GetCachedObjectMetadata( + const gd::String& objectType) const { + auto it = cachedObjectMetadata.find(objectType); + if (it != cachedObjectMetadata.end()) return *it->second; + + const gd::ObjectMetadata& metadata = + MetadataProvider::GetObjectMetadata(platform, objectType); + cachedObjectMetadata[objectType] = &metadata; + return metadata; +} + gd::String EventsCodeGenerator::GenerateConditionCode( gd::Instruction& condition, gd::String returnBoolean, @@ -351,7 +371,7 @@ gd::String EventsCodeGenerator::GenerateConditionCode( const auto& expectedObjectType = instrInfos.parameters.GetParameter(pNb).GetExtraInfo(); const auto& actualObjectType = - GetObjectsContainersList().GetTypeOfObject(objectInParameter); + GetCachedTypeOfObject(objectInParameter); if (!GetObjectsContainersList().HasObjectOrGroupNamed( objectInParameter)) { gd::ProjectDiagnostic projectDiagnostic( @@ -388,9 +408,9 @@ gd::String EventsCodeGenerator::GenerateConditionCode( for (std::size_t i = 0; i < realObjects.size(); ++i) { // Set up the context gd::String objectType = - GetObjectsContainersList().GetTypeOfObject(realObjects[i]); + GetCachedTypeOfObject(realObjects[i]); const ObjectMetadata& objInfo = - MetadataProvider::GetObjectMetadata(platform, objectType); + GetCachedObjectMetadata(objectType); AddIncludeFiles(objInfo.includeFiles); context.SetCurrentObject(realObjects[i]); @@ -638,7 +658,7 @@ gd::String EventsCodeGenerator::GenerateActionCode( const auto& expectedObjectType = instrInfos.parameters.GetParameter(pNb).GetExtraInfo(); const auto& actualObjectType = - GetObjectsContainersList().GetTypeOfObject(objectInParameter); + GetCachedTypeOfObject(objectInParameter); if (!GetObjectsContainersList().HasObjectOrGroupNamed( objectInParameter)) { gd::ProjectDiagnostic projectDiagnostic( @@ -677,9 +697,9 @@ gd::String EventsCodeGenerator::GenerateActionCode( for (std::size_t i = 0; i < realObjects.size(); ++i) { // Setup context gd::String objectType = - GetObjectsContainersList().GetTypeOfObject(realObjects[i]); + GetCachedTypeOfObject(realObjects[i]); const ObjectMetadata& objInfo = - MetadataProvider::GetObjectMetadata(platform, objectType); + GetCachedObjectMetadata(objectType); AddIncludeFiles(objInfo.includeFiles); context.SetCurrentObject(realObjects[i]); diff --git a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h index 8dbee6643948..bbfe5b38dced 100644 --- a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h +++ b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h @@ -6,6 +6,7 @@ #pragma once #include +#include #include #include @@ -847,10 +848,39 @@ class GD_CORE_API EventsCodeGenerator { const gd::String &objectName, const gd::Instruction &instruction, const gd::InstructionMetadata &instrInfos, bool isObjectInGroup); + /** + * \brief Return the type of an object, memoizing the result for the lifetime + * of this code generator. + * + * `gd::ObjectsContainersList::GetTypeOfObject` does linear scans over the + * objects (and, for groups, over all their members), which becomes very + * expensive when many events reference the same objects or large groups. The + * objects are immutable during code generation, so caching object name -> + * type turns these repeated O(objects) lookups into O(1). + */ + const gd::String &GetCachedTypeOfObject(const gd::String &objectName) const; + + /** + * \brief Return the metadata of an object type, memoizing the result for the + * lifetime of this code generator. + * + * `gd::MetadataProvider::GetObjectMetadata` linearly scans every platform + * extension (rebuilding a types list each call) to find the type. This is + * called once per object (and per group member) during code generation, so + * caching object type -> metadata avoids repeating the scan. + */ + const gd::ObjectMetadata &GetCachedObjectMetadata( + const gd::String &objectType) const; + const gd::Platform& platform; ///< The platform being used. gd::ProjectScopedContainers projectScopedContainers; + mutable std::unordered_map + cachedObjectTypes; ///< Memoization of GetTypeOfObject (see above). + mutable std::unordered_map + cachedObjectMetadata; ///< Memoization of GetObjectMetadata (see above). + bool hasProjectAndLayout; ///< true only if project and layout are valid ///< references. If false, they should not be used. const gd::Project* project; ///< The project being used. From f36d470a74f3311540433b7c44858c307d6df6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:17:44 +0200 Subject: [PATCH 2/5] Prevent looking up resource for non-resources --- Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp b/Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp index 712cca772fd2..2dd5d7a1dc98 100644 --- a/Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp +++ b/Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp @@ -129,7 +129,6 @@ void ArbitraryResourceWorker::ExposeEmbeddeds(gd::String& resourceName) { child.second->GetValue().GetString(); if (resourcesManager->HasResource(targetResourceName)) { - std::cout << targetResourceName << std::endl; gd::Resource& targetResource = resourcesManager->GetResource(targetResourceName); @@ -234,6 +233,14 @@ bool ResourceWorkerInEventsWorker::DoVisitInstruction(gd::Instruction& instructi const gd::ParameterMetadata ¶meterMetadata, const gd::Expression ¶meterExpression, size_t parameterIndex, const gd::String &lastObjectName, size_t lastObjectIndex) { + // Only resource parameters can refer to a resource. Checking this + // before looking the value up in the resources containers avoids an + // expensive (linear) resources lookup for the many non-resource + // parameters (numbers, strings, objects, expressions...). + if (!parameterMetadata.GetValueTypeMetadata().IsResource()) { + return; + } + const String& parameterValue = parameterExpression.GetPlainString(); const auto resourceSourceType = resourcesContainersList.GetResourcesContainerSourceType( From 967306461b2c9cde7e20a7d38d6d5a79bc6f6121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:24:00 +0200 Subject: [PATCH 3/5] Add scripts to benchmark resource finding and events code generation These synthetic benchmarks complement scripts/profile-events-code-generation.js (which profiles a real project) by isolating how resource finding and events code generation scale with project size, making perf regressions easy to spot. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../scripts/bench-events-code-generation.js | 122 ++++++++++++++++++ GDevelop.js/scripts/bench-resource-finding.js | 105 +++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 GDevelop.js/scripts/bench-events-code-generation.js create mode 100644 GDevelop.js/scripts/bench-resource-finding.js diff --git a/GDevelop.js/scripts/bench-events-code-generation.js b/GDevelop.js/scripts/bench-events-code-generation.js new file mode 100644 index 000000000000..73db59927434 --- /dev/null +++ b/GDevelop.js/scripts/bench-events-code-generation.js @@ -0,0 +1,122 @@ +// Benchmark events code generation on synthetic scenes (the work done by +// "Events code export" on every preview/export), scaling objects, events and +// object-group usage independently. +// +// This isolates how code generation scales, which is useful to catch +// regressions in the per-instruction work. In particular, object/behavior type +// resolution (gd::ObjectsContainersList::GetTypeOfObject) is a linear scan over +// the objects, so referencing a large object GROUP in many events is the +// pathological case (each reference expands to every member, each resolved with +// an O(objects) scan). +// +// To profile a REAL project's per-scene code generation instead, see +// scripts/profile-events-code-generation.js. +// +// Requires GDevelop.js to be built (Binaries/embuild/GDevelop.js/libGD.js). +// +// Usage: +// node scripts/bench-events-code-generation.js + +const path = require('path'); +const init = require( + path.join(__dirname, '../../Binaries/embuild/GDevelop.js/libGD.js') +); + +// Build a single scene with `numObjects` objects and `numEvents` events. Each +// event has a position condition targeting either a single object or a group +// containing all objects (`useGroup`), and a variable action whose expression +// is `exprLen` operands long. +function buildLayout(gd, project, name, numObjects, numEvents, useGroup, exprLen) { + const layout = project.insertNewLayout(name, project.getLayoutsCount()); + for (let o = 0; o < numObjects; o++) { + layout.getObjects().insertNewObject(project, 'Sprite', 'Obj' + o, o); + } + if (useGroup) { + const group = layout.getObjects().getObjectGroups().insertNew('AllObjects', 0); + for (let o = 0; o < numObjects; o++) group.addObject('Obj' + o); + } + const target = useGroup ? 'AllObjects' : 'Obj0'; + + const events = layout.getEvents(); + for (let e = 0; e < numEvents; e++) { + const event = gd.asStandardEvent( + events.insertNewEvent(project, 'BuiltinCommonInstructions::Standard', e) + ); + + const condition = new gd.Instruction(); + condition.setType('PosX'); + condition.setParametersCount(3); + condition.setParameter(0, target); + condition.setParameter(1, '<'); + condition.setParameter(2, '100'); + event.getConditions().insert(condition, 0); + condition.delete(); + + const expression = + exprLen > 0 + ? Array.from({ length: exprLen }, (_, i) => (i % 9) + 1).join('+') + : '1'; + const action = new gd.Instruction(); + action.setType('BuiltinCommonInstructions::SetNumberVariable'); + action.setParametersCount(3); + action.setParameter(0, 'MyVar'); + action.setParameter(1, '='); + action.setParameter(2, expression); + event.getActions().insert(action, 0); + action.delete(); + } + return layout; +} + +function timeCodegen(gd, project, layout, runs) { + let best = Infinity; + for (let i = 0; i < runs; i++) { + const includeFiles = new gd.SetString(); + const generator = new gd.LayoutCodeGenerator(project); + const report = new gd.DiagnosticReport(); + const t = process.hrtime.bigint(); + generator.generateLayoutCompleteCode(layout, includeFiles, report, true); + best = Math.min(best, Number(process.hrtime.bigint() - t) / 1e6); + generator.delete(); + includeFiles.delete(); + report.delete(); + } + return best; +} + +(async () => { + const gd = await init({ print: () => {}, printErr: () => {} }); + + const scenarios = [ + // Vary object count, fixed events (single-object reference -> flat). + { label: 'objects', objects: 500, events: 2000, group: false, exprLen: 1 }, + { label: 'objects', objects: 2000, events: 2000, group: false, exprLen: 1 }, + // Vary event count (linear). + { label: 'events', objects: 200, events: 2000, group: false, exprLen: 1 }, + { label: 'events', objects: 200, events: 8000, group: false, exprLen: 1 }, + // Object groups: each reference expands to every member (pathological). + { label: 'group', objects: 100, events: 200, group: true, exprLen: 1 }, + { label: 'group', objects: 200, events: 200, group: true, exprLen: 1 }, + { label: 'group', objects: 400, events: 200, group: true, exprLen: 1 }, + // Large expressions. + { label: 'big expr', objects: 50, events: 200, group: false, exprLen: 4000 }, + ]; + + console.log('scenario | objects | events | group | exprLen | codegen ms'); + let i = 0; + for (const s of scenarios) { + const project = gd.ProjectHelper.createNewGDJSProject(); + const layout = buildLayout( + gd, project, 'Scene' + i++, s.objects, s.events, s.group, s.exprLen + ); + const ms = timeCodegen(gd, project, layout, 2); + console.log( + `${s.label.padEnd(8)} | ${String(s.objects).padStart(7)} | ${String( + s.events + ).padStart(6)} | ${String(s.group).padStart(5)} | ${String( + s.exprLen + ).padStart(7)} | ${ms.toFixed(0).padStart(8)}` + ); + project.delete(); + } +})(); diff --git a/GDevelop.js/scripts/bench-resource-finding.js b/GDevelop.js/scripts/bench-resource-finding.js new file mode 100644 index 000000000000..e2e2c7d5f782 --- /dev/null +++ b/GDevelop.js/scripts/bench-resource-finding.js @@ -0,0 +1,105 @@ +// Benchmark resource finding on a synthetic project (the work done by +// "Resource export" and "Project data export" on every preview/export). +// +// It runs `gd::ResourceExposer::ExposeWholeProjectResources` (which walks every +// scene's objects and events looking for used resources) and times it while +// scaling the number of resources. This is handy to catch regressions where a +// per-parameter/per-object resource lookup becomes expensive: resource +// containers look resources up with a linear scan, so an O(1)-looking call in a +// hot loop is actually O(resources). +// +// To profile a REAL project's per-scene code generation instead, see +// scripts/profile-events-code-generation.js. +// +// Requires GDevelop.js to be built (Binaries/embuild/GDevelop.js/libGD.js). +// +// Usage: +// node scripts/bench-resource-finding.js [scenes] [objects] [events] +// [scenes] Scenes in the project. Default: 10. +// [objects] Objects per scene. Default: 50. +// [events] Events per scene (each with non-resource parameters, the +// worst case for the resource lookup). Default: 600. + +const path = require('path'); +const init = require( + path.join(__dirname, '../../Binaries/embuild/GDevelop.js/libGD.js') +); + +const numScenes = Math.max(1, parseInt(process.argv[2], 10) || 10); +const numObjects = Math.max(1, parseInt(process.argv[3], 10) || 50); +const numEvents = Math.max(1, parseInt(process.argv[4], 10) || 600); + +// Build a project with `numResources` image resources and, in every scene, +// `numObjects` objects and `numEvents` events made of non-resource instructions +// (a variable action and an object condition). Non-resource parameters are the +// worst case: each one is looked up against every resource and not found. +function buildProject(gd, numResources) { + const project = gd.ProjectHelper.createNewGDJSProject(); + const resourcesManager = project.getResourcesManager(); + for (let r = 0; r < numResources; r++) { + const resource = new gd.ImageResource(); + resource.setName('res' + r + '.png'); + resourcesManager.addResource(resource); + resource.delete(); + } + + for (let s = 0; s < numScenes; s++) { + const layout = project.insertNewLayout('Scene' + s, project.getLayoutsCount()); + for (let o = 0; o < numObjects; o++) { + layout.getObjects().insertNewObject(project, 'Sprite', 'Obj' + o, o); + } + const events = layout.getEvents(); + for (let e = 0; e < numEvents; e++) { + const event = gd.asStandardEvent( + events.insertNewEvent(project, 'BuiltinCommonInstructions::Standard', e) + ); + + const action = new gd.Instruction(); + action.setType('BuiltinCommonInstructions::SetNumberVariable'); + action.setParametersCount(3); + action.setParameter(0, 'MyVar'); + action.setParameter(1, '='); + action.setParameter(2, String(e % 100)); + event.getActions().insert(action, 0); + action.delete(); + + const condition = new gd.Instruction(); + condition.setType('PosX'); + condition.setParametersCount(3); + condition.setParameter(0, 'Obj' + (e % numObjects)); + condition.setParameter(1, '<'); + condition.setParameter(2, '100'); + event.getConditions().insert(condition, 0); + condition.delete(); + } + } + return project; +} + +function timeResourceFinding(gd, project, runs) { + let best = Infinity; + for (let i = 0; i < runs; i++) { + const worker = new gd.ResourcesInUseHelper(project.getResourcesManager()); + const t = process.hrtime.bigint(); + gd.ResourceExposer.exposeWholeProjectResources(project, worker); + best = Math.min(best, Number(process.hrtime.bigint() - t) / 1e6); + worker.delete(); + } + return best; +} + +(async () => { + const gd = await init({ print: () => {}, printErr: () => {} }); + + console.log( + `Scenes: ${numScenes}, objects/scene: ${numObjects}, events/scene: ${numEvents}` + ); + console.log(''); + console.log('resources | resource finding (best of 3)'); + for (const numResources of [200, 400, 800, 1600, 3200]) { + const project = buildProject(gd, numResources); + const ms = timeResourceFinding(gd, project, 3); + console.log(`${String(numResources).padStart(9)} | ${ms.toFixed(0).padStart(6)} ms`); + project.delete(); + } +})(); From 60cb6d00594055be854d3093254bd3c109c62023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:06:07 +0200 Subject: [PATCH 4/5] Make MetadataProvider lookups constant-time; drop codegen metadata/type caches Introduce gd::PlatformMetadataIndex, a hash-map index of all metadata declared by a platform's extensions, built lazily and owned by gd::Platform. It is discarded whenever extensions change (Platform::AddExtension / RemoveExtension, which also covers replacement/regeneration), so it never returns stale metadata. Lookup semantics are unchanged (first extension wins; object/behavior expressions resolve the specific type before the base). This replaces the per-code-generation caches added earlier: - the object-metadata cache is now redundant (MetadataProvider is O(1)); - the object-type cache only helped pathological object groups and had no measurable effect on real projects, so it is removed for simplicity. Fixing the lookup at the source instead measurably speeds up resource finding (per-instruction metadata lookups) and any project with many extensions, where the previous O(extensions) scan grew without bound. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CodeGeneration/EventsCodeGenerator.cpp | 32 +- .../CodeGeneration/EventsCodeGenerator.h | 30 - .../Extensions/Metadata/MetadataProvider.cpp | 842 ++++++++---------- .../Metadata/PlatformMetadataIndex.cpp | 212 +++++ .../Metadata/PlatformMetadataIndex.h | 119 +++ Core/GDCore/Extensions/Platform.cpp | 15 + Core/GDCore/Extensions/Platform.h | 16 + .../__tests__/MetadataProviderIndex.js | 140 +++ 8 files changed, 873 insertions(+), 533 deletions(-) create mode 100644 Core/GDCore/Extensions/Metadata/PlatformMetadataIndex.cpp create mode 100644 Core/GDCore/Extensions/Metadata/PlatformMetadataIndex.h create mode 100644 GDevelop.js/__tests__/MetadataProviderIndex.js diff --git a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp index 77d7bc4d46ea..147a1b67e3c2 100644 --- a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp +++ b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.cpp @@ -305,26 +305,6 @@ gd::String EventsCodeGenerator::GenerateMutatorCall( rhs + ")"; } -const gd::String& EventsCodeGenerator::GetCachedTypeOfObject( - const gd::String& objectName) const { - auto it = cachedObjectTypes.find(objectName); - if (it != cachedObjectTypes.end()) return it->second; - - return cachedObjectTypes[objectName] = - GetObjectsContainersList().GetTypeOfObject(objectName); -} - -const gd::ObjectMetadata& EventsCodeGenerator::GetCachedObjectMetadata( - const gd::String& objectType) const { - auto it = cachedObjectMetadata.find(objectType); - if (it != cachedObjectMetadata.end()) return *it->second; - - const gd::ObjectMetadata& metadata = - MetadataProvider::GetObjectMetadata(platform, objectType); - cachedObjectMetadata[objectType] = &metadata; - return metadata; -} - gd::String EventsCodeGenerator::GenerateConditionCode( gd::Instruction& condition, gd::String returnBoolean, @@ -371,7 +351,7 @@ gd::String EventsCodeGenerator::GenerateConditionCode( const auto& expectedObjectType = instrInfos.parameters.GetParameter(pNb).GetExtraInfo(); const auto& actualObjectType = - GetCachedTypeOfObject(objectInParameter); + GetObjectsContainersList().GetTypeOfObject(objectInParameter); if (!GetObjectsContainersList().HasObjectOrGroupNamed( objectInParameter)) { gd::ProjectDiagnostic projectDiagnostic( @@ -408,9 +388,9 @@ gd::String EventsCodeGenerator::GenerateConditionCode( for (std::size_t i = 0; i < realObjects.size(); ++i) { // Set up the context gd::String objectType = - GetCachedTypeOfObject(realObjects[i]); + GetObjectsContainersList().GetTypeOfObject(realObjects[i]); const ObjectMetadata& objInfo = - GetCachedObjectMetadata(objectType); + MetadataProvider::GetObjectMetadata(platform, objectType); AddIncludeFiles(objInfo.includeFiles); context.SetCurrentObject(realObjects[i]); @@ -658,7 +638,7 @@ gd::String EventsCodeGenerator::GenerateActionCode( const auto& expectedObjectType = instrInfos.parameters.GetParameter(pNb).GetExtraInfo(); const auto& actualObjectType = - GetCachedTypeOfObject(objectInParameter); + GetObjectsContainersList().GetTypeOfObject(objectInParameter); if (!GetObjectsContainersList().HasObjectOrGroupNamed( objectInParameter)) { gd::ProjectDiagnostic projectDiagnostic( @@ -697,9 +677,9 @@ gd::String EventsCodeGenerator::GenerateActionCode( for (std::size_t i = 0; i < realObjects.size(); ++i) { // Setup context gd::String objectType = - GetCachedTypeOfObject(realObjects[i]); + GetObjectsContainersList().GetTypeOfObject(realObjects[i]); const ObjectMetadata& objInfo = - GetCachedObjectMetadata(objectType); + MetadataProvider::GetObjectMetadata(platform, objectType); AddIncludeFiles(objInfo.includeFiles); context.SetCurrentObject(realObjects[i]); diff --git a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h index bbfe5b38dced..8dbee6643948 100644 --- a/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h +++ b/Core/GDCore/Events/CodeGeneration/EventsCodeGenerator.h @@ -6,7 +6,6 @@ #pragma once #include -#include #include #include @@ -848,39 +847,10 @@ class GD_CORE_API EventsCodeGenerator { const gd::String &objectName, const gd::Instruction &instruction, const gd::InstructionMetadata &instrInfos, bool isObjectInGroup); - /** - * \brief Return the type of an object, memoizing the result for the lifetime - * of this code generator. - * - * `gd::ObjectsContainersList::GetTypeOfObject` does linear scans over the - * objects (and, for groups, over all their members), which becomes very - * expensive when many events reference the same objects or large groups. The - * objects are immutable during code generation, so caching object name -> - * type turns these repeated O(objects) lookups into O(1). - */ - const gd::String &GetCachedTypeOfObject(const gd::String &objectName) const; - - /** - * \brief Return the metadata of an object type, memoizing the result for the - * lifetime of this code generator. - * - * `gd::MetadataProvider::GetObjectMetadata` linearly scans every platform - * extension (rebuilding a types list each call) to find the type. This is - * called once per object (and per group member) during code generation, so - * caching object type -> metadata avoids repeating the scan. - */ - const gd::ObjectMetadata &GetCachedObjectMetadata( - const gd::String &objectType) const; - const gd::Platform& platform; ///< The platform being used. gd::ProjectScopedContainers projectScopedContainers; - mutable std::unordered_map - cachedObjectTypes; ///< Memoization of GetTypeOfObject (see above). - mutable std::unordered_map - cachedObjectMetadata; ///< Memoization of GetObjectMetadata (see above). - bool hasProjectAndLayout; ///< true only if project and layout are valid ///< references. If false, they should not be used. const gd::Project* project; ///< The project being used. diff --git a/Core/GDCore/Extensions/Metadata/MetadataProvider.cpp b/Core/GDCore/Extensions/Metadata/MetadataProvider.cpp index 72cf108e7ae8..9720becba0a7 100644 --- a/Core/GDCore/Extensions/Metadata/MetadataProvider.cpp +++ b/Core/GDCore/Extensions/Metadata/MetadataProvider.cpp @@ -1,477 +1,365 @@ -/* - * GDevelop Core - * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights - * reserved. This project is released under the MIT License. - */ -#include "GDCore/Extensions/Metadata/MetadataProvider.h" - -#include - -#include "GDCore/Extensions/Metadata/BehaviorMetadata.h" -#include "GDCore/Extensions/Metadata/EffectMetadata.h" -#include "GDCore/Extensions/Metadata/InstructionMetadata.h" -#include "GDCore/Extensions/Metadata/ObjectMetadata.h" -#include "GDCore/Extensions/Platform.h" -#include "GDCore/Extensions/PlatformExtension.h" -#include "GDCore/Project/Layout.h" // For GetTypeOfObject and GetTypeOfBehavior -#include "GDCore/Project/ObjectsContainersList.h" -#include "GDCore/String.h" -#include "GDCore/Events/Parsers/ExpressionParser2.h" - -using namespace std; - -namespace gd { - -gd::BehaviorMetadata MetadataProvider::badBehaviorMetadata; -gd::ObjectMetadata MetadataProvider::badObjectInfo; -gd::EffectMetadata MetadataProvider::badEffectMetadata; -gd::InstructionMetadata MetadataProvider::badInstructionMetadata; -gd::ExpressionMetadata MetadataProvider::badExpressionMetadata; -gd::PlatformExtension MetadataProvider::badExtension; - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndBehaviorMetadata(const gd::Platform& platform, - gd::String behaviorType) { - for (auto& extension : platform.GetAllPlatformExtensions()) { - if (extension->HasBehavior(behaviorType)) - return ExtensionAndMetadata( - *extension, extension->GetBehaviorMetadata(behaviorType)); - } - - return ExtensionAndMetadata(badExtension, badBehaviorMetadata); -} - -const BehaviorMetadata& MetadataProvider::GetBehaviorMetadata( - const gd::Platform& platform, gd::String behaviorType) { - return GetExtensionAndBehaviorMetadata(platform, behaviorType).GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndObjectMetadata(const gd::Platform& platform, - gd::String objectType) { - for (auto& extension : platform.GetAllPlatformExtensions()) { - auto objectsTypes = extension->GetExtensionObjectsTypes(); - for (std::size_t j = 0; j < objectsTypes.size(); ++j) { - if (objectsTypes[j] == objectType) - return ExtensionAndMetadata( - *extension, extension->GetObjectMetadata(objectType)); - } - } - - return ExtensionAndMetadata(badExtension, badObjectInfo); -} - -const ObjectMetadata& MetadataProvider::GetObjectMetadata( - const gd::Platform& platform, gd::String objectType) { - return GetExtensionAndObjectMetadata(platform, objectType).GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndEffectMetadata(const gd::Platform& platform, - gd::String type) { - for (auto& extension : platform.GetAllPlatformExtensions()) { - auto objectsTypes = extension->GetExtensionEffectTypes(); - for (std::size_t j = 0; j < objectsTypes.size(); ++j) { - if (objectsTypes[j] == type) - return ExtensionAndMetadata( - *extension, extension->GetEffectMetadata(type)); - } - } - - return ExtensionAndMetadata(badExtension, badEffectMetadata); -} - -const EffectMetadata& MetadataProvider::GetEffectMetadata( - const gd::Platform& platform, gd::String objectType) { - return GetExtensionAndEffectMetadata(platform, objectType).GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndActionMetadata(const gd::Platform& platform, - gd::String actionType) { - auto& extensions = platform.GetAllPlatformExtensions(); - for (auto& extension : extensions) { - const auto& allActions = extension->GetAllActions(); - if (allActions.find(actionType) != allActions.end()) - return ExtensionAndMetadata( - *extension, allActions.find(actionType)->second); - - const auto& objects = extension->GetExtensionObjectsTypes(); - for (const gd::String& extObjectType : objects) { - const auto& allObjectsActions = - extension->GetAllActionsForObject(extObjectType); - if (allObjectsActions.find(actionType) != allObjectsActions.end()) - return ExtensionAndMetadata( - *extension, allObjectsActions.find(actionType)->second); - } - - const auto& autos = extension->GetBehaviorsTypes(); - for (std::size_t j = 0; j < autos.size(); ++j) { - const auto& allAutosActions = - extension->GetAllActionsForBehavior(autos[j]); - if (allAutosActions.find(actionType) != allAutosActions.end()) - return ExtensionAndMetadata( - *extension, allAutosActions.find(actionType)->second); - } - } - - return ExtensionAndMetadata(badExtension, - badInstructionMetadata); -} - -const gd::InstructionMetadata& MetadataProvider::GetActionMetadata( - const gd::Platform& platform, gd::String actionType) { - return GetExtensionAndActionMetadata(platform, actionType).GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndConditionMetadata(const gd::Platform& platform, - gd::String conditionType) { - auto& extensions = platform.GetAllPlatformExtensions(); - for (auto& extension : extensions) { - const auto& allConditions = extension->GetAllConditions(); - if (allConditions.find(conditionType) != allConditions.end()) - return ExtensionAndMetadata( - *extension, allConditions.find(conditionType)->second); - - const auto& objects = extension->GetExtensionObjectsTypes(); - for (const gd::String& extObjectType : objects) { - const auto& allObjectsConditions = - extension->GetAllConditionsForObject(extObjectType); - if (allObjectsConditions.find(conditionType) != allObjectsConditions.end()) - return ExtensionAndMetadata( - *extension, allObjectsConditions.find(conditionType)->second); - } - - const auto& autos = extension->GetBehaviorsTypes(); - for (std::size_t j = 0; j < autos.size(); ++j) { - const auto& allAutosConditions = - extension->GetAllConditionsForBehavior(autos[j]); - if (allAutosConditions.find(conditionType) != allAutosConditions.end()) - return ExtensionAndMetadata( - *extension, allAutosConditions.find(conditionType)->second); - } - } - - return ExtensionAndMetadata(badExtension, - badInstructionMetadata); -} - -const gd::InstructionMetadata& MetadataProvider::GetConditionMetadata( - const gd::Platform& platform, gd::String conditionType) { - return GetExtensionAndConditionMetadata(platform, conditionType) - .GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndObjectExpressionMetadata( - const gd::Platform& platform, gd::String objectType, gd::String exprType) { - auto& extensions = platform.GetAllPlatformExtensions(); - for (auto& extension : extensions) { - const auto& objects = extension->GetExtensionObjectsTypes(); - if (find(objects.begin(), objects.end(), objectType) != objects.end()) { - const auto& allObjectExpressions = - extension->GetAllExpressionsForObject(objectType); - if (allObjectExpressions.find(exprType) != allObjectExpressions.end()) - return ExtensionAndMetadata( - *extension, allObjectExpressions.find(exprType)->second); - } - } - - // Then check base - for (auto& extension : extensions) { - const auto& allObjectExpressions = - extension->GetAllExpressionsForObject(""); - if (allObjectExpressions.find(exprType) != allObjectExpressions.end()) - return ExtensionAndMetadata( - *extension, allObjectExpressions.find(exprType)->second); - } - - return ExtensionAndMetadata(badExtension, - badExpressionMetadata); -} - -const gd::ExpressionMetadata& MetadataProvider::GetObjectExpressionMetadata( - const gd::Platform& platform, gd::String objectType, gd::String exprType) { - return GetExtensionAndObjectExpressionMetadata(platform, objectType, exprType) - .GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndBehaviorExpressionMetadata( - const gd::Platform& platform, gd::String autoType, gd::String exprType) { - auto& extensions = platform.GetAllPlatformExtensions(); - for (auto& extension : extensions) { - if (extension->HasBehavior(autoType)) { - const auto& allAutoExpressions = - extension->GetAllExpressionsForBehavior(autoType); - if (allAutoExpressions.find(exprType) != allAutoExpressions.end()) - return ExtensionAndMetadata( - *extension, allAutoExpressions.find(exprType)->second); - } - } - - // Then check base - for (auto& extension : extensions) { - const auto& allAutoExpressions = - extension->GetAllExpressionsForBehavior(""); - if (allAutoExpressions.find(exprType) != allAutoExpressions.end()) - return ExtensionAndMetadata( - *extension, allAutoExpressions.find(exprType)->second); - } - - return ExtensionAndMetadata(badExtension, - badExpressionMetadata); -} - -const gd::ExpressionMetadata& MetadataProvider::GetBehaviorExpressionMetadata( - const gd::Platform& platform, gd::String autoType, gd::String exprType) { - return GetExtensionAndBehaviorExpressionMetadata(platform, autoType, exprType) - .GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndExpressionMetadata( - const gd::Platform& platform, gd::String exprType) { - auto& extensions = platform.GetAllPlatformExtensions(); - for (auto& extension : extensions) { - const auto& allExpr = extension->GetAllExpressions(); - if (allExpr.find(exprType) != allExpr.end()) - return ExtensionAndMetadata( - *extension, allExpr.find(exprType)->second); - } - - return ExtensionAndMetadata(badExtension, - badExpressionMetadata); -} - -const gd::ExpressionMetadata& MetadataProvider::GetExpressionMetadata( - const gd::Platform& platform, gd::String exprType) { - return GetExtensionAndExpressionMetadata(platform, exprType).GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndObjectStrExpressionMetadata( - const gd::Platform& platform, gd::String objectType, gd::String exprType) { - auto& extensions = platform.GetAllPlatformExtensions(); - for (auto& extension : extensions) { - const auto& objects = extension->GetExtensionObjectsTypes(); - if (find(objects.begin(), objects.end(), objectType) != objects.end()) { - const auto& allObjectStrExpressions = - extension->GetAllStrExpressionsForObject(objectType); - if (allObjectStrExpressions.find(exprType) != - allObjectStrExpressions.end()) - return ExtensionAndMetadata( - *extension, allObjectStrExpressions.find(exprType)->second); - } - } - - // Then check in functions of "Base object". - for (auto& extension : extensions) { - const auto& allObjectStrExpressions = - extension->GetAllStrExpressionsForObject(""); - if (allObjectStrExpressions.find(exprType) != allObjectStrExpressions.end()) - return ExtensionAndMetadata( - *extension, allObjectStrExpressions.find(exprType)->second); - } - - return ExtensionAndMetadata(badExtension, - badExpressionMetadata); -} - -const gd::ExpressionMetadata& MetadataProvider::GetObjectStrExpressionMetadata( - const gd::Platform& platform, gd::String objectType, gd::String exprType) { - return GetExtensionAndObjectStrExpressionMetadata( - platform, objectType, exprType) - .GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndBehaviorStrExpressionMetadata( - const gd::Platform& platform, gd::String autoType, gd::String exprType) { - auto& extensions = platform.GetAllPlatformExtensions(); - for (auto& extension : extensions) { - if (extension->HasBehavior(autoType)) { - const auto& allBehaviorStrExpressions = - extension->GetAllStrExpressionsForBehavior(autoType); - if (allBehaviorStrExpressions.find(exprType) != - allBehaviorStrExpressions.end()) - return ExtensionAndMetadata( - *extension, allBehaviorStrExpressions.find(exprType)->second); - } - } - - // Then check in functions of "Base object". - for (auto& extension : extensions) { - const auto& allBehaviorStrExpressions = - extension->GetAllStrExpressionsForBehavior(""); - if (allBehaviorStrExpressions.find(exprType) != - allBehaviorStrExpressions.end()) - return ExtensionAndMetadata( - *extension, allBehaviorStrExpressions.find(exprType)->second); - } - - return ExtensionAndMetadata(badExtension, - badExpressionMetadata); -} - -const gd::ExpressionMetadata& -MetadataProvider::GetBehaviorStrExpressionMetadata(const gd::Platform& platform, - gd::String autoType, - gd::String exprType) { - return GetExtensionAndBehaviorStrExpressionMetadata( - platform, autoType, exprType) - .GetMetadata(); -} - -ExtensionAndMetadata -MetadataProvider::GetExtensionAndStrExpressionMetadata( - const gd::Platform& platform, gd::String exprType) { - auto& extensions = platform.GetAllPlatformExtensions(); - for (auto& extension : extensions) { - const auto& allExpr = extension->GetAllStrExpressions(); - if (allExpr.find(exprType) != allExpr.end()) - return ExtensionAndMetadata( - *extension, allExpr.find(exprType)->second); - } - - return ExtensionAndMetadata(badExtension, - badExpressionMetadata); -} - -const gd::ExpressionMetadata& MetadataProvider::GetStrExpressionMetadata( - const gd::Platform& platform, gd::String exprType) { - return GetExtensionAndStrExpressionMetadata(platform, exprType).GetMetadata(); -} - -const gd::ExpressionMetadata& MetadataProvider::GetAnyExpressionMetadata( - const gd::Platform& platform, gd::String exprType) { - const auto& numberExpressionMetadata = - GetExpressionMetadata(platform, exprType); - if (&numberExpressionMetadata != &badExpressionMetadata) { - return numberExpressionMetadata; - } - const auto& stringExpressionMetadata = - GetStrExpressionMetadata(platform, exprType); - if (&stringExpressionMetadata != &badExpressionMetadata) { - return stringExpressionMetadata; - } - return badExpressionMetadata; -} - -const gd::ExpressionMetadata& MetadataProvider::GetObjectAnyExpressionMetadata( - const gd::Platform& platform, gd::String objectType, gd::String exprType) { - const auto& numberExpressionMetadata = - GetObjectExpressionMetadata(platform, objectType, exprType); - if (&numberExpressionMetadata != &badExpressionMetadata) { - return numberExpressionMetadata; - } - const auto& stringExpressionMetadata = - GetObjectStrExpressionMetadata(platform, objectType, exprType); - if (&stringExpressionMetadata != &badExpressionMetadata) { - return stringExpressionMetadata; - } - return badExpressionMetadata; -} - -const gd::ExpressionMetadata& -MetadataProvider::GetBehaviorAnyExpressionMetadata(const gd::Platform& platform, - gd::String autoType, - gd::String exprType) { - const auto& numberExpressionMetadata = - GetBehaviorExpressionMetadata(platform, autoType, exprType); - if (&numberExpressionMetadata != &badExpressionMetadata) { - return numberExpressionMetadata; - } - const auto& stringExpressionMetadata = - GetBehaviorStrExpressionMetadata(platform, autoType, exprType); - if (&stringExpressionMetadata != &badExpressionMetadata) { - return stringExpressionMetadata; - } - return badExpressionMetadata; -} - -const gd::ExpressionMetadata& MetadataProvider::GetFunctionCallMetadata( - const gd::Platform& platform, - const gd::ObjectsContainersList &objectsContainersList, - FunctionCallNode& node) { - - if (!node.behaviorName.empty()) { - gd::String behaviorType = - objectsContainersList.GetTypeOfBehavior(node.behaviorName); - return MetadataProvider::GetBehaviorAnyExpressionMetadata( - platform, behaviorType, node.functionName); - } - else if (!node.objectName.empty()) { - gd::String objectType = - objectsContainersList.GetTypeOfObject(node.objectName); - return MetadataProvider::GetObjectAnyExpressionMetadata( - platform, objectType, node.functionName); - } - - return MetadataProvider::GetAnyExpressionMetadata(platform, node.functionName); -} - -const gd::ParameterMetadata* MetadataProvider::GetFunctionCallParameterMetadata( - const gd::Platform& platform, - const gd::ObjectsContainersList &objectsContainersList, - FunctionCallNode& functionCall, - ExpressionNode& parameter) { - int parameterIndex = -1; - for (int i = 0; i < functionCall.parameters.size(); i++) { - if (functionCall.parameters.at(i).get() == ¶meter) { - parameterIndex = i; - break; - } - } - if (parameterIndex < 0) { - return nullptr; - } - return MetadataProvider::GetFunctionCallParameterMetadata( - platform, - objectsContainersList, - functionCall, - parameterIndex); -} - -const gd::ParameterMetadata* MetadataProvider::GetFunctionCallParameterMetadata( - const gd::Platform& platform, - const gd::ObjectsContainersList &objectsContainersList, - FunctionCallNode& functionCall, - int parameterIndex) { - // Search the parameter metadata index skipping invisible ones. - size_t visibleParameterIndex = 0; - size_t metadataParameterIndex = - ExpressionParser2::WrittenParametersFirstIndex( - functionCall.objectName, functionCall.behaviorName); - const gd::ExpressionMetadata &metadata = MetadataProvider::GetFunctionCallMetadata( - platform, objectsContainersList, functionCall); - - if (IsBadExpressionMetadata(metadata)) { - return nullptr; - } - - // TODO use a badMetadata instead of a nullptr? - const gd::ParameterMetadata* parameterMetadata = nullptr; - while (metadataParameterIndex < - metadata.GetParameters().GetParametersCount()) { - if (!metadata.GetParameters().GetParameter(metadataParameterIndex) - .IsCodeOnly()) { - if (visibleParameterIndex == parameterIndex) { - parameterMetadata = - &metadata.GetParameters().GetParameter(metadataParameterIndex); - } - visibleParameterIndex++; - } - metadataParameterIndex++; - } - const int visibleParameterCount = visibleParameterIndex; - // It can be null if there are too many parameters in the expression, this text node is - // not actually linked to a parameter expected by the function call. - return parameterMetadata; -} - -MetadataProvider::~MetadataProvider() {} -MetadataProvider::MetadataProvider() {} - -} // namespace gd +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "GDCore/Extensions/Metadata/MetadataProvider.h" + +#include + +#include "GDCore/Extensions/Metadata/BehaviorMetadata.h" +#include "GDCore/Extensions/Metadata/EffectMetadata.h" +#include "GDCore/Extensions/Metadata/InstructionMetadata.h" +#include "GDCore/Extensions/Metadata/ObjectMetadata.h" +#include "GDCore/Extensions/Metadata/PlatformMetadataIndex.h" +#include "GDCore/Extensions/Platform.h" +#include "GDCore/Extensions/PlatformExtension.h" +#include "GDCore/Project/Layout.h" // For GetTypeOfObject and GetTypeOfBehavior +#include "GDCore/Project/ObjectsContainersList.h" +#include "GDCore/String.h" +#include "GDCore/Events/Parsers/ExpressionParser2.h" + +using namespace std; + +namespace gd { + +gd::BehaviorMetadata MetadataProvider::badBehaviorMetadata; +gd::ObjectMetadata MetadataProvider::badObjectInfo; +gd::EffectMetadata MetadataProvider::badEffectMetadata; +gd::InstructionMetadata MetadataProvider::badInstructionMetadata; +gd::ExpressionMetadata MetadataProvider::badExpressionMetadata; +gd::PlatformExtension MetadataProvider::badExtension; + +// The lookups below all delegate to the platform's gd::PlatformMetadataIndex, +// which resolves a type to its metadata in constant time (instead of scanning +// every extension). The index is rebuilt by the platform whenever its +// extensions change, so the returned pointers are never stale. + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndBehaviorMetadata(const gd::Platform& platform, + gd::String behaviorType) { + const auto* entry = platform.GetMetadataIndex().GetBehaviorMetadata(behaviorType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, badBehaviorMetadata); + return ExtensionAndMetadata(*entry->extension, *entry->metadata); +} + +const BehaviorMetadata& MetadataProvider::GetBehaviorMetadata( + const gd::Platform& platform, gd::String behaviorType) { + const auto* entry = platform.GetMetadataIndex().GetBehaviorMetadata(behaviorType); + return entry != nullptr ? *entry->metadata : badBehaviorMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndObjectMetadata(const gd::Platform& platform, + gd::String type) { + const auto* entry = platform.GetMetadataIndex().GetObjectMetadata(type); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, badObjectInfo); + return ExtensionAndMetadata(*entry->extension, *entry->metadata); +} + +const ObjectMetadata& MetadataProvider::GetObjectMetadata( + const gd::Platform& platform, gd::String type) { + const auto* entry = platform.GetMetadataIndex().GetObjectMetadata(type); + return entry != nullptr ? *entry->metadata : badObjectInfo; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndEffectMetadata(const gd::Platform& platform, + gd::String type) { + const auto* entry = platform.GetMetadataIndex().GetEffectMetadata(type); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, badEffectMetadata); + return ExtensionAndMetadata(*entry->extension, *entry->metadata); +} + +const EffectMetadata& MetadataProvider::GetEffectMetadata( + const gd::Platform& platform, gd::String type) { + const auto* entry = platform.GetMetadataIndex().GetEffectMetadata(type); + return entry != nullptr ? *entry->metadata : badEffectMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndActionMetadata(const gd::Platform& platform, + gd::String actionType) { + const auto* entry = platform.GetMetadataIndex().GetActionMetadata(actionType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, + badInstructionMetadata); + return ExtensionAndMetadata(*entry->extension, + *entry->metadata); +} + +const gd::InstructionMetadata& MetadataProvider::GetActionMetadata( + const gd::Platform& platform, gd::String actionType) { + const auto* entry = platform.GetMetadataIndex().GetActionMetadata(actionType); + return entry != nullptr ? *entry->metadata : badInstructionMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndConditionMetadata(const gd::Platform& platform, + gd::String conditionType) { + const auto* entry = + platform.GetMetadataIndex().GetConditionMetadata(conditionType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, + badInstructionMetadata); + return ExtensionAndMetadata(*entry->extension, + *entry->metadata); +} + +const gd::InstructionMetadata& MetadataProvider::GetConditionMetadata( + const gd::Platform& platform, gd::String conditionType) { + const auto* entry = + platform.GetMetadataIndex().GetConditionMetadata(conditionType); + return entry != nullptr ? *entry->metadata : badInstructionMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndObjectExpressionMetadata( + const gd::Platform& platform, gd::String objectType, gd::String exprType) { + const auto* entry = platform.GetMetadataIndex().GetObjectExpressionMetadata( + objectType, exprType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, + badExpressionMetadata); + return ExtensionAndMetadata(*entry->extension, + *entry->metadata); +} + +const gd::ExpressionMetadata& MetadataProvider::GetObjectExpressionMetadata( + const gd::Platform& platform, gd::String objectType, gd::String exprType) { + const auto* entry = platform.GetMetadataIndex().GetObjectExpressionMetadata( + objectType, exprType); + return entry != nullptr ? *entry->metadata : badExpressionMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndBehaviorExpressionMetadata( + const gd::Platform& platform, gd::String autoType, gd::String exprType) { + const auto* entry = platform.GetMetadataIndex().GetBehaviorExpressionMetadata( + autoType, exprType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, + badExpressionMetadata); + return ExtensionAndMetadata(*entry->extension, + *entry->metadata); +} + +const gd::ExpressionMetadata& MetadataProvider::GetBehaviorExpressionMetadata( + const gd::Platform& platform, gd::String autoType, gd::String exprType) { + const auto* entry = platform.GetMetadataIndex().GetBehaviorExpressionMetadata( + autoType, exprType); + return entry != nullptr ? *entry->metadata : badExpressionMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndExpressionMetadata(const gd::Platform& platform, + gd::String exprType) { + const auto* entry = platform.GetMetadataIndex().GetExpressionMetadata(exprType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, + badExpressionMetadata); + return ExtensionAndMetadata(*entry->extension, + *entry->metadata); +} + +const gd::ExpressionMetadata& MetadataProvider::GetExpressionMetadata( + const gd::Platform& platform, gd::String exprType) { + const auto* entry = platform.GetMetadataIndex().GetExpressionMetadata(exprType); + return entry != nullptr ? *entry->metadata : badExpressionMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndObjectStrExpressionMetadata( + const gd::Platform& platform, gd::String objectType, gd::String exprType) { + const auto* entry = platform.GetMetadataIndex().GetObjectStrExpressionMetadata( + objectType, exprType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, + badExpressionMetadata); + return ExtensionAndMetadata(*entry->extension, + *entry->metadata); +} + +const gd::ExpressionMetadata& MetadataProvider::GetObjectStrExpressionMetadata( + const gd::Platform& platform, gd::String objectType, gd::String exprType) { + const auto* entry = platform.GetMetadataIndex().GetObjectStrExpressionMetadata( + objectType, exprType); + return entry != nullptr ? *entry->metadata : badExpressionMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndBehaviorStrExpressionMetadata( + const gd::Platform& platform, gd::String autoType, gd::String exprType) { + const auto* entry = + platform.GetMetadataIndex().GetBehaviorStrExpressionMetadata(autoType, + exprType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, + badExpressionMetadata); + return ExtensionAndMetadata(*entry->extension, + *entry->metadata); +} + +const gd::ExpressionMetadata& +MetadataProvider::GetBehaviorStrExpressionMetadata(const gd::Platform& platform, + gd::String autoType, + gd::String exprType) { + const auto* entry = + platform.GetMetadataIndex().GetBehaviorStrExpressionMetadata(autoType, + exprType); + return entry != nullptr ? *entry->metadata : badExpressionMetadata; +} + +ExtensionAndMetadata +MetadataProvider::GetExtensionAndStrExpressionMetadata( + const gd::Platform& platform, gd::String exprType) { + const auto* entry = + platform.GetMetadataIndex().GetStrExpressionMetadata(exprType); + if (entry == nullptr) + return ExtensionAndMetadata(badExtension, + badExpressionMetadata); + return ExtensionAndMetadata(*entry->extension, + *entry->metadata); +} + +const gd::ExpressionMetadata& MetadataProvider::GetStrExpressionMetadata( + const gd::Platform& platform, gd::String exprType) { + const auto* entry = + platform.GetMetadataIndex().GetStrExpressionMetadata(exprType); + return entry != nullptr ? *entry->metadata : badExpressionMetadata; +} + +const gd::ExpressionMetadata& MetadataProvider::GetAnyExpressionMetadata( + const gd::Platform& platform, gd::String exprType) { + const auto& numberExpressionMetadata = + GetExpressionMetadata(platform, exprType); + if (&numberExpressionMetadata != &badExpressionMetadata) { + return numberExpressionMetadata; + } + const auto& stringExpressionMetadata = + GetStrExpressionMetadata(platform, exprType); + if (&stringExpressionMetadata != &badExpressionMetadata) { + return stringExpressionMetadata; + } + return badExpressionMetadata; +} + +const gd::ExpressionMetadata& MetadataProvider::GetObjectAnyExpressionMetadata( + const gd::Platform& platform, gd::String objectType, gd::String exprType) { + const auto& numberExpressionMetadata = + GetObjectExpressionMetadata(platform, objectType, exprType); + if (&numberExpressionMetadata != &badExpressionMetadata) { + return numberExpressionMetadata; + } + const auto& stringExpressionMetadata = + GetObjectStrExpressionMetadata(platform, objectType, exprType); + if (&stringExpressionMetadata != &badExpressionMetadata) { + return stringExpressionMetadata; + } + return badExpressionMetadata; +} + +const gd::ExpressionMetadata& +MetadataProvider::GetBehaviorAnyExpressionMetadata(const gd::Platform& platform, + gd::String autoType, + gd::String exprType) { + const auto& numberExpressionMetadata = + GetBehaviorExpressionMetadata(platform, autoType, exprType); + if (&numberExpressionMetadata != &badExpressionMetadata) { + return numberExpressionMetadata; + } + const auto& stringExpressionMetadata = + GetBehaviorStrExpressionMetadata(platform, autoType, exprType); + if (&stringExpressionMetadata != &badExpressionMetadata) { + return stringExpressionMetadata; + } + return badExpressionMetadata; +} + +const gd::ExpressionMetadata& MetadataProvider::GetFunctionCallMetadata( + const gd::Platform& platform, + const gd::ObjectsContainersList &objectsContainersList, + FunctionCallNode& node) { + + if (!node.behaviorName.empty()) { + gd::String behaviorType = + objectsContainersList.GetTypeOfBehavior(node.behaviorName); + return MetadataProvider::GetBehaviorAnyExpressionMetadata( + platform, behaviorType, node.functionName); + } + else if (!node.objectName.empty()) { + gd::String objectType = + objectsContainersList.GetTypeOfObject(node.objectName); + return MetadataProvider::GetObjectAnyExpressionMetadata( + platform, objectType, node.functionName); + } + + return MetadataProvider::GetAnyExpressionMetadata(platform, node.functionName); +} + +const gd::ParameterMetadata* MetadataProvider::GetFunctionCallParameterMetadata( + const gd::Platform& platform, + const gd::ObjectsContainersList &objectsContainersList, + FunctionCallNode& functionCall, + ExpressionNode& parameter) { + int parameterIndex = -1; + for (int i = 0; i < functionCall.parameters.size(); i++) { + if (functionCall.parameters.at(i).get() == ¶meter) { + parameterIndex = i; + break; + } + } + if (parameterIndex < 0) { + return nullptr; + } + return MetadataProvider::GetFunctionCallParameterMetadata( + platform, + objectsContainersList, + functionCall, + parameterIndex); +} + +const gd::ParameterMetadata* MetadataProvider::GetFunctionCallParameterMetadata( + const gd::Platform& platform, + const gd::ObjectsContainersList &objectsContainersList, + FunctionCallNode& functionCall, + int parameterIndex) { + // Search the parameter metadata index skipping invisible ones. + size_t visibleParameterIndex = 0; + size_t metadataParameterIndex = + ExpressionParser2::WrittenParametersFirstIndex( + functionCall.objectName, functionCall.behaviorName); + const gd::ExpressionMetadata &metadata = MetadataProvider::GetFunctionCallMetadata( + platform, objectsContainersList, functionCall); + + if (IsBadExpressionMetadata(metadata)) { + return nullptr; + } + + // TODO use a badMetadata instead of a nullptr? + const gd::ParameterMetadata* parameterMetadata = nullptr; + while (metadataParameterIndex < + metadata.GetParameters().GetParametersCount()) { + if (!metadata.GetParameters().GetParameter(metadataParameterIndex) + .IsCodeOnly()) { + if (visibleParameterIndex == parameterIndex) { + parameterMetadata = + &metadata.GetParameters().GetParameter(metadataParameterIndex); + } + visibleParameterIndex++; + } + metadataParameterIndex++; + } + const int visibleParameterCount = visibleParameterIndex; + // It can be null if there are too many parameters in the expression, this text node is + // not actually linked to a parameter expected by the function call. + return parameterMetadata; +} + +MetadataProvider::~MetadataProvider() {} +MetadataProvider::MetadataProvider() {} + +} // namespace gd diff --git a/Core/GDCore/Extensions/Metadata/PlatformMetadataIndex.cpp b/Core/GDCore/Extensions/Metadata/PlatformMetadataIndex.cpp new file mode 100644 index 000000000000..483900da9ce1 --- /dev/null +++ b/Core/GDCore/Extensions/Metadata/PlatformMetadataIndex.cpp @@ -0,0 +1,212 @@ +/* + * GDevelop Core + * Copyright 2008-2026 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "GDCore/Extensions/Metadata/PlatformMetadataIndex.h" + +#include + +#include "GDCore/Extensions/Metadata/BehaviorMetadata.h" +#include "GDCore/Extensions/Metadata/EffectMetadata.h" +#include "GDCore/Extensions/Metadata/ExpressionMetadata.h" +#include "GDCore/Extensions/Metadata/InstructionMetadata.h" +#include "GDCore/Extensions/Metadata/ObjectMetadata.h" +#include "GDCore/Extensions/Platform.h" +#include "GDCore/Extensions/PlatformExtension.h" + +namespace gd { + +namespace { + +// Index expressions (number or string) of an object/behavior type. `emplace` +// keeps the first inserted entry, so the first extension declaring a type wins, +// matching the previous linear-scan implementation. +void IndexExpressions( + std::unordered_map< + gd::String, + std::unordered_map>>& + expressionsByType, + const gd::String& type, + const gd::PlatformExtension* extension, + std::map& expressions) { + auto& byExprType = expressionsByType[type]; + for (auto& it : expressions) + byExprType.emplace(it.first, + MetadataEntry{extension, &it.second}); +} + +} // namespace + +PlatformMetadataIndex::PlatformMetadataIndex(const gd::Platform& platform) { + for (const auto& extensionPtr : platform.GetAllPlatformExtensions()) { + gd::PlatformExtension& extension = *extensionPtr; + const gd::PlatformExtension* ext = extensionPtr.get(); + + const std::vector objectTypes = + extension.GetExtensionObjectsTypes(); + const std::vector behaviorTypes = extension.GetBehaviorsTypes(); + + // Objects, behaviors and effects. + for (const auto& type : objectTypes) + objectMetadata.emplace( + type, + MetadataEntry{ext, &extension.GetObjectMetadata(type)}); + for (const auto& type : behaviorTypes) + behaviorMetadata.emplace( + type, MetadataEntry{ + ext, &extension.GetBehaviorMetadata(type)}); + for (const auto& type : extension.GetExtensionEffectTypes()) + effectMetadata.emplace( + type, + MetadataEntry{ext, &extension.GetEffectMetadata(type)}); + + // Actions and conditions: free, then per-object, then per-behavior. The + // instruction type is globally unique, so they share a single index. + for (auto& it : extension.GetAllActions()) + actions.emplace(it.first, + MetadataEntry{ext, &it.second}); + for (auto& it : extension.GetAllConditions()) + conditions.emplace(it.first, + MetadataEntry{ext, &it.second}); + for (const auto& objectType : objectTypes) { + for (auto& it : extension.GetAllActionsForObject(objectType)) + actions.emplace(it.first, + MetadataEntry{ext, &it.second}); + for (auto& it : extension.GetAllConditionsForObject(objectType)) + conditions.emplace(it.first, + MetadataEntry{ext, &it.second}); + } + for (const auto& behaviorType : behaviorTypes) { + for (auto& it : extension.GetAllActionsForBehavior(behaviorType)) + actions.emplace(it.first, + MetadataEntry{ext, &it.second}); + for (auto& it : extension.GetAllConditionsForBehavior(behaviorType)) + conditions.emplace(it.first, + MetadataEntry{ext, &it.second}); + } + + // Free expressions. + for (auto& it : extension.GetAllExpressions()) + expressions.emplace(it.first, + MetadataEntry{ext, &it.second}); + for (auto& it : extension.GetAllStrExpressions()) + strExpressions.emplace( + it.first, MetadataEntry{ext, &it.second}); + + // Object/behavior expressions, including the base ("") type. + IndexExpressions(objectExpressions, "", ext, + extension.GetAllExpressionsForObject("")); + IndexExpressions(objectStrExpressions, "", ext, + extension.GetAllStrExpressionsForObject("")); + for (const auto& objectType : objectTypes) { + IndexExpressions(objectExpressions, objectType, ext, + extension.GetAllExpressionsForObject(objectType)); + IndexExpressions(objectStrExpressions, objectType, ext, + extension.GetAllStrExpressionsForObject(objectType)); + } + + IndexExpressions(behaviorExpressions, "", ext, + extension.GetAllExpressionsForBehavior("")); + IndexExpressions(behaviorStrExpressions, "", ext, + extension.GetAllStrExpressionsForBehavior("")); + for (const auto& behaviorType : behaviorTypes) { + IndexExpressions(behaviorExpressions, behaviorType, ext, + extension.GetAllExpressionsForBehavior(behaviorType)); + IndexExpressions(behaviorStrExpressions, behaviorType, ext, + extension.GetAllStrExpressionsForBehavior(behaviorType)); + } + } +} + +namespace { +template +const MetadataEntry* FindInMap( + const std::unordered_map>& map, + const gd::String& type) { + auto it = map.find(type); + return it != map.end() ? &it->second : nullptr; +} +} // namespace + +const MetadataEntry* PlatformMetadataIndex::GetObjectMetadata( + const gd::String& type) const { + return FindInMap(objectMetadata, type); +} + +const MetadataEntry* +PlatformMetadataIndex::GetBehaviorMetadata(const gd::String& type) const { + return FindInMap(behaviorMetadata, type); +} + +const MetadataEntry* PlatformMetadataIndex::GetEffectMetadata( + const gd::String& type) const { + return FindInMap(effectMetadata, type); +} + +const MetadataEntry* +PlatformMetadataIndex::GetActionMetadata(const gd::String& type) const { + return FindInMap(actions, type); +} + +const MetadataEntry* +PlatformMetadataIndex::GetConditionMetadata(const gd::String& type) const { + return FindInMap(conditions, type); +} + +const MetadataEntry* +PlatformMetadataIndex::GetExpressionMetadata(const gd::String& type) const { + return FindInMap(expressions, type); +} + +const MetadataEntry* +PlatformMetadataIndex::GetStrExpressionMetadata(const gd::String& type) const { + return FindInMap(strExpressions, type); +} + +const MetadataEntry* +PlatformMetadataIndex::FindInExpressionsByType( + const ExpressionsByType& expressionsByType, + const gd::String& type, + const gd::String& exprType) { + auto outer = expressionsByType.find(type); + if (outer != expressionsByType.end()) { + auto inner = outer->second.find(exprType); + if (inner != outer->second.end()) return &inner->second; + } + + // Fall back to the base ("") object/behavior type. + auto base = expressionsByType.find(""); + if (base != expressionsByType.end()) { + auto inner = base->second.find(exprType); + if (inner != base->second.end()) return &inner->second; + } + + return nullptr; +} + +const MetadataEntry* +PlatformMetadataIndex::GetObjectExpressionMetadata( + const gd::String& objectType, const gd::String& exprType) const { + return FindInExpressionsByType(objectExpressions, objectType, exprType); +} + +const MetadataEntry* +PlatformMetadataIndex::GetObjectStrExpressionMetadata( + const gd::String& objectType, const gd::String& exprType) const { + return FindInExpressionsByType(objectStrExpressions, objectType, exprType); +} + +const MetadataEntry* +PlatformMetadataIndex::GetBehaviorExpressionMetadata( + const gd::String& behaviorType, const gd::String& exprType) const { + return FindInExpressionsByType(behaviorExpressions, behaviorType, exprType); +} + +const MetadataEntry* +PlatformMetadataIndex::GetBehaviorStrExpressionMetadata( + const gd::String& behaviorType, const gd::String& exprType) const { + return FindInExpressionsByType(behaviorStrExpressions, behaviorType, exprType); +} + +} // namespace gd diff --git a/Core/GDCore/Extensions/Metadata/PlatformMetadataIndex.h b/Core/GDCore/Extensions/Metadata/PlatformMetadataIndex.h new file mode 100644 index 000000000000..d23f024919e1 --- /dev/null +++ b/Core/GDCore/Extensions/Metadata/PlatformMetadataIndex.h @@ -0,0 +1,119 @@ +/* + * GDevelop Core + * Copyright 2008-2026 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#pragma once + +#include + +#include "GDCore/String.h" + +namespace gd { +class Platform; +class PlatformExtension; +class ObjectMetadata; +class BehaviorMetadata; +class EffectMetadata; +class InstructionMetadata; +class ExpressionMetadata; +} // namespace gd + +namespace gd { + +/** + * \brief A resolved metadata entry: the metadata and the extension declaring it. + * + * Both pointers are owned by the gd::PlatformExtension they come from. They stay + * valid as long as the index does: the index is rebuilt from scratch whenever + * the platform extensions change (see gd::Platform::AddExtension / + * RemoveExtension), so it never holds pointers to freed metadata. + */ +template +struct MetadataEntry { + const gd::PlatformExtension* extension; + const T* metadata; + + MetadataEntry() : extension(nullptr), metadata(nullptr) {} + MetadataEntry(const gd::PlatformExtension* extension_, const T* metadata_) + : extension(extension_), metadata(metadata_) {} +}; + +/** + * \brief Hash-map index of all the metadata declared by a platform's + * extensions, so that gd::MetadataProvider lookups are O(1) instead of a linear + * scan over every extension (and, for instructions/expressions, over every + * object and behavior type of every extension). + * + * It is built lazily and owned by gd::Platform, which discards it whenever its + * extensions change. The semantics of every lookup match the previous + * linear-scan implementation exactly: the first extension (in load order) + * declaring a type wins, and object/behavior expressions resolve the specific + * object/behavior type before falling back to the base ("") type. + * + * \ingroup PlatformDefinition + */ +class GD_CORE_API PlatformMetadataIndex { + public: + /** + * \brief Build the index from all the extensions of the given platform. + */ + explicit PlatformMetadataIndex(const gd::Platform& platform); + + const MetadataEntry* GetObjectMetadata( + const gd::String& type) const; + const MetadataEntry* GetBehaviorMetadata( + const gd::String& type) const; + const MetadataEntry* GetEffectMetadata( + const gd::String& type) const; + const MetadataEntry* GetActionMetadata( + const gd::String& type) const; + const MetadataEntry* GetConditionMetadata( + const gd::String& type) const; + const MetadataEntry* GetExpressionMetadata( + const gd::String& type) const; + const MetadataEntry* GetStrExpressionMetadata( + const gd::String& type) const; + + /** + * Object/behavior expressions resolve the specific type first, then fall back + * to the base ("") type, matching the previous implementation. + */ + const MetadataEntry* GetObjectExpressionMetadata( + const gd::String& objectType, const gd::String& exprType) const; + const MetadataEntry* GetObjectStrExpressionMetadata( + const gd::String& objectType, const gd::String& exprType) const; + const MetadataEntry* GetBehaviorExpressionMetadata( + const gd::String& behaviorType, const gd::String& exprType) const; + const MetadataEntry* GetBehaviorStrExpressionMetadata( + const gd::String& behaviorType, const gd::String& exprType) const; + + private: + // Single-key indexes (the type is globally unique). + std::unordered_map> objectMetadata; + std::unordered_map> + behaviorMetadata; + std::unordered_map> effectMetadata; + std::unordered_map> actions; + std::unordered_map> conditions; + std::unordered_map> expressions; + std::unordered_map> + strExpressions; + + // Composite-key indexes: object/behavior type -> (expression type -> entry). + // The base ("") type is stored under the "" outer key. + using ExpressionsByType = std::unordered_map< + gd::String, + std::unordered_map>>; + ExpressionsByType objectExpressions; + ExpressionsByType objectStrExpressions; + ExpressionsByType behaviorExpressions; + ExpressionsByType behaviorStrExpressions; + + static const MetadataEntry* FindInExpressionsByType( + const ExpressionsByType& expressionsByType, + const gd::String& type, + const gd::String& exprType); +}; + +} // namespace gd diff --git a/Core/GDCore/Extensions/Platform.cpp b/Core/GDCore/Extensions/Platform.cpp index 97ebf6330be0..ae95649c2491 100644 --- a/Core/GDCore/Extensions/Platform.cpp +++ b/Core/GDCore/Extensions/Platform.cpp @@ -5,6 +5,7 @@ */ #include "Platform.h" +#include "GDCore/Extensions/Metadata/PlatformMetadataIndex.h" #include "GDCore/Extensions/PlatformExtension.h" #include "GDCore/Project/Object.h" #include "GDCore/Project/ObjectConfiguration.h" @@ -24,9 +25,19 @@ Platform::Platform() : enableExtensionLoadingLogs(false) {} Platform::~Platform() {} +const gd::PlatformMetadataIndex& Platform::GetMetadataIndex() const { + if (!metadataIndex) + metadataIndex.reset(new gd::PlatformMetadataIndex(*this)); + return *metadataIndex; +} + bool Platform::AddExtension(std::shared_ptr extension) { if (!extension) return false; + // The set of extensions is changing: discard the metadata index so it is + // rebuilt on the next lookup. + metadataIndex.reset(); + if (enableExtensionLoadingLogs) std::cout << "Loading " << extension->GetName() << "..."; if (IsExtensionLoaded(extension->GetName())) { @@ -57,6 +68,10 @@ bool Platform::AddExtension(std::shared_ptr extension) { } void Platform::RemoveExtension(const gd::String& name) { + // The set of extensions is changing: discard the metadata index so it is + // rebuilt on the next lookup. + metadataIndex.reset(); + // Unload all creation/destruction functions for objects provided by the // extension for (std::size_t i = 0; i < extensionsLoaded.size(); ++i) { diff --git a/Core/GDCore/Extensions/Platform.h b/Core/GDCore/Extensions/Platform.h index 9e088a2d5459..97b7f0b1e2ee 100644 --- a/Core/GDCore/Extensions/Platform.h +++ b/Core/GDCore/Extensions/Platform.h @@ -25,6 +25,7 @@ class BehaviorsSharedData; class PlatformExtension; class LayoutEditorCanvas; class ProjectExporter; +class PlatformMetadataIndex; } // namespace gd typedef std::function()> @@ -116,6 +117,15 @@ class GD_CORE_API Platform { return extensionsLoaded; }; + /** + * \brief Get the (lazily built) index of all the metadata declared by the + * platform's extensions, allowing constant-time lookups by gd::MetadataProvider. + * + * \note The index is discarded whenever extensions change (see AddExtension + * and RemoveExtension), so it never returns stale metadata. + */ + const gd::PlatformMetadataIndex& GetMetadataIndex() const; + /** * \brief Remove an extension from the platform. * @@ -174,6 +184,12 @@ class GD_CORE_API Platform { instructionOrExpressionGroupMetadata; static InstructionOrExpressionGroupMetadata badInstructionOrExpressionGroupMetadata; bool enableExtensionLoadingLogs; + + /// Lazily built metadata index, discarded whenever extensions change. + /// A shared_ptr (rather than unique_ptr) keeps gd::Platform copyable; copies + /// share the index until either one changes its extensions, which resets only + /// that instance's pointer and rebuilds it independently. + mutable std::shared_ptr metadataIndex; }; } // namespace gd diff --git a/GDevelop.js/__tests__/MetadataProviderIndex.js b/GDevelop.js/__tests__/MetadataProviderIndex.js new file mode 100644 index 000000000000..ab29f1ddfd07 --- /dev/null +++ b/GDevelop.js/__tests__/MetadataProviderIndex.js @@ -0,0 +1,140 @@ +const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.js'); + +// Checks that gd::MetadataProvider lookups (backed by the platform's +// gd::PlatformMetadataIndex) stay correct when extensions are added, replaced +// and removed at runtime - i.e. that the index is properly invalidated. +describe('MetadataProvider index invalidation', () => { + let gd = null; + beforeAll(async () => { + gd = await initializeGDevelopJs(); + }); + + const extensionName = 'MetadataIndexTestExtension'; + const objectType = extensionName + '::IndexTestObject'; + const actionType = extensionName + '::IndexTestAction'; + + const makeExtension = (objectFullName) => { + const extension = new gd.PlatformExtension(); + extension.setExtensionInformation( + extensionName, + 'Metadata index test extension', + 'Description', + 'Author', + 'MIT' + ); + const objectConfiguration = new gd.ObjectJsImplementation(); + extension.addObject( + 'IndexTestObject', + objectFullName, + 'A test object', + '', + objectConfiguration + ); + extension.addAction( + 'IndexTestAction', + 'Index test action', + 'Does something', + 'Does something', + '', + '', + '' + ); + return extension; + }; + + afterEach(() => { + if (gd.JsPlatform.get().isExtensionLoaded(extensionName)) + gd.JsPlatform.get().removeExtension(extensionName); + }); + + it('does not find metadata before the extension is added', () => { + const platform = gd.JsPlatform.get(); + expect( + gd.MetadataProvider.isBadObjectMetadata( + gd.MetadataProvider.getObjectMetadata(platform, objectType) + ) + ).toBe(true); + expect( + gd.MetadataProvider.isBadInstructionMetadata( + gd.MetadataProvider.getActionMetadata(platform, actionType) + ) + ).toBe(true); + }); + + it('finds metadata right after the extension is added (index rebuilt)', () => { + const platform = gd.JsPlatform.get(); + + // Build the index once so the test proves it is invalidated, not just + // lazily built for the first time. + gd.MetadataProvider.getObjectMetadata(platform, 'Sprite'); + + const extension = makeExtension('First name'); + platform.addNewExtension(extension); + extension.delete(); + + expect( + gd.MetadataProvider.isBadObjectMetadata( + gd.MetadataProvider.getObjectMetadata(platform, objectType) + ) + ).toBe(false); + expect( + gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() + ).toBe('First name'); + expect( + gd.MetadataProvider.isBadInstructionMetadata( + gd.MetadataProvider.getActionMetadata(platform, actionType) + ) + ).toBe(false); + }); + + it('returns updated metadata after the extension is replaced (not stale)', () => { + const platform = gd.JsPlatform.get(); + + const firstExtension = makeExtension('First name'); + platform.addNewExtension(firstExtension); + firstExtension.delete(); + + // Resolve once so the index caches the first version's metadata. + expect( + gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() + ).toBe('First name'); + + // Replacing an extension goes through RemoveExtension + AddExtension, which + // must discard the cached (now dangling) metadata. + const secondExtension = makeExtension('Second name'); + platform.addNewExtension(secondExtension); + secondExtension.delete(); + + expect( + gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() + ).toBe('Second name'); + }); + + it('stops finding metadata after the extension is removed', () => { + const platform = gd.JsPlatform.get(); + + const extension = makeExtension('First name'); + platform.addNewExtension(extension); + extension.delete(); + + // Resolve once so the index is populated before removal. + expect( + gd.MetadataProvider.isBadObjectMetadata( + gd.MetadataProvider.getObjectMetadata(platform, objectType) + ) + ).toBe(false); + + platform.removeExtension(extensionName); + + expect( + gd.MetadataProvider.isBadObjectMetadata( + gd.MetadataProvider.getObjectMetadata(platform, objectType) + ) + ).toBe(true); + expect( + gd.MetadataProvider.isBadInstructionMetadata( + gd.MetadataProvider.getActionMetadata(platform, actionType) + ) + ).toBe(true); + }); +}); From bc662995f4b8e07f014dfd5b66cd6aba40164829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:32:24 +0200 Subject: [PATCH 5/5] Add tests for the MetadataProvider constant-time index - Core/tests/PlatformMetadataIndex.cpp (native/catch): every lookup kind against DummyPlatform, object-expression fallback to the base ("") type, behavior actions indexed alongside free/object ones, bad-metadata for unknown types, the declaring extension is returned, and the index rebuilds after an extension is removed. - Expand the jest MetadataProviderIndex test to assert *all* metadata kinds resolve after an extension is added and *all* become bad after it is removed (invalidation covers the whole index, not just objects/actions). Co-Authored-By: Claude Opus 4.8 (1M context) --- Core/tests/PlatformMetadataIndex.cpp | 114 +++++++++++++ .../__tests__/MetadataProviderIndex.js | 152 +++++++++--------- 2 files changed, 193 insertions(+), 73 deletions(-) create mode 100644 Core/tests/PlatformMetadataIndex.cpp diff --git a/Core/tests/PlatformMetadataIndex.cpp b/Core/tests/PlatformMetadataIndex.cpp new file mode 100644 index 000000000000..cb27adc09978 --- /dev/null +++ b/Core/tests/PlatformMetadataIndex.cpp @@ -0,0 +1,114 @@ +/* + * GDevelop Core + * Copyright 2008-2026 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +/** + * @file Tests for gd::MetadataProvider lookups, which are backed by + * gd::PlatformMetadataIndex (constant-time index of a platform's metadata). + */ +#include "DummyPlatform.h" +#include "GDCore/Extensions/Metadata/BehaviorMetadata.h" +#include "GDCore/Extensions/Metadata/EffectMetadata.h" +#include "GDCore/Extensions/Metadata/ExpressionMetadata.h" +#include "GDCore/Extensions/Metadata/InstructionMetadata.h" +#include "GDCore/Extensions/Metadata/MetadataProvider.h" +#include "GDCore/Extensions/Metadata/ObjectMetadata.h" +#include "GDCore/Extensions/Platform.h" +#include "GDCore/Extensions/PlatformExtension.h" +#include "GDCore/Project/Project.h" +#include "catch.hpp" + +using namespace gd; + +TEST_CASE("PlatformMetadataIndex (via MetadataProvider)", "[common]") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + + SECTION("It resolves every kind of metadata declared by an extension") { + REQUIRE_FALSE(MetadataProvider::IsBadObjectMetadata( + MetadataProvider::GetObjectMetadata(platform, "MyExtension::Sprite"))); + REQUIRE_FALSE(MetadataProvider::IsBadBehaviorMetadata( + MetadataProvider::GetBehaviorMetadata(platform, + "MyExtension::MyBehavior"))); + REQUIRE_FALSE(MetadataProvider::IsBadInstructionMetadata( + MetadataProvider::GetActionMetadata(platform, "MyExtension::DoSomething"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetExpressionMetadata(platform, "MyExtension::GetNumber"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetStrExpressionMetadata(platform, "MyExtension::ToString"))); + } + + SECTION("Behavior actions are indexed alongside free/object actions") { + REQUIRE_FALSE(MetadataProvider::IsBadInstructionMetadata( + MetadataProvider::GetActionMetadata( + platform, "MyExtension::BehaviorDoSomething"))); + } + + SECTION("It resolves object/behavior expressions on their own type") { + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetObjectExpressionMetadata( + platform, "MyExtension::Sprite", "GetObjectNumber"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetObjectStrExpressionMetadata( + platform, "MyExtension::Sprite", "GetObjectStringWith1Param"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetBehaviorExpressionMetadata( + platform, "MyExtension::MyBehavior", "GetBehaviorNumberWith1Param"))); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetBehaviorStrExpressionMetadata( + platform, "MyExtension::MyBehavior", "GetBehaviorStringWith1Param"))); + } + + SECTION("Object expressions fall back to the base object type") { + // "GetFromBaseExpression" is declared on the base object (""), not on + // Sprite, so it must be resolved through the base-type fallback. + const auto& fromBase = MetadataProvider::GetObjectExpressionMetadata( + platform, "MyExtension::Sprite", "GetFromBaseExpression"); + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata(fromBase)); + + // It must also be found for an object type that is not declared by any + // extension (still resolved via the base object). + REQUIRE_FALSE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetObjectExpressionMetadata( + platform, "UnknownObjectType", "GetFromBaseExpression"))); + } + + SECTION("Unknown types resolve to the bad metadata") { + REQUIRE(MetadataProvider::IsBadObjectMetadata( + MetadataProvider::GetObjectMetadata(platform, "MyExtension::DoesNotExist"))); + REQUIRE(MetadataProvider::IsBadBehaviorMetadata( + MetadataProvider::GetBehaviorMetadata(platform, "Does::NotExist"))); + REQUIRE(MetadataProvider::IsBadInstructionMetadata( + MetadataProvider::GetActionMetadata(platform, "Does::NotExist"))); + REQUIRE(MetadataProvider::IsBadInstructionMetadata( + MetadataProvider::GetConditionMetadata(platform, "Does::NotExist"))); + REQUIRE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetExpressionMetadata(platform, "Does::NotExist"))); + REQUIRE(MetadataProvider::IsBadExpressionMetadata( + MetadataProvider::GetObjectExpressionMetadata( + platform, "MyExtension::Sprite", "DoesNotExist"))); + } + + SECTION("It returns the extension that declares the metadata") { + auto extensionAndMetadata = + MetadataProvider::GetExtensionAndObjectMetadata(platform, + "MyExtension::Sprite"); + REQUIRE(extensionAndMetadata.GetExtension().GetName() == "MyExtension"); + + auto badExtensionAndMetadata = + MetadataProvider::GetExtensionAndObjectMetadata(platform, + "MyExtension::DoesNotExist"); + REQUIRE(badExtensionAndMetadata.GetExtension().GetName().empty()); + } + + SECTION("The index is rebuilt after the platform's extensions change") { + REQUIRE_FALSE(MetadataProvider::IsBadObjectMetadata( + MetadataProvider::GetObjectMetadata(platform, "MyExtension::Sprite"))); + + platform.RemoveExtension("MyExtension"); + REQUIRE(MetadataProvider::IsBadObjectMetadata( + MetadataProvider::GetObjectMetadata(platform, "MyExtension::Sprite"))); + } +} diff --git a/GDevelop.js/__tests__/MetadataProviderIndex.js b/GDevelop.js/__tests__/MetadataProviderIndex.js index ab29f1ddfd07..46f0bdba0a9a 100644 --- a/GDevelop.js/__tests__/MetadataProviderIndex.js +++ b/GDevelop.js/__tests__/MetadataProviderIndex.js @@ -2,16 +2,28 @@ const initializeGDevelopJs = require('../../Binaries/embuild/GDevelop.js/libGD.j // Checks that gd::MetadataProvider lookups (backed by the platform's // gd::PlatformMetadataIndex) stay correct when extensions are added, replaced -// and removed at runtime - i.e. that the index is properly invalidated. -describe('MetadataProvider index invalidation', () => { +// and removed at runtime - i.e. that the index is properly invalidated - and +// that every kind of metadata is indexed and resolved. +describe('MetadataProvider index', () => { let gd = null; beforeAll(async () => { gd = await initializeGDevelopJs(); }); const extensionName = 'MetadataIndexTestExtension'; - const objectType = extensionName + '::IndexTestObject'; - const actionType = extensionName + '::IndexTestAction'; + const prefix = extensionName + '::'; + const objectType = prefix + 'TestObject'; + const behaviorType = prefix + 'TestBehavior'; + const actionType = prefix + 'TestAction'; + const conditionType = prefix + 'TestCondition'; + const expressionType = prefix + 'TestExpression'; + const strExpressionType = prefix + 'TestStrExpression'; + // Object/behavior expression names are not namespaced (they are scoped to the + // object/behavior type they belong to). + const objectExpressionName = 'TestObjectExpression'; + const objectStrExpressionName = 'TestObjectStrExpression'; + const behaviorExpressionName = 'TestBehaviorExpression'; + const behaviorStrExpressionName = 'TestBehaviorStrExpression'; const makeExtension = (objectFullName) => { const extension = new gd.PlatformExtension(); @@ -22,69 +34,92 @@ describe('MetadataProvider index invalidation', () => { 'Author', 'MIT' ); + const objectConfiguration = new gd.ObjectJsImplementation(); - extension.addObject( - 'IndexTestObject', + const objectMetadata = extension.addObject( + 'TestObject', objectFullName, 'A test object', '', objectConfiguration ); - extension.addAction( - 'IndexTestAction', - 'Index test action', - 'Does something', - 'Does something', + objectMetadata.addExpression(objectExpressionName, 'Obj expr', 'd', 'g', ''); + objectMetadata.addStrExpression(objectStrExpressionName, 'Obj str', 'd', 'g', ''); + + const behaviorInstance = new gd.BehaviorJsImplementation(); + behaviorInstance.initializeContent = function (behaviorContent) {}; + const behaviorMetadata = extension.addBehavior( + 'TestBehavior', + 'Test behavior', + 'TestBehavior', + 'A test behavior', '', '', - '' + 'TestBehavior', + behaviorInstance, + new gd.BehaviorsSharedData() ); + behaviorMetadata.addExpression(behaviorExpressionName, 'Beh expr', 'd', 'g', ''); + behaviorMetadata.addStrExpression(behaviorStrExpressionName, 'Beh str', 'd', 'g', ''); + + extension.addAction('TestAction', 'Test action', 'Does', 'Does', '', '', ''); + extension.addCondition('TestCondition', 'Test cond', 'Is', 'Is', '', '', ''); + extension.addExpression('TestExpression', 'Test expr', 'd', 'g', ''); + extension.addStrExpression('TestStrExpression', 'Test str', 'd', 'g', ''); + return extension; }; + // Asserts every kind of metadata declared by makeExtension is found (when + // expectFound) or resolves to the "bad" metadata (when not). + const expectAllResolved = (platform, expectFound) => { + const P = gd.MetadataProvider; + const cases = [ + P.isBadObjectMetadata(P.getObjectMetadata(platform, objectType)), + P.isBadBehaviorMetadata(P.getBehaviorMetadata(platform, behaviorType)), + P.isBadInstructionMetadata(P.getActionMetadata(platform, actionType)), + P.isBadInstructionMetadata(P.getConditionMetadata(platform, conditionType)), + P.isBadExpressionMetadata(P.getExpressionMetadata(platform, expressionType)), + P.isBadExpressionMetadata(P.getStrExpressionMetadata(platform, strExpressionType)), + P.isBadExpressionMetadata( + P.getObjectExpressionMetadata(platform, objectType, objectExpressionName) + ), + P.isBadExpressionMetadata( + P.getObjectStrExpressionMetadata(platform, objectType, objectStrExpressionName) + ), + P.isBadExpressionMetadata( + P.getBehaviorExpressionMetadata(platform, behaviorType, behaviorExpressionName) + ), + P.isBadExpressionMetadata( + P.getBehaviorStrExpressionMetadata(platform, behaviorType, behaviorStrExpressionName) + ), + ]; + // When found, none should be "bad"; when not, all should be "bad". + for (const isBad of cases) expect(isBad).toBe(!expectFound); + }; + afterEach(() => { if (gd.JsPlatform.get().isExtensionLoaded(extensionName)) gd.JsPlatform.get().removeExtension(extensionName); }); - it('does not find metadata before the extension is added', () => { + it('resolves every kind of metadata after the extension is added, and none before/after removal', () => { const platform = gd.JsPlatform.get(); - expect( - gd.MetadataProvider.isBadObjectMetadata( - gd.MetadataProvider.getObjectMetadata(platform, objectType) - ) - ).toBe(true); - expect( - gd.MetadataProvider.isBadInstructionMetadata( - gd.MetadataProvider.getActionMetadata(platform, actionType) - ) - ).toBe(true); - }); - it('finds metadata right after the extension is added (index rebuilt)', () => { - const platform = gd.JsPlatform.get(); - - // Build the index once so the test proves it is invalidated, not just - // lazily built for the first time. + // Build the index once first so we test invalidation, not lazy first build. gd.MetadataProvider.getObjectMetadata(platform, 'Sprite'); + expectAllResolved(platform, false); + const extension = makeExtension('First name'); platform.addNewExtension(extension); extension.delete(); - expect( - gd.MetadataProvider.isBadObjectMetadata( - gd.MetadataProvider.getObjectMetadata(platform, objectType) - ) - ).toBe(false); - expect( - gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() - ).toBe('First name'); - expect( - gd.MetadataProvider.isBadInstructionMetadata( - gd.MetadataProvider.getActionMetadata(platform, actionType) - ) - ).toBe(false); + expectAllResolved(platform, true); + + platform.removeExtension(extensionName); + + expectAllResolved(platform, false); }); it('returns updated metadata after the extension is replaced (not stale)', () => { @@ -94,13 +129,12 @@ describe('MetadataProvider index invalidation', () => { platform.addNewExtension(firstExtension); firstExtension.delete(); - // Resolve once so the index caches the first version's metadata. expect( gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() ).toBe('First name'); - // Replacing an extension goes through RemoveExtension + AddExtension, which - // must discard the cached (now dangling) metadata. + // Replacing goes through RemoveExtension + AddExtension and must discard the + // cached (now dangling) metadata. const secondExtension = makeExtension('Second name'); platform.addNewExtension(secondExtension); secondExtension.delete(); @@ -109,32 +143,4 @@ describe('MetadataProvider index invalidation', () => { gd.MetadataProvider.getObjectMetadata(platform, objectType).getFullName() ).toBe('Second name'); }); - - it('stops finding metadata after the extension is removed', () => { - const platform = gd.JsPlatform.get(); - - const extension = makeExtension('First name'); - platform.addNewExtension(extension); - extension.delete(); - - // Resolve once so the index is populated before removal. - expect( - gd.MetadataProvider.isBadObjectMetadata( - gd.MetadataProvider.getObjectMetadata(platform, objectType) - ) - ).toBe(false); - - platform.removeExtension(extensionName); - - expect( - gd.MetadataProvider.isBadObjectMetadata( - gd.MetadataProvider.getObjectMetadata(platform, objectType) - ) - ).toBe(true); - expect( - gd.MetadataProvider.isBadInstructionMetadata( - gd.MetadataProvider.getActionMetadata(platform, actionType) - ) - ).toBe(true); - }); });