From 84d278da75e9b71cacd0665075531e4bdef54c51 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 23 Feb 2026 17:48:37 -0800 Subject: [PATCH 1/3] Tool and initial change to page size --- .../Microsoft/SQLiteIndex.cpp | 2 +- .../Public/winget/SQLiteStorageBase.h | 2 +- .../Public/winget/SQLiteWrapper.h | 4 + .../SQLiteStorageBase.cpp | 6 +- src/AppInstallerSharedLib/SQLiteWrapper.cpp | 9 + .../IndexComparisonTool.vcxproj | 116 +++ .../IndexComparisonTool.vcxproj.filters | 29 + tools/IndexComparisonTool/WinGetUtil.h | 29 + tools/IndexComparisonTool/main.cpp | 709 ++++++++++++++++++ tools/IndexComparisonTool/pch.cpp | 3 + tools/IndexComparisonTool/pch.h | 21 + 11 files changed, 927 insertions(+), 3 deletions(-) create mode 100644 tools/IndexComparisonTool/IndexComparisonTool.vcxproj create mode 100644 tools/IndexComparisonTool/IndexComparisonTool.vcxproj.filters create mode 100644 tools/IndexComparisonTool/WinGetUtil.h create mode 100644 tools/IndexComparisonTool/main.cpp create mode 100644 tools/IndexComparisonTool/pch.cpp create mode 100644 tools/IndexComparisonTool/pch.h diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index ba4c089158..4564c0725b 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -37,7 +37,7 @@ namespace AppInstaller::Repository::Microsoft return { filePath, source }; } - SQLiteIndex::SQLiteIndex(const std::string& target, const SQLite::Version& version) : SQLiteStorageBase(target, version) + SQLiteIndex::SQLiteIndex(const std::string& target, const SQLite::Version& version) : SQLiteStorageBase(target, version, 65536) { m_dbconn.EnableICU(); m_interface = Schema::CreateISQLiteIndex(version); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h index 88dba69628..e89d7adab3 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h @@ -39,7 +39,7 @@ namespace AppInstaller::SQLite static void RenameSQLiteDatabase(const std::filesystem::path& source, const std::filesystem::path& destination, bool overwrite = false); protected: - SQLiteStorageBase(const std::string& target, const Version& version); + SQLiteStorageBase(const std::string& target, const Version& version, int pageSize = 0); SQLiteStorageBase(const std::string& filePath, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h b/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h index abf2a9ffa8..ad5af5cabb 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h @@ -264,6 +264,10 @@ namespace AppInstaller::SQLite // Must be performed outside of a transaction. bool SetJournalMode(std::string_view mode); + // Sets the page size for a new, empty database. + // Must be called before the first write to take effect. + void SetPageSize(int pageSize); + operator sqlite3* () const { return m_dbconn->Get(); } protected: diff --git a/src/AppInstallerSharedLib/SQLiteStorageBase.cpp b/src/AppInstallerSharedLib/SQLiteStorageBase.cpp index 9cc46cf21c..d977167a8b 100644 --- a/src/AppInstallerSharedLib/SQLiteStorageBase.cpp +++ b/src/AppInstallerSharedLib/SQLiteStorageBase.cpp @@ -156,10 +156,14 @@ namespace AppInstaller::SQLite m_version = Version::GetSchemaVersion(m_dbconn); } - SQLiteStorageBase::SQLiteStorageBase(const std::string& target, const Version& version) : + SQLiteStorageBase::SQLiteStorageBase(const std::string& target, const Version& version, int pageSize) : m_dbconn(SQLite::Connection::Create(target, SQLite::Connection::OpenDisposition::Create)) { m_version = version; + if (pageSize > 0) + { + m_dbconn.SetPageSize(pageSize); + } MetadataTable::Create(m_dbconn); // Write a new identifier for this database diff --git a/src/AppInstallerSharedLib/SQLiteWrapper.cpp b/src/AppInstallerSharedLib/SQLiteWrapper.cpp index 22c8c06f10..e08417cd8c 100644 --- a/src/AppInstallerSharedLib/SQLiteWrapper.cpp +++ b/src/AppInstallerSharedLib/SQLiteWrapper.cpp @@ -251,6 +251,15 @@ namespace AppInstaller::SQLite return ToLower(setJournalMode.GetColumn(0)) == ToLower(mode); } + void Connection::SetPageSize(int pageSize) + { + std::ostringstream stream; + stream << "PRAGMA page_size=" << pageSize; + + Statement setPageSize = Statement::Create(*this, stream.str()); + setPageSize.Step(); + } + std::shared_ptr Connection::GetSharedConnection() const { return m_dbconn; diff --git a/tools/IndexComparisonTool/IndexComparisonTool.vcxproj b/tools/IndexComparisonTool/IndexComparisonTool.vcxproj new file mode 100644 index 0000000000..1b7a11945d --- /dev/null +++ b/tools/IndexComparisonTool/IndexComparisonTool.vcxproj @@ -0,0 +1,116 @@ + + + + + Debug + ARM64 + + + Debug + x64 + + + Release + ARM64 + + + Release + x64 + + + + + 16.0 + {2F8A1C3E-7B4D-4E9F-A6C2-1D5E8B3F0A7C} + Win32Proj + IndexComparisonTool + 10.0.26100.0 + 10.0.17763.0 + true + + + + + + Application + Unicode + v143 + + + true + true + + + false + true + false + + + + + + + + + + + + $(ProjectDir)bin\$(Platform)\$(Configuration)\ + $(ProjectDir)obj\$(Platform)\$(Configuration)\ + + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + true + true + /permissive- /std:c++17 %(AdditionalOptions) + + UNICODE;_UNICODE;%(PreprocessorDefinitions) + + + Console + + + %(AdditionalDependencies) + + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + + Create + + + + + diff --git a/tools/IndexComparisonTool/IndexComparisonTool.vcxproj.filters b/tools/IndexComparisonTool/IndexComparisonTool.vcxproj.filters new file mode 100644 index 0000000000..dbb44d8f44 --- /dev/null +++ b/tools/IndexComparisonTool/IndexComparisonTool.vcxproj.filters @@ -0,0 +1,29 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + diff --git a/tools/IndexComparisonTool/WinGetUtil.h b/tools/IndexComparisonTool/WinGetUtil.h new file mode 100644 index 0000000000..94531e46aa --- /dev/null +++ b/tools/IndexComparisonTool/WinGetUtil.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Minimal WinGetUtil types and function pointer declarations for use +// with runtime loading via LoadLibrary. Only the APIs used by +// IndexComparisonTool are defined here. +#pragma once + +typedef void* WINGET_SQLITE_INDEX_HANDLE; +typedef wchar_t const* WINGET_STRING; + +#define WINGET_SQLITE_INDEX_VERSION_LATEST ((UINT32)-1) + +typedef HRESULT (__stdcall *PFN_WinGetSQLiteIndexCreate)( + WINGET_STRING filePath, + UINT32 majorVersion, + UINT32 minorVersion, + WINGET_SQLITE_INDEX_HANDLE* index); + +typedef HRESULT (__stdcall *PFN_WinGetSQLiteIndexAddManifest)( + WINGET_SQLITE_INDEX_HANDLE index, + WINGET_STRING manifestPath, + WINGET_STRING relativePath); + +typedef HRESULT (__stdcall *PFN_WinGetSQLiteIndexPrepareForPackaging)( + WINGET_SQLITE_INDEX_HANDLE index); + +typedef HRESULT (__stdcall *PFN_WinGetSQLiteIndexClose)( + WINGET_SQLITE_INDEX_HANDLE index); diff --git a/tools/IndexComparisonTool/main.cpp b/tools/IndexComparisonTool/main.cpp new file mode 100644 index 0000000000..c012250df5 --- /dev/null +++ b/tools/IndexComparisonTool/main.cpp @@ -0,0 +1,709 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "WinGetUtil.h" + +#pragma comment(lib, "Cabinet.lib") +#pragma comment(lib, "winsqlite3.lib") + +namespace +{ + // ------------------------------------------------------------------------- + // WinGetUtil runtime loading + // ------------------------------------------------------------------------- + + struct WinGetApi + { + HMODULE module = nullptr; + PFN_WinGetSQLiteIndexCreate Create = nullptr; + PFN_WinGetSQLiteIndexAddManifest AddManifest = nullptr; + PFN_WinGetSQLiteIndexPrepareForPackaging PrepareForPackaging = nullptr; + PFN_WinGetSQLiteIndexClose Close = nullptr; + + ~WinGetApi() { if (module) FreeLibrary(module); } + + explicit operator bool() const { return module && Create && AddManifest && PrepareForPackaging && Close; } + }; + + template + T GetProc(HMODULE mod, const char* name) + { + return reinterpret_cast(GetProcAddress(mod, name)); + } + + // Loads WinGetUtil.dll from dllPath (if non-empty) or via the standard DLL + // search order (which includes the application directory first). + WinGetApi LoadWinGetUtil(const std::filesystem::path& dllPath) + { + WinGetApi api; + + if (!dllPath.empty()) + { + api.module = LoadLibraryExW(dllPath.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + if (!api.module) + { + std::wcerr << L"Error: could not load WinGetUtil.dll from: " << dllPath << L"\n" + << L" GetLastError: " << GetLastError() << L"\n"; + return api; + } + } + else + { + api.module = LoadLibraryW(L"WinGetUtil.dll"); + if (!api.module) + { + std::wcerr << L"Error: could not load WinGetUtil.dll.\n" + << L" Place WinGetUtil.dll in the same directory as this executable,\n" + << L" or specify its path with --wingetutil .\n" + << L" GetLastError: " << GetLastError() << L"\n"; + return api; + } + } + + api.Create = GetProc (api.module, "WinGetSQLiteIndexCreate"); + api.AddManifest = GetProc (api.module, "WinGetSQLiteIndexAddManifest"); + api.PrepareForPackaging = GetProc(api.module, "WinGetSQLiteIndexPrepareForPackaging"); + api.Close = GetProc (api.module, "WinGetSQLiteIndexClose"); + + if (!api) + { + std::wcerr << L"Error: WinGetUtil.dll is missing one or more required exports.\n" + << L" Ensure the DLL matches the expected version.\n"; + } + + return api; + } + + // ------------------------------------------------------------------------- + // String helpers + // ------------------------------------------------------------------------- + + std::wstring ToUTF16(std::string_view utf8) + { + if (utf8.empty()) return {}; + int len = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); + std::wstring result(len, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), result.data(), len); + return result; + } + + std::string ToUTF8(std::wstring_view utf16) + { + if (utf16.empty()) return {}; + int len = WideCharToMultiByte(CP_UTF8, 0, utf16.data(), static_cast(utf16.size()), nullptr, 0, nullptr, nullptr); + std::string result(len, '\0'); + WideCharToMultiByte(CP_UTF8, 0, utf16.data(), static_cast(utf16.size()), result.data(), len, nullptr, nullptr); + return result; + } + + // ------------------------------------------------------------------------- + // Argument parsing + // ------------------------------------------------------------------------- + + struct Args + { + std::filesystem::path manifestsDir; + std::filesystem::path wingetUtilPath; // optional: explicit path to WinGetUtil.dll + std::optional outputFile; + std::optional baselineFile; + bool verbose = false; + bool showHelp = false; + }; + + void PrintUsage(const wchar_t* programName) + { + std::wcout + << L"Usage: " << programName << L" [options]\n" + << L"\n" + << L"Builds a WinGet source index from a directory of YAML manifests and\n" + << L"measures its raw and compressed size (XPRESS Huffman, approximating MSIX).\n" + << L"\n" + << L"Options:\n" + << L" --wingetutil Path to WinGetUtil.dll\n" + << L" (default: searches application directory and PATH)\n" + << L" --output Write results as JSON to \n" + << L" --baseline Compare against a prior JSON result (from --output)\n" + << L" --verbose Show per-table row counts and page usage\n" + << L" --help Show this help\n"; + } + + Args ParseArgs(int argc, wchar_t* argv[]) + { + Args args; + for (int i = 1; i < argc; ++i) + { + std::wstring_view arg{ argv[i] }; + if (arg == L"--help" || arg == L"-h" || arg == L"-?") + { + args.showHelp = true; + } + else if (arg == L"--verbose" || arg == L"-v") + { + args.verbose = true; + } + else if ((arg == L"--output" || arg == L"-o") && i + 1 < argc) + { + args.outputFile = argv[++i]; + } + else if ((arg == L"--baseline" || arg == L"-b") && i + 1 < argc) + { + args.baselineFile = argv[++i]; + } + else if (arg == L"--wingetutil" && i + 1 < argc) + { + args.wingetUtilPath = argv[++i]; + } + else if (args.manifestsDir.empty() && !arg.empty() && arg[0] != L'-') + { + args.manifestsDir = arg; + } + } + return args; + } + + // ------------------------------------------------------------------------- + // Manifest discovery + // ------------------------------------------------------------------------- + + // Returns directories that directly contain at least one .yaml file. + // Each such directory represents one package version manifest. + std::vector FindManifestDirs(const std::filesystem::path& root) + { + std::vector result; + std::error_code ec; + for (const auto& entry : std::filesystem::recursive_directory_iterator( + root, std::filesystem::directory_options::skip_permission_denied, ec)) + { + if (!entry.is_directory(ec)) continue; + for (const auto& f : std::filesystem::directory_iterator(entry.path(), ec)) + { + if (f.is_regular_file(ec) && f.path().extension() == L".yaml") + { + result.push_back(entry.path()); + break; + } + } + } + return result; + } + + // ------------------------------------------------------------------------- + // Index building + // ------------------------------------------------------------------------- + + struct ManifestStats + { + uint64_t added = 0; + uint64_t failed = 0; + }; + + HRESULT AddManifest( + const WinGetApi& api, + WINGET_SQLITE_INDEX_HANDLE index, + const std::filesystem::path& manifestDir, + const std::filesystem::path& relPath) + { + std::wstring manifestPathW = manifestDir.wstring(); + std::wstring relPathW = relPath.generic_wstring(); // forward slashes + return api.AddManifest(index, manifestPathW.c_str(), relPathW.c_str()); + } + + ManifestStats BuildIndex( + const WinGetApi& api, + WINGET_SQLITE_INDEX_HANDLE index, + const std::filesystem::path& manifestsDir) + { + auto manifestDirs = FindManifestDirs(manifestsDir); + std::wcout << L"Found " << manifestDirs.size() << L" manifest directories. Adding to index...\n"; + + ManifestStats stats; + std::vector retryList; + + uint64_t processed = 0; + for (const auto& dir : manifestDirs) + { + std::filesystem::path relPath = std::filesystem::relative(dir, manifestsDir); + if (SUCCEEDED(AddManifest(api, index, dir, relPath))) + { + ++stats.added; + } + else + { + retryList.push_back(dir); + } + + if (++processed % 1000 == 0) + { + std::wcout << L" " << processed << L" / " << manifestDirs.size() << L" processed...\r"; + } + } + + // Retry failures once (handles dependency ordering issues) + if (!retryList.empty()) + { + std::wcout << L"\nRetrying " << retryList.size() << L" failed manifests...\n"; + for (const auto& dir : retryList) + { + std::filesystem::path relPath = std::filesystem::relative(dir, manifestsDir); + if (SUCCEEDED(AddManifest(api, index, dir, relPath))) + { + ++stats.added; + } + else + { + ++stats.failed; + } + } + } + + std::wcout << L"\nAdded: " << stats.added << L" Failed: " << stats.failed << L"\n"; + return stats; + } + + // ------------------------------------------------------------------------- + // Compression measurement (XPRESS Huffman, the algorithm used by MSIX) + // ------------------------------------------------------------------------- + + uint64_t MeasureCompressed(const std::filesystem::path& file) + { + std::ifstream ifs(file, std::ios::binary | std::ios::ate); + if (!ifs) return 0; + + auto fileSize = static_cast(ifs.tellg()); + ifs.seekg(0); + std::vector data(fileSize); + ifs.read(reinterpret_cast(data.data()), fileSize); + ifs.close(); + + COMPRESSOR_HANDLE compressor = nullptr; + if (!CreateCompressor(COMPRESS_ALGORITHM_XPRESS_HUFF, nullptr, &compressor)) + { + return fileSize; + } + + SIZE_T compressedSize = 0; + // First call determines required output buffer size + Compress(compressor, data.data(), data.size(), nullptr, 0, &compressedSize); + + std::vector compressed(compressedSize); + if (!Compress(compressor, data.data(), data.size(), compressed.data(), compressedSize, &compressedSize)) + { + CloseCompressor(compressor); + return fileSize; + } + + CloseCompressor(compressor); + return static_cast(compressedSize); + } + + // ------------------------------------------------------------------------- + // Table stats (--verbose) + // ------------------------------------------------------------------------- + + struct TableInfo + { + std::string name; + int64_t rowCount = 0; + int64_t pageCount = 0; + }; + + std::vector QueryTableStats(const std::filesystem::path& dbFile) + { + std::vector result; + + sqlite3* db = nullptr; + if (sqlite3_open_v2(ToUTF8(dbFile.wstring()).c_str(), &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) + { + return result; + } + + // Enumerate tables + std::vector tableNames; + { + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name", + -1, &stmt, nullptr) == SQLITE_OK) + { + while (sqlite3_step(stmt) == SQLITE_ROW) + { + tableNames.emplace_back(reinterpret_cast(sqlite3_column_text(stmt, 0))); + } + sqlite3_finalize(stmt); + } + } + + for (const auto& name : tableNames) + { + TableInfo info; + info.name = name; + + // Row count + { + sqlite3_stmt* stmt = nullptr; + std::string q = "SELECT COUNT(*) FROM [" + name + "]"; + if (sqlite3_prepare_v2(db, q.c_str(), -1, &stmt, nullptr) == SQLITE_OK) + { + if (sqlite3_step(stmt) == SQLITE_ROW) + { + info.rowCount = sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + } + } + + // Page count via dbstat virtual table (may not be available on all builds) + { + sqlite3_stmt* stmt = nullptr; + if (sqlite3_prepare_v2(db, + "SELECT COUNT(*) FROM dbstat WHERE name=?", + -1, &stmt, nullptr) == SQLITE_OK) + { + sqlite3_bind_text(stmt, 1, name.c_str(), -1, SQLITE_TRANSIENT); + if (sqlite3_step(stmt) == SQLITE_ROW) + { + info.pageCount = sqlite3_column_int64(stmt, 0); + } + sqlite3_finalize(stmt); + } + } + + result.push_back(std::move(info)); + } + + // Sort by page count descending so the largest tables appear first + std::sort(result.begin(), result.end(), + [](const TableInfo& a, const TableInfo& b) { return a.pageCount > b.pageCount; }); + + sqlite3_close(db); + return result; + } + + // ------------------------------------------------------------------------- + // Output + // ------------------------------------------------------------------------- + + struct Results + { + std::filesystem::path manifestsDir; + uint64_t manifestCount = 0; + uint64_t failedCount = 0; + uint64_t rawBytes = 0; + uint64_t compressedBytes = 0; + std::vector tables; + + double CompressionRatio() const + { + return rawBytes > 0 ? static_cast(compressedBytes) / rawBytes : 0.0; + } + }; + + std::string FormatBytes(uint64_t bytes) + { + std::ostringstream ss; + if (bytes >= 1024ULL * 1024) + { + ss << std::fixed << std::setprecision(1) << (bytes / (1024.0 * 1024.0)) << " MB"; + } + else if (bytes >= 1024) + { + ss << std::fixed << std::setprecision(1) << (bytes / 1024.0) << " KB"; + } + else + { + ss << bytes << " B"; + } + ss << " (" << bytes << " bytes)"; + return ss.str(); + } + + void PrintResults(const Results& r, bool verbose) + { + double ratio = r.CompressionRatio(); + double savings = 1.0 - ratio; + + std::wcout << L"\n"; + std::wcout << L"Index built from: " << r.manifestsDir << L"\n"; + std::wcout << L"Manifests added: " << r.manifestCount; + if (r.failedCount > 0) { std::wcout << L" (" << r.failedCount << L" failed)"; } + std::wcout << L"\n"; + std::wcout << L"Raw size: " << ToUTF16(FormatBytes(r.rawBytes)) << L"\n"; + std::wcout << L"Compressed size: " << ToUTF16(FormatBytes(r.compressedBytes)) << L" (XPRESS Huffman)\n"; + std::wcout << std::fixed << std::setprecision(1); + std::wcout << L"Compression ratio: " << (ratio * 100) << L"%" + << L" (" << (savings * 100) << L"% savings)\n"; + + if (verbose && !r.tables.empty()) + { + std::wcout << L"\nTable breakdown (sorted by page count):\n"; + std::wcout << std::left + << std::setw(40) << L"Table" + << std::setw(12) << L"Rows" + << std::setw(8) << L"Pages" + << L"\n" + << std::wstring(60, L'-') << L"\n"; + for (const auto& t : r.tables) + { + std::wcout << std::setw(40) << ToUTF16(t.name) + << std::setw(12) << t.rowCount + << std::setw(8) << t.pageCount + << L"\n"; + } + } + } + + // ------------------------------------------------------------------------- + // JSON output + // ------------------------------------------------------------------------- + + std::string JsonEscapeString(const std::string& s) + { + std::string r; + r.reserve(s.size() + 2); + r += '"'; + for (char c : s) + { + if (c == '"') r += "\\\""; + else if (c == '\\') r += "\\\\"; + else r += c; + } + r += '"'; + return r; + } + + void WriteJson(const Results& r, bool verbose, const std::filesystem::path& outFile) + { + std::ofstream ofs(outFile); + if (!ofs) { throw std::runtime_error("Cannot open output file: " + ToUTF8(outFile.wstring())); } + + ofs << "{\n"; + ofs << " \"manifestsDir\": " << JsonEscapeString(ToUTF8(r.manifestsDir.wstring())) << ",\n"; + ofs << " \"manifestCount\": " << r.manifestCount << ",\n"; + ofs << " \"failedCount\": " << r.failedCount << ",\n"; + ofs << " \"rawBytes\": " << r.rawBytes << ",\n"; + ofs << " \"compressedBytes\": " << r.compressedBytes << ",\n"; + ofs << std::fixed << std::setprecision(4); + ofs << " \"compressionRatio\": " << r.CompressionRatio(); + + if (verbose && !r.tables.empty()) + { + ofs << ",\n \"tables\": [\n"; + for (size_t i = 0; i < r.tables.size(); ++i) + { + const auto& t = r.tables[i]; + ofs << " { \"name\": " << JsonEscapeString(t.name) + << ", \"rowCount\": " << t.rowCount + << ", \"pageCount\": " << t.pageCount << " }"; + if (i + 1 < r.tables.size()) ofs << ","; + ofs << "\n"; + } + ofs << " ]\n"; + } + else + { + ofs << "\n"; + } + ofs << "}\n"; + } + + // ------------------------------------------------------------------------- + // Baseline comparison + // ------------------------------------------------------------------------- + + bool ExtractJsonUint64(const std::string& json, const std::string& key, uint64_t& value) + { + std::string searchKey = "\"" + key + "\": "; + auto pos = json.find(searchKey); + if (pos == std::string::npos) return false; + pos += searchKey.size(); + try { value = std::stoull(json.substr(pos)); return true; } + catch (...) { return false; } + } + + bool ExtractJsonDouble(const std::string& json, const std::string& key, double& value) + { + std::string searchKey = "\"" + key + "\": "; + auto pos = json.find(searchKey); + if (pos == std::string::npos) return false; + pos += searchKey.size(); + try { value = std::stod(json.substr(pos)); return true; } + catch (...) { return false; } + } + + void CompareWithBaseline(const Results& current, const std::filesystem::path& baselineFile) + { + std::ifstream ifs(baselineFile); + if (!ifs) + { + std::wcerr << L"Warning: cannot open baseline file: " << baselineFile << L"\n"; + return; + } + std::string json{ std::istreambuf_iterator(ifs), std::istreambuf_iterator() }; + + uint64_t baseRaw = 0, baseCompressed = 0; + double baseRatio = 0.0; + if (!ExtractJsonUint64(json, "rawBytes", baseRaw) || + !ExtractJsonUint64(json, "compressedBytes", baseCompressed) || + !ExtractJsonDouble(json, "compressionRatio", baseRatio)) + { + std::wcerr << L"Warning: could not parse required fields from baseline JSON.\n"; + return; + } + + auto signedBytes = [](int64_t delta) -> std::wstring + { + std::wostringstream ss; + if (delta >= 0) ss << L"+"; + ss << delta << L" bytes"; + return ss.str(); + }; + auto pctChange = [](uint64_t cur, uint64_t base) -> std::wstring + { + if (base == 0) return L"N/A"; + double pct = 100.0 * (static_cast(cur) - base) / base; + std::wostringstream ss; + if (pct >= 0) ss << L"+"; + ss << std::fixed << std::setprecision(1) << pct << L"%"; + return ss.str(); + }; + + int64_t rawDelta = static_cast(current.rawBytes) - static_cast(baseRaw); + int64_t compDelta = static_cast(current.compressedBytes) - static_cast(baseCompressed); + double ratioDelta = current.CompressionRatio() - baseRatio; + + std::wostringstream ratioSS; + if (ratioDelta >= 0) ratioSS << L"+"; + ratioSS << std::fixed << std::setprecision(2) << (ratioDelta * 100) << L"pp" + << L" (was " << std::setprecision(1) << (baseRatio * 100) << L"%)"; + + std::wcout << L"\nvs baseline (" << baselineFile.filename().wstring() << L"):\n"; + std::wcout << L" Raw size: " << signedBytes(rawDelta) + << L" (" << pctChange(current.rawBytes, baseRaw) << L")\n"; + std::wcout << L" Compressed size: " << signedBytes(compDelta) + << L" (" << pctChange(current.compressedBytes, baseCompressed) << L")\n"; + std::wcout << L" Compression ratio: " << ratioSS.str() << L"\n"; + } + +} // anonymous namespace + +// ------------------------------------------------------------------------- +// Entry point +// ------------------------------------------------------------------------- + +int wmain(int argc, wchar_t* argv[]) +{ + Args args = ParseArgs(argc, argv); + + if (args.showHelp || args.manifestsDir.empty()) + { + PrintUsage(argc > 0 ? argv[0] : L"IndexComparisonTool"); + return args.showHelp ? 0 : 1; + } + + if (!std::filesystem::is_directory(args.manifestsDir)) + { + std::wcerr << L"Error: not a directory: " << args.manifestsDir << L"\n"; + return 1; + } + + // Load WinGetUtil.dll at runtime + WinGetApi api = LoadWinGetUtil(args.wingetUtilPath); + if (!api) { return 1; } + + // Create a temp file for the index + wchar_t tempDir[MAX_PATH + 1]{}; + wchar_t tempFile[MAX_PATH + 1]{}; + GetTempPathW(MAX_PATH, tempDir); + GetTempFileNameW(tempDir, L"idx", 0, tempFile); + std::filesystem::path indexPath{ tempFile }; + + struct TempFileGuard + { + std::filesystem::path path; + ~TempFileGuard() { std::error_code ec; std::filesystem::remove(path, ec); } + } tempGuard{ indexPath }; + + // Create the index + std::wcout << L"Creating index...\n"; + WINGET_SQLITE_INDEX_HANDLE index = nullptr; + HRESULT hr = api.Create( + indexPath.c_str(), + WINGET_SQLITE_INDEX_VERSION_LATEST, + WINGET_SQLITE_INDEX_VERSION_LATEST, + &index); + + if (FAILED(hr)) + { + std::wcerr << L"Error: WinGetSQLiteIndexCreate failed: 0x" + << std::hex << std::uppercase << hr << L"\n"; + return 1; + } + + // RAII guard: close index on early return + bool indexClosed = false; + struct IndexGuard + { + PFN_WinGetSQLiteIndexClose close; + WINGET_SQLITE_INDEX_HANDLE handle; + bool& closed; + ~IndexGuard() { if (!closed && handle) close(handle); } + } indexGuard{ api.Close, index, indexClosed }; + + // Populate the index + ManifestStats mstats = BuildIndex(api, index, args.manifestsDir); + + // Finalize: VACUUM and drop build-time indices + std::wcout << L"Preparing for packaging (VACUUM + drop indices)...\n"; + hr = api.PrepareForPackaging(index); + if (FAILED(hr)) + { + std::wcerr << L"Error: WinGetSQLiteIndexPrepareForPackaging failed: 0x" + << std::hex << std::uppercase << hr << L"\n"; + return 1; + } + + indexClosed = true; + api.Close(index); + + // Optionally query table stats from the now-closed database + std::vector tables; + if (args.verbose) + { + tables = QueryTableStats(indexPath); + } + + // Measure sizes + uint64_t rawBytes = std::filesystem::file_size(indexPath); + std::wcout << L"Measuring compression...\n"; + uint64_t compressedBytes = MeasureCompressed(indexPath); + + Results results; + results.manifestsDir = std::filesystem::weakly_canonical(args.manifestsDir); + results.manifestCount = mstats.added; + results.failedCount = mstats.failed; + results.rawBytes = rawBytes; + results.compressedBytes = compressedBytes; + results.tables = std::move(tables); + + PrintResults(results, args.verbose); + + if (args.outputFile) + { + try + { + WriteJson(results, args.verbose, *args.outputFile); + std::wcout << L"\nResults written to: " << args.outputFile->wstring() << L"\n"; + } + catch (const std::exception& e) + { + std::wcerr << L"Warning: failed to write JSON: " << ToUTF16(e.what()) << L"\n"; + } + } + + if (args.baselineFile) + { + CompareWithBaseline(results, *args.baselineFile); + } + + return 0; +} \ No newline at end of file diff --git a/tools/IndexComparisonTool/pch.cpp b/tools/IndexComparisonTool/pch.cpp new file mode 100644 index 0000000000..5494963b40 --- /dev/null +++ b/tools/IndexComparisonTool/pch.cpp @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" diff --git a/tools/IndexComparisonTool/pch.h b/tools/IndexComparisonTool/pch.h new file mode 100644 index 0000000000..a5170a8a47 --- /dev/null +++ b/tools/IndexComparisonTool/pch.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include From fb86f7450923092599793d555cc2451f34341a4b Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 4 Mar 2026 11:03:36 -0800 Subject: [PATCH 2/3] Changes --- .../Microsoft/SQLiteIndex.cpp | 12 +- .../Microsoft/SQLiteIndex.h | 2 +- .../Microsoft/Schema/ISQLiteIndex.h | 5 + .../Public/winget/SQLiteStorageBase.h | 2 +- .../Public/winget/SQLiteWrapper.h | 3 +- .../SQLiteStorageBase.cpp | 2 +- src/AppInstallerSharedLib/SQLiteWrapper.cpp | 2 +- tools/IndexComparisonTool/main.cpp | 173 ++++++++++-------- tools/IndexComparisonTool/pch.h | 1 + 9 files changed, 118 insertions(+), 84 deletions(-) diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp index 4564c0725b..509ec2b526 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.cpp @@ -8,10 +8,18 @@ namespace AppInstaller::Repository::Microsoft { + namespace + { + size_t GetPageSizeFromOptions(SQLiteIndex::CreateOptions options) + { + return WI_IsFlagSet(options, SQLiteIndex::CreateOptions::LargePageSize) ? 65536 : 0; + } + } + SQLiteIndex SQLiteIndex::CreateNew(const std::string& filePath, SQLite::Version version, CreateOptions options) { AICLI_LOG(Repo, Info, << "Creating new SQLite Index with version [" << version << "] at '" << filePath << "'"); - SQLiteIndex result{ filePath, version }; + SQLiteIndex result{ filePath, version, options }; SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "sqliteindex_createnew"); @@ -37,7 +45,7 @@ namespace AppInstaller::Repository::Microsoft return { filePath, source }; } - SQLiteIndex::SQLiteIndex(const std::string& target, const SQLite::Version& version) : SQLiteStorageBase(target, version, 65536) + SQLiteIndex::SQLiteIndex(const std::string& target, const SQLite::Version& version, CreateOptions options) : SQLiteStorageBase(target, version, GetPageSizeFromOptions(options)) { m_dbconn.EnableICU(); m_interface = Schema::CreateISQLiteIndex(version); diff --git a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h index 6dc3cac54e..6f132dc46d 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/SQLiteIndex.h @@ -175,7 +175,7 @@ namespace AppInstaller::Repository::Microsoft private: // Constructor used to create a new index. - SQLiteIndex(const std::string& target, const SQLite::Version& version); + SQLiteIndex(const std::string& target, const SQLite::Version& version, CreateOptions options); // Constructor used to open an existing index. SQLiteIndex(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h index 08011309f4..e953c4ea8f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ISQLiteIndex.h @@ -52,6 +52,11 @@ namespace AppInstaller::Repository::Microsoft::Schema SupportPathless = 0x1, // Disable support for dependencies DisableDependenciesSupport = 0x2, + // Use maximum page size in SQLite. + // This was part of an exploration of ways to reduce file size but ultimately led to a larger + // compressed file with a worse ratio (limited testing but was significant enough to warrant abandonment). + // Leaving this here as a valid null result to prevent future maintainers from needing to investigate. + LargePageSize = 0x4, }; // Contains both the object representation of the version key and the rows. diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h index e89d7adab3..56e9c495b5 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h @@ -39,7 +39,7 @@ namespace AppInstaller::SQLite static void RenameSQLiteDatabase(const std::filesystem::path& source, const std::filesystem::path& destination, bool overwrite = false); protected: - SQLiteStorageBase(const std::string& target, const Version& version, int pageSize = 0); + SQLiteStorageBase(const std::string& target, const Version& version, size_t pageSize = 0); SQLiteStorageBase(const std::string& filePath, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h b/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h index ad5af5cabb..369c515fb5 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h @@ -266,7 +266,8 @@ namespace AppInstaller::SQLite // Sets the page size for a new, empty database. // Must be called before the first write to take effect. - void SetPageSize(int pageSize); + // Must be a power of two between 512 and 65536 (inclusive), but we let SQLite enforce that. + void SetPageSize(size_t pageSize); operator sqlite3* () const { return m_dbconn->Get(); } diff --git a/src/AppInstallerSharedLib/SQLiteStorageBase.cpp b/src/AppInstallerSharedLib/SQLiteStorageBase.cpp index d977167a8b..bdb5fdb680 100644 --- a/src/AppInstallerSharedLib/SQLiteStorageBase.cpp +++ b/src/AppInstallerSharedLib/SQLiteStorageBase.cpp @@ -156,7 +156,7 @@ namespace AppInstaller::SQLite m_version = Version::GetSchemaVersion(m_dbconn); } - SQLiteStorageBase::SQLiteStorageBase(const std::string& target, const Version& version, int pageSize) : + SQLiteStorageBase::SQLiteStorageBase(const std::string& target, const Version& version, size_t pageSize) : m_dbconn(SQLite::Connection::Create(target, SQLite::Connection::OpenDisposition::Create)) { m_version = version; diff --git a/src/AppInstallerSharedLib/SQLiteWrapper.cpp b/src/AppInstallerSharedLib/SQLiteWrapper.cpp index e08417cd8c..d0a79870b8 100644 --- a/src/AppInstallerSharedLib/SQLiteWrapper.cpp +++ b/src/AppInstallerSharedLib/SQLiteWrapper.cpp @@ -251,7 +251,7 @@ namespace AppInstaller::SQLite return ToLower(setJournalMode.GetColumn(0)) == ToLower(mode); } - void Connection::SetPageSize(int pageSize) + void Connection::SetPageSize(size_t pageSize) { std::ostringstream stream; stream << "PRAGMA page_size=" << pageSize; diff --git a/tools/IndexComparisonTool/main.cpp b/tools/IndexComparisonTool/main.cpp index c012250df5..a1605f392d 100644 --- a/tools/IndexComparisonTool/main.cpp +++ b/tools/IndexComparisonTool/main.cpp @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" #include "WinGetUtil.h" @@ -106,6 +106,7 @@ namespace std::filesystem::path wingetUtilPath; // optional: explicit path to WinGetUtil.dll std::optional outputFile; std::optional baselineFile; + bool prebuilt = false; bool verbose = false; bool showHelp = false; }; @@ -119,6 +120,7 @@ namespace << L"measures its raw and compressed size (XPRESS Huffman, approximating MSIX).\n" << L"\n" << L"Options:\n" + << L" --prebuilt The provided path is a prebuilt index to check\n" << L" --wingetutil Path to WinGetUtil.dll\n" << L" (default: searches application directory and PATH)\n" << L" --output Write results as JSON to \n" @@ -137,6 +139,10 @@ namespace { args.showHelp = true; } + else if (arg == L"--prebuilt" || arg == L"-v") + { + args.prebuilt = true; + } else if (arg == L"--verbose" || arg == L"-v") { args.verbose = true; @@ -218,8 +224,10 @@ namespace ManifestStats stats; std::vector retryList; - - uint64_t processed = 0; + + retry: + uint64_t processed = 0; + auto start = std::chrono::steady_clock::now(); for (const auto& dir : manifestDirs) { std::filesystem::path relPath = std::filesystem::relative(dir, manifestsDir); @@ -233,26 +241,28 @@ namespace } if (++processed % 1000 == 0) - { - std::wcout << L" " << processed << L" / " << manifestDirs.size() << L" processed...\r"; + { + auto now = std::chrono::steady_clock::now(); + auto duration = now - start; + start = now; + std::wcout << L" " << processed << L" / " << manifestDirs.size() << L" processed (~" << std::chrono::duration_cast(duration / 1000).count() << L"ms per)...\r"; } } - // Retry failures once (handles dependency ordering issues) + // Retry failures until no progress is made if (!retryList.empty()) - { - std::wcout << L"\nRetrying " << retryList.size() << L" failed manifests...\n"; - for (const auto& dir : retryList) + { + if (retryList.size() < manifestDirs.size()) { - std::filesystem::path relPath = std::filesystem::relative(dir, manifestsDir); - if (SUCCEEDED(AddManifest(api, index, dir, relPath))) - { - ++stats.added; - } - else - { - ++stats.failed; - } + std::wcout << L"\nRetrying " << retryList.size() << L" failed manifests...\n"; + manifestDirs = std::move(retryList); + retryList.clear(); + goto retry; + } + else + { + std::wcout << L"\nDropping " << retryList.size() << L" failed manifests...\n"; + stats.failed = retryList.size(); } } @@ -267,7 +277,7 @@ namespace uint64_t MeasureCompressed(const std::filesystem::path& file) { std::ifstream ifs(file, std::ios::binary | std::ios::ate); - if (!ifs) return 0; + if (!ifs) throw std::exception{ "Couldn't open file." }; auto fileSize = static_cast(ifs.tellg()); ifs.seekg(0); @@ -278,7 +288,7 @@ namespace COMPRESSOR_HANDLE compressor = nullptr; if (!CreateCompressor(COMPRESS_ALGORITHM_XPRESS_HUFF, nullptr, &compressor)) { - return fileSize; + throw std::exception{ "Couldn't create compressor." }; } SIZE_T compressedSize = 0; @@ -289,7 +299,7 @@ namespace if (!Compress(compressor, data.data(), data.size(), compressed.data(), compressedSize, &compressedSize)) { CloseCompressor(compressor); - return fileSize; + throw std::exception{ "Couldn't compress." }; } CloseCompressor(compressor); @@ -423,7 +433,7 @@ namespace double savings = 1.0 - ratio; std::wcout << L"\n"; - std::wcout << L"Index built from: " << r.manifestsDir << L"\n"; + std::wcout << L"Index built from: " << r.manifestsDir.c_str() << L"\n"; std::wcout << L"Manifests added: " << r.manifestCount; if (r.failedCount > 0) { std::wcout << L" (" << r.failedCount << L" failed)"; } std::wcout << L"\n"; @@ -600,70 +610,79 @@ int wmain(int argc, wchar_t* argv[]) return args.showHelp ? 0 : 1; } - if (!std::filesystem::is_directory(args.manifestsDir)) + if (!std::filesystem::is_directory(args.manifestsDir) && !args.prebuilt) { std::wcerr << L"Error: not a directory: " << args.manifestsDir << L"\n"; return 1; } - // Load WinGetUtil.dll at runtime - WinGetApi api = LoadWinGetUtil(args.wingetUtilPath); - if (!api) { return 1; } - - // Create a temp file for the index - wchar_t tempDir[MAX_PATH + 1]{}; - wchar_t tempFile[MAX_PATH + 1]{}; - GetTempPathW(MAX_PATH, tempDir); - GetTempFileNameW(tempDir, L"idx", 0, tempFile); - std::filesystem::path indexPath{ tempFile }; - - struct TempFileGuard + if (std::filesystem::is_directory(args.manifestsDir) && args.prebuilt) { - std::filesystem::path path; - ~TempFileGuard() { std::error_code ec; std::filesystem::remove(path, ec); } - } tempGuard{ indexPath }; - - // Create the index - std::wcout << L"Creating index...\n"; - WINGET_SQLITE_INDEX_HANDLE index = nullptr; - HRESULT hr = api.Create( - indexPath.c_str(), - WINGET_SQLITE_INDEX_VERSION_LATEST, - WINGET_SQLITE_INDEX_VERSION_LATEST, - &index); - - if (FAILED(hr)) - { - std::wcerr << L"Error: WinGetSQLiteIndexCreate failed: 0x" - << std::hex << std::uppercase << hr << L"\n"; + std::wcerr << L"Error: not a prebuilt file: " << args.manifestsDir << L"\n"; return 1; } - // RAII guard: close index on early return - bool indexClosed = false; - struct IndexGuard - { - PFN_WinGetSQLiteIndexClose close; - WINGET_SQLITE_INDEX_HANDLE handle; - bool& closed; - ~IndexGuard() { if (!closed && handle) close(handle); } - } indexGuard{ api.Close, index, indexClosed }; - - // Populate the index - ManifestStats mstats = BuildIndex(api, index, args.manifestsDir); - - // Finalize: VACUUM and drop build-time indices - std::wcout << L"Preparing for packaging (VACUUM + drop indices)...\n"; - hr = api.PrepareForPackaging(index); - if (FAILED(hr)) - { - std::wcerr << L"Error: WinGetSQLiteIndexPrepareForPackaging failed: 0x" - << std::hex << std::uppercase << hr << L"\n"; - return 1; - } + std::filesystem::path indexPath; + ManifestStats mstats{}; + if (!args.prebuilt) + { + // Load WinGetUtil.dll at runtime + WinGetApi api = LoadWinGetUtil(args.wingetUtilPath); + if (!api) { return 1; } + + // Create a temp file for the index + wchar_t tempDir[MAX_PATH + 1]{}; + wchar_t tempFile[MAX_PATH + 1]{}; + GetTempPathW(MAX_PATH, tempDir); + GetTempFileNameW(tempDir, L"idx", 0, tempFile); + indexPath = tempFile; + + // Create the index + std::wcout << L"Creating index at " << indexPath.c_str() << L"...\n"; + WINGET_SQLITE_INDEX_HANDLE index = nullptr; + HRESULT hr = api.Create( + indexPath.c_str(), + 2, + 0, + &index); + + if (FAILED(hr)) + { + std::wcerr << L"Error: WinGetSQLiteIndexCreate failed: 0x" + << std::hex << std::uppercase << hr << L"\n"; + return 1; + } + + // RAII guard: close index on early return + bool indexClosed = false; + struct IndexGuard + { + PFN_WinGetSQLiteIndexClose close; + WINGET_SQLITE_INDEX_HANDLE handle; + bool& closed; + ~IndexGuard() { if (!closed && handle) close(handle); } + } indexGuard{ api.Close, index, indexClosed }; + + // Populate the index + mstats = BuildIndex(api, index, args.manifestsDir); + + // Finalize: VACUUM and drop build-time indices + std::wcout << L"Preparing for packaging (VACUUM + drop indices)...\n"; + hr = api.PrepareForPackaging(index); + if (FAILED(hr)) + { + std::wcerr << L"Error: WinGetSQLiteIndexPrepareForPackaging failed: 0x" + << std::hex << std::uppercase << hr << L"\n"; + return 1; + } - indexClosed = true; - api.Close(index); + indexClosed = true; + api.Close(index); + } + else + { + indexPath = args.manifestsDir; + } // Optionally query table stats from the now-closed database std::vector tables; @@ -706,4 +725,4 @@ int wmain(int argc, wchar_t* argv[]) } return 0; -} \ No newline at end of file +} diff --git a/tools/IndexComparisonTool/pch.h b/tools/IndexComparisonTool/pch.h index a5170a8a47..a571c2abbd 100644 --- a/tools/IndexComparisonTool/pch.h +++ b/tools/IndexComparisonTool/pch.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include From 244da13a4d319d235f202be1614d0dd4ab0ef5fb Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 4 Mar 2026 11:30:14 -0800 Subject: [PATCH 3/3] spelling --- .github/actions/spelling/expect.txt | 1 + tools/IndexComparisonTool/main.cpp | 49 +++++++++++++++-------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 955fbe49ed..3a7c77e9ba 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -169,6 +169,7 @@ ewgs execustom EXEHASH experimentalfeatures +XPRESS fdw fdwgp FECAFEB diff --git a/tools/IndexComparisonTool/main.cpp b/tools/IndexComparisonTool/main.cpp index a1605f392d..51bd1903a1 100644 --- a/tools/IndexComparisonTool/main.cpp +++ b/tools/IndexComparisonTool/main.cpp @@ -483,37 +483,37 @@ namespace void WriteJson(const Results& r, bool verbose, const std::filesystem::path& outFile) { - std::ofstream ofs(outFile); - if (!ofs) { throw std::runtime_error("Cannot open output file: " + ToUTF8(outFile.wstring())); } - - ofs << "{\n"; - ofs << " \"manifestsDir\": " << JsonEscapeString(ToUTF8(r.manifestsDir.wstring())) << ",\n"; - ofs << " \"manifestCount\": " << r.manifestCount << ",\n"; - ofs << " \"failedCount\": " << r.failedCount << ",\n"; - ofs << " \"rawBytes\": " << r.rawBytes << ",\n"; - ofs << " \"compressedBytes\": " << r.compressedBytes << ",\n"; - ofs << std::fixed << std::setprecision(4); - ofs << " \"compressionRatio\": " << r.CompressionRatio(); + std::ofstream stream(outFile); + if (!stream) { throw std::runtime_error("Cannot open output file: " + ToUTF8(outFile.wstring())); } + + stream << "{\n"; + stream << " \"manifestsDir\": " << JsonEscapeString(ToUTF8(r.manifestsDir.wstring())) << ",\n"; + stream << " \"manifestCount\": " << r.manifestCount << ",\n"; + stream << " \"failedCount\": " << r.failedCount << ",\n"; + stream << " \"rawBytes\": " << r.rawBytes << ",\n"; + stream << " \"compressedBytes\": " << r.compressedBytes << ",\n"; + stream << std::fixed << std::setprecision(4); + stream << " \"compressionRatio\": " << r.CompressionRatio(); if (verbose && !r.tables.empty()) { - ofs << ",\n \"tables\": [\n"; + stream << ",\n \"tables\": [\n"; for (size_t i = 0; i < r.tables.size(); ++i) { const auto& t = r.tables[i]; - ofs << " { \"name\": " << JsonEscapeString(t.name) + stream << " { \"name\": " << JsonEscapeString(t.name) << ", \"rowCount\": " << t.rowCount << ", \"pageCount\": " << t.pageCount << " }"; - if (i + 1 < r.tables.size()) ofs << ","; - ofs << "\n"; + if (i + 1 < r.tables.size()) stream << ","; + stream << "\n"; } - ofs << " ]\n"; + stream << " ]\n"; } else { - ofs << "\n"; + stream << "\n"; } - ofs << "}\n"; + stream << "}\n"; } // ------------------------------------------------------------------------- @@ -585,8 +585,9 @@ namespace if (ratioDelta >= 0) ratioSS << L"+"; ratioSS << std::fixed << std::setprecision(2) << (ratioDelta * 100) << L"pp" << L" (was " << std::setprecision(1) << (baseRatio * 100) << L"%)"; - - std::wcout << L"\nvs baseline (" << baselineFile.filename().wstring() << L"):\n"; + + std::wcout << L'\n'; + std::wcout << L" vs baseline (" << baselineFile.filename().wstring() << L"):\n"; std::wcout << L" Raw size: " << signedBytes(rawDelta) << L" (" << pctChange(current.rawBytes, baseRaw) << L")\n"; std::wcout << L" Compressed size: " << signedBytes(compDelta) @@ -623,7 +624,7 @@ int wmain(int argc, wchar_t* argv[]) } std::filesystem::path indexPath; - ManifestStats mstats{}; + ManifestStats stats{}; if (!args.prebuilt) { // Load WinGetUtil.dll at runtime @@ -664,7 +665,7 @@ int wmain(int argc, wchar_t* argv[]) } indexGuard{ api.Close, index, indexClosed }; // Populate the index - mstats = BuildIndex(api, index, args.manifestsDir); + stats = BuildIndex(api, index, args.manifestsDir); // Finalize: VACUUM and drop build-time indices std::wcout << L"Preparing for packaging (VACUUM + drop indices)...\n"; @@ -698,8 +699,8 @@ int wmain(int argc, wchar_t* argv[]) Results results; results.manifestsDir = std::filesystem::weakly_canonical(args.manifestsDir); - results.manifestCount = mstats.added; - results.failedCount = mstats.failed; + results.manifestCount = stats.added; + results.failedCount = stats.failed; results.rawBytes = rawBytes; results.compressedBytes = compressedBytes; results.tables = std::move(tables);