diff --git a/ApplicationLibCode/Application/CMakeLists_files.cmake b/ApplicationLibCode/Application/CMakeLists_files.cmake index 97c8879326..e61a2c36e2 100644 --- a/ApplicationLibCode/Application/CMakeLists_files.cmake +++ b/ApplicationLibCode/Application/CMakeLists_files.cmake @@ -23,6 +23,7 @@ set(SOURCE_GROUP_HEADER_FILES ${CMAKE_CURRENT_LIST_DIR}/RiaEclipseFileNameTools.h ${CMAKE_CURRENT_LIST_DIR}/RiaFeatureCommandContext.h ${CMAKE_CURRENT_LIST_DIR}/RiaStringListSerializer.h + ${CMAKE_CURRENT_LIST_DIR}/RiaStringPool.h ${CMAKE_CURRENT_LIST_DIR}/RiaNncDefines.h ${CMAKE_CURRENT_LIST_DIR}/RiaPlotDefines.h ${CMAKE_CURRENT_LIST_DIR}/RiaStimPlanModelDefines.h @@ -67,6 +68,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RiaEclipseFileNameTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaFeatureCommandContext.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaStringListSerializer.cpp + ${CMAKE_CURRENT_LIST_DIR}/RiaStringPool.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaNncDefines.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaPlotDefines.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaStimPlanModelDefines.cpp diff --git a/ApplicationLibCode/Application/RiaStringPool.cpp b/ApplicationLibCode/Application/RiaStringPool.cpp new file mode 100644 index 0000000000..260efc605d --- /dev/null +++ b/ApplicationLibCode/Application/RiaStringPool.cpp @@ -0,0 +1,87 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026 Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RiaStringPool.h" + +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaStringPool& RiaStringPool::instance() +{ + static RiaStringPool pool; + return pool; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaStringPool::RiaStringPool() +{ + // Pre-allocate empty string at index 0 + m_strings.push_back( "" ); + m_stringToIndex[""] = 0; + m_emptyIndex = 0; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaStringPool::IndexType RiaStringPool::getIndex( const std::string& str ) +{ + // First try with shared lock for reading + { + std::shared_lock lock( m_mutex ); + auto it = m_stringToIndex.find( str ); + if ( it != m_stringToIndex.end() ) + { + return it->second; + } + } + + // Need to insert, acquire exclusive lock + std::unique_lock lock( m_mutex ); + + // Double-check after acquiring exclusive lock (another thread might have inserted it) + auto it = m_stringToIndex.find( str ); + if ( it != m_stringToIndex.end() ) + { + return it->second; + } + + IndexType newIndex = static_cast( m_strings.size() ); + m_strings.push_back( str ); + m_stringToIndex[m_strings.back()] = newIndex; + return newIndex; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const std::string& RiaStringPool::getString( IndexType index ) const +{ + std::shared_lock lock( m_mutex ); + + if ( index >= m_strings.size() ) + { + throw std::out_of_range( "String pool index out of range" ); + } + return m_strings[index]; +} diff --git a/ApplicationLibCode/Application/RiaStringPool.h b/ApplicationLibCode/Application/RiaStringPool.h new file mode 100644 index 0000000000..5c2a50ffbd --- /dev/null +++ b/ApplicationLibCode/Application/RiaStringPool.h @@ -0,0 +1,57 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026 Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +//================================================================================================== +/// String pool for memory-efficient storage of shared strings +/// Stores strings once and returns indices for lookups +//================================================================================================== +class RiaStringPool +{ +public: + using IndexType = uint32_t; + static constexpr IndexType INVALID_INDEX = static_cast( -1 ); + + static RiaStringPool& instance(); + + // Get or create an index for a string + IndexType getIndex( const std::string& str ); + + // Get string by index + const std::string& getString( IndexType index ) const; + + // Get empty string index + IndexType getEmptyIndex() const { return m_emptyIndex; } + +private: + RiaStringPool(); + RiaStringPool( const RiaStringPool& ) = delete; + RiaStringPool& operator=( const RiaStringPool& ) = delete; + + mutable std::shared_mutex m_mutex; + std::vector m_strings; + std::unordered_map m_stringToIndex; + IndexType m_emptyIndex; +}; diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index e9ebcf2d1d..81efe36ba8 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -69,6 +69,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RifCsvDataTableFormatter-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaSummaryAddressAnalyzer-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaStdStringTools-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RiaStringPool-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaInterpolationTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifWellMeasurementReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaDateStringParser-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RiaStringPool-Test.cpp b/ApplicationLibCode/UnitTests/RiaStringPool-Test.cpp new file mode 100644 index 0000000000..69c2838d46 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RiaStringPool-Test.cpp @@ -0,0 +1,349 @@ +#include "gtest/gtest.h" + +#include "RiaStringPool.h" + +#include +#include +#include +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, EmptyStringPreAllocated ) +{ + auto& pool = RiaStringPool::instance(); + + auto emptyIndex = pool.getIndex( "" ); + EXPECT_EQ( pool.getEmptyIndex(), emptyIndex ); + EXPECT_EQ( "", pool.getString( emptyIndex ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, BasicStringStorage ) +{ + auto& pool = RiaStringPool::instance(); + + std::string testStr = "FOPT"; + auto index = pool.getIndex( testStr ); + + EXPECT_EQ( testStr, pool.getString( index ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, DuplicateStringsReturnSameIndex ) +{ + auto& pool = RiaStringPool::instance(); + + std::string testStr = "WOPT:WELL1"; + auto index1 = pool.getIndex( testStr ); + auto index2 = pool.getIndex( testStr ); + auto index3 = pool.getIndex( testStr ); + + EXPECT_EQ( index1, index2 ); + EXPECT_EQ( index2, index3 ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, MultipleUniqueStrings ) +{ + auto& pool = RiaStringPool::instance(); + + std::vector testStrings = { "FOPT", "WOPT:WELL1", "GOPT:GROUP1", "BPR:10,20,30", "ROPR" }; + + std::vector indices; + for ( const auto& str : testStrings ) + { + indices.push_back( pool.getIndex( str ) ); + } + + // All indices should be unique + std::set uniqueIndices( indices.begin(), indices.end() ); + EXPECT_EQ( testStrings.size(), uniqueIndices.size() ); + + // Verify retrieval + for ( size_t i = 0; i < testStrings.size(); ++i ) + { + EXPECT_EQ( testStrings[i], pool.getString( indices[i] ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, OutOfRangeThrows ) +{ + auto& pool = RiaStringPool::instance(); + + // Attempt to access index far beyond current size + EXPECT_THROW( pool.getString( 9999999 ), std::out_of_range ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, ThreadSafety ) +{ + auto& pool = RiaStringPool::instance(); + + const int numThreads = 10; + const int stringsPerThread = 100; + + std::vector threads; + std::vector> threadResults( numThreads ); + + // Create threads that all add the same strings + for ( int t = 0; t < numThreads; ++t ) + { + threads.emplace_back( + [&pool, &threadResults, t, stringsPerThread]() + { + for ( int i = 0; i < stringsPerThread; ++i ) + { + std::string str = "STRING_" + std::to_string( i ); + auto index = pool.getIndex( str ); + threadResults[t].push_back( index ); + } + } ); + } + + // Wait for all threads to complete + for ( auto& thread : threads ) + { + thread.join(); + } + + // Verify all threads got the same indices for the same strings + for ( int i = 1; i < numThreads; ++i ) + { + EXPECT_EQ( threadResults[0].size(), threadResults[i].size() ); + for ( size_t j = 0; j < threadResults[0].size(); ++j ) + { + EXPECT_EQ( threadResults[0][j], threadResults[i][j] ); + } + } + + // Verify string retrieval + for ( int i = 0; i < stringsPerThread; ++i ) + { + std::string expectedStr = "STRING_" + std::to_string( i ); + auto index = threadResults[0][i]; + EXPECT_EQ( expectedStr, pool.getString( index ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, LargeNumberOfStrings ) +{ + auto& pool = RiaStringPool::instance(); + + const int numStrings = 10000; + std::vector indices; + + // Add many unique strings + for ( int i = 0; i < numStrings; ++i ) + { + std::string str = "VECTOR_" + std::to_string( i ) + "_DATA"; + indices.push_back( pool.getIndex( str ) ); + } + + // Verify all can be retrieved + for ( int i = 0; i < numStrings; ++i ) + { + std::string expectedStr = "VECTOR_" + std::to_string( i ) + "_DATA"; + EXPECT_EQ( expectedStr, pool.getString( indices[i] ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, SpecialCharacters ) +{ + auto& pool = RiaStringPool::instance(); + + std::vector specialStrings = { "String with spaces", + "String:with:colons", + "String/with/slashes", + "String,with,commas", + "String-with-dashes", + "String_with_underscores", + "String.with.dots", + "String|with|pipes", + "String@with@at", + "String#with#hash" }; + + for ( const auto& str : specialStrings ) + { + auto index = pool.getIndex( str ); + EXPECT_EQ( str, pool.getString( index ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, VeryLongStrings ) +{ + auto& pool = RiaStringPool::instance(); + + // Create a very long string + std::string longStr( 10000, 'A' ); + longStr += "_UNIQUE"; + + auto index = pool.getIndex( longStr ); + EXPECT_EQ( longStr, pool.getString( index ) ); + + // Verify same index on duplicate + auto index2 = pool.getIndex( longStr ); + EXPECT_EQ( index, index2 ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, ConcurrentInsertAndRead ) +{ + auto& pool = RiaStringPool::instance(); + + const int numWriteThreads = 5; + const int numReadThreads = 5; + const int stringsPerWrite = 50; + + std::vector threads; + + // Create strings in multiple write threads + for ( int t = 0; t < numWriteThreads; ++t ) + { + threads.emplace_back( + [&pool, t, stringsPerWrite]() + { + for ( int i = 0; i < stringsPerWrite; ++i ) + { + std::string str = "WRITE_" + std::to_string( t ) + "_" + std::to_string( i ); + pool.getIndex( str ); + } + } ); + } + + // Read strings concurrently + for ( int t = 0; t < numReadThreads; ++t ) + { + threads.emplace_back( + [&pool, t, stringsPerWrite]() + { + for ( int i = 0; i < stringsPerWrite; ++i ) + { + // Try to read strings that might be being created + std::string str = "WRITE_0_" + std::to_string( i ); + auto index = pool.getIndex( str ); + // Should not crash even if racing with writes + EXPECT_EQ( str, pool.getString( index ) ); + } + } ); + } + + for ( auto& thread : threads ) + { + thread.join(); + } + + // Verify all written strings are accessible + for ( int t = 0; t < numWriteThreads; ++t ) + { + for ( int i = 0; i < stringsPerWrite; ++i ) + { + std::string str = "WRITE_" + std::to_string( t ) + "_" + std::to_string( i ); + auto index = pool.getIndex( str ); + EXPECT_EQ( str, pool.getString( index ) ); + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, InvalidIndex ) +{ + auto& pool = RiaStringPool::instance(); + + // Test with INVALID_INDEX constant + EXPECT_THROW( pool.getString( RiaStringPool::INVALID_INDEX ), std::out_of_range ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, SimilarStrings ) +{ + auto& pool = RiaStringPool::instance(); + + // Test strings that are similar but not identical + std::vector similarStrings = { "WOPT:WELL-1", + "WOPT:WELL-2", + "WOPT:WELL-10", + "WOPT:WELL-1A", + "WOPT:WELL_1", + "WOPT: WELL-1" }; + + std::vector indices; + for ( const auto& str : similarStrings ) + { + indices.push_back( pool.getIndex( str ) ); + } + + // All should have different indices + std::set uniqueIndices( indices.begin(), indices.end() ); + EXPECT_EQ( similarStrings.size(), uniqueIndices.size() ); + + // Verify each string retrieval + for ( size_t i = 0; i < similarStrings.size(); ++i ) + { + EXPECT_EQ( similarStrings[i], pool.getString( indices[i] ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RiaStringPoolTest, CommonSummaryVectorNames ) +{ + auto& pool = RiaStringPool::instance(); + + // Test with realistic summary vector names + std::vector vectorNames = { "FOPT", + "FWPT", + "FGPT", + "FOPR", + "FWPR", + "FGPR", + "WOPT:PROD-1", + "WWPT:PROD-1", + "WBHP:PROD-1", + "WOPT:INJ-1", + "GOPT:FIELD", + "BPR:10,15,20" }; + + // Store indices + std::vector indices; + for ( const auto& name : vectorNames ) + { + indices.push_back( pool.getIndex( name ) ); + } + + // Request same strings again - should get same indices + for ( size_t i = 0; i < vectorNames.size(); ++i ) + { + auto index = pool.getIndex( vectorNames[i] ); + EXPECT_EQ( indices[i], index ); + EXPECT_EQ( vectorNames[i], pool.getString( index ) ); + } +}