From 3f8bff5a625835343d23ee3ed40e3f19d77d28a5 Mon Sep 17 00:00:00 2001 From: Emma Broman Date: Thu, 28 Mar 2024 01:10:07 +0100 Subject: [PATCH 1/4] General performance improvements (#3142) * Adding some Tracy zones * Move point cloud dataset loading to initialize function to speed up startup * Switch order of operations in memoryaware tile cache to speed up startup * Move point cloud dataset loading to initialize function * Add more Zone Scoped and rearrange SDSS loading * More speed up --------- Co-authored-by: Alexander Bock --- apps/OpenSpace/ext/sgct | 2 +- data/assets/base.asset | 5 +- .../digitaluniverse/digitaluniverse.asset | 2 +- ext/ghoul | 2 +- include/openspace/rendering/labelscomponent.h | 25 ++++--- include/openspace/util/screenlog.h | 3 +- .../pointcloud/renderablepointcloud.cpp | 60 ++++++++++------- .../pointcloud/renderablepointcloud.h | 4 ++ modules/galaxy/rendering/renderablegalaxy.cpp | 2 + .../src/memoryawaretilecache.cpp | 4 +- modules/volume/rawvolumereader.inl | 20 +++--- src/data/dataloader.cpp | 67 +++++++++++++++++-- src/data/datamapping.cpp | 2 + src/engine/openspaceengine.cpp | 4 +- src/properties/propertyowner.cpp | 4 ++ src/rendering/colormappingcomponent.cpp | 2 + src/rendering/labelscomponent.cpp | 39 ++++++----- src/rendering/renderable.cpp | 2 + src/scene/assetmanager.cpp | 1 + src/scene/rotation.cpp | 2 + src/scene/scale.cpp | 2 + src/scene/scene.cpp | 6 +- src/scene/scene_lua.inl | 2 + src/scene/scenegraphnode.cpp | 27 +++++++- src/scene/sceneinitializer.cpp | 4 ++ src/scene/timeframe.cpp | 2 + src/scene/translation.cpp | 2 + src/util/openspacemodule.cpp | 1 + src/util/screenlog.cpp | 2 + 29 files changed, 217 insertions(+), 83 deletions(-) diff --git a/apps/OpenSpace/ext/sgct b/apps/OpenSpace/ext/sgct index e8ee8b48fd..49d1c03a28 160000 --- a/apps/OpenSpace/ext/sgct +++ b/apps/OpenSpace/ext/sgct @@ -1 +1 @@ -Subproject commit e8ee8b48fdcfd9164efcac03e7914af5fb5b1ce2 +Subproject commit 49d1c03a2857d370afcb419c361a158cda0e31a7 diff --git a/data/assets/base.asset b/data/assets/base.asset index 3c3b08a2d6..3f07199c09 100644 --- a/data/assets/base.asset +++ b/data/assets/base.asset @@ -4,6 +4,10 @@ asset.require("./base_blank") +-- We load the SDSS dataset first as that is the one that takes the longest, so the +-- earlier we start, the sooner the loading is done +asset.require("scene/digitaluniverse/sdss") + -- Specifying which other assets should be loaded in this scene asset.require("scene/solarsystem/sun/sun") asset.require("scene/solarsystem/sun/glare") @@ -61,7 +65,6 @@ asset.require("scene/digitaluniverse/openclusters") asset.require("scene/digitaluniverse/planetarynebulae") asset.require("scene/digitaluniverse/pulsars") asset.require("scene/digitaluniverse/quasars") -asset.require("scene/digitaluniverse/sdss") asset.require("scene/digitaluniverse/starlabels") asset.require("scene/digitaluniverse/starorbits") asset.require("scene/digitaluniverse/stars") diff --git a/data/assets/scene/digitaluniverse/digitaluniverse.asset b/data/assets/scene/digitaluniverse/digitaluniverse.asset index b128554315..8b929314bd 100644 --- a/data/assets/scene/digitaluniverse/digitaluniverse.asset +++ b/data/assets/scene/digitaluniverse/digitaluniverse.asset @@ -1,3 +1,4 @@ +asset.require("./sdss") asset.require("./2dF") asset.require("./2mass") asset.require("./6dF") @@ -26,7 +27,6 @@ asset.require("./openclusters") asset.require("./planetarynebulae") asset.require("./pulsars") asset.require("./quasars") -asset.require("./sdss") asset.require("./starlabels") asset.require("./starorbits") asset.require("./stars") diff --git a/ext/ghoul b/ext/ghoul index c8a56a6e63..c6d49eda51 160000 --- a/ext/ghoul +++ b/ext/ghoul @@ -1 +1 @@ -Subproject commit c8a56a6e63b81e13a3a957c5af43edc4ab1b838b +Subproject commit c6d49eda51ce34e729bf931d69e638c73c8e254a diff --git a/include/openspace/rendering/labelscomponent.h b/include/openspace/rendering/labelscomponent.h index 3d4aef8740..c729670534 100644 --- a/include/openspace/rendering/labelscomponent.h +++ b/include/openspace/rendering/labelscomponent.h @@ -48,19 +48,6 @@ class LabelsComponent : public properties::PropertyOwner, public Fadeable { public: explicit LabelsComponent(const ghoul::Dictionary& dictionary); - /** - * Create a labels component from an already loaded dataset. That dataset should have - * a comment per point to be used for the labels. - * - * \param dictionary A dictionary with the other information used for constructing - * the dataset - * \param dataset The dataset to create the labelset from, including xyz position and - * a string to be used for the text. - * \param unit The unit to use when interpreting the point information in the dataset - */ - explicit LabelsComponent(const ghoul::Dictionary& dictionary, - const dataloader::Dataset& dataset, DistanceUnit unit); - ~LabelsComponent() override = default; dataloader::Labelset& labelSet(); @@ -68,6 +55,18 @@ public: void initialize(); + /** + * Create the labels from an already loaded dataset. That dataset should have a comment + * per point to be used for the labels. + * + * This function should be called before the labels are initialized + * + * \param dataset The dataset to create the labelset from, including xyz position and + * a string to be used for the text. + * \param unit The unit to use when interpreting the point information in the dataset + */ + void loadLabelsFromDataset(const dataloader::Dataset& dataset, DistanceUnit unit); + void loadLabels(); bool isReady() const; diff --git a/include/openspace/util/screenlog.h b/include/openspace/util/screenlog.h index b5e883f295..7b9b8333f2 100644 --- a/include/openspace/util/screenlog.h +++ b/include/openspace/util/screenlog.h @@ -27,6 +27,7 @@ #include +#include #include #include #include @@ -124,7 +125,7 @@ private: /// A mutex to ensure thread-safety since the logging and the removal of expired /// entires can occur on different threads - mutable std::mutex _mutex; + mutable TracyLockable(std::mutex, _mutex); }; } // namespace openspace diff --git a/modules/base/rendering/pointcloud/renderablepointcloud.cpp b/modules/base/rendering/pointcloud/renderablepointcloud.cpp index f2ab0ff57c..e102bab029 100644 --- a/modules/base/rendering/pointcloud/renderablepointcloud.cpp +++ b/modules/base/rendering/pointcloud/renderablepointcloud.cpp @@ -586,6 +586,8 @@ RenderablePointCloud::RenderablePointCloud(const ghoul::Dictionary& dictionary) , _colorSettings(dictionary) , _sizeSettings(dictionary) { + ZoneScoped; + const Parameters p = codegen::bake(dictionary); addProperty(Fadeable::_opacity); @@ -697,38 +699,19 @@ RenderablePointCloud::RenderablePointCloud(const ghoul::Dictionary& dictionary) }); } - if (_hasDataFile) { - bool useCaching = p.useCaching.value_or(true); - if (useCaching) { - _dataset = dataloader::data::loadFileWithCache(_dataFile, _dataMapping); - } - else { - _dataset = dataloader::data::loadFile(_dataFile, _dataMapping); - } - _nDataPoints = static_cast(_dataset.entries.size()); + _useCaching = p.useCaching.value_or(true); - // If no scale exponent was specified, compute one that will at least show the - // points based on the scale of the positions in the dataset - if (!p.sizeSettings.has_value() || !p.sizeSettings->scaleExponent.has_value()) { - double dist = _dataset.maxPositionComponent * toMeter(_unit); - if (dist > 0.0) { - float exponent = static_cast(std::log10(dist)); - // Reduce the actually used exponent a little bit, as just using the - // logarithm as is leads to very large points - _sizeSettings.scaleExponent = 0.9f * exponent; - } - } + // If no scale exponent was specified, compute one that will at least show the + // points based on the scale of the positions in the dataset + if (!p.sizeSettings.has_value() || !p.sizeSettings->scaleExponent.has_value()) { + _shouldComputeScaleExponent = true; } if (p.labels.has_value()) { if (!p.labels->hasKey("File") && _hasDataFile) { - // Load the labelset from the dataset if no label file was included - _labels = std::make_unique(*p.labels, _dataset, _unit); + _createLabelsFromDataset = true; } - else { - _labels = std::make_unique(*p.labels); - } - + _labels = std::make_unique(*p.labels); _hasLabels = true; addPropertySubOwner(_labels.get()); // Fading of the labels should depend on the fading of the renderable @@ -764,11 +747,36 @@ void RenderablePointCloud::initialize() { break; } + if (_hasDataFile) { + if (_useCaching) { + _dataset = dataloader::data::loadFileWithCache(_dataFile, _dataMapping); + } + else { + _dataset = dataloader::data::loadFile(_dataFile, _dataMapping); + } + _nDataPoints = static_cast(_dataset.entries.size()); + + // If no scale exponent was specified, compute one that will at least show the + // points based on the scale of the positions in the dataset + if (_shouldComputeScaleExponent) { + double dist = _dataset.maxPositionComponent * toMeter(_unit); + if (dist > 0.0) { + float exponent = static_cast(std::log10(dist)); + // Reduce the actually used exponent a little bit, as just using the + // logarithm as is leads to very large points + _sizeSettings.scaleExponent = 0.9f * exponent; + } + } + } + if (_hasDataFile && _hasColorMapFile) { _colorSettings.colorMapping->initialize(_dataset); } if (_hasLabels) { + if (_createLabelsFromDataset) { + _labels->loadLabelsFromDataset(_dataset, _unit); + } _labels->initialize(); } } diff --git a/modules/base/rendering/pointcloud/renderablepointcloud.h b/modules/base/rendering/pointcloud/renderablepointcloud.h index 05e2cf3a65..428aaf44ee 100644 --- a/modules/base/rendering/pointcloud/renderablepointcloud.h +++ b/modules/base/rendering/pointcloud/renderablepointcloud.h @@ -227,6 +227,10 @@ protected: DistanceUnit _unit = DistanceUnit::Parsec; + bool _useCaching = true; + bool _shouldComputeScaleExponent = false; + bool _createLabelsFromDataset = false; + dataloader::Dataset _dataset; dataloader::DataMapping _dataMapping; diff --git a/modules/galaxy/rendering/renderablegalaxy.cpp b/modules/galaxy/rendering/renderablegalaxy.cpp index 8069010734..7843120f92 100644 --- a/modules/galaxy/rendering/renderablegalaxy.cpp +++ b/modules/galaxy/rendering/renderablegalaxy.cpp @@ -781,6 +781,8 @@ RenderableGalaxy::Result RenderableGalaxy::loadPointFile() { RenderableGalaxy::Result RenderableGalaxy::loadCachedFile( const std::filesystem::path& file) { + ZoneScoped; + std::ifstream fileStream = std::ifstream(file, std::ifstream::binary); if (!fileStream.good()) { LERROR(std::format("Error opening file '{}' for loading cache file", file)); diff --git a/modules/globebrowsing/src/memoryawaretilecache.cpp b/modules/globebrowsing/src/memoryawaretilecache.cpp index ac35f07a88..e9d0a5d5c0 100644 --- a/modules/globebrowsing/src/memoryawaretilecache.cpp +++ b/modules/globebrowsing/src/memoryawaretilecache.cpp @@ -221,6 +221,8 @@ void MemoryAwareTileCache::TextureContainer::reset() { ghoul::opengl::Texture::FilterMode::AnisotropicMipMap; for (size_t i = 0; i < _numTextures; i++) { + ZoneScopedN("Texture"); + using namespace ghoul::opengl; std::unique_ptr tex = std::make_unique( @@ -235,8 +237,8 @@ void MemoryAwareTileCache::TextureContainer::reset() { ); tex->setDataOwnership(Texture::TakeOwnership::Yes); - tex->uploadTexture(); tex->setFilter(mode); + tex->uploadTexture(); _textures.push_back(std::move(tex)); } diff --git a/modules/volume/rawvolumereader.inl b/modules/volume/rawvolumereader.inl index f17fcc1215..0b3ed74269 100644 --- a/modules/volume/rawvolumereader.inl +++ b/modules/volume/rawvolumereader.inl @@ -79,22 +79,22 @@ glm::uvec3 RawVolumeReader::indexToCoords(size_t linear) const { template std::unique_ptr> RawVolumeReader::read(bool invertZ) { - glm::uvec3 dims = dimensions(); - auto volume = std::make_unique>(dims); - - std::ifstream file(_path, std::ios::binary); - char* buffer = reinterpret_cast(volume->data()); + ZoneScoped; + std::ifstream file = std::ifstream(_path, std::ios::binary); if (file.fail()) { throw ghoul::FileNotFoundError("Volume file not found"); } - size_t length = static_cast(dims.x) * - static_cast(dims.y) * - static_cast(dims.z) * - sizeof(VoxelType); + glm::uvec3 dims = dimensions(); + auto volume = std::make_unique>(dims); - file.read(buffer, length); + char* buffer = reinterpret_cast(volume->data()); + size_t length = glm::compMul(dims) * sizeof(VoxelType); + { + ZoneScopedN("read"); + file.read(buffer, length); + } if (file.fail()) { throw ghoul::RuntimeError("Error reading volume file"); diff --git a/src/data/dataloader.cpp b/src/data/dataloader.cpp index 057b82dece..9402acc9bc 100644 --- a/src/data/dataloader.cpp +++ b/src/data/dataloader.cpp @@ -40,7 +40,7 @@ #include namespace { - constexpr int8_t DataCacheFileVersion = 11; + constexpr int8_t DataCacheFileVersion = 12; constexpr int8_t LabelCacheFileVersion = 11; constexpr int8_t ColorCacheFileVersion = 11; @@ -75,6 +75,8 @@ namespace { std::is_same_v ); + ZoneScoped; + std::string info; if (specs.has_value()) { info = openspace::dataloader::generateHashString(*specs); @@ -93,12 +95,13 @@ namespace { std::optional dataset = loadCacheFunction(cached); if (dataset.has_value()) { // We could load the cache file and we are now done with this - return *dataset; + return std::move(*dataset); } else { FileSys.cacheManager()->removeCacheFile(cached); } } + LINFOC("DataLoader", std::format("Loading file '{}'", filePath)); T dataset = loadFunction(filePath, specs); @@ -116,6 +119,8 @@ namespace openspace::dataloader { namespace data { Dataset loadFile(std::filesystem::path path, std::optional specs) { + ZoneScoped; + ghoul_assert(std::filesystem::exists(path), "File must exist"); const std::ifstream file = std::ifstream(path); @@ -143,6 +148,8 @@ Dataset loadFile(std::filesystem::path path, std::optional specs) { } std::optional loadCachedFile(const std::filesystem::path& path) { + ZoneScoped; + std::ifstream file = std::ifstream(path, std::ios::binary); if (!file.good()) { return std::nullopt; @@ -163,6 +170,8 @@ std::optional loadCachedFile(const std::filesystem::path& path) { file.read(reinterpret_cast(&nVariables), sizeof(uint16_t)); result.variables.resize(nVariables); for (int i = 0; i < nVariables; i += 1) { + ZoneScopedN("Variable"); + Dataset::Variable var; int16_t idx = 0; @@ -183,6 +192,8 @@ std::optional loadCachedFile(const std::filesystem::path& path) { file.read(reinterpret_cast(&nTextures), sizeof(uint16_t)); result.textures.resize(nTextures); for (int i = 0; i < nTextures; i += 1) { + ZoneScopedN("Texture"); + Dataset::Texture tex; int16_t idx = 0; @@ -213,28 +224,55 @@ std::optional loadCachedFile(const std::filesystem::path& path) { file.read(reinterpret_cast(&nEntries), sizeof(uint64_t)); result.entries.reserve(nEntries); for (uint64_t i = 0; i < nEntries; i += 1) { + ZoneScopedN("Dataset"); + Dataset::Entry e; - file.read(reinterpret_cast(&e.position.x), sizeof(float)); - file.read(reinterpret_cast(&e.position.y), sizeof(float)); - file.read(reinterpret_cast(&e.position.z), sizeof(float)); + file.read(reinterpret_cast(&e.position.x), 3 * sizeof(float)); uint16_t nValues = 0; file.read(reinterpret_cast(&nValues), sizeof(uint16_t)); e.data.resize(nValues); file.read(reinterpret_cast(e.data.data()), nValues * sizeof(float)); + // For now we just store the length of the comment. Since the comments are stored + // in one block after the data entries, we can use the length later to extract the + // contents of this entries comment out of the big block uint16_t len = 0; file.read(reinterpret_cast(&len), sizeof(uint16_t)); if (len > 0) { + // If there is a comment, we already allocate the space for it here. This way + // we don't need to separately store the length of it, but can use the size of + // the vector instead std::string comment; comment.resize(len); - file.read(comment.data(), len); e.comment = std::move(comment); } result.entries.push_back(std::move(e)); } + // + // Read comments in one block and then assign them to the data entries + uint64_t totalCommentLength = 0; + file.read(reinterpret_cast(&totalCommentLength), sizeof(uint64_t)); + std::vector commentBuffer; + commentBuffer.resize(totalCommentLength); + file.read(commentBuffer.data(), totalCommentLength); + // idx is the running index into the total comment buffer + int idx = 0; + for (Dataset::Entry& e : result.entries) { + if (e.comment.has_value()) { + ghoul_assert(idx < commentBuffer.size(), "Index too large"); + + // If we have a comment, we need to extract its length's worth of characters + // from the buffer + std::memcpy(e.comment->data(), &commentBuffer[idx], e.comment->size()); + + // and then advance the index + idx += e.comment->size(); + } + } + // // Read max data point variable float max = 0.f; @@ -245,6 +283,8 @@ std::optional loadCachedFile(const std::filesystem::path& path) { } void saveCachedFile(const Dataset& dataset, const std::filesystem::path& path) { + ZoneScoped; + std::ofstream file = std::ofstream(path, std::ofstream::binary); file.write(reinterpret_cast(&DataCacheFileVersion), sizeof(int8_t)); @@ -297,6 +337,7 @@ void saveCachedFile(const Dataset& dataset, const std::filesystem::path& path) { checkSize(dataset.entries.size(), "Too many entries"); uint64_t nEntries = static_cast(dataset.entries.size()); file.write(reinterpret_cast(&nEntries), sizeof(uint64_t)); + uint64_t totalCommentLength = 0; for (const Dataset::Entry& e : dataset.entries) { file.write(reinterpret_cast(&e.position.x), sizeof(float)); file.write(reinterpret_cast(&e.position.y), sizeof(float)); @@ -317,6 +358,18 @@ void saveCachedFile(const Dataset& dataset, const std::filesystem::path& path) { static_cast(e.comment->size()) : 0; file.write(reinterpret_cast(&commentLen), sizeof(uint16_t)); + totalCommentLength += commentLen; + //if (e.comment.has_value()) { + // file.write(e.comment->data(), e.comment->size()); + //} + } + + // + // Write all of the comments next. We don't have to store the individual comment + // lengths as the data values written before already have those stored. And since we + // are reading the comments in the same order as the dataset entries, we're good + file.write(reinterpret_cast(&totalCommentLength), sizeof(uint64_t)); + for (const Dataset::Entry& e : dataset.entries) { if (e.comment.has_value()) { file.write(e.comment->data(), e.comment->size()); } @@ -345,6 +398,8 @@ Dataset loadFileWithCache(std::filesystem::path path, std::optional namespace label { Labelset loadFile(std::filesystem::path path, std::optional) { + ZoneScoped; + ghoul_assert(std::filesystem::exists(path), "File must exist"); const std::ifstream file = std::ifstream(path); diff --git a/src/data/datamapping.cpp b/src/data/datamapping.cpp index 9907d49ba5..5e523b9092 100644 --- a/src/data/datamapping.cpp +++ b/src/data/datamapping.cpp @@ -151,6 +151,8 @@ documentation::Documentation DataMapping::Documentation() { } DataMapping DataMapping::createFromDictionary(const ghoul::Dictionary& dictionary) { + ZoneScoped; + const Parameters p = codegen::bake(dictionary); DataMapping result; diff --git a/src/engine/openspaceengine.cpp b/src/engine/openspaceengine.cpp index faacf68ece..cb64544433 100644 --- a/src/engine/openspaceengine.cpp +++ b/src/engine/openspaceengine.cpp @@ -757,8 +757,8 @@ void OpenSpaceEngine::loadAssets() { std::unique_ptr sceneInitializer; if (global::configuration->useMultithreadedInitialization) { - const unsigned int nAvailableThreads = std::min( - std::thread::hardware_concurrency() - 1, + const unsigned int nAvailableThreads = std::max( + std::thread::hardware_concurrency() / 2, 4u ); const unsigned int nThreads = nAvailableThreads == 0 ? 2 : nAvailableThreads; diff --git a/src/properties/propertyowner.cpp b/src/properties/propertyowner.cpp index bbe708ba3a..1585493e25 100644 --- a/src/properties/propertyowner.cpp +++ b/src/properties/propertyowner.cpp @@ -163,6 +163,8 @@ std::string PropertyOwner::propertyGroupName(const std::string& groupID) const { } void PropertyOwner::addProperty(Property* prop) { + ZoneScoped; + ghoul_precondition(prop != nullptr, "prop must not be nullptr"); if (prop->identifier().empty()) { @@ -207,6 +209,8 @@ void PropertyOwner::addProperty(Property& prop) { } void PropertyOwner::addPropertySubOwner(openspace::properties::PropertyOwner* owner) { + ZoneScoped; + ghoul_precondition(owner != nullptr, "owner must not be nullptr"); ghoul_precondition( !owner->identifier().empty(), diff --git a/src/rendering/colormappingcomponent.cpp b/src/rendering/colormappingcomponent.cpp index 6070f3c4af..4dbf91e9ee 100644 --- a/src/rendering/colormappingcomponent.cpp +++ b/src/rendering/colormappingcomponent.cpp @@ -330,6 +330,8 @@ ghoul::opengl::Texture* ColorMappingComponent::texture() const { } void ColorMappingComponent::initialize(const dataloader::Dataset& dataset) { + ZoneScoped; + _colorMap = dataloader::color::loadFileWithCache(colorMapFile.value()); initializeParameterData(dataset); diff --git a/src/rendering/labelscomponent.cpp b/src/rendering/labelscomponent.cpp index 4d5e47643f..8222e4c663 100644 --- a/src/rendering/labelscomponent.cpp +++ b/src/rendering/labelscomponent.cpp @@ -223,21 +223,6 @@ LabelsComponent::LabelsComponent(const ghoul::Dictionary& dictionary) _transformationMatrix = p.transformationMatrix.value_or(_transformationMatrix); } -LabelsComponent::LabelsComponent(const ghoul::Dictionary& dictionary, - const dataloader::Dataset& dataset, - DistanceUnit unit) - : LabelsComponent(dictionary) -{ - // The unit should match the one in the dataset, not the one that was included in the - // asset (if any) - _unit = unit; - - // Load the labelset directly based on the dataset, and keep track of that it has - // already been loaded this way - _labelset = dataloader::label::loadFromDataset(dataset); - _createdFromDataset = true; -} - dataloader::Labelset& LabelsComponent::labelSet() { return _labelset; } @@ -247,6 +232,8 @@ const dataloader::Labelset& LabelsComponent::labelSet() const { } void LabelsComponent::initialize() { + ZoneScoped; + _font = global::fontManager->font( "Mono", _fontSize, @@ -257,14 +244,34 @@ void LabelsComponent::initialize() { loadLabels(); } +void LabelsComponent::loadLabelsFromDataset(const dataloader::Dataset& dataset, + DistanceUnit unit) +{ + ZoneScoped; + + LINFO("Loading labels from dataset"); + + // The unit should match the one in the dataset, not the one that was included in the + // asset (if any) + _unit = unit; + + // Load the labelset directly based on the dataset, and keep track of that it has + // already been loaded this way + _labelset = dataloader::label::loadFromDataset(dataset); + + _createdFromDataset = true; +} + void LabelsComponent::loadLabels() { - LINFO(std::format("Loading label file '{}'", _labelFile)); + ZoneScoped; if (_createdFromDataset) { // The labelset should already have been loaded return; } + LINFO(std::format("Loading label file '{}'", _labelFile)); + if (_useCache) { _labelset = dataloader::label::loadFileWithCache(_labelFile); } diff --git a/src/rendering/renderable.cpp b/src/rendering/renderable.cpp index 732adc40e8..2f76630659 100644 --- a/src/rendering/renderable.cpp +++ b/src/rendering/renderable.cpp @@ -118,6 +118,8 @@ documentation::Documentation Renderable::Documentation() { ghoul::mm_unique_ptr Renderable::createFromDictionary( const ghoul::Dictionary& dictionary) { + ZoneScoped; + if (!dictionary.hasKey(KeyType)) { throw ghoul::RuntimeError("Tried to create Renderable but no 'Type' was found"); } diff --git a/src/scene/assetmanager.cpp b/src/scene/assetmanager.cpp index 7a9dc5e14f..21a5772115 100644 --- a/src/scene/assetmanager.cpp +++ b/src/scene/assetmanager.cpp @@ -918,6 +918,7 @@ Asset* AssetManager::retrieveAsset(const std::filesystem::path& path, void AssetManager::callOnInitialize(Asset* asset) const { ZoneScoped; + ZoneText(asset->path().string().c_str(), asset->path().string().length()); ghoul_precondition(asset, "Asset must not be nullptr"); auto it = _onInitializeFunctionRefs.find(asset); diff --git a/src/scene/rotation.cpp b/src/scene/rotation.cpp index 1ac84efc36..2ba4a76e3e 100644 --- a/src/scene/rotation.cpp +++ b/src/scene/rotation.cpp @@ -54,6 +54,8 @@ documentation::Documentation Rotation::Documentation() { ghoul::mm_unique_ptr Rotation::createFromDictionary( const ghoul::Dictionary& dictionary) { + ZoneScoped; + const Parameters p = codegen::bake(dictionary); Rotation* result = FactoryManager::ref().factory()->create( diff --git a/src/scene/scale.cpp b/src/scene/scale.cpp index 0a75b5af80..812a029fd5 100644 --- a/src/scene/scale.cpp +++ b/src/scene/scale.cpp @@ -53,6 +53,8 @@ documentation::Documentation Scale::Documentation() { ghoul::mm_unique_ptr Scale::createFromDictionary( const ghoul::Dictionary& dictionary) { + ZoneScoped; + const Parameters p = codegen::bake(dictionary); Scale* result = FactoryManager::ref().factory()->create( diff --git a/src/scene/scene.cpp b/src/scene/scene.cpp index 0f9af62620..802beb24c4 100644 --- a/src/scene/scene.cpp +++ b/src/scene/scene.cpp @@ -200,6 +200,8 @@ void Scene::updateNodeRegistry() { } void Scene::sortTopologically() { + ZoneScoped; + _topologicallySortedNodes.insert( _topologicallySortedNodes.end(), std::make_move_iterator(_circularNodes.begin()), @@ -310,7 +312,7 @@ void Scene::update(const UpdateData& data) { void Scene::render(const RenderData& data, RendererTasks& tasks) { ZoneScoped; - ZoneName( + ZoneText( renderBinToString(data.renderBinMask), strlen(renderBinToString(data.renderBinMask)) ); @@ -366,6 +368,8 @@ const std::vector& Scene::allSceneGraphNodes() const { } SceneGraphNode* Scene::loadNode(const ghoul::Dictionary& nodeDictionary) { + ZoneScoped; + // First interpret the dictionary std::vector dependencyNames; diff --git a/src/scene/scene_lua.inl b/src/scene/scene_lua.inl index 765193ab7c..f49a254de3 100644 --- a/src/scene/scene_lua.inl +++ b/src/scene/scene_lua.inl @@ -619,6 +619,8 @@ namespace { * Loads the SceneGraphNode described in the table and adds it to the SceneGraph. */ [[codegen::luawrap]] void addSceneGraphNode(ghoul::Dictionary node) { + ZoneScoped; + using namespace openspace; try { SceneGraphNode* n = global::renderEngine->scene()->loadNode(node); diff --git a/src/scene/scenegraphnode.cpp b/src/scene/scenegraphnode.cpp index e35d569367..df5c9537e0 100644 --- a/src/scene/scenegraphnode.cpp +++ b/src/scene/scenegraphnode.cpp @@ -330,6 +330,8 @@ int SceneGraphNode::nextIndex = 0; ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( const ghoul::Dictionary& dictionary) { + ZoneScoped; + const Parameters p = codegen::bake(dictionary); SceneGraphNode* n = global::memoryManager->PersistentMemory.alloc(); @@ -342,6 +344,8 @@ ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( result->setIdentifier(p.identifier); if (p.gui.has_value()) { + ZoneScopedN("GUI"); + if (p.gui->name.has_value()) { result->setGuiName(*p.gui->name); result->_guiDisplayName = result->guiName(); @@ -375,6 +379,8 @@ ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( result->_reachFactor = p.reachFactor.value_or(result->_reachFactor); if (p.transform.has_value()) { + ZoneScopedN("Transform"); + if (p.transform->translation.has_value()) { result->_transform.translation = Translation::createFromDictionary( *p.transform->translation @@ -409,6 +415,8 @@ ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( if (p.timeFrame.has_value()) { + ZoneScopedN("TimeFrame"); + result->_timeFrame = TimeFrame::createFromDictionary(*p.timeFrame); LDEBUG(std::format( @@ -419,17 +427,21 @@ ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( // We initialize the renderable last as it probably has the most dependencies if (p.renderable.has_value()) { + ZoneScopedN("Renderable"); + result->_renderable = Renderable::createFromDictionary(*p.renderable); ghoul_assert(result->_renderable, "Failed to create Renderable"); result->_renderable->_parent = result.get(); result->addPropertySubOwner(result->_renderable.get()); - LDEBUG(std::format( - "Successfully created renderable for '{}'", result->identifier() - )); + //LDEBUG(std::format( + // "Successfully created renderable for '{}'", result->identifier() + //)); } // Extracting the actions from the dictionary if (p.onApproach.has_value()) { + ZoneScopedN("OnApproach"); + if (std::holds_alternative(*p.onApproach)) { result->_onApproachAction = { std::get(*p.onApproach) }; } @@ -439,6 +451,8 @@ ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( } if (p.onReach.has_value()) { + ZoneScopedN("OnReach"); + if (std::holds_alternative(*p.onReach)) { result->_onReachAction = { std::get(*p.onReach) }; } @@ -448,6 +462,8 @@ ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( } if (p.onRecede.has_value()) { + ZoneScopedN("OnRecede"); + if (std::holds_alternative(*p.onRecede)) { result->_onRecedeAction = { std::get(*p.onRecede) }; } @@ -457,6 +473,8 @@ ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( } if (p.onExit.has_value()) { + ZoneScopedN("OnExit"); + if (std::holds_alternative(*p.onExit)) { result->_onExitAction = { std::get(*p.onExit) }; } @@ -466,6 +484,8 @@ ghoul::mm_unique_ptr SceneGraphNode::createFromDictionary( } if (p.tag.has_value()) { + ZoneScopedN("Tag"); + if (std::holds_alternative(*p.tag)) { result->addTag(std::get(*p.tag)); } @@ -611,6 +631,7 @@ void SceneGraphNode::initialize() { void SceneGraphNode::initializeGL() { ZoneScoped; ZoneName(identifier().c_str(), identifier().size()); + TracyGpuZone("initializeGL") LDEBUG(std::format("Initializing GL: {}", identifier())); diff --git a/src/scene/sceneinitializer.cpp b/src/scene/sceneinitializer.cpp index 9425a13e99..79d911d296 100644 --- a/src/scene/sceneinitializer.cpp +++ b/src/scene/sceneinitializer.cpp @@ -51,7 +51,11 @@ MultiThreadedSceneInitializer::MultiThreadedSceneInitializer(unsigned int nThrea {} void MultiThreadedSceneInitializer::initializeNode(SceneGraphNode* node) { + ZoneScoped; + auto initFunction = [this, node]() { + ZoneScopedN("MultiThreadedInit"); + LoadingScreen* loadingScreen = global::openSpaceEngine->loadingScreen(); LoadingScreen::ProgressInfo progressInfo; diff --git a/src/scene/timeframe.cpp b/src/scene/timeframe.cpp index 7af39c2632..4d79d3d6c4 100644 --- a/src/scene/timeframe.cpp +++ b/src/scene/timeframe.cpp @@ -53,6 +53,8 @@ documentation::Documentation TimeFrame::Documentation() { ghoul::mm_unique_ptr TimeFrame::createFromDictionary( const ghoul::Dictionary& dict) { + ZoneScoped; + const Parameters p = codegen::bake(dict); TimeFrame* result = FactoryManager::ref().factory()->create(p.type, dict); diff --git a/src/scene/translation.cpp b/src/scene/translation.cpp index f2e70b6a83..717242f402 100644 --- a/src/scene/translation.cpp +++ b/src/scene/translation.cpp @@ -52,6 +52,8 @@ documentation::Documentation Translation::Documentation() { ghoul::mm_unique_ptr Translation::createFromDictionary( const ghoul::Dictionary& dictionary) { + ZoneScoped; + const Parameters p = codegen::bake(dictionary); Translation* result = FactoryManager::ref().factory()->create( diff --git a/src/util/openspacemodule.cpp b/src/util/openspacemodule.cpp index 92e8153e8c..3200400660 100644 --- a/src/util/openspacemodule.cpp +++ b/src/util/openspacemodule.cpp @@ -66,6 +66,7 @@ void OpenSpaceModule::initialize(const ghoul::Dictionary& configuration) { void OpenSpaceModule::initializeGL() { ZoneScoped; ZoneName(identifier().c_str(), identifier().size()); + TracyGpuZone("initializeGL") internalInitializeGL(); } diff --git a/src/util/screenlog.cpp b/src/util/screenlog.cpp index ba70dba597..e3d45623a6 100644 --- a/src/util/screenlog.cpp +++ b/src/util/screenlog.cpp @@ -49,6 +49,8 @@ void ScreenLog::removeExpiredEntries() { } void ScreenLog::log(LogLevel level, std::string_view category, std::string_view message) { + ZoneScoped; + const std::lock_guard guard(_mutex); if (level >= _logLevel) { _entries.push_back({ From cdf98f78899f5040361c17cf7498c2bdd0b9c270 Mon Sep 17 00:00:00 2001 From: Alexander Bock Date: Thu, 28 Mar 2024 14:30:12 +0100 Subject: [PATCH 2/4] Optimizing the data loading. Adding more Tracy macros --- modules/server/src/connection.cpp | 5 ++ .../server/src/topics/getpropertytopic.cpp | 6 +- modules/skybrowser/src/wwtdatahandler.cpp | 2 + src/data/dataloader.cpp | 70 ++++++++++++------- src/engine/openspaceengine.cpp | 5 +- src/scripting/scriptengine.cpp | 3 +- support/coding/codegen | 2 +- 7 files changed, 61 insertions(+), 32 deletions(-) diff --git a/modules/server/src/connection.cpp b/modules/server/src/connection.cpp index 6007ee706c..74b367af8c 100644 --- a/modules/server/src/connection.cpp +++ b/modules/server/src/connection.cpp @@ -167,6 +167,8 @@ void Connection::handleJson(const nlohmann::json& json) { auto topicIt = _topics.find(topicId); if (topicIt == _topics.end()) { + ZoneScopedN("New Topic"); + // The topic id is not registered: Initialize a new topic. auto typeJson = json.find(MessageKeyType); if (typeJson == json.end() || !typeJson->is_string()) { @@ -174,6 +176,7 @@ void Connection::handleJson(const nlohmann::json& json) { return; } const std::string type = *typeJson; + ZoneText(type.c_str(), type.size()); if (!isAuthorized() && (type != "authorize")) { LERROR("Connection is not authorized"); @@ -188,6 +191,8 @@ void Connection::handleJson(const nlohmann::json& json) { } } else { + ZoneScopedN("Existing Topic"); + if (!isAuthorized()) { LERROR("Connection is not authorized"); return; diff --git a/modules/server/src/topics/getpropertytopic.cpp b/modules/server/src/topics/getpropertytopic.cpp index 4c0f037ba6..9089704baf 100644 --- a/modules/server/src/topics/getpropertytopic.cpp +++ b/modules/server/src/topics/getpropertytopic.cpp @@ -52,14 +52,18 @@ namespace { namespace openspace { void GetPropertyTopic::handleJson(const nlohmann::json& json) { + ZoneScoped; + const std::string requestedKey = json.at("property").get(); + ZoneText(requestedKey.c_str(), requestedKey.size()); LDEBUG("Getting property '" + requestedKey + "'..."); nlohmann::json response; if (requestedKey == AllPropertiesValue) { response = allProperties(); } else if (requestedKey == AllNodesValue) { - response = wrappedPayload(sceneGraph()->allSceneGraphNodes()); + const std::vector& nodes = sceneGraph()->allSceneGraphNodes(); + response = wrappedPayload(nodes); } else if (requestedKey == AllScreenSpaceRenderablesValue) { response = wrappedPayload({ diff --git a/modules/skybrowser/src/wwtdatahandler.cpp b/modules/skybrowser/src/wwtdatahandler.cpp index 7da2fe075b..3789c998a2 100644 --- a/modules/skybrowser/src/wwtdatahandler.cpp +++ b/modules/skybrowser/src/wwtdatahandler.cpp @@ -253,6 +253,8 @@ namespace openspace { void WwtDataHandler::loadImages(const std::string& root, const std::filesystem::path& directory) { + ZoneScoped; + // Steps to download new images // 1. Create the target directory if it doesn't already exist // 2. If the 'root' has an associated hash file, download and compare it with the diff --git a/src/data/dataloader.cpp b/src/data/dataloader.cpp index 9402acc9bc..bf3c16bae7 100644 --- a/src/data/dataloader.cpp +++ b/src/data/dataloader.cpp @@ -40,7 +40,7 @@ #include namespace { - constexpr int8_t DataCacheFileVersion = 12; + constexpr int8_t DataCacheFileVersion = 13; constexpr int8_t LabelCacheFileVersion = 11; constexpr int8_t ColorCacheFileVersion = 11; @@ -224,16 +224,9 @@ std::optional loadCachedFile(const std::filesystem::path& path) { file.read(reinterpret_cast(&nEntries), sizeof(uint64_t)); result.entries.reserve(nEntries); for (uint64_t i = 0; i < nEntries; i += 1) { - ZoneScopedN("Dataset"); - Dataset::Entry e; file.read(reinterpret_cast(&e.position.x), 3 * sizeof(float)); - uint16_t nValues = 0; - file.read(reinterpret_cast(&nValues), sizeof(uint16_t)); - e.data.resize(nValues); - file.read(reinterpret_cast(e.data.data()), nValues * sizeof(float)); - // For now we just store the length of the comment. Since the comments are stored // in one block after the data entries, we can use the length later to extract the // contents of this entries comment out of the big block @@ -251,6 +244,17 @@ std::optional loadCachedFile(const std::filesystem::path& path) { result.entries.push_back(std::move(e)); } + // + // Read the data values next + uint16_t nValues = 0; + file.read(reinterpret_cast(&nValues), sizeof(uint16_t)); + std::vector entriesBuffer; + entriesBuffer.resize(nEntries * nValues); + file.read( + reinterpret_cast(entriesBuffer.data()), + nEntries * nValues * sizeof(float) + ); + // // Read comments in one block and then assign them to the data entries uint64_t totalCommentLength = 0; @@ -258,18 +262,28 @@ std::optional loadCachedFile(const std::filesystem::path& path) { std::vector commentBuffer; commentBuffer.resize(totalCommentLength); file.read(commentBuffer.data(), totalCommentLength); - // idx is the running index into the total comment buffer - int idx = 0; + + // + // Now we have the comments and the data values, we need to implant them into the + // data entries + + // commentIdx is the running index into the total comment buffer + int commentIdx = 0; + int valuesIdx = 0; for (Dataset::Entry& e : result.entries) { + e.data.resize(nValues); + std::memcpy(e.data.data(), entriesBuffer.data() + valuesIdx, nValues); + valuesIdx += nValues; + if (e.comment.has_value()) { - ghoul_assert(idx < commentBuffer.size(), "Index too large"); + ghoul_assert(commentIdx < commentBuffer.size(), "Index too large"); // If we have a comment, we need to extract its length's worth of characters // from the buffer - std::memcpy(e.comment->data(), &commentBuffer[idx], e.comment->size()); + std::memcpy(e.comment->data(), &commentBuffer[commentIdx], e.comment->size()); // and then advance the index - idx += e.comment->size(); + commentIdx += e.comment->size(); } } @@ -337,19 +351,19 @@ void saveCachedFile(const Dataset& dataset, const std::filesystem::path& path) { checkSize(dataset.entries.size(), "Too many entries"); uint64_t nEntries = static_cast(dataset.entries.size()); file.write(reinterpret_cast(&nEntries), sizeof(uint64_t)); + + // We assume the number of values for each dataset to be the same, so we can store + // them upfront + uint16_t nValues = dataset.entries.empty() ? 0 : dataset.entries[0].data.size(); + checkSize(nValues, "Too many data variables"); + std::vector valuesBuffer; + valuesBuffer.reserve(dataset.entries.size() * nValues); + uint64_t totalCommentLength = 0; for (const Dataset::Entry& e : dataset.entries) { - file.write(reinterpret_cast(&e.position.x), sizeof(float)); - file.write(reinterpret_cast(&e.position.y), sizeof(float)); - file.write(reinterpret_cast(&e.position.z), sizeof(float)); + file.write(reinterpret_cast(&e.position.x), 3 * sizeof(float)); - checkSize(e.data.size(), "Too many data variables"); - uint16_t nValues = static_cast(e.data.size()); - file.write(reinterpret_cast(&nValues), sizeof(uint16_t)); - file.write( - reinterpret_cast(e.data.data()), - e.data.size() * sizeof(float) - ); + valuesBuffer.insert(valuesBuffer.end(), e.data.begin(), e.data.end()); if (e.comment.has_value()) { checkSize(e.comment->size(), "Comment too long"); @@ -359,11 +373,15 @@ void saveCachedFile(const Dataset& dataset, const std::filesystem::path& path) { 0; file.write(reinterpret_cast(&commentLen), sizeof(uint16_t)); totalCommentLength += commentLen; - //if (e.comment.has_value()) { - // file.write(e.comment->data(), e.comment->size()); - //} } + // Write all of the datavalues next + file.write(reinterpret_cast(&nValues), sizeof(uint16_t)); + file.write( + reinterpret_cast(valuesBuffer.data()), + valuesBuffer.size() * sizeof(float) + ); + // // Write all of the comments next. We don't have to store the individual comment // lengths as the data values written before already have those stored. And since we diff --git a/src/engine/openspaceengine.cpp b/src/engine/openspaceengine.cpp index cb64544433..54d74d0f65 100644 --- a/src/engine/openspaceengine.cpp +++ b/src/engine/openspaceengine.cpp @@ -757,11 +757,10 @@ void OpenSpaceEngine::loadAssets() { std::unique_ptr sceneInitializer; if (global::configuration->useMultithreadedInitialization) { - const unsigned int nAvailableThreads = std::max( - std::thread::hardware_concurrency() / 2, + const unsigned int nThreads = std::max( + std::thread::hardware_concurrency() / 4, 4u ); - const unsigned int nThreads = nAvailableThreads == 0 ? 2 : nAvailableThreads; sceneInitializer = std::make_unique(nThreads); } else { diff --git a/src/scripting/scriptengine.cpp b/src/scripting/scriptengine.cpp index 1fbadf409f..f7361e9705 100644 --- a/src/scripting/scriptengine.cpp +++ b/src/scripting/scriptengine.cpp @@ -165,6 +165,7 @@ bool ScriptEngine::hasLibrary(const std::string& name) { bool ScriptEngine::runScript(const std::string& script, const ScriptCallback& callback) { ZoneScoped; + ZoneText(script.c_str(), script.size()); ghoul_assert(!script.empty(), "Script must not be empty"); @@ -177,7 +178,7 @@ bool ScriptEngine::runScript(const std::string& script, const ScriptCallback& ca if (callback) { ghoul::Dictionary returnValue = ghoul::lua::loadArrayDictionaryFromString(script, _state); - callback(returnValue); + callback(std::move(returnValue)); } else { ghoul::lua::runScript(_state, script); diff --git a/support/coding/codegen b/support/coding/codegen index ef31f904b6..e8fe5144e0 160000 --- a/support/coding/codegen +++ b/support/coding/codegen @@ -1 +1 @@ -Subproject commit ef31f904b6ee02c5d1cc4c4bbddef821dbb505c8 +Subproject commit e8fe5144e0903e85ca931b6c59992119579aeb69 From 2203eb6df66c27d21fd70613a2d8ffa0a8a10286 Mon Sep 17 00:00:00 2001 From: Alexander Bock Date: Thu, 28 Mar 2024 16:05:06 +0100 Subject: [PATCH 3/4] Enable memory profiling and fix compile issues when building without tracy --- apps/OpenSpace/CMakeLists.txt | 3 +++ apps/OpenSpace/ext/sgct | 2 +- ext/ghoul | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/OpenSpace/CMakeLists.txt b/apps/OpenSpace/CMakeLists.txt index f79ee8f174..3afa51cfed 100644 --- a/apps/OpenSpace/CMakeLists.txt +++ b/apps/OpenSpace/CMakeLists.txt @@ -115,6 +115,9 @@ set(SGCT_DEP_INCLUDE_FREETYPE OFF CACHE BOOL "" FORCE) set(SGCT_DEP_INCLUDE_FMT OFF CACHE BOOL "" FORCE) set(SGCT_DEP_INCLUDE_SCN OFF CACHE BOOL "" FORCE) set(SGCT_DEP_INCLUDE_CATCH2 OFF CACHE BOOL "" FORCE) +if (TRACY_ENABLE) + set(SGCT_MEMORY_PROFILING ON CACHE BOOL "" FORCE) +endif () add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/sgct) target_link_libraries(OpenSpace PRIVATE sgct) diff --git a/apps/OpenSpace/ext/sgct b/apps/OpenSpace/ext/sgct index 49d1c03a28..3a92028182 160000 --- a/apps/OpenSpace/ext/sgct +++ b/apps/OpenSpace/ext/sgct @@ -1 +1 @@ -Subproject commit 49d1c03a2857d370afcb419c361a158cda0e31a7 +Subproject commit 3a9202818242eb707f34093e068cf9b8f8f9dbb1 diff --git a/ext/ghoul b/ext/ghoul index c6d49eda51..9198ad2135 160000 --- a/ext/ghoul +++ b/ext/ghoul @@ -1 +1 @@ -Subproject commit c6d49eda51ce34e729bf931d69e638c73c8e254a +Subproject commit 9198ad2135c73dea10168a8f6e9072b95755ae52 From 5115638c097266647bc89c30ad71a42637c6f065 Mon Sep 17 00:00:00 2001 From: Alexander Bock Date: Thu, 28 Mar 2024 16:39:44 +0100 Subject: [PATCH 4/4] Disable memory profiling again as it causes exceptions in tracy --- apps/OpenSpace/CMakeLists.txt | 3 --- ext/ghoul | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/OpenSpace/CMakeLists.txt b/apps/OpenSpace/CMakeLists.txt index 3afa51cfed..f79ee8f174 100644 --- a/apps/OpenSpace/CMakeLists.txt +++ b/apps/OpenSpace/CMakeLists.txt @@ -115,9 +115,6 @@ set(SGCT_DEP_INCLUDE_FREETYPE OFF CACHE BOOL "" FORCE) set(SGCT_DEP_INCLUDE_FMT OFF CACHE BOOL "" FORCE) set(SGCT_DEP_INCLUDE_SCN OFF CACHE BOOL "" FORCE) set(SGCT_DEP_INCLUDE_CATCH2 OFF CACHE BOOL "" FORCE) -if (TRACY_ENABLE) - set(SGCT_MEMORY_PROFILING ON CACHE BOOL "" FORCE) -endif () add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/sgct) target_link_libraries(OpenSpace PRIVATE sgct) diff --git a/ext/ghoul b/ext/ghoul index 9198ad2135..c26a8b1686 160000 --- a/ext/ghoul +++ b/ext/ghoul @@ -1 +1 @@ -Subproject commit 9198ad2135c73dea10168a8f6e9072b95755ae52 +Subproject commit c26a8b1686732143892ab063de4a9c60c6dd3240