From 543e2da1e1b93fd15097bcbaaedf71c2ede9fb67 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Sun, 19 Apr 2026 15:05:20 +0300 Subject: [PATCH 1/7] test: add sphere mesh compress-to-zip test Creates a sphere with ~100K vertices, saves it as a .mrmesh file in a temporary folder, then calls compressZip to produce a .zip archive in a second temporary folder. Verifies: - mesh and zip files both exist and are non-empty - the zip is not absurdly larger than the source (envelope sanity) Serves as a realistic end-to-end exercise of MeshLib's zip write path (libzip + zlib deflate) against mesh-sized data. Suitable for timing comparisons of different zlib backends (e.g., stock zlib vs zlib-ng compat) when added to a run's measurement targets. Placed in source/MRTest/ as MRZipCompressTests.cpp; no CMake changes needed (the existing file(GLOB SOURCES "*.cpp") picks it up). --- source/MRTest/MRZipCompressTests.cpp | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 source/MRTest/MRZipCompressTests.cpp diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp new file mode 100644 index 000000000000..94e1035c86b0 --- /dev/null +++ b/source/MRTest/MRZipCompressTests.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include +#include + +#include + +namespace MR +{ + +// Writes a ~100K-vertex sphere to a .mrmesh file in a temporary folder, then +// compresses that folder to a .zip and verifies the archive was created and +// is non-empty. Serves as a realistic end-to-end exercise of MeshLib's zip +// write path (libzip + deflate) on mesh-sized data. +TEST( MRMesh, CompressSphereToZip ) +{ + UniqueTemporaryFolder srcFolder; + ASSERT_TRUE( bool( srcFolder ) ); + + // Generate a sphere with ~100K vertices. makeSphere's subdivision + // targets the requested count but may land a handful over. + constexpr int targetVerts = 100'000; + SphereParams params; + params.radius = 1.0f; + params.numMeshVertices = targetVerts; + const Mesh sphere = makeSphere( params ); + EXPECT_GE( (int)sphere.topology.numValidVerts(), targetVerts ); + + // Save mesh as a .mrmesh file in the temp folder. + const std::filesystem::path meshPath = srcFolder / "sphere.mrmesh"; + const auto saveRes = MeshSave::toMrmesh( sphere, meshPath ); + ASSERT_TRUE( saveRes.has_value() ) << saveRes.error(); + ASSERT_TRUE( std::filesystem::exists( meshPath ) ); + const auto meshSize = std::filesystem::file_size( meshPath ); + EXPECT_GT( meshSize, 0u ); + + // Compress the temp folder into a .zip located in a second temp folder + // (so the zip isn't inside the folder being compressed). + UniqueTemporaryFolder dstFolder; + ASSERT_TRUE( bool( dstFolder ) ); + const std::filesystem::path zipPath = dstFolder / "sphere.zip"; + + const auto compressRes = compressZip( zipPath, srcFolder ); + ASSERT_TRUE( compressRes.has_value() ) << compressRes.error(); + ASSERT_TRUE( std::filesystem::exists( zipPath ) ); + const auto zipSize = std::filesystem::file_size( zipPath ); + EXPECT_GT( zipSize, 0u ); + + // Sanity: the zip should not be absurdly larger than the source + // (that would indicate something is wrong with the envelope); and + // since .mrmesh is a raw binary dump of topology plus coordinate + // floats, deflate typically produces a modestly smaller archive. + EXPECT_LT( zipSize, meshSize * 2u ); +} + +} // namespace MR From 2ceef41bc18b38dc4fd84b634a735dd91c076c27 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Sun, 19 Apr 2026 15:32:22 +0300 Subject: [PATCH 2/7] fix --- source/MRTest/MRTest.vcxproj | 1 + source/MRTest/MRTest.vcxproj.filters | 3 +++ source/MRTest/MRZipCompressTests.cpp | 11 ++++++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/source/MRTest/MRTest.vcxproj b/source/MRTest/MRTest.vcxproj index ad37b725622c..1a3181cfb554 100644 --- a/source/MRTest/MRTest.vcxproj +++ b/source/MRTest/MRTest.vcxproj @@ -61,6 +61,7 @@ + diff --git a/source/MRTest/MRTest.vcxproj.filters b/source/MRTest/MRTest.vcxproj.filters index dc07b1c25139..4fa6f4ceb63c 100644 --- a/source/MRTest/MRTest.vcxproj.filters +++ b/source/MRTest/MRTest.vcxproj.filters @@ -184,6 +184,9 @@ Source Files + + Source Files + diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp index 94e1035c86b0..00e69d7f5819 100644 --- a/source/MRTest/MRZipCompressTests.cpp +++ b/source/MRTest/MRZipCompressTests.cpp @@ -26,14 +26,15 @@ TEST( MRMesh, CompressSphereToZip ) params.radius = 1.0f; params.numMeshVertices = targetVerts; const Mesh sphere = makeSphere( params ); - EXPECT_GE( (int)sphere.topology.numValidVerts(), targetVerts ); + EXPECT_EQ( (int)sphere.topology.numValidVerts(), targetVerts ); // Save mesh as a .mrmesh file in the temp folder. const std::filesystem::path meshPath = srcFolder / "sphere.mrmesh"; const auto saveRes = MeshSave::toMrmesh( sphere, meshPath ); ASSERT_TRUE( saveRes.has_value() ) << saveRes.error(); - ASSERT_TRUE( std::filesystem::exists( meshPath ) ); - const auto meshSize = std::filesystem::file_size( meshPath ); + std::error_code ec; + ASSERT_TRUE( std::filesystem::exists( meshPath, ec ) ); + const auto meshSize = std::filesystem::file_size( meshPath, ec ); EXPECT_GT( meshSize, 0u ); // Compress the temp folder into a .zip located in a second temp folder @@ -44,8 +45,8 @@ TEST( MRMesh, CompressSphereToZip ) const auto compressRes = compressZip( zipPath, srcFolder ); ASSERT_TRUE( compressRes.has_value() ) << compressRes.error(); - ASSERT_TRUE( std::filesystem::exists( zipPath ) ); - const auto zipSize = std::filesystem::file_size( zipPath ); + ASSERT_TRUE( std::filesystem::exists( zipPath, ec ) ); + const auto zipSize = std::filesystem::file_size( zipPath, ec ); EXPECT_GT( zipSize, 0u ); // Sanity: the zip should not be absurdly larger than the source From 96b10efbc709512b41e769f5cfa449dc14f83b0b Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Sun, 19 Apr 2026 15:40:44 +0300 Subject: [PATCH 3/7] test: log mesh and zip sizes via spdlog::info --- source/MRTest/MRZipCompressTests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp index 00e69d7f5819..f45666171a95 100644 --- a/source/MRTest/MRZipCompressTests.cpp +++ b/source/MRTest/MRZipCompressTests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -36,6 +37,7 @@ TEST( MRMesh, CompressSphereToZip ) ASSERT_TRUE( std::filesystem::exists( meshPath, ec ) ); const auto meshSize = std::filesystem::file_size( meshPath, ec ); EXPECT_GT( meshSize, 0u ); + spdlog::info( "sphere.mrmesh size: {} bytes", meshSize ); // Compress the temp folder into a .zip located in a second temp folder // (so the zip isn't inside the folder being compressed). @@ -48,6 +50,7 @@ TEST( MRMesh, CompressSphereToZip ) ASSERT_TRUE( std::filesystem::exists( zipPath, ec ) ); const auto zipSize = std::filesystem::file_size( zipPath, ec ); EXPECT_GT( zipSize, 0u ); + spdlog::info( "sphere.zip size: {} bytes", zipSize ); // Sanity: the zip should not be absurdly larger than the source // (that would indicate something is wrong with the envelope); and From 51b8d23808f366c9237af15dd92878b558a99571 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Mon, 20 Apr 2026 19:03:28 +0300 Subject: [PATCH 4/7] test: add many-small-files zip compression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New GTest case MRMesh.CompressManySmallFilesToZip writes 100 binary files and 100 JSON files to a temp folder (each 60 000 bytes, 12 000 000 bytes total — matching the sphere.mrmesh size from the existing CompressSphereToZip test) and compresses the folder to a .zip. Binary file content is deterministic pseudo-random bytes from a per-index-seeded LCG — low-compressibility, representative of mesh coordinate floats and similar near-incompressible payloads. JSON file content is deterministic structured-looking text padded to exactly 60 000 bytes with trailing whitespace — highly compressible, representative of scene-description metadata and logs. Pairs with CompressSphereToZip so a single run of MRTest produces two direct data points: one-large-file vs many-small-files on roughly the same total byte budget. Useful for assessing libzip's per-entry overhead (separate deflate session, CRC pass, local file header) when comparing zlib backends or compression-level changes. Per-file byte count is constant and deterministic so the aggregate input size and zip size are stable across runs (modulo whatever small variation deflate itself introduces on different zlib builds). Logs totals via spdlog::info for easy diff with the other test. --- source/MRTest/MRZipCompressTests.cpp | 133 +++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp index f45666171a95..b428ccb2d122 100644 --- a/source/MRTest/MRZipCompressTests.cpp +++ b/source/MRTest/MRZipCompressTests.cpp @@ -6,7 +6,13 @@ #include #include +#include +#include +#include #include +#include +#include +#include namespace MR { @@ -59,4 +65,131 @@ TEST( MRMesh, CompressSphereToZip ) EXPECT_LT( zipSize, meshSize * 2u ); } +// Writes ~100 binary files and ~100 JSON files to a temporary folder (total +// content size ~= the single .mrmesh file in CompressSphereToZip above), then +// compresses the folder to a .zip. Pairs with CompressSphereToZip to compare +// compression of one large binary vs many small mixed-type entries. +// +// libzip compresses each entry independently, so per-entry overhead (local +// file header, CRC32 pass, separate deflate session) can dominate when the +// archive is made of many small files. This test makes that cost visible. +TEST( MRMesh, CompressManySmallFilesToZip ) +{ + UniqueTemporaryFolder srcFolder; + ASSERT_TRUE( bool( srcFolder ) ); + + constexpr int numBinaryFiles = 100; + constexpr int numJsonFiles = 100; + constexpr size_t bytesPerFile = 60'000; + // 200 files * 60_000 bytes = 12_000_000 bytes, very close to the + // sphere.mrmesh size from the previous test (11_999_808 bytes). + + // Simple LCG used to produce deterministic pseudo-random bytes. + // Keeps the test reproducible across runs and platforms while avoiding + // trivially-compressible input (an all-zeros buffer would make deflate + // look unrealistically good). + auto nextLcg = []( uint64_t & state ) -> uint64_t + { + state = state * 6364136223846793005ULL + 1442695040888963407ULL; + return state; + }; + + auto makeName = []( const char * prefix, int i, const char * ext ) + { + char buf[64]; + std::snprintf( buf, sizeof( buf ), "%s_%03d.%s", prefix, i, ext ); + return std::string( buf ); + }; + + // 100 binary files of pseudo-random bytes. Poor compressibility on + // purpose — representative of mesh coordinate floats, compressed-texture + // blobs, and other near-incompressible payloads that often live in a + // MeshLib scene save. + std::size_t totalBinaryBytes = 0; + std::vector binBuf( bytesPerFile ); + for ( int i = 0; i < numBinaryFiles; ++i ) + { + uint64_t state = 0x1234567890ABCDEFULL ^ ( (uint64_t)i << 1 ); + for ( size_t j = 0; j < bytesPerFile; ++j ) + binBuf[j] = (char)( nextLcg( state ) >> 56 ); + + const std::filesystem::path p = srcFolder / makeName( "data", i, "bin" ); + std::ofstream out( p, std::ios::binary ); + ASSERT_TRUE( out.is_open() ); + out.write( binBuf.data(), (std::streamsize)binBuf.size() ); + ASSERT_TRUE( out.good() ); + out.close(); + totalBinaryBytes += bytesPerFile; + } + + // 100 JSON files of deterministic structured-looking text. Highly + // compressible — representative of scene-description metadata, logs, + // shader source, and other textual payloads. + std::size_t totalJsonBytes = 0; + for ( int i = 0; i < numJsonFiles; ++i ) + { + uint64_t state = 0xDEADBEEFCAFEBABEULL ^ ( (uint64_t)i << 1 ); + + std::string text; + text.reserve( bytesPerFile + 256 ); + text += "[\n"; + int idx = 0; + while ( text.size() + 96 < bytesPerFile ) + { + if ( idx > 0 ) + text += ",\n"; + const uint32_t rx = (uint32_t)( nextLcg( state ) >> 32 ); + const uint32_t ry = (uint32_t)( nextLcg( state ) >> 32 ); + const uint32_t rz = (uint32_t)( nextLcg( state ) >> 32 ); + char line[128]; + const int n = std::snprintf( line, sizeof( line ), + " {\"id\": %d, \"x\": %.6f, \"y\": %.6f, \"z\": %.6f}", + idx, + (double)rx / 4294967296.0, + (double)ry / 4294967296.0, + (double)rz / 4294967296.0 ); + ASSERT_GT( n, 0 ); + text.append( line, (size_t)n ); + ++idx; + } + text += "\n]\n"; + // Pad to exactly bytesPerFile with trailing spaces so the per-file + // size — and therefore the total — is deterministic across runs. + // The file is never parsed, so trailing whitespace past the final + // ']' is harmless. + if ( text.size() < bytesPerFile ) + text.append( bytesPerFile - text.size(), ' ' ); + else if ( text.size() > bytesPerFile ) + text.resize( bytesPerFile ); + + const std::filesystem::path p = srcFolder / makeName( "meta", i, "json" ); + std::ofstream out( p, std::ios::binary ); + ASSERT_TRUE( out.is_open() ); + out.write( text.data(), (std::streamsize)text.size() ); + ASSERT_TRUE( out.good() ); + out.close(); + totalJsonBytes += text.size(); + } + + const std::size_t totalInput = totalBinaryBytes + totalJsonBytes; + spdlog::info( "many-files input: {} binary + {} json = {} bytes", + totalBinaryBytes, totalJsonBytes, totalInput ); + + // Compress to a zip in a separate temp folder. + UniqueTemporaryFolder dstFolder; + ASSERT_TRUE( bool( dstFolder ) ); + const std::filesystem::path zipPath = dstFolder / "many.zip"; + + const auto compressRes = compressZip( zipPath, srcFolder ); + ASSERT_TRUE( compressRes.has_value() ) << compressRes.error(); + std::error_code ec; + ASSERT_TRUE( std::filesystem::exists( zipPath, ec ) ); + const auto zipSize = std::filesystem::file_size( zipPath, ec ); + EXPECT_GT( zipSize, 0u ); + spdlog::info( "many.zip size: {} bytes", zipSize ); + + // Sanity envelope: same bound as the sphere test. + EXPECT_LT( zipSize, totalInput * 2u ); +} + } // namespace MR From 26890ed21638b31b98e10a9928f8cad5679baed6 Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Tue, 21 Apr 2026 16:24:17 +0300 Subject: [PATCH 5/7] CompressManySmallFilesToZip test only with 2 times more files --- source/MRTest/MRZipCompressTests.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp index b428ccb2d122..b7697c26a357 100644 --- a/source/MRTest/MRZipCompressTests.cpp +++ b/source/MRTest/MRZipCompressTests.cpp @@ -21,7 +21,7 @@ namespace MR // compresses that folder to a .zip and verifies the archive was created and // is non-empty. Serves as a realistic end-to-end exercise of MeshLib's zip // write path (libzip + deflate) on mesh-sized data. -TEST( MRMesh, CompressSphereToZip ) +/*TEST( MRMesh, CompressSphereToZip ) { UniqueTemporaryFolder srcFolder; ASSERT_TRUE( bool( srcFolder ) ); @@ -63,10 +63,10 @@ TEST( MRMesh, CompressSphereToZip ) // since .mrmesh is a raw binary dump of topology plus coordinate // floats, deflate typically produces a modestly smaller archive. EXPECT_LT( zipSize, meshSize * 2u ); -} +}*/ -// Writes ~100 binary files and ~100 JSON files to a temporary folder (total -// content size ~= the single .mrmesh file in CompressSphereToZip above), then +// Writes ~200 binary files and ~200 JSON files to a temporary folder (total +// content size ~= 2 * the single .mrmesh file in CompressSphereToZip above), then // compresses the folder to a .zip. Pairs with CompressSphereToZip to compare // compression of one large binary vs many small mixed-type entries. // @@ -78,8 +78,8 @@ TEST( MRMesh, CompressManySmallFilesToZip ) UniqueTemporaryFolder srcFolder; ASSERT_TRUE( bool( srcFolder ) ); - constexpr int numBinaryFiles = 100; - constexpr int numJsonFiles = 100; + constexpr int numBinaryFiles = 200; + constexpr int numJsonFiles = 200; constexpr size_t bytesPerFile = 60'000; // 200 files * 60_000 bytes = 12_000_000 bytes, very close to the // sphere.mrmesh size from the previous test (11_999_808 bytes). From 3176f94d834d3b3e922f88c12bb0e15d3f2b3e5d Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Tue, 21 Apr 2026 18:08:14 +0300 Subject: [PATCH 6/7] reduce the test time for everyday run --- source/MRTest/MRZipCompressTests.cpp | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp index b7697c26a357..ac8a67ddf8b7 100644 --- a/source/MRTest/MRZipCompressTests.cpp +++ b/source/MRTest/MRZipCompressTests.cpp @@ -17,18 +17,16 @@ namespace MR { -// Writes a ~100K-vertex sphere to a .mrmesh file in a temporary folder, then +// Writes a sphere to a .mrmesh file in a temporary folder, then // compresses that folder to a .zip and verifies the archive was created and // is non-empty. Serves as a realistic end-to-end exercise of MeshLib's zip // write path (libzip + deflate) on mesh-sized data. -/*TEST( MRMesh, CompressSphereToZip ) +TEST( MRMesh, CompressSphereToZip ) { UniqueTemporaryFolder srcFolder; ASSERT_TRUE( bool( srcFolder ) ); - // Generate a sphere with ~100K vertices. makeSphere's subdivision - // targets the requested count but may land a handful over. - constexpr int targetVerts = 100'000; + constexpr int targetVerts = 1000; // increase it to make the file being compressed larger, 100'000 vertices -> 12M bytes SphereParams params; params.radius = 1.0f; params.numMeshVertices = targetVerts; @@ -63,10 +61,9 @@ namespace MR // since .mrmesh is a raw binary dump of topology plus coordinate // floats, deflate typically produces a modestly smaller archive. EXPECT_LT( zipSize, meshSize * 2u ); -}*/ +} -// Writes ~200 binary files and ~200 JSON files to a temporary folder (total -// content size ~= 2 * the single .mrmesh file in CompressSphereToZip above), then +// Writes many binary files and same number JSON files to a temporary folder, then // compresses the folder to a .zip. Pairs with CompressSphereToZip to compare // compression of one large binary vs many small mixed-type entries. // @@ -78,11 +75,10 @@ TEST( MRMesh, CompressManySmallFilesToZip ) UniqueTemporaryFolder srcFolder; ASSERT_TRUE( bool( srcFolder ) ); - constexpr int numBinaryFiles = 200; - constexpr int numJsonFiles = 200; - constexpr size_t bytesPerFile = 60'000; - // 200 files * 60_000 bytes = 12_000_000 bytes, very close to the - // sphere.mrmesh size from the previous test (11_999_808 bytes). + // increase both below numbers to make the files being compressed larger, 200 * 2 files * 60'000 bytes -> 24M bytes + constexpr int numBinaryFiles = 20; + constexpr int numJsonFiles = numBinaryFiles; + constexpr size_t bytesPerFile = 6000; // Simple LCG used to produce deterministic pseudo-random bytes. // Keeps the test reproducible across runs and platforms while avoiding @@ -122,7 +118,7 @@ TEST( MRMesh, CompressManySmallFilesToZip ) totalBinaryBytes += bytesPerFile; } - // 100 JSON files of deterministic structured-looking text. Highly + // JSON files of deterministic structured-looking text. Highly // compressible — representative of scene-description metadata, logs, // shader source, and other textual payloads. std::size_t totalJsonBytes = 0; From baff1582c3d77af6fc8de4d8ce2c4230a88c0a9b Mon Sep 17 00:00:00 2001 From: Fedor Chelnokov Date: Tue, 21 Apr 2026 21:18:59 +0300 Subject: [PATCH 7/7] comment --- source/MRTest/MRZipCompressTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/MRTest/MRZipCompressTests.cpp b/source/MRTest/MRZipCompressTests.cpp index ac8a67ddf8b7..ee1f34f6821c 100644 --- a/source/MRTest/MRZipCompressTests.cpp +++ b/source/MRTest/MRZipCompressTests.cpp @@ -97,7 +97,7 @@ TEST( MRMesh, CompressManySmallFilesToZip ) return std::string( buf ); }; - // 100 binary files of pseudo-random bytes. Poor compressibility on + // Binary files of pseudo-random bytes. Poor compressibility on // purpose — representative of mesh coordinate floats, compressed-texture // blobs, and other near-incompressible payloads that often live in a // MeshLib scene save.