From a07a876bf21fbcd6d95bc99d6ad019675ce9d570 Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Tue, 25 Aug 2026 09:12:22 -0400 Subject: [PATCH 1/3] oversampler: defer initialization --- include/element/oversampler.hpp | 13 +++++++++ src/engine/oversampler.cpp | 33 +++++++++++++++++++---- src/engine/processor.cpp | 2 ++ test/OversamplerTests.cpp | 47 ++++++++++++++++++++++++--------- 4 files changed, 78 insertions(+), 17 deletions(-) diff --git a/include/element/oversampler.hpp b/include/element/oversampler.hpp index 2a383c606..e368e7d47 100644 --- a/include/element/oversampler.hpp +++ b/include/element/oversampler.hpp @@ -22,6 +22,19 @@ class Oversampler final { float getLatencySamples (int index) const; int getFactor (int index) const; void prepare (int numChannels, int blockSize); + + /** Returns the oversampling chain for the given index, creating and + initializing it on demand with the spec given to prepare(). + + Chains are expensive to build (IIR filter design + buffers), so they + are only created here, never in prepare(). Must be called on the + message thread, and only after prepare() has set a valid spec. + + @param index the processor index; the oversample factor is 2^(index + 1) + @return the chain, or nullptr if the index or spec is invalid + */ + ProcessorType* ensureProcessor (int index); + void reset(); private: diff --git a/src/engine/oversampler.cpp b/src/engine/oversampler.cpp index 620bbcba7..773f7a7d2 100644 --- a/src/engine/oversampler.cpp +++ b/src/engine/oversampler.cpp @@ -1,6 +1,7 @@ // Copyright 2023 Kushview, LLC // SPDX-License-Identifier: GPL-3.0-or-later +#include #include namespace element { @@ -36,22 +37,44 @@ void Oversampler::prepare (int numChannels, int blockSize) channels = numChannels; buffer = blockSize; - if (processors.size() <= 0 || procSpecChanged) + if (procSpecChanged) { + // Existing chains were built for the old spec. Drop them; chains are + // (re)built on demand by ensureProcessor(). processors.clear(); - for (int f = 0; f < maxProc; ++f) - processors.add (new ProcessorType (channels, f + 1, ProcessorType::FilterType::filterHalfBandPolyphaseIIR)); } for (auto* proc : processors) - proc->initProcessing (buffer); + if (proc != nullptr) + proc->initProcessing ((size_t) buffer); +} + +template +typename Oversampler::ProcessorType* Oversampler::ensureProcessor (int index) +{ + JUCE_ASSERT_MESSAGE_THREAD + + if (index < 0 || index >= maxProc || channels <= 0 || buffer <= 0) + return nullptr; + + while (processors.size() <= index) + processors.add (nullptr); + + if (auto* const existing = processors.getUnchecked (index)) + return existing; + + auto* const proc = new ProcessorType (channels, index + 1, ProcessorType::FilterType::filterHalfBandPolyphaseIIR); + proc->initProcessing ((size_t) buffer); + processors.set (index, proc, true); + return proc; } template void Oversampler::reset() { for (auto* const proc : processors) - proc->reset(); + if (proc != nullptr) + proc->reset(); } template class Oversampler; diff --git a/src/engine/processor.cpp b/src/engine/processor.cpp index 0926493ce..27e27b9de 100644 --- a/src/engine/processor.cpp +++ b/src/engine/processor.cpp @@ -346,6 +346,8 @@ void Processor::prepare (const double newSampleRate, oversampler->prepare (jmax (getNumPorts (PortType::Audio, true), getNumPorts (PortType::Audio, false)), blockSize); + if (osPow > 0) + oversampler->ensureProcessor (osPow - 1); if (auto* const osProc = getOversamplingProcessor()) osLatency = osProc->getLatencyInSamples(); diff --git a/test/OversamplerTests.cpp b/test/OversamplerTests.cpp index a7a7841b8..4495bd571 100644 --- a/test/OversamplerTests.cpp +++ b/test/OversamplerTests.cpp @@ -15,22 +15,45 @@ BOOST_AUTO_TEST_CASE (Basics) BOOST_REQUIRE (os.getProcessor (0) == nullptr); BOOST_REQUIRE (os.getLatencySamples (0) == 0); BOOST_REQUIRE (os.getFactor (0) == 1); + + // prepare() records the spec but does not build any chains. os.prepare (2, 1024); - BOOST_REQUIRE (os.getNumProcessors() == 3); - BOOST_REQUIRE_EQUAL (os.getFactor (0), 2); - for (int i = 0; i < os.getNumProcessors(); ++i) { - BOOST_REQUIRE (nullptr != os.getProcessor (i)); - if (auto* const proc = os.getProcessor (i)) { - size_t factora = static_cast (std::pow (2.0, (double) (i + 1))); - size_t factorb = proc->getOversamplingFactor(); - BOOST_REQUIRE_EQUAL (os.getFactor (i), (int) proc->getOversamplingFactor()); - BOOST_REQUIRE_EQUAL (factora, factorb); - BOOST_REQUIRE (proc->getLatencyInSamples() > 0.f); - BOOST_REQUIRE (os.getLatencySamples (i) > 0.f); - } + BOOST_REQUIRE (os.getNumProcessors() == 0); + BOOST_REQUIRE (os.getProcessor (0) == nullptr); + + // Chains are created on demand. + for (int i = 0; i < 3; ++i) { + auto* const proc = os.ensureProcessor (i); + BOOST_REQUIRE (nullptr != proc); + BOOST_REQUIRE (proc == os.getProcessor (i)); + size_t factora = static_cast (std::pow (2.0, (double) (i + 1))); + size_t factorb = proc->getOversamplingFactor(); + BOOST_REQUIRE_EQUAL (os.getFactor (i), (int) proc->getOversamplingFactor()); + BOOST_REQUIRE_EQUAL (factora, factorb); + BOOST_REQUIRE (proc->getLatencyInSamples() > 0.f); + BOOST_REQUIRE (os.getLatencySamples (i) > 0.f); } + // ensureProcessor is idempotent. + auto* const first = os.getProcessor (1); + BOOST_REQUIRE (first == os.ensureProcessor (1)); + + // Out of range or invalid indexes return nullptr. + BOOST_REQUIRE (os.ensureProcessor (-1) == nullptr); + BOOST_REQUIRE (os.ensureProcessor (3) == nullptr); + + // Changing the spec drops stale chains until re-ensured. + os.prepare (2, 512); + BOOST_REQUIRE (os.getProcessor (1) == nullptr); + BOOST_REQUIRE (nullptr != os.ensureProcessor (1)); + os.reset(); } +BOOST_AUTO_TEST_CASE (EnsureWithoutPrepare) +{ + Oversampler os; + BOOST_REQUIRE (os.ensureProcessor (0) == nullptr); +} + BOOST_AUTO_TEST_SUITE_END() From 5a8dd5e31bfb4c1deeab8e50c2556ca33259d9fd Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Tue, 25 Aug 2026 09:14:36 -0400 Subject: [PATCH 2/3] session: various cleanups to help loading time. --- include/element/node.hpp | 10 +- src/engine/graphmanager.cpp | 159 ++++++++++++++++------- src/engine/graphmanager.hpp | 7 ++ src/engine/graphnode.cpp | 51 +++++--- src/engine/graphnode.hpp | 17 ++- src/node.cpp | 14 ++- src/presetmanager.hpp | 39 ++++-- src/services/engineservice.cpp | 5 +- src/services/guiservice.cpp | 3 + src/services/presetservice.cpp | 3 + src/services/sessionservice.cpp | 1 - src/tracer.hpp | 73 +++++++++++ src/ui/sessiondocument.cpp | 2 + test/CMakeLists.txt | 1 + test/SessionLoadBenchTests.cpp | 217 ++++++++++++++++++++++++++++++++ 15 files changed, 519 insertions(+), 83 deletions(-) create mode 100644 src/tracer.hpp create mode 100644 test/SessionLoadBenchTests.cpp diff --git a/include/element/node.hpp b/include/element/node.hpp index 7191819b1..e787e2154 100644 --- a/include/element/node.hpp +++ b/include/element/node.hpp @@ -383,9 +383,17 @@ class EL_API Node : public Model { /** Saves the node state from Processor to state property */ void savePluginState(); - /** Reads state property and applies to Processor */ + /** Reads state property and applies to Processor. + Recurses into child nodes; use restoreOwnPluginState() when children + are restored elsewhere. + */ void restorePluginState(); + /** Reads state property and applies to Processor for this node only, + without recursing into child nodes. + */ + void restoreOwnPluginState(); + //========================================================================= /** Get the number of factory presets */ int getNumPrograms() const; diff --git a/src/engine/graphmanager.cpp b/src/engine/graphmanager.cpp index 096b77817..0c20b76cb 100644 --- a/src/engine/graphmanager.cpp +++ b/src/engine/graphmanager.cpp @@ -13,6 +13,7 @@ #include "nodes/placeholder.hpp" #include "engine/rootgraph.hpp" +#include "tracer.hpp" #include "utils.hpp" namespace element { @@ -149,8 +150,7 @@ struct IONodeEnforcer jassert (ioNodes[t] != nullptr); } - for (const auto& nodeId : nodesToRemove) - manager.removeNode (nodeId); + manager.removeNodes (nodesToRemove); model.resetPorts(); } @@ -238,7 +238,6 @@ class GraphManager::Binding manager = std::make_unique (*sub, owner.pluginManager); manager->setNodeModel (node); - IONodeEnforcer addIO (*manager); } else { @@ -344,6 +343,7 @@ bool GraphManager::contains (const uint32 nodeId) const Processor* GraphManager::createFilter (const PluginDescription* desc, double x, double y, uint32 nodeId) { + EL_LOAD_TRACE (String ("createFilter: ") + desc->name); String errorMessage; auto node = std::unique_ptr ( pluginManager.createGraphNode (*desc, errorMessage)); @@ -360,7 +360,9 @@ Processor* GraphManager::createFilter (const PluginDescription* desc, double x, errorMessage = "Could not find node"; } - return node != nullptr ? processor.addNode (node.release(), nodeId) : nullptr; + // Defer the prepare: the caller configures buses and restores plugin + // state first, then prepares once the configuration is final. + return node != nullptr ? processor.addNode (node.release(), nodeId, true) : nullptr; } Processor* GraphManager::createPlaceholder (const Node& node) @@ -490,17 +492,28 @@ uint32 GraphManager::addNode (const PluginDescription* desc, double rx, double r if (tryStereo != nullptr && proc->checkBusesLayoutSupported (*tryStereo)) { - proc->suspendProcessing (true); - proc->releaseResources(); + if (object->isPrepared) + { + proc->suspendProcessing (true); + proc->releaseResources(); - if (! proc->setBusesLayout (*tryStereo)) - proc->setBusesLayout (oldLayout); + if (! proc->setBusesLayout (*tryStereo)) + proc->setBusesLayout (oldLayout); - proc->prepareToPlay (processor.getSampleRate(), processor.getBlockSize()); - proc->suspendProcessing (false); + proc->prepareToPlay (processor.getSampleRate(), processor.getBlockSize()); + proc->suspendProcessing (false); + } + else if (! proc->setBusesLayout (*tryStereo)) + { + proc->setBusesLayout (oldLayout); + } } } + // Deferred by createFilter: prepare once the bus layout is final. + if (processor.prepared() && ! object->isPrepared) + object->prepare (processor.getSampleRate(), processor.getBlockSize(), &processor); + nodes.addChild (data, -1, nullptr); changed(); } @@ -515,34 +528,45 @@ uint32 GraphManager::addNode (const PluginDescription* desc, double rx, double r void GraphManager::removeNode (const uint32 uid) { - if (! processor.removeNode (uid)) + Array uids; + uids.add (uid); + removeNodes (uids); +} + +void GraphManager::removeNodes (const juce::Array& uids) +{ + if (! processor.removeNodes (uids)) return; - for (int i = 0; i < nodes.getNumChildren(); ++i) + + for (const auto& uid : uids) { - const Node node (nodes.getChild (i), false); - if (node.getNodeId() == uid) + for (int i = 0; i < nodes.getNumChildren(); ++i) { - // the model was probably referencing the node ptr - ProcessorPtr obj = node.getObject(); - if (obj) + const Node node (nodes.getChild (i), false); + if (node.getNodeId() == uid) { - obj->willBeRemoved(); - obj->releaseResources(); - } + // the model was probably referencing the node ptr + ProcessorPtr obj = node.getObject(); + if (obj) + { + obj->willBeRemoved(); + obj->releaseResources(); + } - for (int i = bindings.size(); --i >= 0;) - { - auto binding = bindings.getUnchecked (i); - if (binding->object == obj) - bindings.remove (i, true); - } + for (int j = bindings.size(); --j >= 0;) + { + auto binding = bindings.getUnchecked (j); + if (binding->object == obj) + bindings.remove (j, true); + } - auto data = node.data(); - nodes.removeChild (data, nullptr); - // clear all referecnce counted objects - Node::sanitizeProperties (data, true); - // finally delete the node + plugin instance. - obj = nullptr; + auto data = node.data(); + nodes.removeChild (data, nullptr); + // clear all referecnce counted objects + Node::sanitizeProperties (data, true); + // finally delete the node + plugin instance. + obj = nullptr; + } } } @@ -626,6 +650,7 @@ void GraphManager::removeConnection (uint32 sourceNode, uint32 sourcePort, uint3 void GraphManager::setNodeModel (const Node& node) { + EL_LOAD_TRACE (String ("setNodeModel: ") + node.getName()); loaded = false; processor.clear(); @@ -675,9 +700,9 @@ void GraphManager::setNodeModel (const Node& node) // If you hit this, then failed nodes didn't get handled properly jassert (nodes.getNumChildren() == processor.getNumNodes()); - // Cheap way to refresh engine-side nodes + // Refresh engine-side nodes. Coalesced: the render sequence would be + // invalidated again by the arc loop below, so don't force a build here. processor.triggerAsyncUpdate(); - processor.handleUpdateNowIfNeeded(); for (int i = 0; i < arcs.getNumChildren(); ++i) { @@ -726,8 +751,11 @@ void GraphManager::setNodeModel (const Node& node) jassert (arcs.getNumChildren() == processor.getNumConnections()); failed.clearQuick(); - IONodeEnforcer enforceIONodes (*this); - processorArcsChanged(); + { + EL_LOAD_TRACE ("setNodeModel: enforce IO + sync arcs"); + IONodeEnforcer enforceIONodes (*this); + processorArcsChanged(); + } } void GraphManager::savePluginStates() @@ -812,6 +840,7 @@ void GraphManager::setupNode (const ValueTree& data, ProcessorPtr obj) if (auto* const proc = obj->getAudioProcessor()) { + EL_LOAD_TRACE (String ("setupNode: buses: ") + node.getName()); bool busesConfigured = false; { // try to load buses layout. @@ -836,11 +865,20 @@ void GraphManager::setupNode (const ValueTree& data, ProcessorPtr obj) if (proc->checkBusesLayoutSupported (layout)) { - proc->suspendProcessing (true); - proc->releaseResources(); - busesConfigured = proc->setBusesLayoutWithoutEnabling (layout); - proc->prepareToPlay (processor.getSampleRate(), processor.getBlockSize()); - proc->suspendProcessing (false); + if (obj->isPrepared) + { + proc->suspendProcessing (true); + proc->releaseResources(); + busesConfigured = proc->setBusesLayoutWithoutEnabling (layout); + proc->prepareToPlay (processor.getSampleRate(), processor.getBlockSize()); + proc->suspendProcessing (false); + } + else + { + // Not prepared yet (deferred by createFilter): the + // layout can be applied directly. + busesConfigured = proc->setBusesLayoutWithoutEnabling (layout); + } } } } @@ -855,19 +893,44 @@ void GraphManager::setupNode (const ValueTree& data, ProcessorPtr obj) if (proc->checkBusesLayoutSupported (layout)) { - proc->suspendProcessing (true); - proc->releaseResources(); - proc->setBusesLayoutWithoutEnabling (layout); - proc->prepareToPlay (processor.getSampleRate(), processor.getBlockSize()); - proc->suspendProcessing (false); + if (obj->isPrepared) + { + proc->suspendProcessing (true); + proc->releaseResources(); + proc->setBusesLayoutWithoutEnabling (layout); + proc->prepareToPlay (processor.getSampleRate(), processor.getBlockSize()); + proc->suspendProcessing (false); + } + else + { + proc->setBusesLayoutWithoutEnabling (layout); + } } resetPorts = true; } } - node.restorePluginState(); - node.resetPorts(); + { + EL_LOAD_TRACE (String ("setupNode: restore state: ") + node.getName()); + // A subgraph's descendants were already restored one-by-one by the + // child GraphManager the Binding created above — restoring them again + // here would call setStateInformation twice per nesting level. + if (obj->isSubGraph()) + node.restoreOwnPluginState(); + else + node.restorePluginState(); + } + { + EL_LOAD_TRACE (String ("setupNode: reset ports: ") + node.getName()); + node.resetPorts(); + } + + // Deferred by createFilter: prepare only now that the bus layout and + // plugin state are final, so the plugin is prepared exactly once. + if (processor.prepared() && ! obj->isPrepared) + obj->prepare (processor.getSampleRate(), processor.getBlockSize(), &processor); + if (node.isA ("Element", EL_NODE_ID_MIDI_INPUT_DEVICE) || node.isA ("Element", EL_NODE_ID_MIDI_OUTPUT_DEVICE)) { jassert (node.getNumPorts() == 1); diff --git a/src/engine/graphmanager.hpp b/src/engine/graphmanager.hpp index 00cf2ab55..127e2cafc 100644 --- a/src/engine/graphmanager.hpp +++ b/src/engine/graphmanager.hpp @@ -54,6 +54,13 @@ class GraphManager : public juce::ChangeBroadcaster /** Remove a node by ID */ void removeNode (const uint32 nodeId); + /** Removes several nodes by ID, rebuilding the rendering sequence and the + arcs model once for the whole batch. + + @param nodeIds the IDs of the nodes to remove + */ + void removeNodes (const juce::Array& nodeIds); + /** Disconnect a node from other nodes */ void disconnectNode (const uint32 nodeId, const bool inputs = true, const bool outputs = true, const bool audio = true, const bool midi = true); diff --git a/src/engine/graphnode.cpp b/src/engine/graphnode.cpp index 4201428e7..d9e973763 100644 --- a/src/engine/graphnode.cpp +++ b/src/engine/graphnode.cpp @@ -11,6 +11,7 @@ #include "engine/ionode.hpp" #include "nodes/audioprocessor.hpp" #include "engine/graphnode.hpp" +#include "tracer.hpp" #ifndef EL_GRAPH_NODE_NAME #define EL_GRAPH_NODE_NAME "Graph" @@ -61,7 +62,7 @@ Processor* GraphNode::getNodeForId (const uint32 nodeId) const return nullptr; } -Processor* GraphNode::addNode (Processor* newNode, uint32 nodeId) +Processor* GraphNode::addNode (Processor* newNode, uint32 nodeId, bool deferPrepare) { if (newNode == nullptr || (void*) newNode->getAudioProcessor() == (void*) this) { @@ -109,7 +110,7 @@ Processor* GraphNode::addNode (Processor* newNode, uint32 nodeId) newNode->setPlayHead (playhead); newNode->setParentGraph (this); newNode->refreshPorts(); - if (prepared()) + if (! deferPrepare && prepared()) newNode->prepare (getSampleRate(), getBlockSize(), this); triggerAsyncUpdate(); return added; @@ -117,28 +118,44 @@ Processor* GraphNode::addNode (Processor* newNode, uint32 nodeId) bool GraphNode::removeNode (const uint32 nodeId) { - disconnectNode (nodeId); - for (int i = nodes.size(); --i >= 0;) + Array nodeIds; + nodeIds.add (nodeId); + return removeNodes (nodeIds); +} + +bool GraphNode::removeNodes (const Array& nodeIds) +{ + ReferenceCountedArray removed; + + for (const auto& nodeId : nodeIds) { - ProcessorPtr n = nodes.getUnchecked (i); - if (n->nodeId == nodeId) + disconnectNode (nodeId); + for (int i = nodes.size(); --i >= 0;) { + ProcessorPtr n = nodes.getUnchecked (i); + if (n->nodeId != nodeId) + continue; nodes.remove (i); + removed.add (n.get()); + break; + } + } - handleAsyncUpdate(); - n->setParentGraph (nullptr); - n->setPlayHead (nullptr); + if (removed.isEmpty()) + return false; - if (n->isSubGraph()) - { - DBG ("[element] sub graph removed"); - } + // Rebuild synchronously before detaching: the current rendering ops hold + // raw pointers to the removed nodes, so they must stay alive and attached + // until the sequence no longer references them. + handleAsyncUpdate(); - return true; - } + for (auto* n : removed) + { + n->setParentGraph (nullptr); + n->setPlayHead (nullptr); } - return false; + return true; } const GraphNode::Connection* @@ -403,6 +420,8 @@ bool GraphNode::isAnInputTo (const uint32 possibleInputId, void GraphNode::buildRenderingSequence() { + EL_LOAD_TRACE (String ("buildRenderingSequence: ") + getName()); + EL_LOAD_TRACE_COUNT ("buildRenderingSequence"); Array newRenderingOps; int numRenderingBuffersNeeded = 2; int numMidiBuffersNeeded = 1; diff --git a/src/engine/graphnode.hpp b/src/engine/graphnode.hpp index e6203a2ac..af8ba9a24 100644 --- a/src/engine/graphnode.hpp +++ b/src/engine/graphnode.hpp @@ -75,9 +75,13 @@ class GraphNode : public Processor, The optional nodeId parameter lets you specify an ID to use for the node, but if the value is already in use, this new node will overwrite the old one. + When deferPrepare is true the node is registered but not prepared, even + if this graph is already prepared. The caller must prepare it once its + configuration (bus layout, plugin state) is final. + If this succeeds, it returns a pointer to the newly-created node. */ - Processor* addNode (Processor* newNode, uint32 nodeId = 0); + Processor* addNode (Processor* newNode, uint32 nodeId = 0, bool deferPrepare = false); /** Deletes a node within the graph which has the specified ID. @@ -85,6 +89,17 @@ class GraphNode : public Processor, */ bool removeNode (uint32 nodeId); + /** Deletes several nodes in one pass. + + Connections attached to the removed nodes are also deleted. The + rendering sequence is rebuilt once for the whole batch instead of once + per node. + + @param nodeIds the IDs of the nodes to delete + @return true if at least one node was removed + */ + bool removeNodes (const Array& nodeIds); + /** Builds an array of ordered nodes */ void getOrderedNodes (ReferenceCountedArray& res); diff --git a/src/node.cpp b/src/node.cpp index fd33d19ee..0221537e4 100644 --- a/src/node.cpp +++ b/src/node.cpp @@ -486,7 +486,9 @@ ValueTree Node::addScript (const Script& script) void Node::setMissingProperties() { - stabilizePropertyString (tags::uuid, Uuid().toString()); + // Generating a Uuid isn't free: only make one when actually missing. + if (! objectData.hasProperty (tags::uuid)) + objectData.setProperty (tags::uuid, Uuid().toString(), nullptr); stabilizePropertyString (tags::type, types::Node.toString()); stabilizePropertyString (tags::name, "Node"); stabilizeProperty (tags::bypass, false); @@ -822,6 +824,13 @@ MidiChannels Node::getMidiChannels() const } void Node::restorePluginState() +{ + restoreOwnPluginState(); + for (int i = 0; i < getNumNodes(); ++i) + getNode (i).restorePluginState(); +} + +void Node::restoreOwnPluginState() { if (! isValid()) return; @@ -930,9 +939,6 @@ void Node::restorePluginState() const bool clearStateProperty = false; if (clearStateProperty) objectData.removeProperty (tags::state, 0); - - for (int i = 0; i < getNumNodes(); ++i) - getNode (i).restorePluginState(); } void Node::savePluginState() diff --git a/src/presetmanager.hpp b/src/presetmanager.hpp index dbb2e9aa9..5b04689d0 100644 --- a/src/presetmanager.hpp +++ b/src/presetmanager.hpp @@ -8,6 +8,8 @@ #include +#include "tracer.hpp" + namespace element { class PresetManager @@ -50,6 +52,7 @@ class PresetManager inline void refresh() { + EL_LOAD_TRACE ("PresetManager::refresh"); clear(); StringArray files; @@ -58,22 +61,36 @@ class PresetManager for (const auto& filename : files) { const File file (filename); - const Node node (Node::parse (file), false); - if (node.isValid()) + + auto item = std::make_unique(); + item->file = file.getFullPathName().toStdString(); + + // Only the root element's attributes are needed here, so for XML + // presets skip materializing the node state as a ValueTree. + if (auto xml = juce::XmlDocument::parse (file)) + { + if (! xml->hasTagName (types::Node.toString())) + continue; + item->name = xml->getStringAttribute (tags::name.toString()).toStdString(); + item->format = xml->getStringAttribute (tags::format.toString()).toStdString(); + item->ID = xml->getStringAttribute (tags::identifier.toString()).toStdString(); + } + else { - std::unique_ptr item; - item.reset (new PresetInfo()); - item->file = file.getFullPathName().toStdString(); + const Node node (Node::parse (file), false); + if (! node.isValid()) + continue; item->name = node.getName().toStdString(); - if (item->name.empty()) - item->name = file.getFileNameWithoutExtension().toStdString(); item->format = node.getFormat().toString().toStdString(); item->ID = node.getIdentifier().toString().toStdString(); - if (item->format.empty() || item->ID.empty()) - continue; - - presets.add (item.release()); } + + if (item->name.empty()) + item->name = file.getFileNameWithoutExtension().toStdString(); + if (item->format.empty() || item->ID.empty()) + continue; + + presets.add (item.release()); } presets.minimiseStorageOverheads(); diff --git a/src/services/engineservice.cpp b/src/services/engineservice.cpp index 381595dc6..cedbdecb8 100644 --- a/src/services/engineservice.cpp +++ b/src/services/engineservice.cpp @@ -15,8 +15,9 @@ #include "engine/graphmanager.hpp" #include "engine/rootgraph.hpp" #include "nodes/mididevice.hpp" +#include "tracer.hpp" -#define ELEMENT_TRACE_SESSION_LOAD 0 +#define ELEMENT_TRACE_SESSION_LOAD EL_TRACE_SESSION_LOAD using namespace juce; @@ -100,6 +101,7 @@ struct RootGraphHolder */ bool attach (AudioEnginePtr engine) { + EL_LOAD_TRACE (String ("RootGraphHolder::attach: ") + model.getName()); jassert (engine); if (! engine) { @@ -936,6 +938,7 @@ Node EngineService::addPlugin (const Node& graph, const PluginDescription& desc, void EngineService::sessionReloaded() { + EL_LOAD_TRACE ("EngineService::sessionReloaded"); graphs->clear(); auto session = context().session(); diff --git a/src/services/guiservice.cpp b/src/services/guiservice.cpp index afd9a5821..35f7ed683 100644 --- a/src/services/guiservice.cpp +++ b/src/services/guiservice.cpp @@ -18,6 +18,7 @@ #include "auth.hpp" #include "engine/midipanic.hpp" +#include "tracer.hpp" #include "messages.hpp" #include "services/mappingservice.hpp" #include "services/sessionservice.hpp" @@ -602,6 +603,7 @@ void GuiService::showPluginWindowsFor (const Node& node, const bool recursive, c void GuiService::presentPluginWindow (const Node& node, const bool focus) { + EL_LOAD_TRACE (String ("presentPluginWindow: ") + node.getName()); if (! windowManager) return; @@ -1106,6 +1108,7 @@ bool GuiService::perform (const InvocationInfo& info) void GuiService::stabilizeContent() { + EL_LOAD_TRACE ("GuiService::stabilizeContent"); if (auto* cc = _content.get()) cc->stabilize(); refreshMainMenu(); diff --git a/src/services/presetservice.cpp b/src/services/presetservice.cpp index f1318e4cd..fe7340132 100644 --- a/src/services/presetservice.cpp +++ b/src/services/presetservice.cpp @@ -37,6 +37,9 @@ PresetService::~PresetService() void PresetService::activate() { + // Scan the preset library once at startup. Session loads no longer + // refresh it; in-app preset writes go through add() which does. + context().presets().refresh(); } void PresetService::deactivate() diff --git a/src/services/sessionservice.cpp b/src/services/sessionservice.cpp index de30ac773..48aa77a57 100644 --- a/src/services/sessionservice.cpp +++ b/src/services/sessionservice.cpp @@ -302,7 +302,6 @@ void SessionService::refreshOtherControllers() sibling()->sessionReloaded(); sibling()->refresh(); sibling()->learn (false); - sibling()->refresh(); sigSessionLoaded(); } diff --git a/src/tracer.hpp b/src/tracer.hpp new file mode 100644 index 000000000..1aee7099b --- /dev/null +++ b/src/tracer.hpp @@ -0,0 +1,73 @@ +// Copyright 2026 Kushview, LLC +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +/** Set to 1 (or define via CMake) to log timing of session load phases to the + main log. Kept off by default: probes compile to nothing. + */ +#ifndef EL_TRACE_SESSION_LOAD +#define EL_TRACE_SESSION_LOAD 0 +#endif + +#if EL_TRACE_SESSION_LOAD + +#include + +#include + +namespace element { + +/** RAII timer which logs "[load]